
1. 从“复读机”到“专家”为什么你的Agent需要RAG如果你已经跟着上一关的教程成功搭建了一个能调用工具、能规划任务的初级Agent你可能会发现一个尴尬的现实它像个“万事通”却又“一问三不知”。它能帮你查天气、算汇率但当你问它“我们公司去年Q3的销售数据报告里关于华东区新产品的用户反馈有哪些关键点”时它大概率会开始一本正经地胡说八道或者干脆告诉你“我无法回答这个问题”。这不是Agent的错而是LLM大语言模型的固有局限。它就像一个拥有海量通识知识、受过严格逻辑训练的“超级大脑”但这个大脑的记忆是静态的、泛化的。它不知道你公司的内部文档不了解你团队刚开完的会议纪要更无法实时获取最新的行业动态。当问题触及这些“私有知识”或“最新知识”时LLM就变成了一个聪明的“复读机”只能基于训练数据中的泛化模式进行猜测结果往往南辕北辙。RAGRetrieval-Augmented Generation检索增强生成技术就是为了解决这个核心痛点而生的。它本质上是一种“外挂大脑”或“知识库插件”的机制。其核心思想非常直观当用户提出一个问题时不直接让LLM凭空生成答案而是先从一个专属的、动态的知识库可以是你的文档、数据库、网页中检索出与问题最相关的几段信息“证据”或“上下文”然后将“问题”和“检索到的上下文”一起喂给LLM让它基于这些确凿的证据来组织答案。想象一下这个场景你是一位新入职的律师面对一个复杂的案件你不会只凭自己的法学教科书记忆来撰写辩护词而是会先去档案室调取相关的案卷、判例、证据清单检索然后结合这些材料和你自身的法律知识综合形成最终的辩护意见增强生成。RAG让Agent做的就是这件事。所以为Agent集成RAG能力意味着将它从一个“通才聊天机器人”升级为一个“领域专家助手”。它现在可以回答关于特定知识库的精准问题比如公司制度、产品手册、技术文档。结合最新信息进行推理比如基于今天的新闻分析市场或基于刚刚上传的会议纪要做总结。提供有据可查的答案每个回答背后都有“出处”可追溯、可验证极大提升了可信度。网络上热门的llm wiki、rag知识库、rag qa项目等概念其内核都是RAG。而agentic rag则更进一步强调将RAG作为一个核心能力模块深度集成到Agent的决策与行动循环中让Agent能主动根据任务去检索知识而不仅仅是被动问答。接下来我们就从零开始手搓这个能让你的Agent“博闻强识”的RAG模块。我们会分上下两篇上篇聚焦于最核心、最经典的RAG流水线搭建下篇则会深入高级模式与避坑指南。本篇我们先搞定基础版。2. RAG核心流水线拆解四步把知识“喂”给Agent一个最基础的RAG系统其工作流程可以清晰地分为四个步骤文档加载 - 文本分割 - 向量化与存储 - 检索与生成。我们一步步来看每个环节具体做什么以及为什么这么做。2.1 文档加载与预处理给知识“卸货”第一步是获取你的原始知识材料。这些材料可能以各种形式存在本地的一堆PDF、Word、PPT、TXT文件公司Confluence或Notion里的页面甚至是一系列网页链接。这个环节的目标是将这些不同格式的“原材料”统一转换成纯文本。这里就会用到各种文档加载器Document Loader。例如PyPDFLoader用于加载PDF文件提取其中的文字。UnstructuredFileLoader一个多功能加载器能处理多种格式依赖unstructured库。NotionDirectoryLoader如果你将Notion页面导出为文件夹可以用它来加载。WebBaseLoader用于抓取网页内容。关键细节与避坑编码问题处理TXT或CSV文件时务必注意文件编码UTF-8, GBK等错误的编码会导致乱码。扫描版PDF如果PDF是扫描图片生成的上述基于文本提取的加载器会失效。你需要先进行OCR光学字符识别处理例如使用pytesseract库或调用OCR API如百度OCR、Azure OCR将图片转为文字后再加载。这是一个常见的“坑”。文档元数据好的加载器在提取文本时会尽量保留元数据如source文件路径或URL、page页码等。这些元数据在后续的答案溯源中至关重要。加载完成后你得到的是一个或一组Document对象每个对象通常包含page_content文本内容和metadata元数据两个属性。2.2 文本分割把知识“切块”你不可能将一整本100页的产品手册作为一个整体丢给LLM。原因有三上下文长度限制主流LLM的上下文窗口如4K、8K、16K、128K Token是有限的长文档会轻易超出限制。检索精度检索时我们需要找到与问题最相关的“片段”。如果文档块太大会包含大量无关信息稀释相关性太小则可能语义不完整。生成质量LLM在生成长文本答案时容易在中间部分出现注意力分散或信息遗忘。因此我们需要将长文本分割成大小适中、语义相对完整的“块”Chunks。最常用的方法是递归字符分割和按标记分割。递归字符分割这是LangChain等框架的默认方法。它尝试优先用双换行符(\n\n)分割不行再用单换行符(\n)再不行用句号(.)以此类推直到每个块的大小接近预设值如chunk_size500。同时可以设置chunk_overlap50让相邻块之间有少量重叠避免一个句子或关键词被生生切断保持上下文的连贯性。# 示例使用LangChain的递归文本分割器 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块大约500字符 chunk_overlap50, # 块之间重叠50字符 separators[\n\n, \n, 。, , , , , 、, , ] # 中文环境下的分隔符优先级 ) docs text_splitter.split_documents(loaded_documents) # 得到分割后的Document列表按标记分割更精确的方法是按LLM的Token数来分割例如使用tiktoken库为GPT模型计数。这能确保每个块消耗的上下文窗口是可控的。分割策略的心得chunk_size没有黄金标准。对于事实性问答小块200-500字符检索更精准对于需要理解长段落逻辑的总结或分析大块1000-2000字符可能更合适。需要根据你的知识库类型和查询需求进行测试。chunk_overlap非常有用通常设置为chunk_size的10%-20%能有效缓解“边界效应”。对于高度结构化的文档如Markdown可以使用MarkdownHeaderTextSplitter按照标题层级进行分割能更好地保留文档结构。2.3 向量化与存储给知识块“建索引”这是RAG的“魔法”发生之处。我们需要一种方法能够快速地从成千上万个文本块中找到与用户问题最相关的几个。基于关键词匹配如传统搜索引擎效果有限因为它无法理解语义相似性例如“苹果公司”和“iPhone制造商”。解决方案是向量化Embedding。我们将每一个文本块通过一个嵌入模型Embedding Model转换成一个高维度的向量一组数字。这个向量就像是这个文本块在“语义空间”中的坐标。语义相近的文本其向量在空间中的距离通常用余弦相似度衡量也会很近。流程如下选择一个嵌入模型可以是OpenAI的text-embedding-ada-002也可以是开源模型如BGE、text2vec、M3E等。siglip2向量化是近期一个在多模态和文本向量化上表现突出的模型但通常我们更关注纯文本模型。使用该模型将上一步分割得到的所有文本块批量转换为向量。将这些(向量, 文本块, 元数据)组合存储到一个专门的向量数据库中。向量数据库如Milvus、Pinecone、Chroma、Qdrant、Weaviate的核心能力就是能高效地进行“向量相似性搜索”。以Chroma一个轻量级、易用的向量数据库为例from langchain.embeddings import OpenAIEmbeddings # 或用 HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 1. 初始化嵌入模型 embeddings OpenAIEmbeddings(modeltext-embedding-ada-002) # 需要设置OPENAI_API_KEY # 2. 从分割后的文档创建向量库 vectorstore Chroma.from_documents( documentsdocs, # 上一步分割后的Document列表 embeddingembeddings, persist_directory./chroma_db # 指定持久化目录 ) # 这样向量库就创建并保存到本地了关键选择与考量嵌入模型闭源API如OpenAI简单稳定但产生费用开源模型如BGE-large-zh可私有部署更适合处理中文或敏感数据。选择时需权衡效果、成本、速度。向量数据库Milvus、Pinecone适合大规模、生产级应用Chroma、FAISS更偏向索引库适合原型开发和小规模应用。spring boot milvus langchain4j这个技术栈就是Java生态中构建生产级RAG的典型组合。元数据存储确保向量数据库在存储向量时也一并存储了文本块的元数据如source, page。这是后续答案溯源的生命线。2.4 检索与生成问答的临门一脚当用户提问时RAG系统的工作流程如下问题向量化使用与建库时相同的嵌入模型将用户问题也转换为一个向量。语义检索在向量数据库中搜索与“问题向量”最相似的K个文本块向量例如K4。这就是语义检索它找到的是“意思上”最相关的段落而不是仅仅包含相同关键词的段落。上下文组装将这K个检索到的文本块连同它们的元数据作为“上下文”或“参考文档”与用户的原始问题一起组合成一个新的、更详细的提示Prompt。增强生成将这个组装好的Prompt发送给LLM如GPT-4、Claude或开源LLM指令它“基于以下上下文回答问题”。LLM会像一位查阅了资料的专家基于给定的证据生成最终答案。# 接上例进行检索问答 from langchain.chains import RetrievalQA from langchain.chat_models import ChatOpenAI # 1. 加载已存在的向量库 vectorstore Chroma(persist_directory./chroma_db, embedding_functionembeddings) # 2. 将向量库转换为一个检索器设置检索数量 retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 3. 创建检索增强生成链 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # temperature0使输出更确定 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最常用的方式将所有上下文塞进Prompt retrieverretriever, return_source_documentsTrue # 非常重要返回源文档用于溯源 ) # 4. 进行查询 query 我们公司去年Q3的销售数据报告里关于华东区新产品的用户反馈有哪些关键点 result qa_chain({query: query}) print(答案, result[result]) print(\n--- 来源 ---) for doc in result[source_documents]: print(f内容片段{doc.page_content[:200]}...) print(f来源{doc.metadata.get(source, N/A)}, 页码{doc.metadata.get(page, N/A)}) print(-*20)“Stuff”链的奥秘这里的chain_typestuff是最简单直接的方式意为“塞进去”。它把检索到的所有上下文文本直接拼接起来放在Prompt里。这种方式简单有效但受限于LLM的上下文窗口总长度。如果检索到的文本块总长度超限就会报错。对于超长文档还有map_reduce、refine等更复杂的链式处理方法我们会在下篇讨论。至此一个具备基础RAG能力的Agent核心模块就搭建完成了。它现在可以回答你知识库内的特定问题了。但这只是起点一个健壮的RAG系统还需要解决很多实际问题。3. 实战搭建用LangChain快速构建你的第一个RAG问答链理论讲完了我们动手搭一个。这里我们以处理本地PDF文件为例使用LangChain这个流行的llm框架因为它封装了大量组件能让开发变得非常快捷。我们将构建一个完整的脚本涵盖从文档加载到问答的全过程。3.1 环境准备与依赖安装首先创建一个新的Python虚拟环境并安装必要依赖。这里我们选择OpenAI Embeddings和Chroma向量库因为它们对新手最友好。# 创建并激活虚拟环境可选 python -m venv rag_env source rag_env/bin/activate # Linux/Mac # rag_env\Scripts\activate # Windows # 安装核心依赖 pip install langchain langchain-community langchain-openai chromadb pypdf # pypdf 用于读取PDF # langchain-community 包含很多社区维护的文档加载器 # langchain-openai 包含OpenAI模型接口确保你已准备好OpenAI的API Key并设置为环境变量export OPENAI_API_KEY你的-api-key # 或在代码中设置 os.environ[OPENAI_API_KEY] 你的-api-key3.2 完整代码实现与逐行解析创建一个名为basic_rag.py的文件写入以下代码import os from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain.vectorstores import Chroma from langchain.chains import RetrievalQA def build_and_query_rag(pdf_path, query, persist_dir./chroma_db_books): 构建RAG系统并查询 Args: pdf_path: PDF文件路径 query: 用户问题 persist_dir: 向量数据库存储目录 # 1. 加载文档 print(f[步骤1] 正在加载文档: {pdf_path}) loader PyPDFLoader(pdf_path) documents loader.load() print(f 共加载了 {len(documents)} 页。) # 2. 分割文本 print(f[步骤2] 正在分割文本...) text_splitter RecursiveCharacterTextSplitter( chunk_size1000, # 每个块约1000字符 chunk_overlap200, # 块间重叠200字符确保上下文连贯 separators[\n\n, \n, 。, , , , , , ] ) splits text_splitter.split_documents(documents) print(f 文档被分割成 {len(splits)} 个文本块。) # 3. 向量化与存储 print(f[步骤3] 正在生成向量并存入数据库...) embeddings OpenAIEmbeddings(modeltext-embedding-ada-002) # 检查是否已有持久化的向量库避免重复计算 if os.path.exists(persist_dir) and len(os.listdir(persist_dir)) 0: print(f 检测到已有向量库在 {persist_dir}直接加载...) vectorstore Chroma(persist_directorypersist_dir, embedding_functionembeddings) else: print(f 创建新的向量库并持久化到 {persist_dir}...) vectorstore Chroma.from_documents( documentssplits, embeddingembeddings, persist_directorypersist_dir ) # from_documents 内部会自动调用 persist() # 4. 创建检索器与问答链 print(f[步骤4] 创建检索问答链...) # 检索器从向量库中搜索相似内容 retriever vectorstore.as_retriever( search_typesimilarity, # 相似度搜索 search_kwargs{k: 4} # 返回最相似的4个块 ) # LLM选择Chat模型temperature0使输出更稳定 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 创建检索增强生成链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 简单直接的“塞入”方式 retrieverretriever, return_source_documentsTrue, # 关键返回源文档用于溯源 chain_type_kwargs{verbose: False} # 设为True可看到详细的Prompt构造 ) # 5. 进行查询 print(f[步骤5] 执行查询: \{query}\) print(- * 50) result qa_chain.invoke({query: query}) # 6. 输出结果 print(答案) print(result[result]) print(\n *50) print(答案来源前4个相关片段) for i, doc in enumerate(result[source_documents][:4], 1): # 展示前4个来源 print(f\n--- 来源 {i} ---) # 打印片段前300字符避免输出过长 snippet doc.page_content[:300].replace(\n, ) print(f内容{snippet}...) print(f元数据{doc.metadata}) print(*50) if __name__ __main__: # 替换为你的PDF文件路径 your_pdf_path ./sample_data/your_document.pdf # 你的问题 your_question 文档中主要讨论了哪几个方面的内容 # 运行 build_and_query_rag(your_pdf_path, your_question)3.3 运行与测试将上述代码中的your_pdf_path替换为你本地的一个PDF文件路径例如一份产品说明书、一篇技术文章。将your_question替换为你感兴趣的问题。在终端运行python basic_rag.py。你会看到控制台打印出每一步的进度最后给出答案以及答案所引用的文本片段及其元数据如页码。恭喜你的第一个RAG系统已经跑通了代码解析与注意事项持久化Chroma.from_documents会计算向量并存储到指定目录persist_directory。下次运行时如果检测到该目录已存在数据就会直接加载避免重复计算嵌入向量这能节省大量时间和API费用。检索器配置search_typesimilarity是默认的余弦相似度搜索。search_kwargs{k: 4}控制返回多少个相关块。k值需要权衡太小可能信息不全太大可能引入噪声并消耗更多Token。return_source_documentsTrue这个参数至关重要它让链返回检索到的原始文档对象使我们能够实现答案溯源这是生产级RAG的必备功能能极大增强可信度。chain_typestuff这是我们使用的简单链。它的Prompt模板大致是“请基于以下上下文回答问题{context}。问题{question}”。如果检索到的所有context总长度超过LLM的上下文窗口就会报错。对于长文档需要更高级的策略。这个基础版本已经可以实现很多功能。但当你把它用于真实场景时会发现一堆“坑”在等着你。比如检索出来的内容好像不怎么相关答案有时会胡编乱造这些正是RAG系统需要优化的核心问题。4. 基础RAG的典型问题与初步优化策略搭建起来容易但要让它好用就必须直面以下几个最常见的问题。这里我们先给出基础的优化方向更深入的方案将在下篇探讨。4.1 问题一检索不到相关内容召回率低现象用户问了一个知识库里明明有的问题但系统检索出来的文本块完全不相关导致LLM只能瞎猜或说“不知道”。根因分析文本分割不当块太大包含了太多无关信息导致向量“焦点”模糊或者块太小割裂了完整的语义单元。嵌入模型不匹配使用的嵌入模型如纯英文模型对中文语义理解不佳或者模型本身能力较弱。问题表述差异用户的问题表述和知识库中的原文表述差异很大例如同义词、缩写、口语化 vs 书面语导致向量相似度不高。初步优化策略调整分割策略尝试不同的chunk_size和chunk_overlap。对于技术文档可以尝试按章节或子标题分割使用MarkdownHeaderTextSplitter。升级嵌入模型对于中文场景强烈建议使用优秀的开源中文嵌入模型如BGE-large-zh、text2vec-large-chinese或M3E。它们对中文语义的捕捉能力远强于通用的多语言模型。查询扩展/重写在检索前对原始查询进行简单的扩展。例如利用LLM生成原始问题的几个同义句或关键词然后用这些扩展后的查询一起去检索取结果的并集。这能提高召回率。4.2 问题二检索到内容但答案不准精确率低或幻觉现象系统检索到了相关段落但LLM生成的答案要么细节错误要么干脆基于检索到的内容“脑补”出不存在的信息即“幻觉”。根因分析上下文噪声检索到的多个文本块中可能混入了少量相关但主体不相关的信息或者包含了相互矛盾的信息干扰了LLM的判断。LLM的“自由发挥”倾向即使给出了上下文如果Prompt指令不够强LLM仍可能倾向于依赖其内部知识或进行推断而不是严格忠于上下文。上下文过长或位置不佳当检索到的上下文很长时LLM可能会忽略中间或末尾部分的信息“中间丢失”问题。或者关键信息没有被放在Prompt中显眼的位置。初步优化策略优化Prompt指令使用更强硬的指令。不要只说“基于以下上下文”可以改为“严格且仅基于以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说‘根据提供的信息无法回答此问题’。不要使用你自身已有的知识。”重排序Re-ranking在初步检索例如召回10个块之后使用一个更精细的、专门用于重排序的模型如BGE-reranker对这10个块进行相关性重排只保留最Top如3个的块送给LLM。这能有效提升送入上下文的整体质量。引用与溯源就像我们的示例代码所做的那样强制要求LLM在答案中引用来源如【来源1】并在输出时展示源文本。这不仅能帮助用户验证也能在一定程度上约束LLM。4.3 问题三无法处理复杂或多跳问题现象用户的问题需要结合知识库中多个分散的段落进行推理才能回答例如“文档A中提到的项目和文档B中哪个部门的预算有关”但基础RAG的简单检索可能只找到其中一个相关段落。根因分析基础RAG是“单跳”检索即一次检索一次生成。它缺乏一个“推理-再检索”的循环机制。初步优化思路这需要更高级的agentic rag模式。让Agent或一个复杂的链来主导流程先理解问题规划需要检索哪些信息进行首次检索根据初步结果提出新的子问题再进行检索最后综合所有信息生成答案。这涉及到agent框架与编排如LangChain的AgentExecutor或AutoGen等我们会在后续关卡中深入。4.4 一个简单的优化示例增强Prompt与中文嵌入模型让我们对之前的代码做两处关键优化1) 使用更强的Prompt模板2) 切换到本地部署的中文嵌入模型。首先安装中文嵌入模型text2vecpip install sentence-transformers然后修改代码中的嵌入模型和Prompt部分# ... 前面的导入和文档加载、分割步骤保持不变 ... from langchain.embeddings import HuggingFaceEmbeddings from langchain.prompts import PromptTemplate # 3. 向量化与存储 - 使用中文模型 print(f[步骤3] 正在生成向量并存入数据库...) # 使用 text2vec 中文模型 model_name GanymedeNil/text2vec-large-chinese embeddings HuggingFaceEmbeddings( model_namemodel_name, model_kwargs{device: cpu}, # 如果有GPU可改为 cuda encode_kwargs{normalize_embeddings: True} # 归一化通常有利于相似度计算 ) # ... Chroma 向量库创建/加载部分保持不变 ... # 4. 创建检索器与问答链 - 使用自定义Prompt print(f[步骤4] 创建检索问答链...) retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 定义更强硬的Prompt模板 prompt_template 请严格根据以下提供的上下文信息来回答问题。如果你无法从上下文中找到答案请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 上下文 {context} 问题{question} 请基于上下文给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 创建链时传入自定义Prompt qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{ prompt: PROMPT, # 使用自定义Prompt verbose: False } ) # ... 后续查询和输出部分保持不变 ...这两处改动能显著提升基础RAG在中文场景下的回答质量和可控性。使用本地嵌入模型避免了API调用也更好地适配了中文语义。5. 将RAG模块接入你的Agent从独立工具到核心能力到目前为止我们构建的还是一个独立的RAG问答链。如何让它成为你Agent的一项能力呢关键在于让Agent学会在需要时“调用”这个RAG工具。在LangChain的Agent框架中这通常通过创建一个Tool来实现。Tool是一个可以被Agent调用的功能单元它有一个名称、描述和一个执行函数。我们可以将上面构建的RAG问答链包装成一个Toolfrom langchain.agents import Tool, initialize_agent, AgentType # 假设我们已经有了上面创建好的 qa_chain # 将其包装成一个工具 rag_tool Tool( name公司知识库查询, funclambda q: qa_chain.invoke({query: q})[result], # 工具执行函数输入问题返回答案字符串 description当需要查询公司内部文档、产品手册、制度规范等非公开知识时使用此工具。输入一个具体的问题。 ) # 假设你还有其他工具比如计算器、搜索引擎工具 tools [rag_tool, ...] # 将RAG工具和其他工具放在一起 # 创建Agent agent initialize_agent( toolstools, llmllm, # 一个LLM作为Agent的大脑 agentAgentType.ZERO_SHOT_REACT_DESCRIPTION, # 一种常用的Agent类型 verboseTrue, # 打印Agent的思考过程便于调试 handle_parsing_errorsTrue # 处理解析错误 ) # 现在你可以用自然语言向Agent提问了 # Agent会自己判断“这个问题需要查内部知识库吗需要的话就调用‘公司知识库查询’工具。” result agent.run(请帮我查一下公司最新的差旅报销标准是什么) print(result)在这个架构下你的Agent就具备了“知识库查询”的能力。当用户的问题涉及私有知识时Agent通过思考由LLM驱动会决定调用公司知识库查询这个工具工具内部执行我们搭建的RAG流程并将结果返回给AgentAgent再组织最终的语言回复给用户。这就实现了RAG与Agent的初步集成。更高级的集成方式比如让RAG检索的结果作为Agent规划下一步行动的依据agentic rag或者实现多跳检索就需要更复杂的agent框架和流程编排了。至此你已经掌握了搭建一个基础RAG系统的全部核心步骤并了解了如何将其集成到Agent中。我们解决了从无到有的问题也看到了它当前版本的局限性。在下篇中我们将深入探讨高级RAG技术包括如何用重排序提升检索质量如何用HyDE等技术优化查询如何应对长文档的上下文窗口限制以及多模态RAG、图数据库增强等前沿思路。同时我们会分享更多在真实项目中踩过的坑和调试经验让你的Agent真正从“可用”变得“好用”。