☰
从 0 到 1 搭 Agent:RAG 知识检索管道全解析
2026/9/28 9:03:44 网站建设 项目流程

从 0 到 1 搭一个 Agent,很多人第一反应是“选个大模型”,第二反应是“配个提示词”。这些当然重要,但真正让 Agent 从“玩具”变成“生产力工具”的,往往是知识怎么进去、怎么被拿到。这篇聊的就是这件事——RAG,也就是检索增强生成,说白了,就是给 Agent 外接一个可查询的记忆库。

我会用做 Agent 开发的视角,把 RAG 拆开揉碎:为什么需要它、管道上每一环在解决什么问题、怎么落地一个小但完整的例子,以及那些不实际跑几遍根本发现不了的坑。适合刚开始做 RAG、或者已经跑通 demo 但总感觉效果不稳定的读者。不需要你有很强的机器学习背景,但最好写过一点 Python,能看懂基本代码。

1. 为什么 Agent 需要一条知识管道

1.1 模型参数不是你想要的“知识”

先说一个很多人忽略的事实:大模型里存的知识,本质上是预训练阶段从公开语料里统计出来的“印象”。它知道 2022 年以前的很多事实,但不一定知道你们公司的内部制度、你上周刚更新的产品手册、或者那个只有 Excel 表格里才有的历史报价。更麻烦的是,大模型在回答它不确定的问题时,会非常自然地“编”一个看起来合理的答案。

我见过很多团队踩这个坑:老板说“把我们的知识库接入大模型”,于是开发同学直接调 API,然后把公司文档一股脑塞进上下文窗口。结果上下文超限、费用暴涨、回答还经常串内容。这不是模型笨,而是你根本没有给它一条“查找资料”的通道。RAG 解决的就是这件事:不是让模型背下所有知识,而是让它在需要的时候,自己去文档堆里找到相关段落,再基于这些段落组织答案。

换个生活类比:你新入职一家公司,问老员工问题。如果老员工凭记忆乱猜,那就是“裸模型”;如果老员工先查一下内部 wiki、翻一下聊天记录,然后根据查到的内容回答你,这就是 RAG。它的核心价值,是把“死记硬背”变成“按需查阅”。

1.2 RAG 不是什么高深魔法,而是一条数据管道

很多刚接触 RAG 的人,以为它就是一个“搜索 + 提示词”的拼凑。实际上,RAG 是一整条管道,从文档进来,到最后回答出去,中间每个环节都会影响结果质量。你可以把它拆成四个阶段:

  • 文档接入与处理:从 PDF、网页、数据库、Wiki 等来源抽取文本,清洗、去重、切成小块。
  • 向量化与索引:把文本块变成向量,存进向量数据库,建好索引。
  • 检索:拿到用户问题时,先向量化问题,再去库里找最相关的文本块。
  • 增强生成:把检索到的文本块作为上下文,连同用户问题一起交给大模型生成回答。

这四个阶段,每一环都有“看起来能用但其实很糙”的默认做法。比如很多人直接用 PDF 文本抽取,结果表格全乱;还有人把整个文档切成 500 字的小块,切碎了语义。RAG 效果不好,往往就坏在这些细节上,而不是模型不够强。

1.3 在 Agent 场景里,RAG 跟传统搜索问答还不一样

如果你只做一个“知识库问答机器人”,那么 RAG 就是“用户问一句,你查一次,答一句”,这个链路相对简单。但 Agent 的麻烦在于:它有对话状态、会调用工具、可能需要多轮推理,甚至要在一次任务里多次检索不同资料。

举一个我实际做过的例子:一个内部运维 Agent,用户说“帮我查一下昨晚备份失败的任务,然后看看存储空间是不是不够了”。这句话至少要两次检索:一次查备份日志,一次查存储监控。而且第二次检索要依赖第一次的结果来决定查询词——如果只是机械地把用户原话转成向量去搜索,很可能搜不到。所以 Agent 场景下的 RAG 需要“检索决策”,也就是让 Agent 自己判断“现在要不要查、查什么、查完怎么用”。这也是最近大家总说的 Agentic RAG 的雏形。

后面我会先从最基础的管道讲起,把每一步都跑通,再回到 Agent 场景,说说怎么把 RAG 封装成 Agent 的内部工具。地基一定要扎实,因为后面所有“智能”都建立在“能查得对”上面。

2. RAG 管道的核心环节拆解

2.1 文档接入与清洗:管道的入口最容易埋雷

绝大多数团队的第一版 RAG,用的都是现成的文档,比如 Word、PDF、Markdown。但“能读”和“读得对”是两回事。PDF 尤其麻烦,很多 PDF 内部是图片格式,文字根本抽不出来;还有多栏排版、页眉页脚、表格换页,这些都会让提取出来的文本顺序错乱。

