1. Agent 的瓶颈从来不是推理,而是"它不知道你的数据"
做 AI Agent 做到第四篇,我越来越确定一件事:多数 Agent 项目跑不动,不是模型不够聪明,也不是框架选得不对,而是Agent 没有一条可靠的知识获取管道。模型再强,脑子里装的也是训练时见过的公开数据;你让它处理公司内部的采购流程、售后工单、产品手册,它只能靠猜。
我见过很多从零搭 Agent 的团队,第一版都喜欢把资料直接塞进 system prompt。几千字还行,几个 PDF 一进来,上下文就爆炸了。而且 prompt 里的内容越多,模型越容易抓错重点,回答里还经常出现一本正经的编造。这就是典型的"知识供给"出了问题。
RAG(Retrieval-Augmented Generation,检索增强生成)解决的就是这个问题。它把知识的存储、查找和生成拆开:先建一个外部知识库,回答问题时先把相关资料检索出来,再交给模型组织语言。用大白话说,模型还是那个模型,但它的办公桌上多了一堆可以随时翻阅的资料。
这篇是"走进 AI Agent"系列的第四篇,重点讲清楚 RAG 作为知识获取管道的基础原理、落地步骤,以及我实测过程中踩过的坑。如果你正在规划一个 Agent 项目,或者已经在用 LangChain、Spring AI、LlamaIndex 这类框架做开发,这篇内容可以直接拿来对照参考。后续我会在这个系列里继续讲 Agent 的记忆机制、工具调用和规划能力,RAG 是这些功能的地基,值得花一整篇把它聊透。
2. 为什么知识获取不能靠"硬塞",RAG 到底解决了什么
把知识交给 Agent,最直观的办法确实是往上下文里塞。多数项目一开始都是这样干的,但当你尝试把一个真实业务的知识量塞进上下文时,三个问题会立刻浮出水面。
2.1 上下文窗口是物理限制,不是设计缺陷
模型的上下文窗口再大,也有上限。目前主流模型的窗口从几万到几十万 token 不等,听起来很多,但一份带表格和截图的 PDF 转成文本可能就有好几万字。你还没法只放一份文档——真实场景里往往有几十份产品文档、变更记录、FAQ、历史工单。上下文窗口并不是越大越好,塞得越满,模型对每部分内容的注意力越分散,关键信息反而容易被淹没。
更重要的是,成本不等人。按 token 计费的时代,每轮对话都把你全部知识库内容重复送进模型,费用会随调用量线性增长。我见过一个内部知识库接近 200MB 的项目,如果全部塞进上下文,单次调用的费用就会高到无法上线。RAG 的做法完全不同:它不是把所有知识都背上,而是每次只挑最相关的几段,开销可控,输出也更聚焦。
2.2 知识的时效性,模型天生解决不了
模型的知识有其截止日期。今天上线的新功能、下个月调整的售后政策、刚入职的专家成员——这些信息模型永远不可能在训练时知道。外部知识库则不同,文档更新后,重新跑一遍索引,Agent 立刻就能用上新知识。这意味着知识维护的闭环从"等模型升级"变成了"更新文档",主动权回到了业务团队手里。
我在实际项目中见过一个典型场景:客服 Agent 需要依据最新的退款政策回答问题,而政策每个月都会调整。用 RAG 之后,运营同事只需要把新版政策 PDF 传进知识库,索引更新完成,Agent 的回答马上跟着变。这种灵活性,是单纯靠模型微调很难实现的。
2.3 幻觉的根源往往是"无据可依"
模型产生幻觉,很多时候不是模型故意编造,而是它确实不知道该用什么作为回答的依据。给它一段检索回来的文档片段,再要求回答必须基于该片段,回答的可信度和可追溯性都会明显改善。
RAG 还带来了一个额外的好处:可溯源。用户问"退货政策是什么",Agent 不仅能回答,还能告诉你依据来自哪份文档、哪个章节。这在企业场景里十分重要——每个回答都需要有据可查,出了问题能定位到源头。我是把 RAG 理解为"给 Agent 配了一台带书签的档案柜",而不是简单地"多塞点资料进去"。
3. 一条 RAG 管道的完整解剖:从原始文档到可用答案
RAG 表面上只有"检索+生成"两步,但一套能上线的管道至少包含两条链:离线的索引构建链和在线的检索生成链。下面按这两条链拆开讲,每一步都说明为什么这样做,以及容易在哪里翻车。
3.1 离线索引:把文档变成可搜索的向量
索引构建的过程可以概括为四个动作:加载、清洗、分块、向量化入库。
加载和清洗是最容易被低估的环节。从 PDF、Word、网页里抽出来的文本往往带着页眉页脚、表格错乱、多余换行,甚至乱码。不处理干净,后面的分块和向量化都会被污染。我曾经处理过一批扫描版 PDF,直接抽取的文本段落顺序都是乱的,后来没办法,只能引入 OCR 模型,先识别再抽取,折腾了两天才把数据洗干净。
分块(Chunking)是整个索引环节里最影响检索效果的参数,没有之一。块太大,一个块里混杂多个主题,向量化后语义被稀释,检索时容易捞回一堆相关度不高的内容;块太小,单个块承载的信息不完整,召回率下降。我的经验是:通用文档从 300-500 字(token 大约 400-600 个)左右起步,带上下文的overlap(重叠)设置在 50-100 字,这样能保证语义连续,又不至于过度冗余。当然,这个数字不是死的——如果知识库以代码为主,可以按函数、类来分块;如果是长表格,可能要整块保留,否则切碎了语义就丢了。
向量化就是把文本块变成一串数字向量。这一步的关键是选 embedding 模型。常用选项包括 OpenAI 的 text-embedding-3-small 系列、开源的 BGE 系列、以及各云厂商自带的 embedding 服务。选型时要考虑三点:效果、维度、成本。效果决定检索准不准,维度影响存储和计算开销,成本则决定了大规模场景下能不能跑得起。国内场景还需要额外考虑中英文混合效果,同一个模型处理中英文的能力差异可能很大,务必拿自己的真实语料做评测,别只跑公开数据集。
向量化完成后,文本块和向量一起写入向量数据库。常见的开源选项包括 Milvus、Qdrant、Chroma、Weaviate,以及各云厂商的向量检索服务。对刚起步的项目,Chroma 和 Qdrant 上手成本低;数据量上了百万级,Milvus 这类分布式方案更合适。我在项目里目前用的 Qdrant,单机版部署简单,性能也足够。索引构建好之后并不算完——需要定期重建或增量更新,因为知识库的内容一直在变。
3.2 在线链路:查询如何变成"带着依据的回答"
在线检索的流程是:接收用户问题 → 将问题向量化 → 在向量库中做相似度检索 → 取回 top-k 相关块 →(可选)重排 → 拼进 prompt → 交给模型生成答案。
这里面有几个关键参数值得认真调:
- top_k:召回几个候选块。太小容易漏,太大容易杂。我的起点一般是 4-6 个块,按实际效果增减。
- 相似度阈值:低于阈值的块直接丢弃。这个参数能挡住"貌似相关、实际无关"的干扰项。阈值的设定没有标准答案,取决于 embedding 模型、文档领域、分块方式,建议在你自己数据集上做一次分布统计再定。
- 重排(Rerank):首次召回用向量相似度快速捞回候选,精度有限。如果想要更高精度,可以在召回后用一个交叉编码器(Cross-Encoder)对候选块精细打分,再按新顺序取前几块。这一步能显著提升最终答案质量,代价是增加一次额外的模型推理,通常几十毫秒级别,可接受。
在线链路的目的不是让回答变得更快,而是让回答更准。检索出来的块如果不对,后面的生成再漂亮是空中楼阁。
3.3 最小可用的 RAG 管道代码长什么样
理论讲完,给一段最基础的实现。我用 Python 加 Qdrant 和 LangChain 举例,这套组合足够跑通一个简单的本地知识库。
from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Qdrant from langchain_openai import ChatOpenAI from langchain.chains import RetrievalQA # 1. 加载文档 loader = TextLoader("product_manual.txt") docs = loader.load() # 2. 分块:每块 400 字,重叠 80 字 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80 ) chunks = splitter.split_documents(docs) # 3. 向量化 + 写入 Qdrant embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5") vectorstore = Qdrant.from_documents( chunks, embeddings, path="./qdrant_data", collection_name="product_kb" ) # 4. 构建检索问答链 llm = ChatOpenAI(model="gpt-4o-mini") qa = RetrievalQA.from_chain_type( llm=llm, retriever=vectorstore.as_retriever(search_kwargs={"k": 4}) ) # 5. 提问 question = "产品的保修期是多久?" print(qa.invoke(question)["result"])这一段代码可以跑,但只能算 Demo。真实项目里你还要处理权限、增量更新、日志追踪、检索失败时的兜底策略等等。从这段代码出发,后面每一步都能单独成一个优化专题。
4. 从能跑到好用,检索质量这道坎怎么过
很多团队把 RAG 跑通之后,兴冲冲上线,结果用户反馈"答非所问",然后又退回纯模型问答。问题几乎都出在同一个地方:检索质量不行。这一节专门聊我在真实项目里怎么排查和提升检索质量。
4.1 先搞清楚 hit rate 和召回链路的问题
Hit Rate(命中率)是衡量检索效果的核心指标,定义是:在标准答案对应的文本块中,有多少比例的块出现在检索结果里。直观理解就是"真正相关的资料有没有被捞回来"。
要测 hit rate,你得先有一套评测集。我和团队的做法是:从知识库里随机挑出 50-100 个文本块,为每个块人工写 1-2 个可能被用户问到的问题,形成"问题-答案来源块"的映射。然后跑检索,看每个问题召回的 top-k 块里是否包含正确的来源块。这个过程的目的是把"检索效果"从模糊感受变成可量化数字。没有评测集的 RAG 优化,基本靠感觉,没法持续迭代。
实测中,hit rate 低最常见的原因有三个:
- 分块粒度不对,正确答案被拆散到两个块里,每个块单独看都不够相关。
- 问题表述和文档表述差异太大,向量相似度匹配不上。
- embedding 模型不适合当前领域文本,比如代码类、法规类、口语化对话类各有最优模型。
排查方法也很直接:把检索结果打出来,亲眼看看召回的块到底是什么。多数情况下一看就明白问题出在哪个环节。
4.2 提高命中率的四板斧:改写、切块、混合检索、元数据过滤
Query 改写是我在一个客服项目里发现的最有效手段。用户的问题通常是口语化、带指代的:"这个怎么退?"——这里"这个"指什么,模型不一定知道。把历史对话中的人工坐席处理记录作为参考,将用户问题改写成完整的检索语句("这个怎么退"变成"退货流程和退款到账时间"),召回效果立竿见影。实现上可以用一个小的 LLM 做前置改写,也可以基于模板做规则改写,成本可控。
切块策略调整:如果一个知识点反复检索不准,优先检查它所在的块是否"主题纯净"。比如一份文档把"售后退款"和"物流配送"写在一起,向量化后语义混合,检索"退款"时,物流内容也会被捞出来。把这类混合主题的块按语义重新切分,命中率提升非常明显。
混合检索(Hybrid Search):向量检索擅长语义匹配,但对精确关键词比较钝;BM25 这类传统关键词检索则相反。把两者结果做融合(RRF 或加权合并),往往能兼顾"用户说了一个精确型号"和"用户换了一种说法表达同一个意思"两类场景。比如用户搜索"SKU-2387 的库存",关键词检索能精准定位型号,向量检索能找到"该型号缺货影响发货"这类语义相关的描述,两者互补,效果大于各自单独使用。
元数据过滤:为每个文档块打上标签(产品线、时间、作者、文档类型等),检索时先用元数据把人限定到子集,再做相似度搜索。比如用户问题涉及"华东区域价格",先过滤出华东区的文档,再检索,既快又准。元数据过滤的价值在大型知识库中尤其明显,没有它,一次检索要扫描的向量太多,噪音也多。
4.3 Rerank 不是可选项,而是精排的刚需
向量检索本质上是"粗排",它的目标是"别漏掉可能的答案",而不是"给出最精确的答案"。要精确,就得靠重排模型把召回的候选再做一次精细排序。
我用过一个实际的对比:召回 20 个块,经过 BGE-reranker 重排后取 top 5,跟直接取向量相似度 top 5 相比,最终答案的准确率大概提升了 15-20 个百分点。原因在于,交叉编码器能同时看到"问题+文档块"两者,做的是整体相关性判断,比单边向量比对精确得多。
代价是多一次模型推理,但对大多数场景而言,这个时延换准确率非常划算。重排是 RAG 管道的性价比极高的一个环节,属于"每次都在吐槽多花时间,但每次都不得不开"的那种功能。
4.4 生成阶段也要设防线:让模型"不知道就说不知道"
检索质量再高,也有失灵的时候。生成阶段需要增加约束,防止模型在找不到依据时胡编。我的做法是在 prompt 里明确规则:
请仅基于提供的参考资料回答用户问题。如果参考资料中没有相关答案,请直接回复"当前知识库中没有查询到相关信息",不要自行推断作答。
同时,开启"引用来源"输出,让模型在回答时标注内容来自哪个文本块。这样既方便用户验证答案,也方便开发者在线上监控检索链路是否正常。衡量 RAG 效果的另一个指标是Faithfulness(忠实度),说白了就是模型回答是否和检索到的依据一致。评测方法可以逐条判断回答中的关键信息点能否在参考文档中找到出处;如果找不齐,就说明生成阶段"跑偏"了。
5. Agent 跟 RAG 的融合:从一次性查资料到主动获取知识
RAG 基础能力搭好之后,Agent 和 RAG 的关系不应该停留在"每次回答前自动查一次资料"。这是最原始的用法,实际情况要复杂得多。
5.1 多轮对话里,RAG 是"带着记忆去检索"
用户的提问往往依赖上下文:"那这个呢?""上面那个人提到的方案。"简单地在每轮对话前重复检索,容易断章取义。好的做法是让 Agent 先理解整个会话的语境,把当前问题补全成一个完整检索语句,再进入知识库。
这一点说起来简单,做起来涉及一个决策:在哪个环节补全语境。可以在检索前单独用一次 LLM 调用做查询补全,也可以让 Agent 在规划阶段直接产出检索关键词。两者各有优劣,前者可控性好,后者更灵活。我倾向于在早期项目中用前者,先保证确定性,再逐步放开自由度。
5.2 Agentic RAG:让"检索"成为 Agent 的一种工具
目前的趋势是"检索"不再是一个隐式步骤,而是一个显式的工具调用:Agent 根据当前任务决定是否检索、检索哪个库、检索几次、是否需要细化问题再查。这就是Agentic RAG的核心思想。
举个实际例子:用户问"XX 型号设备在南京的库存和最近一周的故障工单情况"。传统 RAG 会一次性检索,很可能遗漏分布在多个文档里的信息。Agentic RAG 的做法是:Agent 规划出两个检索任务——先查库存系统里的型号数据,再去服务工单库查最近一周的故障记录,最后把两组信息汇总回答。必要时它还会发现"库存数据过期了",主动去查最新的入库记录。
这种结构让 Agent 具备了更强的"主动获取知识"能力,把 RAG 从"回答前附件"升级为"思考过程中的工具"。实现上并不需要特别复杂的框架,核心在于 Agent 的规划模块能否产出多个细粒度的检索动作,并把每次检索结果作为中间步骤融入上下文。
5.3 下一步:GraphRAG 与知识图谱的引入
单纯向量检索很难处理"多跳关系型问题",比如"哪家供应商提供的零件是用在延误最久的订单里的"。这类问题涉及实体间的多级关联,向量检索的平坦结构力不从心。
GraphRAG 和本体/知识图谱方案正是在这个背景下出现的。先构建实体关系网络,检索时沿着关系路径做多跳查询,再结合向量检索做语义召回。热词里出现的 ontology rag、graphrag、llm wiki 都是这个方向的具体实践。我不是说所有场景都需要上图谱,但它确实是"深层次关系问题"的最终解法。
给团队的落地建议是:先跑通标准 RAG,把 hit rate 和答案忠实度量化出来;如果发现大量问题需要跨文档关联、多跳推理,再考虑在图谱方向上投入。上来直接做 GraphRAG,复杂度会成倍增加,没有基础 RAG 的沉淀很容易失控。
6. 基于这四篇的基础,下一步该往哪走
RAG 作为知识获取管道,解决的是 Agent"知道什么"的问题。走到这里,你的 Agent 已经能够从外部知识库中获取依据、给出可溯源的回答。再往后,按我的经验,优先级应该是:
- 把评测集建起来,持续追踪 hit rate 和 Faithfulness 两个指标,让优化有据可依。
- 如果存在大量精确匹配需求,先把混合检索和元数据过滤做上,收益非常直接。
- 接入长期记忆机制,把用户偏好和历史行为存下来,让 Agent 在多次对话间保持一致性。
- 把工具调用能力逐步扩展,给 Agent 接上企业内部的 API 和数据服务,让它从"能查资料"进化成"能办事"。
我个人在实操中最大的体会是:RAG 不是一个写完就完事的组件,而是一条需要持续维护和评测的管道。知识库会变、用户问法会变、模型和 embedding 也在升级,每一次变化都可能让检索效果悄悄下滑,所以把评测体系尽早建立起来,比任何单个优化技巧都值得投入。
这套系列走到第四篇,Agent 的地基基本打完了。后面的文章我打算深入讲讲 Agent 的记忆机制、工具调用的异常处理,以及多 Agent 协作时知识共享怎么设计。下一篇见。