AI Agent系列写到第四篇,我觉得是时候聊聊那个最容易被低估、却最能决定Agent靠不靠谱的环节——知识获取管道。官方叫法你可能已经听过无数遍:RAG,Retrieval-Augmented Generation,检索增强生成。热搜里那些 rag知识库、rag实战、本地知识库、rag检索、rag框架,说的其实都是同一件事:怎么让大模型在回答问题时,先查到资料、再组织答案,而不是全靠脑子里那点训练记忆硬编。
这文章的目标很直接,把RAG从概念到落地讲透。你会搞清楚:为什么Agent必须有一条知识获取管道,RAG的完整链路里每个环节都在干什么,怎么用一段可运行的代码搭出最小可用的知识问答Agent,以及真实项目中那些文档上不会写的坑。适合准备从0到1搭建AI Agent的开发者、正在选型的企业技术负责人,以及所有被“看似能聊、一测就翻车”的对话机器人折磨过的朋友。
1. 为什么Agent需要一条“知识获取管道”?
1.1 LLM的天生短板与RAG的出现
大语言模型本质上是一个“根据上文预测下文”的统计模型。它确实能把大量知识压缩进参数里,但你得承认它有几个硬伤:知识有截止日期,训练语料里没有你们公司的内部流程,遇到超出预训练分布的问题时会一本正经地编答案。这个“一本正经编答案”就是所谓的幻觉。很多团队在测试Agent时发现,模型面对陌生问题常常给出逻辑完整但完全错误的结论,不是因为模型不够聪明,而是它没有获取外部知识的通道。
RAG的思路其实特别朴素:模型不知道,那就让它去查。提问进来之后,先从外部知识库中检索出相关的候选片段,再把候选片段拼进Prompt,让模型基于这些片段来回答。这个“先检索、后生成”的流程,就是一条知识获取管道。它把模型从“必须什么都记得”解放成“知道去哪里找”,也让每一次回答都有据可查。对于Agent来说,这条管道是运行时唯一的“事实来源”,也是目前低成本让Agent拥有领域知识的主流方式。
1.2 从“靠参数背知识”到“按需查资料”
打个比方。过去的对话机器人像是招了一个记忆力很好但只参加过入职培训的员工,你问什么它都凭印象答,答错了它自己也不知道。而RAG给你的Agent配置了一个档案室,员工回答前先去档案室查资料,查到什么就答什么,查不到就老实说不知道。档案室里的资料你可以随时更新,员工的知识立刻同步;不需要重新培训,也不需要换人。
这就是RAG在工程上最大的价值——知识解耦。模型参数负责语言能力和推理能力,外部索引负责具体事实和领域细节。你换一批文档,Agent掌握的知识就跟着变;你更新一篇公告,检索结果立刻反映出来。相比之下,靠微调更新知识不仅要花钱花时间,还可能破坏模型原有的能力。你可以把RAG理解成给Agent外挂了一块可插拔的记忆硬盘,而不是每次把“记忆”焊死在模型里重做。
1.3 RAG在Agent里的角色,以及Agentic RAG是什么
在单轮问答里,RAG是一次性的:检索、注入、生成。但放在AI Agent的框架里,这个流程不再是一条直线,而是可以被反复调用的工具。Agent会规划任务、拆解步骤、决定什么时候需要查资料、查完资料做什么,这时RAG就成了Agent众多工具中的一项能力。你给Agent接一个“知识检索工具”,它自己判断该不该调用、要不要换关键词再查一次,这就是Agentic RAG的雏形。
Agentic RAG和普通RAG的区别在于控制权。普通RAG由用户问题直接触发一次检索,结果固定;Agentic RAG让Agent自主决定检索策略,比如先查宽泛主题、再根据初步答案查具体细节,或者检索多轮直到信息足够。走得再远一些,还有GraphRAG、Ontology RAG这类把实体关系也纳入检索的变种。但不管名词怎么变,底座还是那条管道:文档入库、索引构建、召回、注入、生成。把这个底座打牢了,后面所有高级玩法才有支撑。
2. RAG完整链路拆解:从文档到上下文
2.1 数据准备:加载、清洗、切分
管道的第一站是让非结构化的文档变成可以检索的单元。现在的框架都提供了现成的加载器:PDF、Word、Markdown、HTML、纯文本、数据库表都可以往里灌。但“能读”和“能读好”是两码事。PDF经常有表格和多栏排版,直接抽出来文字顺序就乱了;扫描件还得先做OCR;HTML里的导航栏和页脚会混进正文。这些脏数据会直接污染后面的切分和向量化,所以清洗是RAG里最脏最累、却最重要的一步。
清洗完就要做切分(chunking)。切分的目的是把一个长文档切成若干小段,每段作为独立的检索单元。常见的做法是按固定字符长度切(比如512字符一段),或者用递归切分器按段落、句子边界回退,尽量保持语义完整。切太短,上下文不够,模型看不到完整逻辑;切太长,向量计算容易模糊,检索精度下降,还浪费Prompt容量。我自己的经验是:先按结构切(标题、段落、列表),再设定chunk_size在300到800字符之间,overlap设成10%到20%,别让一句话被硬生生砍成两半。表格数据更要小心,最好把表头信息拼到每个单元格片段上,否则检索到“12345”你根本不知道它代表什么。
2.2 Embedding与向量索引选型
切分完的文本要变成向量,才能和用户问题做相似度计算。这一步的核心是Embedding模型。中英文混合场景建议直接选对中文支持好的模型,比如BGE系列或者多语言模型,英文为主用OpenAI的text-embedding-3-small也够用。选模型时别只看榜单分数,还要看维度、最大输入长度、推理成本。维度越高信息越丰富,但存储和计算开销也更大;最大输入长度如果小于你的chunk长度,就需要先截断再做向量化。实际项目里,Embedding模型往往比后面的LLM更能影响检索效果。
向量存到哪里,就是向量数据库的活。简单原型可以用FAISS,它在本地跑、内存充裕、毫秒级返回;要做生产级服务,首选Qdrant、Milvus或pgvector。Qdrant对RAG场景支持友好,Milvus适合海量数据和高并发,pgvector则是Postgres用户的最省事方案——不用额外维护一套存储。你在热搜里看到的“本地知识库”“本地RAG”基本就是FAISS或Qdrant加本地Embedding模型跑起来的。索引类型方面,HNSW是主流,参数里的M和ef_construction影响召回速度和内存的平衡,默认值通常够用,不必一上来就调。
2.3 检索:向量检索、混合检索、重排
向量检索的原理是计算用户问题和文档向量的余弦相似度或内积,取Top K。它擅长解决“语义相近但字面不同”的问题,比如搜“有没有退款政策”能匹配到“支持7天无理由退货”。但它也有弱点:纯向量检索对专有名词、精确编号、少见缩写不敏感,搜“API-2024-01”可能因为语义向量差得远而召回失败。这时候需要混合检索,把传统的BM25关键词检索和向量检索结合起来,各取所长。Elasticsearch里有标准的混合检索实现,Qdrant从较新版本开始也原生支持稀疏向量和稠密向量的加权组合。
检索完之后通常还要接一个重排(Rerank)环节。向量检索只负责“粗筛”Top K,重排模型负责“精排”这几十条候选里哪条最相关。很多团队忽略这一步,我建议至少在知识库超过一万条文档时把它加上。重排模型(如BGE-Reranker)会把用户问题和每个候选片段成对打分,比向量相似度更准,但速度慢,所以只对粗筛结果跑。结构上就是:先混合检索取回50到100条,再用重排模型取前5到10条进Prompt。这个“粗召回+精重排”的设计,是提升RAG命中率最立竿见影的招数。
2.4 注入:把证据拼进Prompt
检索回来的片段最终要进Prompt,这一步看似简单,坑其实不少。最基本的结构是:System消息里写清楚规则“你是一个客服助手,只根据提供的参考资料回答问题,不要使用先验知识,如果资料里没有相关内容就直接说不知道”;用户消息里把问题写在最后,把检索到的文档片段按顺序放在问题前面。这里有一个容易被忽略的细节:片段之间要有清晰的分隔标记,并给每个片段编号,比如“文档1、文档2”,这样模型在回答时可以明确说“根据文档3的信息”。你还可以要求模型在回答末尾附上参考来源。
注入时还要注意“上下文污染”问题。如果检索结果里混入大量无关片段,模型会被带偏,所以宁可只保留高置信度的3到5条,也不要贪多。Prompt里检索内容的格式也会影响生成质量,我习惯用XML标签把证据包起来,让模型明确知道哪些是引用材料、哪些是待回答的问题。另一方面,对话类Agent要注意历史消息的裁剪:把之前几轮问答放进去即可,别把整段历史都塞进上下文,否则检索片段很容易被历史噪声淹没,影响回答准确度。
3. 真实项目实操:搭一个最小可用的知识问答Agent
3.1 技术栈选型
理论讲了这么多,咱们直接动手。我用Python来搭一个能跑在本地的最小RAG Agent,技术栈选得尽量轻:LangChain做流程编排,FAISS做向量存储,HuggingFace上用BGE-M3做Embedding(如果网络受限或者不想拉大模型,也可以换成OpenAI的Embedding接口),生成部分用一个在线的大模型API,或者本地基于Ollama跑Qwen等开源模型。这里选LangChain不是因为它是性能最优解,而是它把加载、切分、检索、Prompt拼装都抽象好了,适合快速验证链路。等你确认了整体可行,再替换掉其中的组件也不迟。
需要说明,真实项目里LangChain并非必需品。你完全可以直接调用Embedding模型API、自己写切分逻辑、自己算向量距离、自己拼Prompt。我见过不少生产系统就是完全手写的。但作为一篇偏入门的文章,用LangChain能帮助你少写一堆胶水代码,把注意力集中在理解RAG本身。下面这份代码我用的是最直白的方式,故意不叠加太多框架魔法。
3.2 完整代码流程:文档入库和查询
先安装依赖:langchain、langchain-community、faiss-cpu、bge-m3(这里用sentence-transformers加载)。入库流程分四步:读文档、切块、向量化、存入FAISS。查询流程分三步:向量检索、重排(可选)、拼Prompt送LLM。为了控制篇幅,我用一个简单的文本文件作为示例,生产环境换成你自己的文档加载器就行。
from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载文档 loader = TextLoader("./knowledge_base.txt", encoding="utf-8") documents = loader.load() # 2. 切分 splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=64, separators=["\n\n", "\n", "。", ",", " "], ) chunks = splitter.split_documents(documents) print(f"切分得到 {len(chunks)} 个片段") # 3. Embedding + 向量化入库 embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-m3", encode_kwargs={"normalize_embeddings": True}, ) vectorstore = FAISS.from_documents(chunks, embeddings) # 4. 保存和加载索引(生产环境建议持久化) vectorstore.save_local("./faiss_index") # vectorstore = FAISS.load_local("./faiss_index", embeddings, allow_dangerous_deserialization=True)查询侧代码同样不复杂。先从向量库按相似度检索前20条候选,然后交给重排模型精排,最后拼Prompt给LLM。这里我用了FlagEmbedding的Reranker做示范,如果你不想引入额外模型,也可以跳过重排,直接取Top 5。
from flagembedding import FlagReranker # 从向量库检索候选 query = "你们的退款政策是什么?" docs = vectorstore.similarity_search_with_score(query, k=20) # 重排精排 reranker = FlagReranker("BAAI/bge-reranker-v2-m3") pairs = [(query, doc.page_content) for doc, _score in docs] scores = reranker.compute_score(pairs) ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True) # 取前5条,拼进Prompt context = "\n\n".join( f"[文档{i+1}] {doc.page_content}" for i, (doc, _s) in enumerate(ranked[:5]) ) prompt = f"""请根据以下参考资料回答问题。 参考资料: {context} 问题:{query} 要求: - 只能依据参考资料回答 - 如果资料中没有相关内容,直接回答“资料中未找到相关信息” - 回答后附上引用的文档编号"""把prompt发给大模型,就完成了一次RAG问答。第一次跑通后你会发现,代码本身平平无奇,真正的难点全在数据切分、检索质量这些看不见的地方。
3.3 交互闭环:结果与溯源
最小管道跑通后,我更推荐你先别急着加花活,把“溯源”功能加上。所谓溯源,就是在Answer旁边返回命中的文档片段来源,比如文件名、章节标题、原始段落。这不仅是用户体验问题,更是RAG调试的基础设施:一旦回答错了,你能立刻看到模型是基于哪一段证据编的,问题到底出在检索漏了、切分错了还是Prompt规则没立好。实现上无非是把检索到的metadata一并放进返回结构里,前端展示一个“参考来源”折叠框。
如果做的是对话式Agent,还要处理历史消息。最简单的做法是维护一个消息列表,每次提问前把最近两三轮历史拼进Prompt,然后调用一次RAG。注意,历史消息里的用户旧问题不应该触发检索、也不应该拼进当轮的检索要求。把历史输入单独放在对话历史区块,检索只针对“当前这一问”,效果最干净。这块处理不好,很快会出现“带着上一轮的问题检索出奇怪内容”的毛病。我早期踩过这个坑,后来定了条规矩:检索表达式永远只基于本轮用户问题,历史仅作为LLM生成时的参考背景。
4. 让RAG在Agent里更好用的几个关键经验
4.1 检索质量的度量:不只是“感觉”
RAG上线前一定要评估,不然你会被“偶尔答得好”骗过去。最基础的两个指标:Hit Rate(命中率)和MRR(平均倒数排名)。Hit Rate衡量的是:对于测试集里的每个问题,正确答案的片段是否出现在检索返回的前K条里。MRR更进一步,看正确答案排在第几位,排第一得1分,排第五得0.2分。这两个指标只评估检索环节,不关心生成结果,用来快速验证索引和切分策略是否合理。
更完整的评估还要看生成答案本身。你可以小规模人工评分,也可以拿GPT/其他强模型当裁判,对比标准答案和RAG生成答案的相关性、忠实度、完整性。注意,忠实度在RAG场景里比准确率更关键——就算事实没错,模型如果脱离了检索片段自由发挥,也是失败的。我的习惯是把检索评估和生成评估分开跑:先调检索,Hit Rate低于80%就别急着优化Prompt;检索稳定了再看生成,这时的问题往往出在Prompt规则或上下文结构上。
4.2 切分策略和元数据:RAG的两大隐形变量
很多团队把时间花在调模型上,实际上切分策略才是RAG效果的最大变量。我做过一个对比,同样一份产品手册,固定512字符切分的Hit Rate只有42%,改成按章节和语义段落切分后直接涨到71%。原因是固定切分把很多表格、列表拦腰截断,导致检索到的片段信息残缺。后来我根据文档类型分了三套切分方案:规范制度类按标题层级切,FAQ类一条一问答一个片段,产品参数类把表格转为“键值对描述”再切。切分是一个反复试错的过程,没有万能参数。
元数据也一样重要。给每个片段打上文档名、章节、页码、更新时间、业务线标签,这些信息至少有三个用途:一是作为过滤条件,比如“只检索2024年之后的制度”,在查询时用filter把范围收窄,比单纯靠向量相似度精准得多;二是作为Prompt里的来源信息,增强回答的可信度;三是做权限控制,不同角色的用户只能检索到对应标签的文档。很多RAG项目上线后被人诟病“老资料和公告混在一起、答非所问”,根子就在元数据设计缺失上。
4.3 多路检索、GraphRAG与Ontology RAG的边界
当知识库结构复杂到一定程度,单库单路的RAG就不够用了。实际Agent里,我常常按业务域拆多个索引库:制度库、产品库、工单库各一个Agent工具,由Agent在规划时选择调哪个。这就是你听到的“RAG as a Tool”“skill和RAG结合”的现实场景。多路检索的好处是隔离噪音、能按场景做不同的切分和Embedding配置,坏处是增加了编排复杂度,需要Agent具备准确的“路由选库”能力。
GraphRAG和Ontology RAG是这两年讨论很多的方向。它们的共同点是,在纯文本片段之外,把实体和关系也建进索引里。GraphRAG用知识图谱表达实体之间的连接,擅长回答“A和B之间有什么关联”这类多跳问题;Ontology RAG更进一步,依赖本体定义概念间的语义关系。这些方案确实能解决纯向量RAG在关系推理上的短板,但代价是构建和运维成本高。我的态度是:知识库文档数量在几千篇以内,先老老实实把混合检索+重排调好;等开始频繁遇到“跨文档多跳问题”或“同义词实体合并问题”,再考虑引入图谱,不要为了概念热度给自己上重量。有意思的是,你看到的“ontology rag”“llm wiki 本体rag”热搜,本质也是在聊同一种能力——只是大家都在找最适合自己的落地方式。
4.4 常见问题排查速查表
下面这张表是我在实际项目中反复踩过的坑,按症状、可能原因、排查顺序整理出来,你可以直接拿去当排查手册用。
| 症状 | 可能原因 | 排查和解决思路 |
|---|---|---|
| 回答明显错误,但检索片段看起来有相关 | 切分破坏了关键信息 | 打印命中的chunk原文,看语义是否完整;尝试调小或调大chunk_size |
| 检索结果和问题毫无关联 | Embedding模型和文档语言/领域不匹配 | 换中文/领域专项Embedding模型;检查是否要做基础预处理(如去HTML标签) |
| 真正常用的知识永远排在后面 | 检索返回了太多相似片段,正确答案被埋没 | 增加重排环节;提高Top K再精排;调整向量检索的相似度阈值 |
| 模型完全不按资料回答、自己乱编 | Prompt规则没约束住 | 在System消息中强调只能使用资料,禁用先验知识;必要时用few-shot示范 |
| 更新了文档,回答还是旧内容 | 索引没同步或缓存 | 检查入库流程是否增量更新;向量库中是否存在旧chunk残留;清理缓存 |
| 多个知识库时总选错库 | Agent路由策略太简单 | 给每个工具写清晰的功能描述;先让Agent基于用户问题做意图分类再到库 |
| 对话越聊越乱,后几轮回答质量下降 | 历史消息混入了检索上下文 | 隔离历史消息和检索证据;限制历史轮数;检索只基于当轮问题 |
除了表格里的问题,还有两个容易被忽略的运维要点:一是定期评估索引版本的回归情况,不要把更新EST的知识库直接上线而不跑一遍测试集;二是密切关注Token成本和延迟,RAG链路长了之后,检索+重排+Prompt可能让单次请求从几百毫秒变成几秒,需要结合业务场景决定是否用缓存、是否降级为纯向量检索。
5. 写在最后:把RAG当成Agent的“外挂记忆”
回到开头那句话,RAG的本质是Agent的知识获取管道,但它不是接入一个数据库那么简单。它是一整套数据处理流程,是检索策略和Prompt工程的结合体,也是Agent能否稳定输出事实性回答的关键底座。我见过不少团队痴迷于最新的Agent框架、花哨的工具编排,结果地基没打牢,上线后用户问第一个业务问题就答非所问。与其这样,不如把时间花在清洗文档、设计切分、建立评估集这三件事上。
我个人在后来的项目里体会到一件事:RAG的调试过程极其细致,但每一次优化都是有迹可循的。你改了切分策略,Hit Rate会告诉你有没有进步;你加了重排,答案的忠实度肉眼可见提升;你完善了元数据,用户反馈立刻改善。这种“能感知到改善”的反馈正反馈,是其他模型优化手段很少给到的。
最后再分享一个小技巧:如果团队还在纠结要不要上GraphRAG或重排模型,先挑几十条真实用户问题,把你的RAG系统跑一遍,把失败case按“检索失败”和“生成失败”分类。哪个环节占比高,就优化哪个环节。技术的取舍永远服务于管道本身的质量,而不是热搜上的名词。
这一篇把RAG的地基讲清楚了。下一篇我会沿着知识获取管道继续往上搭,聊聊多路检索怎么跟Agent工具编排结合,以及如何让Agent学会自己判断“该不该查、查到了没有”这里,回归到真正的Agentic RAG实战。你自己动手搭过一遍之后,再看那些概念会轻松很多。祝跑通。