我这里说的清洗,不是简单的去空行,而是要做几件很实际的事:

  • 去除页眉页脚和页码,避免每段文本里混入“第 3 页”“公司名称”这类噪声。
  • 处理表格,要么把表格转成 Markdown 表格,要么转成“列名: 值”的键值对文本,之后再切块。直接按原始布局抽取的表格,检索时几乎必乱。
  • 去重,特别是多份文档互相引用、复制粘贴的情况,同一段知识库里存三份,检索时会被同一内容刷屏。

我用过一款开源文档解析工具叫 unstructured,它对 PDF、Docx、HTML 都有现成的 partition 函数,能按元素类型输出文本和表格。社区里也有很多人直接用 LangChain 的PyPDFLoader,但那个只适合干净的单栏 PDF。如果你们的文档体系比较复杂,建议在加载后用一次结构清洗,这比你在后面调 embedding 参数省心得多。

2.2 切分策略:块大小不是拍脑袋决定的

把清洗后的文档切成小块,是 RAG 里最容易被低估的一步。块太大了,向量化后会混入太多无关信息,检索精度下降;块太小了,单个块信息量不足,号称命中了但内容回答不了问题。

我见过一种很常见的做法:固定 512 个字符,重叠 50 个字符。听起来很标准,但实际跑起来问题很多。比如技术文档里的“前提条件”“注意事项”经常跟上下文分隔很远,固定切分会把它们切到不同块里,检索时只找到了一半。

比较稳妥的策略有两种:

  • 按结构切分:先识别标题层级,以章节为单位切块。Markdown 文档可以用MarkdownHeaderTextSplitter,它会根据#、##标题保留上下文。
  • 按语义切分:利用 embedding 判断文本间的语义断点,在意思变化处切分。LangChain 的SemanticChunker就是这个思路,但计算成本高一些。

切分时还需要设一个 overlap,让相邻块之间重叠一部分内容。这个小参数很关键,它避免“一句话被从中间切断”导致搜索不到。我通常的做法是:块大小 500 到 800 字,重叠 80 到 120 字。具体数字要根据文档类型调整,原则是“一个块要能独立表达一个完整意思,而不是机械地数字符”。

2.3 向量化与索引:Embedding 模型选择要看检索场景

Embedding 模型负责把文本变成向量,这个环节很多人直接选 OpenAI 的text-embedding-3-small,但国内团队做企业知识库时,要考虑数据出域和成本问题。如果你用国产大模型生态,可以考虑智源bge-m3或者阿里的text-embedding-v3,这些在中文语义检索上都不差。

选 embedding 模型时,不要只看 MTEB 榜单分数。要用你自己的业务文档去测,看那些“同义改写”和“专业术语”能不能匹配上。我踩过的一个典型坑是:直接用通用 embedding 处理我们内部的运维文档,里面全是“网关”“节点”“熔断”这种词。模型一般能理解,但遇到“服务雪崩”这种隐喻性说法,纯向量检索就抓瞎了。所以后面我会建议你上“混合检索”,把关键词匹配和向量检索结合起来。

向量库这块,轻量场景用 Chroma、FAISS 都行;团队协作或者要上生产,建议用 Milvus、Qdrant 这类独立服务。FAISS 的好处是简单,一个文件就能存索引,但不好做持久化管理和多副本,数据量大时会比较吃力。

索引结构也有讲究,最简单的就是暴力扫描(FLAT),数据量小没问题;数据量大建议用 HNSW,它通过多层图结构加速搜索,精度损失可控,性能提升明显。

2.4 检索与重排:命中不是终点,排对才是

检索的直观逻辑是:把用户问题变成向量,然后在向量库里找余弦相似度 Top K 的文本块。但现实是,向量相似度高并不代表语义上真的解决用户问题。两个句子可能用了完全不同的表达但意思接近,也可能字面上高度重合但实际风马牛不相及。

所以更靠谱的做法是“召回 + 精排”两步走:

  • 第一步,用向量检索加关键词检索(比如 BM25)各召回一批候选块,合并去重,留出一定余量,比如 Top 20。
  • 第二步,用 Rerank 模型对这些候选块做精细打分,挑出真正对回答有贡献的 Top 5 交给大模型。

Rerank 模型我这边常用的有bge-reranker-v2-m3,它的思路是直接把“用户问题 + 候选块”拼接成对,计算相关分。这一步增加了一点调用成本,但对最终回答质量提升非常明显。我做过一次对比:纯向量检索的 hit rate 大概 68%,加上关键词召回和 rerank 后能到 85% 左右。在知识库问答里,这个差距就是“能用”和“好用”的分界线。

