这是“走进 AI Agent”系列第四篇。前几篇我们把一个 Agent 的主干搭扎实了——意图识别、任务规划、工具调用都跑通了,模型能根据用户的指令自己决定下一步该做什么。但真把 Agent 放到业务场景里,不管是写行业报告、答客户咨询,还是做企业内部的制度问答、产品知识服务,大家很快就会撞上同一个问题:模型脑子里那点知识不够用。这个问题的解决方案,就是这一篇要聊的主题——知识获取管道,而其中最基础、也最常用的实现,就是 RAG。
我自己把知识获取管道拆成三件事:拿什么知识、怎么拿、怎么让模型拿得准。RAG 解决的就是后两件事。它会先把外部文档处理好存进向量库,用户提问时先检索相关片段,再把检索结果和问题一起交给大模型生成答案。这个模式很像我们考开卷试——先翻书,再作答,而不是闭卷硬憋。这篇文章适合正在从 0 到 1 搭建 AI Agent、想给自己的 Agent 接上私有知识库、或者看过 RAG 教程但没真正跑通一个最小项目的开发者。
1. Agent 为什么需要一条“知识获取管道”
1.1 一次失败的问答暴露出的核心短板
我在本地跑通第一个 Agent 原型的时候,做了一个很基础的测试:问它“我们团队 2024 年的年度总结文档里提到的三个核心指标是什么”。结果让我印象很深——模型非常流畅地编出了一套看起来完全合理的答案,指标名称、数值、趋势描述一应俱全,但文档里根本没有那些内容。
这不是模型“笨”,而是它的知识来源决定的。预训练语言模型的知识来自训练数据,有严格的截止日期,也不可能覆盖你们团队的内部文档、你公司刚发的制度文件、或者某个产品的私有规格说明。第二个问题是,就算把文档塞进上下文,模型处理超长文本时也会出现“中间迷失”——几千 token 以后的信息,它经常抓不住重点。
这里还得区分一个容易混淆的概念:记忆和知识获取不是一回事。记忆解决的是“同一段对话里,前面说过的话后面还记得”,它面向的是会话上下文。知识获取管道解决的是“模型本来不知道、但回答这个问题又必须知道的内容”,它面向的是外部语料。两者可以配合用,但记忆不能替代 RAG。这也是我后来在实际项目里反复被教训的一点。
1.2 RAG 不是“查数据库”,而是“开卷考试”
我习惯用“开卷考试”来跟第一次接触 RAG 的同事解释这个机制。闭卷状态下,模型靠的是肚子里那点“预训练存货”,碰到没见过的问题就只能编。RAG 做的事情,是考试时给你一本可以翻的参考书——先通过检索把和问题最相关的几段内容捞出来,再让模型只基于这些内容作答。
但这个“翻书”的动作没那么简单。直接全文检索文档字面关键字,很多语义相近的问题会漏掉。比如用户问“公司今年经费紧张吗”,文档里写的是“预算缩减 30%”,字面上没有“紧张”两个字,传统关键字检索就匹配不上。RAG 的做法是把文档和问题都转换成向量,用数学上的相似度来匹配语义,这样“经费紧张”和“预算缩减”在向量空间里距离足够近,就能被正确召回。
所以完整地说,RAG 是在用户的提问和外部文档之间搭一座语义桥梁。它在 Agent 里的位置也不是一个独立战略模块,而更像Agent管道的“输入处理器”——用户在发起任务前,先把必要的知识检索出来,作为后续规划和回答的事实基础。理解这一点,再往下看三阶段流程就会清楚很多。
2. RAG 三阶段全流程拆解:索引、检索、生成
2.1 索引阶段:把“整本书”拆成“知识卡片”
索引阶段的目标,是把一堆杂乱的原始文档变成可快速检索的结构化知识。前提是先把内容拆碎,再逐个向量化入库。这一步做好了,后面检索质量至少不会太差;做不好,后面花再多时间调参也没用,因为“源头”就是脏的。
整个过程可以想象成给一本书做索引卡片:你不会把整本书作为一个条目塞进卡片目录,那太大了,检索的时候既慢又不准。正确做法是把书按章节、段落拆成若干小块,每一块单独做成一张卡片,卡片上除了原文内容,还要有一个能代表它语义的“特征码”——也就是向量。
具体实现里,我最常用的是RecursiveCharacterTextSplitter,它会按固定长度切文档,同时尽量保留语义边界。切分粒度需要手调,我用过的合理区间是 300 到 800 个 token 一段。切完以后,把每一段文本交给 Embedding 模型,转成一个固定维度的浮点数数组,这就是“向量”。最后,把向量和原文一起写进向量数据库,索引阶段就完成了。
2.2 检索阶段:相似度计算与召回策略
检索阶段解决的核心问题,是把用户的问题也转换成向量,然后在向量库里找出“语义上最接近”的那几段文档。
向量库本质上是一棵高性能的搜索树,或者更准确地说,是一个支持近似最近邻搜索的存储引擎。常见的实现有 Chroma、FAISS、Milvus、Qdrant 等。个人做原型、本地测试,用 Chroma 最顺手;生产环境数据量大、并发高,Milvus 或 Qdrant 更稳。选型逻辑很简单:先用最轻量的把流程跑通,再按瓶颈换方案。
召回时有两个核心参数必须搞清楚。第一个是top_k,意思是取最相似的几个片段回来;第二个是相似度阈值,低于阈值的片段即使排在前几也直接丢弃。这两个参数直接影响答案质量:top_k太小,相关信息可能漏掉;top_k太大,不相关内容会干扰模型判断,还浪费 token。
这里还要补充一个经验:向量检索不等于全文检索的替代品。很多项目用的是混合检索,向量抓语义,BM25 抓关键词,两者结果做加权融合,能显著提升实体名、编号、专有名词的命中率。我的建议是,第一版先只做向量检索跑通流程,等出现“明明有这个文档但搜不到”的情况,再上混合检索。
2.3 生成阶段:Prompt 组装与引用约束
检索出来的片段不会直接丢给用户看,它们要作为“参考资料”和一个精心设计的 Prompt 拼在一起,再交给大模型生成答案。
生成阶段最容易被忽视的是 Prompt 的质量。我的标准做法是:在 Prompt 里明确告诉模型两种情况。第一种,检索内容里有足够信息,就直接基于内容回答,不要额外发挥;第二种,检索内容没有覆盖用户的问题,要明确说“资料中没有相关信息”,而不是强行给出一个可能出错的答案。这个约束能从根上压住一大半的幻觉问题。
还有一个细节叫引用溯源。让模型在回答后附上信息片段的来源编号,比如“根据资料[1][2]”,这样用户能自己去核验。实现上不复杂,只要在组装 Prompt 时给每个片段带上序号,要求模型回答时标注序号即可。这一招对提升用户信任感特别有效,尤其是知识管理、法律、医疗这类对准确性要求高的场景。
三个阶段的输入输出,我用一张表整理一下:
| 阶段 | 输入 | 核心动作 | 输出 |
|---|---|---|---|
| 索引 | 原始文档 | 切分、向量化、入库 | 向量库中的文档片段 |
| 检索 | 用户问题 | 向量化、相似度搜索 | top_k 个相关片段 |
| 生成 | 问题 + 片段 + Prompt | 大模型基于资料作答 | 带引用的最终回答 |
3. 实操:五分钟搭一条最小可用的 RAG 管道
3.1 环境准备与最小代码骨架
我一直觉得,RAG 这个概念听起来很玄,但最小实现其实非常短。这里给你一份我最近在本地跑过的最小骨架,用 Python + LangChain 实现。先装依赖:
pip install langchain langchain-community langchain-text-splitters langchain-chroma chromadb pip install langchain-ollama sentence-transformers我这边用的是本地模型,不需要申请 API key,方便复现。Embedding 用国产开源模型BAAI/bge-small-zh-v1.5,对中文支持相当好,体积小,普通 CPU 也能跑;生成模型用 Ollama 里的qwen2.5:7b,本地跑完全够用。
准备一个测试目录./docs,里面放几个 txt 或 md 文件,比如你们的项目文档、会议纪要。然后跑这段代码:
from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_ollama import OllamaLLM from langchain.chains.retrieval_qa.base import RetrievalQA # 1. 加载文档 loader = DirectoryLoader("./docs", loader_cls=TextLoader) docs = loader.load() # 2. 切分文档 splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每段 500 token 左右 chunk_overlap=50, # 段之间重叠 50 token,防止切断语义 ) chunks = splitter.split_documents(docs) # 3. 向量化入库 embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectordb = Chroma.from_documents( chunks, embedding, persist_directory="./chroma_db", # 持久化路径 ) # 4. 构建检索问答链 qa = RetrievalQA.from_chain_type( llm=OllamaLLM(model="qwen2.5:7b"), retriever=vectordb.as_retriever(search_kwargs={"k": 3}), return_source_documents=True, ) # 5. 提问 result = qa.invoke("根据资料,我们 2024 年的三个核心指标是什么?") print(result["result"])这段代码最简化地串联了索引、检索、生成三步。第一次运行会把文档向量化存入./chroma_db,后续再跑会直接读库,不会重复建索引。
注意:如果你是第一次用
HuggingFaceEmbeddings,模型会从网上下载到本地缓存目录,需要能联网。下载完成之后,之后离线也能跑。生成模型qwen2.5:7b需要提前用ollama pull qwen2.5:7b拉取。
3.2 影响检索效果的三个关键参数
跑通之后,你的直接感受可能是“为什么回答质量忽高忽低”。这时候该调参数了。我从实际测试里总结出三个最值得优先调整的点。
第一个是chunk_size。切太细,比如 200 token,语义容易不完整,一个结论被拦腰切断;切太粗,比如 1500 token,单个片段里塞了太多主题,检索时“踩中了凑合、踩不中拉倒”的偏差很大。我自己的经验是中文场景从 500 起步,根据文档类型微调:规范制度类可以适当放大到 800,问答类、对话类文档建议压到 300 左右。
第二个是top_k。在知识问答场景里,我建议先设 3 到 5。top_k设得太大,比如 10,模型会被不相关内容干扰,回答开始“东拉西扯”;设得太小,又容易出现信息缺失。检索质量不稳定时,你可以在代码里把retriever单独拿出来,打印召回结果,看看真正相关的文档片段有没有出现在前几名里。
第三个是 Embedding 模型本身。这个最容易被忽略,但影响是全局性的。通用场景用bge-small-zh-v1.5足够;如果你的文档非常垂直,比如医疗、法律、代码仓库,建议对比几个模型的表现,别只看网上跑分。这块没什么捷径,多测几组真实问题,肉眼判断召回的语义相关度。
3.3 把 RAG 作为 Agent 的一个工具接入
最小管道跑通以后,下一步就是把 RAG 接进 Agent 的流程里。我的做法是把整个 RAG 问答链封装成 Agent 的一个”工具”,Agent 规划任务时,判断“这个问题需要查资料”就调用这个工具。
这样做的好处是:Agent 的主链路不用做大改动,原有的意图识别、任务规划、工具调用逻辑全部保留,只是多挂了一个新工具。用户说“查一下我们团队的季度报告里关于进度的部分”,Agent 把它拆成两步——先调用 RAG 工具检索相关内容,再把结果作为上下文生成回答。
伪代码思路:
def rag_tool(query: str) -> str: result = qa.invoke({"query": query}) return result["result"] # 注册进 Agent 的工具列表 tools = [ Tool(name="rag_query", func=rag_tool, description="查询本地知识库中的文档内容"), # 其他工具... ]有一定基础的朋友可以去看 LangChain 官方文档里的create_react_agent和Tool注册方式;如果用的是 Spring AI 生态,langchain4j也提供了完整的 RAG 集成——EmbeddingStoreRetriever配合ContentRetriever注册到 AgentService 即可。这就是热词里spring ai rag、langchain4j rag相关的典型用法。
需要特别提醒的是:RAG 作为工具接入 Agent 后,要设计好“检索不到怎么办”的兜底逻辑。我见过不少项目,Agent 调用 RAG 工具没搜到有效内容,但 Agent 为了完成任务,仍然硬着头皮编了一个答案。这比不接 RAG 还危险。我在 Agent 的 system prompt 里明确写了:检索结果不足以回答问题时,必须直接回复“知识库中没有相关信息”,并且终止当前工具链。
4. 接入 Agent 之后的调参与排坑实录
4.1 检索质量差:先查数据,再查代码
这是最常遇到的问题,现象是模型回答“牛头不对马嘴”。很多人第一反应是换大模型,但我觉得要先花 10 分钟做一个“检索体检”:把 RAG 链路中的检索结果单独打印出来,不经过生成阶段,自己先看看召回的内容到底相不相关。
体检结果通常有三种情况。第一种,召回的内容乱七八糟,跟问题毫无关系——多半是 Embedding 模型和领域不匹配,或者文档切分粒度不合理。第二种,召回的内容相关,但信息不完整,比如问题要 A 和 B 两个指标,只召回了一段只讲 A 的内容——调整chunk_size和top_k能解决。第三种,召回内容没问题,但模型回答还是没用好——这是 Prompt 约束问题,不是检索问题。
我踩过最隐蔽的一个坑是:文件加载环节静默出错。之前有一个项目,DirectoryLoader默认只解析txt和md,我放了一批 docx 进去,文档其实压根没被加载,索引库是空的,检索当然什么也搜不到。查了半天,最后用len(docs)输出数量才发现。遇到检索异常,第一件事永远是确认数据真的进库了。
4.2 幻觉问题:怎么让模型老老实实引用资料
很多人以为做完 RAG 就不会有幻觉了,这是误解。RAG 能显著降低幻觉,但不能完全消除。模型生成时仍然可能“脑补”资料里没有的细节,尤其是当检索到的片段里恰好有一句相关的话,但不够完整时,模型会自己补全后半段。
我的处理方式分三层。第一层,Prompt 强约束:要求回答必须基于给定资料,资料中没有的信息一律不答。第二层,引用溯源:给每个检索片段编号,要求回答后标注 [1]、[2],凡是没有编号支撑的内容视为无效。第三层,生成后校验:用简单的规则脚本检测,回答中是否包含资料中不存在的数值、日期、人名,一旦发现就触发“重新生成一次”的流程。
对于高敏场景,我还会加一层“不答策略”。比如用户问的问题超出资料范围,但模型觉得“凭常识也能回答”,这时宁可让它说“知识库中没有相关信息”,也不要让它用常识兜底。你可能会觉得这样会让 Agent 显得“不够聪明”,但用户真正想要的是准确,不是一本正经地胡说。
4.3 响应慢与成本高:缓存、压缩与混合检索
把 RAG 接入 Agent 后,另一个体验上的问题是响应变慢。原因很直接:Agent 本身有规划成本,RAG 又要先检索再生成,链路变长了一倍。实测下来,本地 7B 模型单次回答总耗时在 8 到 15 秒之间,这在很多对话场景里已经超出用户的等待耐心。
我做的第一件事是加缓存。相同或高度相似的问题,直接复用之前的检索结果,不走 Embedding 不走模型。实现上可以用简单的dict存 query 的向量哈希,或者接 Redis。实测对高频重复问题能节省 60% 以上的资源。
第二件事是控制上下文长度。很多 RAG 框架默认把top_k个完整片段全部拼进 Prompt,一个片段 800 token,5 个就是 4000 token,开销非常大。我的做法是先召回 8 到 10 个片段,用一个重排序模型(比如bge-reranker-base)重新排序,只保留最相关的 3 个进入生成阶段。这能显著降低每次请求的 token 消耗,生成速度也更快。
第三件事是混合检索。前面提过,向量检索擅长语义匹配,BM25 擅长精确关键词匹配。两者融合后,实体名、编号、型号这类信息命中率大幅提升,也能减少“该搜到但没搜到”而去反复重试的情况。
经验总结:RAG 的性能优化要按“缓存 > 压缩 > 换模型”的顺序来。先让系统少干活,再让单个活干得快,最后才考虑上更大的模型。一上来就上大模型,只会让速度和成本同时失控。
5. 从基础 RAG 到 Agentic RAG:后续还能怎么走
前面聊的都是静态 RAG——用户问一句,检索一次,生成一次。实际业务中,很多问题需要多步推理:用户问“对比 A 产品和 B 产品的售后政策”,单一检索要么只找到 A 的文档,要么只找到 B 的,简单合并又不够深入。这就是 Agentic RAG 要解决的方向——把检索动作内嵌到 Agent 的推理循环里,允许模型根据初步结果决定要不要二次检索、要不要查更细的文档。
这个方向最近特别热,热词里也有不少相关信息。graph rag和ontology rag是另一条线,它们解决的是“文档之间的关系”问题。传统 RAG 把每段文本当成独立个体,检索时只能按语义找片段;Graph RAG 会把文档里的实体、关系构建成图,检索时既能找到片段,也能顺着关系找到上下游知识。这在企业制度、产品体系这类强关联的知识场景里,效果提升非常明显。
我给想继续深入的朋友一个建议:不要把 Agentic RAG 想得太复杂,它其实就是把这一篇里讲的“RAG 作为工具”再往前推一步——RAG 工具具备自我评价、二次检索、汇总融合的能力。你先要把基础 RAG 的每一个参数、每一个坑都摸清楚,再去碰这些进阶方案。地基不稳,往上加的每一层都会变成新的计数器。
从最早跑通那个“一本正经编答案”的 Agent,到现在能在私有知识库上稳定回答,我最大的体会是:RAG 不是一次性工程,而是一条需要持续维护的管道。前期把文档切分、检索参数、Prompt 约束这几件事做扎实,Agent 会越用越顺;前期偷懒,所有隐患都会在 Agent 的输出里以“幻觉”的形式加倍还回来。所以我的建议始终是——第一版宁可拆得简单、控制得严格,也别把链路做得花里胡哨自己都说不清楚。跑通之后,你自然会知道下一个参数该往哪个方向调。