RAG基础原理与工程实践:从索引、检索到生成,搭建Agent知识获取管道 如果你已经亲手搭过一两个Agent项目大概率会遇到这种场景模型能力看着挺强聊起通用知识头头是道但只要问到你自己业务里的具体细节——某个产品的参数、某份合同里的条款、某个内部流程的步骤——它就明显开始“编”了。这背后的原因并不复杂大模型的知识来自训练语料而训练语料里没有你们公司的私有文档。所以你会发现Agent能不能真正落地拼的往往不是模型多聪明而是它有没有一套可靠的知识获取管道。RAGRetrieval-Augmented Generation检索增强生成就是这条管道的地基。这篇是“走进AI Agent”系列的第四篇我打算把RAG基础这件事彻底讲透。不绕弯子直接说清楚几个问题为什么Agent离不开RAGRAG管道里索引、检索、生成三个环节分别在解决什么事基础RAG和目前很热的Agentic RAG之间差了什么以及你自己动手搭一套本地知识库RAG时哪些坑是避不开的。无论你是刚接触Agent开发还是已经在用LangChain、LlamaIndex、Spring AI这类框架做集成这文章都值得往下看。1. 知识获取管道在Agent体系里的位置1.1 Agent不是“会说话”而是“能办事”很多人一开始理解Agent会把它等同于“一个更会聊天的ChatGPT”这是很常见的误区。Agent的核心价值在于能完成任务而完成任务的前提是它得拥有足够多的“背景知识”。举个例子。你让Agent去处理客服工单它需要知道你们的产品有哪些型号每个型号的保修政策是什么退换货流程走哪个系统遇到投诉升级规则是什么。这些知识散落在企业文档、数据库、ERP系统里模型本身并不知道。没有RAG之前你只有几条路可走把文档全部塞进Prompt里上下文窗口很快就爆了做微调成本高知识更新一次就要重新训一次或者干脆让Agent瞎猜这在业务场景里就是事故。RAG的思路说白了就是“开卷考试”。模型依然是那个模型但我们在它回答问题之前先去外部知识库检索相关资料把最相关的几段内容取回来跟原始问题拼在一起再交给模型生成答案。这样一来模型不需要记住你的文档它只需要学会“读”你给它的参考资料。这也是为什么我说RAG是Agent的知识获取管道——它负责把外部世界的知识变成模型能够理解并使用的上下文。1.2 RAG不是银弹它与微调、长上下文的分工聊RAG之前先分清它和另外两个常见方案的区别否则你很容易在技术选型上跑偏。方案解决什么问题适合场景主要成本/局限RAG让模型“知道”私有/实时知识企业文档问答、知识库客服、私有数据交互需要维护索引检索效果决定上限微调让模型“学会”某种行为/风格/输出格式让模型按特定话术回答、适应特定领域表达训练成本高知识更新需要重新训练长上下文一次性塞入更多原始材料少量长文档分析单次任务成本随上下文线性上涨中间内容容易被忽略我自己见过最典型的误用是有人为了“省事”把几十页的产品手册全塞进上下文指望模型能准确记住第五页的某个参数。实测下来长上下文虽然能容纳这么多字但模型的注意力会分散尤其是埋在中间位置的关键信息经常瞄不准。RAG的思路完全不同它不是让模型读完全部文档而是先通过检索锁定最相关的两三百行再让模型精读。一个是“把图书馆全搬来做开卷考试”一个是“先查目录找到关键几页”后者显然更靠谱。当然RAG不是万能的。如果模型本身的推理能力不够即使你把正确答案摆在它面前它也可能给出不准确的回答。这也是为什么现在大家都在讲“Agent RAG”的组合——RAG负责把材料递上来Agent负责决定怎么用这些材料完成任务。1.3 RAG管道的整体闭环RAG的完整链路可以压缩成三个环节索引Indexing、检索Retrieval、生成Generation。索引是把原始文档切成小块、向量化、写入向量库检索是根据用户的查询从向量库里找出最相关的片段生成是把检索到的片段作为上下文交给大模型产出最终答案。听起来简单但每个环节都藏着不少细节。很多入门项目“看起来能跑”一到真实业务场景就露馅就是因为只把最简链路搭通了没深究每个环节的参数、策略和边界。接下来的内容我就把这三个环节逐个拆开讲重点说清楚每一步在干什么、为什么要这么干、有什么坑。2. RAG管道核心拆解索引、检索、生成2.1 索引阶段文档切分与向量化索引是一切的起点也最容易被低估。很多初学者直接拿一整个PDF丢进向量库然后发现检索出来的东西要么不完整要么语义错乱。问题几乎都出在切分Chunking策略上。为什么必须切分因为一方面embedding模型有输入长度上限比如常见的是512或1024个token另一方面如果一块文本里包含的信息太杂它的向量表示会变得“平均化”跟谁都有点像跟谁都不够像。这就好比你想在图书馆里找一本讲咖啡拉花的书结果系统把一个涵盖咖啡豆、烘焙、拉花、开店运营的全套教材当成一条记录相似度自然被摊薄了。切分策略上我强调几个关键参数块大小Chunk Size按效果和成本平衡一般500~800字是常见区间中文场景我习惯按500字左右切。太短语义不完整太长噪音多。重叠Overlap相邻块之间保留50~100字的重复内容避免一句关键信息正好被拦腰切断导致两边都搜不到。切分边界优先按标题、段落、句子等自然边界切而不是硬按字符数切。用RecursiveCharacterTextSplitter这类递归切分器它会先尝试按段落边界切切不出来再降级按句子切。代码上大概长这样from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ] ) docs splitter.split_text(raw_text)切完之后是向量化。向量化就是把每一块文本变成一个高维向量让语义相近的文本在向量空间里距离更近。这一步的模型选择很关键。英文场景用OpenAI的text-embedding-3-small或3-large很好用但如果你的文档以中文为主又希望本地化部署我比较推荐BGE系列尤其是bge-m3。它在中文语义匹配上表现稳还支持多语言并且可以输出稀疏向量配合混合检索。嵌入维度通常是1024单条文本向量化成本也不高CPU就能跑接入方便。向量化的输出需要写入向量数据库。选型上本地小项目用Chroma或者FAISS就够了数据量大、要求高可用再考虑Milvus、Qdrant或者企业里常见的Elasticsearch向量索引。不管用哪个都要记得在写入时给每个片段带上metadata比如来源文件名、章节标题、页码、更新时间。这些字段在后续做权限过滤、时间过滤时会救命。2.2 检索阶段向量检索、混合检索与重排序检索是RAG管道的“腰部”也是影响最终效果最直接的环节。检索召回的内容不准后面模型再厉害也是白搭。最基础的检索方式就是向量相似度检索把用户的问题向量化在向量库里找余弦距离最近的Top-K个片段。这个方案实现简单但有两个天然缺陷。第一它只考虑语义相似不考虑关键词命中。比如用户问“怎么退款”如果文档里写的是“退货流程”语义上相关向量检索通常也能捞回来但如果文档里写的是“按揭撤销”向量间距可能就很远直接漏掉。第二向量检索对短文本、专有名词、编号类信息不敏感类似“文档编号CW-2024-001”这种纯向量很容易匹配不准。解决办法是引入混合检索向量检索跑一路关键词检索BM25跑一路两条路线的结果做加权合并。你可以把它理解成“一个靠语义理解一个靠字面匹配两个都投一票”。在本地知识库、ERP产品检索这类场景里混合检索对比纯向量检索召回效果经常能提升一截。检索完之后还有一个经常被忽略的动作重排序Rerank。向量检索的Top-K只是“候选”并不是“按相关性精确排序”的结果更何况混合检索还把两路结果拼接在一起顺序就更乱了。重排序的思路是用一个精排模型通常是一个cross-encoder比如bge-reranker-v2-m3把候选片段逐条和用户问题做深度相关性打分再按分数重新排序只把真正相关的前几名交给大模型。一句话总结召回是“宁可多捞不可漏掉”重排是“只留最准的不浪费上下文”。有的框架里这一步可以通过langchain的ContextualCompressionRetrieval实现也可以在检索到自己拼。这一块有个实操细节Top-K和重排后的数量怎么定我个人习惯召回阶段Top-K取20~30重排后留3~5条。数量太少信息不够数量太多Prompt里噪音太多模型容易被无关内容带偏。这个参数不要照抄别人的最好拿自己的数据多试试找到一个“答案能召回、又不至于让模型看花眼”的区间。2.3 生成阶段把资料变成可信答案检索完成后剩下的就是生成。但生成这一步也不是简单地把资料往Prompt里一拼就完事。Prompt的结构直接影响最后答案是否可靠。我的习惯是明确告诉模型三件事你是一名基于企业知识库的问答助手只能依据提供的参考资料回答用户问题。如果参考资料中没有答案请直接回答“根据现有资料无法回答”不要编造。回答时尽量标注信息来源比如“根据《XX产品手册》第3章”。为什么要这么严格要求因为大模型有个本能——遇到不会的问题它会倾向于“编一个合理的答案”而不是“承认自己不知道”。这在闲聊场景问题不大但在企业应用里就是灾难。我在实战里见过太多次模型一本正经地编造合同条款、编造产品参数。要治这个问题Prompt约束只是第一道防线更可靠的手段是引用溯源——让模型在输出答案时同时标出它参考了哪些片段。一旦我们能把答案和原始文档关联起来用户就能自己核对正确率也更容易被信任。生成模型这块如果你本地部署可以考虑用Ollama跑Qwen2.5 7B/14B这类中文模型配合本地向量库整套系统完全离线可跑。如果是云端调用GPT-4o、Claude、GLM等都没问题反而在指令遵循上表现更好。多说一句流式输出。知识库问答通常问题较长、答案也长直接从接口拿全量结果会在等待时间上劝退用户。配置流式输出Streaming让用户看到答案一点点生成体感会好很多。LangChain里用streamTrue或者StreamingStdOutCallbackHandler几行代码的事能省很多抱怨。3. 从基础RAG到Agentic RAG3.1 基础RAG的四个局限“检索一次、生成一次”的基础RAG在简单问答场景里够用但一旦面对真实业务的多轮对话、复杂查询问题就暴露出来了。第一个局限是“一问一答”的线性流程。用户连续问“A方案和B方案有什么区别”基础RAG拿这句话直接去检索可能召回到一堆讲A方案的材料B方案的材料因为没出现在同一片段里全被漏掉。第二个局限是没有上下文意识。用户说“那第二个方案的报价呢”——这里的“第二个方案”指的是上文里的B但基础RAG并不知道它只会把这句短查询拿去检索结果搜出来一堆无关内容。第三个局限是缺乏自查能力。检索结果不好它也不会意识到更不会换个关键词重新检索。第四个局限是面对复杂问题时单次检索很难覆盖全部分析维度。3.2 Agentic RAG把RAG变成Agent的技能Agentic RAG这几年热度一直很高本质上就是把“检索”这个动作从一次简单的函数调用升级成Agent自己可以编排、决策、迭代的技能。区别在哪里基础RAG是“用户问——系统查——模型答”三步走Agentic RAG则是“用户问——Agent分析问题——决定查询策略——执行多次检索——评估结果是否足够——不够再换个方式查——整理答案”。这里的关键词是“决策”和“迭代”。举几个具体的Agentic RAG模式查询改写Query RewritingAgent把用户的原话拆解或改写成语义更清晰的检索词。比如用户问“他们的保修政策怎么样”原问题里的“他们”指代不明Agent先改写为“XX品牌的保修政策”再去做检索。多跳检索Multi-Hop一个问题需要多步才能查清楚。比如“哪个客户投诉最多对应的产品线是什么”这一步需要先查到投诉数据TOP1的客户再拿这个客户名去查关联产品。多跳检索要求Agent把检索结果作为中间变量继续发起下一轮检索。路由Routing根据用户问题类型决定走RAG还是走普通对话或者决定查哪个子知识库。比如查财务制度走财务库查技术参数走技术库。反思/自我校验Reflection/Self-CritiqueAgent拿到第一轮检索结果后先判断“这些资料足够回答用户的问题吗”不够就重新检索甚至换检索词、换切分策略。这些能力在实现上并不神秘可以租用LangChain或LlamaIndex里的Agent机制把“用检索工具”注册成一个Tool然后在ReAct循环里让Agent反复调用。复杂一点儿的场景也可以自己用LangGraph编排一个状态机节点有“查询改写”“检索”“评估”“回答”边上有条件判断让Agent在节点之间跳转。3.3 RAG as a Service企业落地的新形态如果你关注最近一两年的企业级Java开发会发现Spring AI、LangChain4j这些框架都在把RAG能力“服务化”。比如LangChain4j里已经把文档加载器、切分器、嵌入模型、向量存储、检索器这些环节抽象成一套APISpring Boot应用里只需要几行配置就能对外提供一个带RAG问答能力的接口。这在Java生态里尤其吃香因为很多企业的核心系统ERP、CRM都是Java写的能直接在Spring Cloud体系里把知识库问答作为一个微服务暴露出去比单独搭一套Python服务再去对接轻量得多。另个值得留意的方向是“RAG as a Service”思维。与其每个业务线都各自搭一套RAG管道不如把索引、检索、权限过滤这些能力沉淀成通用服务业务方只需要提交文档拿到一个可查询的API。这样做的优势很明显统一管理向量库、统一升级模型、统一控制权限。我见过一些团队已经参考AgentScope 2.0的思路把RAG做成平台能力业务部门只需要关心“文档传上来问答能用上”不需要关心底层的切分、向量、检索细节。4. 从0到1搭一套本地知识库RAG4.1 技术选型与准备理论说再多不如动手搭一次。我就以一套完全本地运行的方案为例带你走一遍从文档到问答的完整流程。这也是我日常做原型验证时的标配组合环节选型理由嵌入模型BAAI/bge-m3中文效果好支持多语言可本地跑向量库Chroma轻量、零配置适合单机原型生成模型Qwen2.5-7B-Instruct通过Ollama本地部署中文对话能力强可离线运行框架LangChain组件齐全适合快速串联各环节评测RAGAS自动化评估检索命中率和生成忠实性安装依赖、拉模型这些基础工作就不浪费篇幅了。如果你用的是Ollama执行ollama run qwen2.5:7b把模型拉下来再确保bge-m3能通过HuggingFaceEmbeddings加载环境就算准备好了。4.2 构建索引的完整流程先准备一批测试文档。最好别用纯英文的真实业务里中文文档的切分难度明显更高。我建议你拿自己手边的操作手册、产品说明、会议纪要练手。索引代码核心步骤如下from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载文档 loader DirectoryLoader(./data, glob**/*.md, loader_clsTextLoader) documents loader.load() # 2. 切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80 ) chunks splitter.split_documents(documents) # 3. 向量化 embedding HuggingFaceEmbeddings(model_nameBAAI/bge-m3) # 4. 写入向量库 vectorstore Chroma.from_documents( documentschunks, embeddingembedding, persist_directory./chroma_db )这里有几个值得留意的细节。切分后最好先打印几个chunk看一眼确认边界合理、没有把表格或列表截断。另外Chroma的persist_directory参数决定了数据落到本地磁盘的路径下次启动只要用Chroma.load加载同一个目录就不需要重新索引了——这个细节很容易被忽略很多人每次启动都重新跑一遍全量索引白白浪费大量时间。如果文档里有非常长的段落比如几页纸的说明我会在切分前先做一次粗清洗去掉无意义的换行、合并孤立的标题、统一列表格式。这一步不写代码实现你可以自己写个正则清洗函数或者直接人工处理文档开头结尾的无用信息。别小看这一步脏数据进索引脏结果出问答后面所有环节都会替它买单。4.3 检索与生成联调索引建好之后就可以写问答接口了。这里我推荐一个比较实用的链路向量检索 关键词检索混合 重排序这一步能在体验和效果之间取得很好平衡。先做一个混合检索from langchain_community.retrievers import BM25Retriever from langchain.retrievers import EnsembleRetriever import jieba # 中文场景做BM25前最好先用jieba分词否则检索效果很差 bm25_retriever BM25Retriever.from_documents(chunks) bm25_retriever.k 20 # 召回20条 # 向量检索也是召回20条 vector_retriever vectorstore.as_retriever(search_kwargs{k: 20}) ensemble_retriever EnsembleRetriever( retrievers[bm25_retriever, vector_retriever], weights[0.3, 0.7] # 权重可以按自己数据调 )再加重排序。重排序模型用bge-reranker-v2-m3把混合召回的40条候选压缩到4条from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker reranker CrossEncoderReranker( modelHuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-v2-m3), top_n4 ) compression_retriever ContextualCompressionRetriever( base_compressorreranker, base_retrieverensemble_retriever )最后是生成。用Ollama提供的OpenAI兼容接口接入Qwen模型Prompt按前面说的规则约束好from langchain_community.chat_models import ChatOllama from langchain.prompts import ChatPromptTemplate llm ChatOllama(modelqwen2.5:7b, base_urlhttp://localhost:11434) prompt ChatPromptTemplate.from_template( 请仅根据以下参考资料回答用户问题。如果参考资料中没有明确信息请回答“根据现有资料无法回答”。回答时尽量标注资料来源。 参考资料 {context} 用户问题{question} ) # 组装成一个检索问答链 from langchain.chains import RetrievalQA qa_chain RetrievalQA.from_chain_type( llmllm, retrievercompression_retriever, chain_typestuff, chain_type_kwargs{prompt: prompt} ) result qa_chain.invoke({query: 如何申请设备维修}) print(result[result])跑通之后你可以试着拿几个不在文档里的问题去问看模型有没有老实说“不知道”。如果模型开始编了优先检查Prompt约束其次再看是不是检索到的上下文里恰好有一句容易误导的内容。另外别忘了打开流式输出真上用户体验差别很大。4.4 用指标而不是感觉来评估效果很多团队做完RAG之后只凭“感觉”判断效果这不是好习惯。RAG需要量化评估两个指标特别实用Hit Rate命中率/召回率在测试问答对中正确答案对应的文档片段是否被成功召回。召回不到正确答案后面全是空谈。Faithfulness忠实性模型的最终回答是否严格基于检索到的资料有没有编造无关信息。手工构造50~100条问答对覆盖文档里的不同章节然后可以用RAGAS框架跑一遍自动评测。它会帮你算Hit Rate、Answer Relevancy、Faithfulness这些分数。我的经验是Hit Rate低于0.7就说明检索环节需要优化Faithfulness低于0.8就要加强Prompt约束或换成指令遵循更强的模型。等这两项达标了再去谈那些花哨的Agent能力顺序不能反。5. 实战踩坑与排查技巧5.1 召回效果差从“搜不到”到“搜不准”最常见的槽点是用户的问题明明在文档里写了但检索就是捞不回来。排查思路我建议按这个顺序来先看切分是否合理。可能答案被拦腰切断了也可能答案所在的chunk太长、向量语义被稀释了。把命中的chunk打印出来肉眼判断它是否语义完整。再看检索策略。纯向量检索漏关键词、纯BM25不理解语义混合检索能覆盖大多数场景。权重按比例调整比如业务偏长文本就加大向量权重偏代码、编号就加大BM25权重。最后看Query本身。中文口语提问和文档书面表达差异很大用户说“东西坏了怎么办”文档里写的是“故障处理流程”。在检索之前加一个“查询改写”步骤让模型把用户问题转换成几个可能的检索词效果立竿见影。5.2 回答幻觉模型不老实怎么办即使用了RAG模型依然可能给出跟资料矛盾的答案。原因不外乎三种资料本身不足、模型指令遵循弱、Prompt里没有给出足够强的约束。我的处理经验有三层。第一层在Prompt里用强制语言约束要求模型“只能依据参考资料回答资料中没有的信息一律说不知道”。第二层要求模型在输出答案的同时输出引用片段编号这样一旦出错我们可以顺着引用回溯问题。第三层在系统层面加“兜底逻辑”——如果检索回来的内容相关度评分低于某阈值直接返回“未找到匹配资料”而不让模型硬答。宁可不答不要错答。5.3 多轮对话中的上下文污染这是对话式RAG最容易翻车的地方。用户在第二轮问“那退款呢”这时如果你把第一轮的“退货流程”和当前问题拼在一起去检索、生成模型很容易混淆概念。处理方式有两种。一种是把多轮对话历史压缩成一行摘要作为检索的上下文——比如“用户上一个问题在问A产品保修当前在问退款流程”。另一种是只把当前意图明确的问题送去检索历史对话则单独放在生成阶段作为上下文。LangChain的Memory或ConversationalRetrievalChain里有现成的机制但记得做压力测试别让历史对话越长越影响结果。5.4 常见问题速查表问题现象可能原因排查动作检索不到相关内容切分太粗/太细、Query改写不到位检查chunk边界尝试查询改写答案正确但找不到引用生成阶段丢失了来源信息Prompt要求必须标注来源metadata中保留原文位置模型回答与资料矛盾指令遵循不足、上下文混入无关片段增强Prompt约束减少Top-N数量知识库更新后答案没变索引未重建或使用了缓存确认向量库持久化被覆盖清缓存相同问题每次答案不同模型温度过高降低temperature到0.1~0.2之间混合检索后效果反而变差BM25权重过高或未分词中文场景BM25务必配jieba分词调整权重6. 我的一点实操心得与扩展思路最后分享几个我在实际项目里的体会不算总结就当经验传递。第一个体会是别急着上GraphRAG、Ontology RAG这些进阶概念。我看过太多团队知识库还没建好就琢磨要不要在文档之间抽实体关系建图谱。GraphRAG确实能解决跨文档关联问题但它的成本和复杂度高一个量级。我建议先把基础RAG的切分、混合检索、重排序、评测这四件事做到位如果发现确实需要回答“两个实体之间怎么联系”这类问题再考虑引入图谱。从热词看Ontology RAG、GraphRAG会一直热下去但那是锦上添花的东西不是地基。第二个体会是嵌入模型的领域适配性价比比想象中高。通用Embedding模型对专业术语的理解经常有偏差比如法律、医疗、制造领域里的专有名词。如果条件允许用一批业务语料做Embedding模型的领域微调或者至少多加几种检索策略技术栈上的收益会非常明显。很多时候你以为“换个大模型就好”实际换了个Embedding模型反而立竿见影。第三也是最想强调的一点RAG的上限是数据治理。索引之前的数据清洗、标注、结构化管理决定了整个管道的天花板。我不止一次看到团队把几十份格式不一的Word、PDF直接丢进管道然后花大量时间在检索和Prompt上找补。与其在后端打补丁不如在前端把源数据准备好统一格式、去除冗余、补全元数据。这些工作不性感但回报率极高。还有一个小技巧写在这里收尾如果你的知识库需要支持在线更新一定要在索引阶段就带上“更新时间”和“来源编号”并在检索时按这个过滤。否则你清理旧文档的时候会发现旧的chunk还在向量库里待着继续污染检索结果。提前做隔离后面能少很多麻烦。希望这套RAG基础流程能帮你在Agent开发路上少走点弯路。下一步你可以在这条管道上继续加查询改写、多跳检索和评估闭环慢慢把它升级成真正的Agentic RAG。