3. 实操:把一个最小可用的 RAG 管道跑起来

3.1 环境准备与依赖

我们用一个相对轻量但完整的方案:LangChain 做管道编排,Chroma 做向量存储,本地 embedding 模型用bge-small-zh-v1.5,生成模型可以接任意 OpenAI 兼容接口。整个流程不需要昂贵硬件,CPU 也能跑。

pip install langchain langchain-community langchain-chroma sentence-transformers pymupdf

说明一下,langchain-chroma是专门适配 Chroma 的包,老版本 LangChain 直接内置 Chroma,新版拆出来了,不装会报错。我这里用的 Python 版本是 3.10,LangChain 版本为 0.2.x。如果你用的版本更高,部分 API 名字可能变了,但整体逻辑一致。

3.2 文档加载、切分与入库

我们先写一个最基础的脚本:把docs目录下的 PDF 加载进来,按 Markdown 结构切分,然后向量化存入 Chroma。

from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载 PDF loader = PyPDFLoader("./docs/运维手册.pdf") documents = loader.load() # 2. 切分 splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], ) chunks = splitter.split_documents(documents) print(f"切分后文本块数量: {len(chunks)}") # 3. 向量化并存储 embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" )

这里要提醒一下:RecursiveCharacterTextSplitter的separators顺序很重要,它代表切分优先级。中文我用“句号、问号、感叹号、分号、逗号”这些作为次级分隔,比按纯字符硬切要人性化得多。如果你的文档是英文为主,分隔符列表的顺序也要相应调整。

3.3 实现检索与生成链路

入库之后,就可以写一个查询链路。这里把检索到的文本块用模板拼进提示词,喂给大模型,让模型“仅根据上下文回答”。

