首页 / 资讯中心 / 文章详情

Agent知识库实战:从零搭建RAG索引、检索与生成全流程

Agent知识库实战:从零搭建RAG索引、检索与生成全流程 ★ FEATURED ARTICLE
这个系列写到第四篇前面聊了 Agent 的基本结构、记忆系统怎么设计、工具调用怎么做今天终于要碰一个很多人绕不过去的坎Agent 的知识从哪里来。先说背景。常见 Agent 做聪明但一到企业内部场景就开始露馅——模型训练完的那一刻它的知识就冻结了。你问它产品手册里最新的参数、公司制度里某条审批流程、某个系统的操作文档它要么一本正经编答案要么就说不知道。RAGRetrieval-Augmented Generation检索增强生成是目前最主流也是最务实的外部知识方案它把大模型不掌握的私域信息切碎、向量化、存起来等到用户提问时先检索相关内容再带着查到的材料去生成回答。说白了就是给 Agent 做一个“先查资料再回答”的管道。本篇是 RAG 的基础篇适合正在从 0 到 1 搭 Agent、想把手头知识库接进系统的朋友。不需要你有很强的机器学习背景但需要你写过一点 Python了解大模型 API 的基本用法。我会从“RAG 在 Agent 体系里到底站在哪个位置”讲起再把索引、检索、生成三个环节逐个拆开最后用 LangChain 搭一个能跑起来的最小 RAG并把实际操作中踩过的坑一并列出来。1. RAG 在 Agent 体系中到底扮演什么角色1.1 为什么 Agent 不能只靠大模型自己的知识很多人第一次接触大模型应用时会产生一个错觉模型什么都知道。实际上大模型的训练数据有截止日期训练过程也决定了它只能记住训练语料里出现过的信息。你给它喂一个 PDF 让它现场学习和它能不能理解无关而是它的上下文窗口就那么大旧的东西很快被新的覆盖聊几句就“失忆”了。这不是模型能力的问题而是架构问题。模型是个“推理引擎”不是“硬盘”。在一套完整的 Agent 系统里推理、规划、工具调用、记忆、知识获取各司其职。知识获取管道Knowledge Acquisition Pipeline要解决的需求很明确模型回答问题时需要事实信息但这些事实信息在模型参数里不存在得有一个快速、可靠、可更新的渠道把“外部事实”传递给“推理引擎”。RAG 就是这个渠道的工程化产物。在原生的 LLM 应用里RAG 可以只是一个提问增强工具但放到 Agent 体系里它的角色更下沉——它往往是 Agent 的“工具之一”。Agent 决定要不要查知识库、查什么、查到什么程度然后把检索到的资料交给生成链路做总结。这也是为什么最近社区里一直在聊 Agentic RAG核心不再是“用户问了就查”而是“Agent 自主判断该不该查如何把查到的信息用于下一步决策”。要理解 Agentic RAG得先把基础知识这一层打牢这也是本篇存在的意义。1.2 为什么不用微调来注入知识这里必须把 RAG 和微调Fine-tuning的选择逻辑说清楚。很多人一上来就问我有一堆私域文档是不是直接微调模型更快其实大多数场景下微调不是最优解原因是它解决的方向和“知识获取”根本不一致。微调整改的是模型的参数分布让它学到某种行为风格、指令遵循方式或者领域表达习惯。但微调之后模型仍然无法做到“精确记住”某条具体的、随时会变的记录——比如某个客户的合同编号、某产品今天的最新库存。就算你反复训练模型也可能把相近的记录混淆这是一种典型的记忆不可控。更现实的是微调成本高要数据标注、要算力、要反复实验而业务文档一旦更新你又得重新训练。RAG 的设计目标恰恰相反知识存在外部、更新走管道、生成只负责“读”。文档变了重新跑一遍索引就完成更新模型本身岿然不动。这带来了几个实战中的关键好处第一检索到的内容可追溯用户能知道回答依据是哪一份文档第二权限可以控制不同 Agent 角色的知识颗粒度不一样第三回答过程的幻觉风险更容易通过提示词约束来压制。当然 RAG 并不完美它和微调其实是互补关系不是一个替代另一个——如果你发现模型总是答非所问、表达风格完全不对那才需要考虑微调如果你只是缺资料、缺上下文先上 RAG。1.3 RAG 的工作流程图书馆管理员模式理解 RAG 最简单的方式是想象图书馆里的管理员。第一步是“上架”新书到馆管理员给每本书贴标签、编索引放到对应书架第二步是“查书”读者来问“有没有讲推荐系统冷启动的书”管理员快速定位到几本第三步是“整理答复”管理员把这几本书翻到相关页把内容摘抄组织成一段话交给读者。对应到技术实现RAG 就是三个环节Ingestion索引、Retrieval检索、Generation生成。知识入库阶段把源文档清洗、切分成小块chunk用 embedding 模型转成向量写入向量数据库查询阶段用户问题也转成向量到库里做相似度搜索取回最相关的若干片段生成阶段把片段作为上下文塞进 prompt让大模型基于这些材料回答。这个模型的精妙之处在于每一步都能替换或优化互不干扰。检索可以换成混合检索生成可以换成更强的模型索引可以用不同的切分策略。做 Agent 应用最忌讳把 RAG 当成一个黑盒接进去理解了这层“管道”思维才能在大模型基础能力波动时准确判断是哪一环出了问题。2. 核心细节解析索引、检索、生成三个环节的工程要点2.1 知识入库切分策略是 RAG 成败的第一道关口知识入库的输入往往是杂乱的PDF、Word、Markdown、网页抓取、数据库导出的表格。第一步是清洗。我个人强烈建议在切分之前先人工或程序化处理掉页眉页脚、重复段落、多余空白必要时用 PyMuPDF 或 unstructured 这类工具把 PDF 表格结构识别出来。很多人犯的错是文档一拿到就切结果向量库里存了一堆噪声后面检索质量怎么修都修不回来。切分chunking是最容易踩坑、也最影响效果的一环。最简单的策略是固定长度切分比如每 500 个字符一刀但这会拦腰截断语义完整的段落。工程上更推荐递归结构切分比如 LangChain 的 RecursiveCharacterTextSplitter它先按段落分隔符切再按句子、最后按字符兜底尽量保住语义边界。chunk_size 和 chunk_overlap 这两个参数要配合调chunk_size 决定每块多大chunk_overlap 决定相邻块之间保留多少重叠文字目的是避免某段关键信息恰好被切在边界处丢失。选 chunk_size 没有标准答案要看下游生成模型对上下文长度的容忍度以及文档自身的特点。我的经验是中文通用文档在 500 到 800 字左右比较平衡overlap 设成 chunk_size 的 10% 到 20%。太大检索时会把不相关内容拉进来稀释精度太小知识碎片化检索找回的片段缺少上下文回答就会显得断断续续。实际操作中最好做一个“迷你实验集”用 20 到 30 个典型问题测试不同参数下哪个组合能召回相关内容再用真实用户问题做人工验收。这一步做好了后面的工作会轻松不少。2.2 向量化与向量数据库选型不是越贵越好切分完的文本片段要变成向量需要选 embedding 模型。市面上选择很多OpenAI 的 text-embedding-3 系列效果稳定阿里百炼的 text-embedding-v4 的中文效果也不错开源里 BAAI/bge-m3、bge-small-zh-v1.5 都有大量中文社区验证。选择的关键是“领域匹配度”通用模型对法律、医疗、编程等垂直领域的效果不一定好有条件的话应该用小批真实文档测试相似度排序是否符合直觉。这里必须强调一个常见误区embedding 的维度不是越高越好。向量维度过高存储开销大、检索速度慢而且低维的 bge-small 在某些中文场景下表现并不输给大模型级别的 embedding。对于刚起步的项目优先用开源小模型挡住成本等指标真上不去了再换商业 API 做对比。相似度度量也就是老生常谈——一般用余弦相似度cosine就够了中文文本用 dot product 的效果差别不大别在此纠结太久。向量数据库选型可以按规模来定个人项目和小型 DemoChroma、FAISS 都很合适有一定规模、需要持久化和过滤功能Milvus、Qdrant 更稳如果团队已经在用 Elasticsearch它的 knn 搜索也能直接当向量库用不用额外引入新组件。选型不是越重型越好一个几百条文档的项目用 Milvus 纯粹是给自己找运维负担。记住一个原则RAG 的核心不是向量数据库而是“检索得好不好”。基础设施只是载体别本末倒置。2.3 检索环节向量检索只是起点检索环节同样有讲究。标准的做法是把用户 query 也做 embedding然后用向量数据库做 Top-K 相似度检索K 通常在 3 到 6 之间。但这套“原生向量检索”在实际业务里远远不够。问题经常出在“用词不对”上用户说的是“我上个月订单退款怎么还没到”文档里写的是“售后时效退款将在申请后 3 个工作日内原路退回”两者的字面差异很大向量相似度不高结果就是召回不到。对付这类问题有两招。一招是 query 改写query rewrite在检索前先用大模型把用户口语化的问题转成几组适合检索的关键词甚至扩充成多个角度的子问题分别去检索再合并结果这叫多路召回multi-query。另一招是混合检索向量检索负责语义相关性BM25 这类传统关键词检索负责精确匹配术语两边召回再融合效果往往明显提升。我在做本地 ERP 产品检索时体会特别深——产品型号“SE-K-300”这种写法向量模型很容易被无关内容干扰但 BM25 对精确字符串的命中非常准混合之后命中率立刻上了一个台阶。再往上走一步是重排rerank。向量库 Top-100 先粗排召回候选然后用更强的排序模型比如 bge-reranker 系列对候选片段和 query 做细粒度相关性打分把最相关的 3 到 5 个放到最前面。这种“粗召回精重排”的结构在业务中几乎是标配。有人一上来就把 K 设成几十个块全扔给大模型以为上下文多就是好结果反而把模型注意力带偏了。正确的思路是召回宽一点重排选准一点最终进入生成环节的上下文越精炼越好。2.4 生成环节prompt 里的硬约束生成环节不再需要堆技术但细节决定回答质量。核心是把检索到的片段按顺序拼接成上下文塞进 prompt然后明确告诉模型“以下内容来自知识库请基于这些内容回答如果内容不足以回答问题请直接说不知道不要编造。”这行话不是可有可无的——没有这个约束大模型看到相关知识只会顺手“扩展”细节编一个像模像样的答案这在知识问答里是不可接受的。另一个实战技巧是给每个片段标注来源。比如在拼接上下文时在每个片段前加一行“文档xxx.docx第 3 页”之类的来源信息并要求模型在回答末尾附上引用。这样用户能自查排查问题时也能快速定位是哪一份文档、哪个段落导致了错误内容。经验上“可溯源”这一设计带来的信任价值往往比模型能力本身还高。最后一个注意点是上下文长度控制。检索回来的片段拼接后动辄两三千字一次问答可能没问题但 Agent 场景里这只是一次工具调用的结果后面可能还有多轮对话、多步工具操作。上下文窗口是有限的塞得越多Agent 留给规划和推理的空间就越小。所以在生成链路里要养成“精确引用而不全文摘抄”的习惯——只把真正相关的片段传给模型而不是把一整块知识库 dump 进去。3. 实操走一遍用 LangChain 搭一个最小 RAG3.1 技术选型与运行环境这一节我用 LangChain 演示因为它链条清晰、适合教学而且社区资料多。实际生产项目里你也可以用 LlamaIndex、Spring AI或者直接裸写调用 embedding API 和向量库原理完全相同。为了这次演示我选的是纯本地最小可行链路本地文档、本地 embedding 模型、Chroma 持久化存储、OpenAI 兼容的模型 API 做生成。环境准备很简单装几个库就行pip install langchain langchain-community langchain-chroma chromadb sentence-transformers pypdf我这边用的 Python 3.10 跑下来没有版本冲突。如果你用的是更新版本的 LangChain注意 langchain-community 里的某些模块名可能会改这里我以 0.2 系列的 API 为准。embedding 模型用的是 BAAI/bge-small-zh-v1.5512 维中文效果稳定资源占用也不高本地跑完全没压力。生成模型我暂时用 OpenAI 兼容接口你换成 DeepSeek、通义或者其他模型的 base_url 就能适配。DeepSeek 的中文效果在同等价位里很能打日常实验足够了。3.2 从文档到向量库三步完成索引先准备一份测试文档比如一个假的《员工考勤管理制度》里面要有请假流程、加班规则、罚款细则等内容。路径放在本地然后用递归切分器入库。注意我故意在切分参数上用了比较常规但不特别激进的值方便你观察实际效果后再调from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_chroma import Chroma # 1. 加载 PDF也可以换成 DirectoryLoader 批量加载 loader PyPDFLoader(./employee_attendance.pdf) documents loader.load() # 2. 切分成 chunk text_splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap80, separators[\n\n, \n, 。, , , , , ], length_functionlen, ) chunks text_splitter.split_documents(documents) print(f共切出 {len(chunks)} 个文本块) # 3. 向量化并持久化存入 Chroma首次运行会下载 bge-small 模型 embedding_model HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) vector_store Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db, )切分器里 separators 的作用是定义逐级切割的优先级。中文场景里我排过经验顺序段落 换行 句号 感叹号问号 分号 空格 兜底字符。“。”这些中文标点能比较自然地保住句子完整性如果你不写“。”很多句子会被从中间截断成无意义的碎片。这也是很多人说切分效果差的一大原因——默认的 separators 对英文友好对中文不一定好。persist_directory 参数让向量库落盘到本地目录第二次运行就不需要重新构建。这是个很省心的小细节项目重启后直接 Chroma 加载已有目录省掉重复向量化的时间。我用一个 30 页的管理制度 PDF 实测整个索引过程不到 15 秒embedding 模型下载一次后本地加载更快。3.3 检索器和完整问答链知识库建好后核心就是检索器和问答链的组合。下面这段是完整可运行的最小链路from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 1. 加载已有向量库 vector_store Chroma( persist_directory./chroma_db, embedding_functionHuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5), ) # 2. 构造检索器 retriever vector_store.as_retriever(search_kwargs{k: 4}) # 3. 定义提示词 system_prompt 你是企业内部知识助手。请基于以下知识库片段回答问题。 规则 1. 只能使用提供的片段作为事实依据。 2. 如果片段中没有足够信息明确回答“知识库中未找到相关内容”不要编造。 3. 回答末尾标注依据的片段来源。 context {context} /context prompt ChatPromptTemplate.from_messages([ (system, system_prompt), (human, {input}), ]) # 4. 构建问答链 llm ChatOpenAI( modeldeepseek-chat, openai_api_basehttps://api.deepseek.com/v1, openai_api_keyYOUR_API_KEY, temperature0.2, ) combine_docs_chain create_stuff_documents_chain(llm, prompt) rag_chain create_retrieval_chain(retriever, combine_docs_chain) # 5. 提问 answer rag_chain.invoke({input: 请假的审批流程是什么}) print(answer[answer]) for doc in answer[context]: print(---来源---, doc.metadata.get(source))几个参数值得说清楚。k4 表示从知识库召回 4 个片段交给生成模型。太小可能漏内容太大可能让回答变得啰嗦4 到 6 之间通常是入门好起点。temperature0.2 把生成随机性压得非常低知识问答这种场景切忌高温度否则同一个问题两次回答可能细节都对不上。create_stuff_documents_chain 的含义是把所有召回片段“堆”到一个 prompt 里一次性让模型生成。这在数据量小时最省事。数据量大以后你就要考虑 map-reduce 或 refine 这类别的链式策略先逐段总结再合成最终答案不过那就是进阶优化的范畴了基础篇先跑通这条链路再说。3.4 跑起来之后先验证这几件事第一次跑通最小 RAG 后别急着接业务先用一个小型评估集做冒烟测试。我习惯准备 10 到 20 个问题覆盖三类能从文档直接回答的事实类问题、需要综合多个段落的问题、文档里根本没有的问题。逐条观察回答是否准确、是否引用了正确来源、遇到未知问题有没有老实说不清楚。这一步能快速暴露 pipeline 的问题。比如我发现“综合多段落”类问题通常失败在召回环节——单条 query 只能召回某一段落另一段相关知识点根本没进入上下文。解决办法就是前面提到的 multi-query 扩展把一个问题拆成几个不同角度的子问题分别召回最后合并去重再加进 context。此类的验证方法越早做越好等部署上线再补评估成本就高了。4. 常见问题与排查技巧实录4.1 检索命中率低先看切分和数据质量再换模型大多数人遇到“回答胡说”第一反应是换更强的模型但实际上 80% 的问题出在检索返回的内容不够相关。排查思路应该是先单独把检索器拎出来看用户问题进来后Top-4 召回片段到底是什么。这一步用 vector_store.similarity_search_with_score(query) 就能看到别直接看最终答案先看召回。如果召回相关的文档完全没出现大概率三件事embedding 模型和领域不匹配chunk 切分把关键信息打散了文档本身术语表达和用户问题差异过大。按顺序试调 chunk_overlap、换领域更贴近的 embedding比如代码文档用 code embedding法律文本用中文法律语料微调过的模型、引入混合检索最后考虑 rerank。这里特别提醒查询和文档的“称呼不一致”是中文场景的高发问题。你文档里写“考勤异常”用户问的是“迟到怎么办”字面距离很远纯向量检索很容易漏混合检索结合关键词后会有明显改善。4.2 回答幻觉问题可能不在模型而在 prompt 和上下文如果召回没问题上下文里有正确内容但模型还是答偏了这时候才轮到生成环节。第一步是检查 prompt 里有没有“只依据上下文回答”的硬约束第二步检查上下文中正确内容和杂质内容的占比——如果 Top-4 里只有一条和问题相关其他三条是无关联片段模型会被带偏。这不是模型笨而是信息噪声太大。我在实践中常用的一个小技巧是把召回的片段按相似度分数排序后只选分数超过阈值的片段同时把“不可回答”的兜底回答写进 system prompt比如“如果上下文不足回复根据现有知识库无法确认”。这其实是一种省事的幻觉抑制手段。真正严格的场景你还可以让模型在生成时同步输出引用索引再程序化校验每个断言都能对应到片段文本这项技术叫 groundedness checking基础篇先了解有这个方向后面再展开。4.3 和 Agent 协作时的典型坑工具描述与上下文管理RAG 接入 Agent 后头号问题不是知识库本身而是 Agent 对工具的调用很随意。我希望它只在用户问内部制度时才调用知识库工具结果它连“帮我写一句宣传语”也要先去查一遍动不动把几百个 token 的上下文填满。排查下来往往发现是 Agent 工具描述写得太模糊比如就叫“knowledge_search”模型根本没法判断适用场景。正确写法是把工具描述写得像个规则引擎“适用于查询公司内部管理制度、产品说明、操作手册。当问题涉及公共常识时请直接回答不要调用此工具。”这样模型才能做准确路由。另一个问题是多轮对话里的记忆错位Agent 上一轮查过某文档下一轮可能不查了直接拿旧上下文作答导致信息过期。这种情况下建议在每次调用 RAG 工具后让 Agent 把“检索到的事实摘要”重新写回工作记忆并删除旧的检索内容不要跨轮保留原始片段。细节很多但核心只有一条RAG 是 Agent 的外部知识来源不是它的永久记忆不能替代记忆系统。4.4 效果优化清单从基础到进阶的实战顺序最后给一份我自己的优化顺序清单遇到问题可以按这个顺序逐项尝试而不是一上来就换架构优化项操作内容适用场景调整切分参数chunk_size 在 400~800 之间调overlap 调 10%~20%召回内容不完整或碎片化数据清洗去掉页眉页脚、表格乱码、重复内容检索结果噪声大多路召回把 query 拆成多个子问题分别检索单条 query 难以覆盖多维度混合检索向量检索 BM25 关键词检索融合精确术语和口语化表达混合的场景重排用 bge-reranker 对粗排结果精排召回分布宽、上下文只能放少量片段引用约束prompt 强制要求标注来源回答可信度要求高权限过滤按用户角色过滤可检索的命名空间多角色系统一条知识库不能全员共享这份清单适用于绝大多数中小型项目。等你把这些都做了一遍RAG 本身的剩余优化空间就很有限了这时再考虑向 Agentic RAG 演进让 Agent 动态决定检索策略、子问题拆解、多轮检索甚至检索后反思。这正好是下一篇可以聊的话题。最后说点个人感受。我从纯 Demo 到真正上线一个内部知识问答 Agent中间走的最大弯路就是一开始把精力都花在选“最好的 embedding 模型”和“最强的向量库”上后来才发现最影响体验的其实是数据清洗和切分策略。先把一手烂数据管好比盲目换模型有效得多。还有一件事——任何 RAG 系统都要尽早建立小规模评估集哪怕只有 20 条问题。没有评估你就永远不知道一次改动到底把系统变好了还是变坏了。RAG 是个工程味道很重的技术它的天花板往往不取决于你用了多贵的模型而是你有没有耐心把细节打磨到位。
阅读完成 · 觉得有帮助?
咨询建站