from langchain.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI prompt = ChatPromptTemplate.from_template( """你是一个知识库问答助手。请仅根据以下资料回答用户的问题。 如果资料中没有相关信息,请明确回答“资料中没有找到相关信息”,不要编造。 资料内容: {context} 用户问题: {question} """ ) llm = ChatOpenAI( base_url="http://localhost:8000/v1", # 你的 OpenAI 兼容服务地址 api_key="EMPTY", model="qwen2.5-14b", temperature=0.1, ) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) def answer(question: str): docs = retriever.get_relevant_documents(question) context = "\n\n".join([doc.page_content for doc in docs]) chain = prompt | llm response = chain.invoke({"context": context, "question": question}) return response.content

这段代码看着简单,但有几个参数值得反复调:

  • k=5是召回数量。太小容易漏,太大会塞入大量无关文本,反而干扰模型判断。
  • temperature=0.1对知识问答要低,因为我们要的是稳定、可复现的答案,而不是天马行空的发挥。

另外,prompt里那句“只根据资料回答,不要编造”不是可有可无的废话。实测下来,如果缺少这句,模型会倾向于动用自身预训练知识去“补充”,一旦补充错了,你很难排查到底是检索的问题还是生成的问题。

3.4 把 RAG 封装成 Agent 的一个工具

跑通上面的链路之后,就可以把它接进 Agent 了。这里我以 LangChain 的 Agent 为例,思路是用@tool装饰器把answer函数包装成一个工具,让 Agent 自主决定何时调用。

from langchain.tools import tool @tool def knowledge_base_search(question: str) -> str: """在内部知识库中检索相关信息,回答用户关于产品手册、运维文档、FAQ 等问题。""" return answer(question)

然后把这个工具挂到 Agent 的工具列表里。这样做的好处是:Agent 可以决定哪些问题走 RAG、哪些问题直接用自身能力回答。比如用户问“今天天气怎么样”,Agent 不会去查知识库;问“备份任务失败的处理流程”,Agent 就知道必须调用知识库检索。

集成之后有一点要特别注意:Agent 是多轮对话的,上一轮检索到的文本块不会自动从上下文里消失。如果你把工具返回结果原封不动丢给大模型,Agent 可能会在下一轮参考到一个已经过时或跟当前问题无关的文档片段。这是 Agentic RAG 里常见的上下文污染问题,后面我会专门讲怎么处理。

4. 踩坑记录与效果调优

4.1 常见问题速查表

我把实际调 RAG 时遇到最多的问题整理成一个表,按“症状-原因-解决”三列说明。

症状可能原因解决思路
检索结果明显不对分块切碎了语义,或 embedding 不匹配调大 chunk size,换成领域相关 embedding,加混合检索
答案答非所问上下文混入太多无关块降低 k 值,加 rerank,过滤掉相似度低于阈值的块
模型总说“没有相关资料”检索没召回,或者召回内容太泛检查向量库是否嵌入了内容,手动打印检索结果观察
答案编造事实prompt 没约束,或检索内容不充分明确“只能根据资料回答”,并补充召回数量
对话到第二轮就开始乱上下文里残留了上一轮检索的文本每次工具调用后裁剪或定期清理历史记录
向量库占用空间增长异常覆盖写入时旧向量没有清理根据 doc_id 先删再插,或使用能更新的集合

4.2 命中率为什么一直上不去

很多人问我:为什么我的 RAG 命中率这么低。我说你先把“命中率”定义清楚,是用标准答案去比对检索结果,还是只看最后回答对不对?前者是 retriever 的hit rate,后者是端到端的准确率。两个指标不一定一致,但你首先要有一个可量化、可重复的测试集。

你可以从知识库里挑 30 到 50 个典型问题,每个问题标注一个或几个正确答案所在的文档片段,然后统计:Top 5 里有没有包含标准片段。如果 hit rate 低于 70%,别急着调大模型,先把检索侧调好。我常用的调试手段是:

  • 打印查询向量和召回的文本块,人工观察相似度高的块是否真的相关。
  • 尝试不同的 chunk size,从 300 到 1000 扫一遍,看 hit rate 的曲线变化。
  • 加入 BM25 关键词召回,跟向量结果融合,因为有些专业词向量模型根本匹配不上。

别怕调参费时间。RAG 的效果上限,从来不是由模型决定的,而是由“你能否准确地把正确答案放到模型的眼皮底下”决定的。

4.3 Agent 多轮对话中的上下文污染问题

这个坑是我做 Agent 集成时最深刻的教训。最开始我把 RAG 工具接进 Agent 后,第一轮效果很好,第二轮突然出现“用上一轮的文档回答这一轮问题”的诡异现象。排查了很久,发现问题是这样的:Agent 每轮对话都会把之前的工具返回结果放进历史消息里,而大模型是自回归的,它会倾向于利用上下文里最近出现过的文本片段,即使那些片段已经跟当前问题无关。

解决办法有几个:

  • 工具返回时只返回精简摘要,而不是完整文档块。这样即使历史里残留,影响也小。
  • 在 prompt 里明确告诉模型:“仅根据当前最新检索结果回答,忽略历史工具消息中的文档信息。”
  • 如果对话轮次很多,定期压缩历史,把旧工具消息裁剪或总结成一句“此前用户问过...,答案是...”。

我实际采用的做法是“摘要裁剪”。每次工具调用后,把原始检索结果在工具内部转换为一段 100 字以内的摘要,再返回给 Agent。这样既保留了信息,又不会污染后续决策。

4.4 从 RAG 到 Agentic RAG:让 Agent 自己决定怎么查

基础 RAG 最大的局限是“一次查询定生死”。用户问题复杂一点,比如“对比 A 产品和 B 产品的规格差异”,你很难用一个查询词同时检索到两个产品的信息。这时你需要把检索拆成多个子查询,分别去查,再汇总。

Agentic RAG 的思路,就是把这个“拆查询、调工具、多次检索、汇总验证”的过程交给 Agent 去编排。它不是一上来就用一个大检索,而是先分析用户意图,生成多个搜索计划,逐步执行。举个例子:

用户问:“我们上周的故障是不是因为磁盘满了?”

Agent 会先检索“上周故障记录”,拿到故障时间段和现象,再检索“磁盘监控告警”,确认是否在同一时间有容量告警,最后综合两份检索结果给出判断。如果只用一次检索,往往会漏掉第二份资料。

从工程实现来看,Agentic RAG 并不复杂,你只要把知识库检索封装成多个工具(比如“故障记录查询”“监控数据查询”“容量报表查询”),然后让 Agent 按需调用。但这里对 Agent 的模型能力要求更高,因为 Agent 必须会做任务分解。如果模型太弱,它可能只会调用一次工具就草率收场。

我自己在做内部工具时会这样分级:问题简单,走单次 RAG;问题可能涉及多个数据源,走 Agentic RAG;再往上才是多智能体协作。别一上来就把架构做复杂,先让基础管道稳定,再逐步开放决策权。


最后分享一点我的个人体会:RAG 这个方向,入门不难,做精很难。很多人以为把文档塞进向量库就完事了,然后被效果打得怀疑人生。其实每一个环节都值得你花时间去测试、量化和优化。我自己的经验是,先用一个小而全的测试集把管道打扎实,再去追求高级编排。否则,Agentic RAG 跑起来之后,你根本分不清问题出在检索、排序还是决策上。从“能用”到“好用”,其实就是把那些不起眼的细节一点点扣干净的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询