这个系列写到第四篇了。前面几篇我们聊了 Agent 的整体结构、任务规划、工具调用,这次终于轮到所有落地场景里最绕不开的一块:知识获取管道,也就是大家口中的 RAG 基础。
先说说为什么 Agent 非得有这条管道。大模型本身不是数据库,它不会知道你公司 ERP 里的产品库存、不会知道你 OA 系统里那条"公休假审批"流程上周刚改过规则、更不会记得你昨天发在飞书文档里的会议结论。Agent 想干活,就得有办法把这些外部知识拿进来,RAG 就是目前最主流的那条知识获取管道。这篇我打算把 RAG 的每一环讲透:为什么这样设计、每个参数到底在调什么、线上最容易在哪里翻车。适合从 0 到 1 搭 Agent 的人、准备做知识库问答的人、以及试过 RAG 但总觉得回答不靠谱、想系统排查一遍的开发者。
1. 为什么 Agent 需要一条知识获取管道
1.1 大模型不是数据库,参数记忆喂不饱 Agent
很多刚接触 Agent 的人会有一个直觉:大模型懂得那么多,我把问题直接抛给它不就行了?这个想法在闲聊场景里没错,但一落到真实业务就会露馅。你可以试一下,问一个没有接任何外部系统的通用大模型:"我们公司最新的差旅报销标准是什么?"它大概率会给你一个看似合理、实际上完全编造的答案。原因很简单:这类私域知识从来没有出现在它的训练语料里,它只能靠"泛化"去猜一个最像样的回答。
即便某些知识恰好出现在训练语料里,模型的参数化记忆也是靠不住的。训练有截止日期,知识有版本变更,同一个公司里不同部门的制度可能还会互相矛盾。指望靠模型的"天生记忆"去支撑 Agent 干活,就像指望一个再博学的顾问背下你公司所有的制度细节,他记不全、也记不牢,更不可能主动意识到哪些文件已经作废。知识获取管道要解决的,就是"让 Agent 在需要的时候,能把外部知识准确地翻出来"这件事。
有人会说:那我把所有文档都塞进上下文窗口不就行了?现在的模型上下文确实越来越长,从 128K 到 1M 的都有,但"能装下"不等于"用得好"。上下文越长,模型对中间段落的注意力就越容易稀释。业界常说的 Lost in the Middle 现象,指的就是模型倾向于记住开头和结尾、忘掉中间部分。你把 500 页文档一次性灌进去,回答问题时模型反而不知道该信哪一段。更别提成本了——按 token 计费的大模型 API,每次请求都携带全部文档,几轮对话下来费用会非常难看。所以 RAG 的做法是:先把知识切碎、索引好,每次只挑最相关的那几段送进模型。这是一种"按需取用"的思路,和人类查资料的习惯是一致的。
1.2 在 Agent 的认知架构里,知识获取是感知层
把 Agent 拆开看,通常可以分成感知、规划、行动三块。感知负责理解外部世界的状态,规划负责决定下一步做什么,行动负责调用工具或输出结果。RAG 在 Agent 里承担的不是一个可有可无的"问答插件",而是感知层的核心组件。Agent 需要先获取到准确的事实,才能做出靠谱的决策和行动。这一步要是错了,后面再怎么调 Prompt、怎么优化工具调用都是白费。
这里要区分一下 RAG 和普通工具调用的边界。工具调用(比如查数据库、调 API)适合结构化、更新快、精确匹配的数据,比如查订单状态、查库存数量,一个 SQL 或者一个 API 请求就搞定了。但企业里大量知识是以非结构化形式存在的:产品手册、制度文档、会议纪要、售后工单、PDF 里的表格。这些内容没法用一个 SQL 直接取出来,需要语义检索才能找到。RAG 擅长处理的正是这一类"说不清具体在哪个字段、但大概在哪些内容里"的知识。
这也是为什么最近"知识割裂"这个词频繁出现在 RAG 相关的讨论里。很多公司的知识散落在 ERP、CRM、本地文件、Wiki、聊天记录里,彼此之间没有打通。员工想找一个答案,得在不同系统间来回切换。RAG 出现之前,这种割裂只能靠人工整理;RAG 出现之后,理论上我们可以把这些异构内容先统一索引到一个语义层上,再通过一套问答接口去访问。当然,工程上要做到这一步并不轻松,后面我会讲到实际落地时的坑。但方向是对的:知识获取管道存在的意义,就是把分散的知识变成 Agent 可以"感知"到的统一资源。
2. 核心链路拆解:索引、检索、生成三件事
2.1 索引:从原始文档到可搜索的向量
RAG 的完整链路可以粗暴地分成三个阶段:索引、检索、生成。很多人第一次上手时会把注意力全放在检索和 Prompt 上,但索引阶段才是决定整个系统上限的地方。索引做不好,后面再怎么调参都是事倍功半。
索引阶段的第一件事是解析清洗。输入进来的文档可能是 PDF、Word、Markdown、HTML 甚至扫描件。PDF 是最容易出问题的:有些 PDF 是双栏排版,直接解析会把左右两栏的文字混在一起;有些是扫描件,必须 OCR 才能提取文字;还有些表格在 PDF 里看起来是规整的,解析出来却变成了一堆乱序文本。我的经验是,这个环节不能图省事直接用默认解析器。Python 生态里 PyMuPDF、pdfplumber 各有擅长,前者速度快,后者对表格坐标的还原更精确。Word 文档则要小心页眉页脚、批注这些干扰信息,转成纯文本后经常混进不相关内容。清洗的目标只有一个:让进入索引的文本尽量干净、忠实于原始内容的阅读顺序。
清洗之后是分块。为什么必须分块?因为 Embedding 模型是对一段文本编码成一个向量,这一段的长度和内容质量直接决定了检索效果。块太小,语义信息不完整,比如一个"报销标准"的知识点被拦腰截断成两半;块太大,向量里混合了太多噪声,检索出来的段落虽然相似度高,但无法精准定位到用户想要的细节。常用的分块策略大致有三类:
| 策略 | 做法 | 适用场景 | 典型参数 |
|---|---|---|---|
| 固定窗口分块 | 按固定 token 数或字符数切,可加重叠窗口 | 通用场景,实现最简单 | 400 token,overlap 50 |
| 递归结构分块 | 优先按段落、标题层级切,切不动再按字符兜底 | 有较强结构化排版的文档 | 按 Markdown 标题层级递归 |
| 语义分块 | 检测句子/段落之间的语义边界,自动聚合成语义完整的块 | 文档结构混乱、话题切换多 | 需要额外模型或规则辅助 |
我给一个可以直接抄的经验值:中文业务文档,块大小建议落在 300 到 600 个字符之间。这个区间对大多数 Embedding 模型来说,既不会因为太短而丢失上下文,也不会因为太长而引入噪声。重叠窗口一般取块大小的 10% 到 15%,作用是避免切分位置刚好把关键句子劈成两半。固定窗口虽然简单,但如果你处理的文档本身有清晰的标题结构,强烈建议先按结构分块,再对超长段落做固定窗口切割。这样既保留了文档原有的逻辑边界,又照顾到了实际长度。
分块之后就是向量化。这一步需要一个 Embedding 模型,把文本转换成高维向量。常见的选择有 OpenAI 的 text-embedding-3-small、开源的 bge-m3、智谱的 embedding-2 等。中文场景下,bge-m3 这类国产模型对中文语义的理解通常更好,而且支持 8192 token 的长文本,性价比很高。向量维度从几百到几千不等,维度越高通常表达能力越强,但存储和计算成本也越高。向量距离度量一般用余弦相似度,它衡量的是方向差异,对向量的绝对大小不敏感,和文本语义相似度的直觉最匹配。如果用的向量库支持归一化,归一化后余弦相似度和内积是等价的,查询效率会更高一些。
最后一步是存入向量数据库。小项目用 FAISS 就足够了,一个本地索引文件就能跑通;如果后续要做多租户、增量更新、权限过滤,需要换成 Milvus、Qdrant 这类专业向量数据库;如果团队已经重度使用 PostgreSQL,可以直接用 pgvector 扩展,一条 SQL 把结构化查询和向量检索结合起来。选型时不用盲目追求规模化,先看你当前的数据量级和更新频率。几万条知识块以内,FAISS 完全扛得住。
这里要特别强调元数据的重要性。每个知识块入库时,至少带上这些字段:文档来源、标题、页码或章节、上传时间、文档版本号。没有元数据,你后面做权限过滤、来源引用、按时间排除旧版本的时候会非常痛苦。我见过很多项目第一版不存元数据,上线后一发现问题,就得把所有文档重新入一遍库,代价巨大。
2.2 检索:不是简单算一下相似度就完事
索引建好之后,进入检索阶段。基础做法是:把用户的问题也编码成向量,然后在知识库里找最相似的 K 个块。第一次跑通的时候,这个流程没有问题,但一旦放到真实场景,你会发现很多问题——检索出来的结果经常和你心里预期的不太一样。
第一个典型问题是精确匹配失效。用户可能会问"FC-3000 这款型号的保修期是多久",而知识库里写的是"FC-3000 产品保修期为 12 个月"。向量检索在语义层面能理解"保修期"和"保修"是相关的,但如果你问的是"FC-3000A"、"FC3000"这类带复杂编号的关键词,向量检索经常把这些 token 打散,反而找不到精确匹配。解决办法是混合检索:向量检索负责语义召回,BM25 这类稀疏检索负责精确关键词匹配,两者结果合并后再做归一化和去重。具体到工程实现,可以简单用"向量相似度 * 0.6 + BM25 分数归一化 * 0.4"这类线性融合,或者先分别取 top 50,扔给重排模型统一排序。
第二个典型问题是检索结果排序不精准。第一轮检索通常用双塔模型:文档和查询分别编码成向量,然后取相似度。双塔模型的特点是快,但精度有限——它对段落级语义的刻画不够细腻。要提升精度,需要在第一轮召回之后加一个重排环节。重排用的是交叉编码器,把查询和文档拼接在一起喂给模型,让模型直接输出两者的相关性分数。这样做更准,但速度明显变慢,所以只能对候选集做重排,比如第一轮召回 top 50 或者 top 100,重排后只取前 5 个。我实测过一个场景:正确的知识块在第一轮排在 23 位,重排之后直接升到第 1 位,效果提升非常明显。现在主流的中文重排模型如 bge-reranker-v2-m3,对长文本的支持和精度都做得不错,值得直接纳入管道。
第三个常见问题是用户的问题太简略、缺少上下文。比如在一个多轮对话里,用户先问"我们公司的年假政策是什么",接着又问"那新入职的能用吗"——如果不做处理,直接拿"那新入职的能用吗"去检索,大概率什么都检索不到。解决思路是 Query Rewriting,也就是用大模型把用户的当前问题和历史对话合并,改写成一个能独立检索的完整问题。实践中可以在检索之前加一个轻量级的改写 Prompt,把改写后的 query 用于检索。这一步对多轮 Agent 场景几乎是刚需。
此外还有检索规模的取舍。top_k 设得太小容易漏,设得太大容易把不相关内容塞进上下文。通用问答场景,top_k 取 5 到 8 之间比较合适;如果是做文档摘要、归纳总结类任务,可以把 top_k 放宽到 15 到 20,因为这类任务需要看到足够多的来源信息才能综合判断。相似度阈值也是一个需要根据模型调出来的参数——不同 Embedding 模型的分数分布差异很大,有的模型结果普遍在 0.7 以上,有的在 0.3 到 0.5 之间徘徊。看到低于某个分数的检索结果时,不要直接丢给模型,先人工检查一下是不是因为 Embedding 模型不擅长这类内容。切莫直接抄网上的阈值,务必自己拿一批真实数据跑一遍分布。
2.3 生成:给大模型立规矩,只许照着资料说话
检索完成后,最后的生成阶段看起来最简单:把检索结果和用户问题拼进 Prompt,让模型作答。但恰恰是这个环节,最容易出现"答得很流畅、细节全是编的"的问题。想让模型"只许照着资料说话",需要在 Prompt 结构、引用约束和维护机制上做文章。
一个可靠的生成 Prompt 至少包含四部分:系统角色说明、参考资料、用户问题、输出约束。系统角色说明告诉模型"你是一个知识库助手,只能基于参考资料回答";参考资料区按顺序列出检索到的知识块,最好标注编号;用户问题就是原问题;输出约束可以写明"如果参考资料中没有明确信息,请直接回答不知道,不要推测"。
很多人会忽略输出约束,总觉得模型"应该"知道不能编造。但实际情况是,大模型的生成习惯就是"顺着说",你给它的参考材料有一点关联,它就敢顺着往下编。我常用的约束写法是在 Prompt 里加一句"回答中必须标注引用的知识块编号,例如 [1][2];如果某个事实在参考资料中无法定位到对应编号,必须明确说明该信息未在资料中找到"。这个约束能帮你实现两个目的:一是让模型在生成时心里绷着一根弦,别乱发挥;二是生成结果出来之后,你可以自动校验——如果模型引用了某个编号,但该编号的内容里找不到对应事实,那答案就有幻觉嫌疑,可以直接拦截。
生成阶段还需要考虑多结果整合。用户的问题往往不是靠一个知识块就能回答完整的。比如问"产品的退货政策是什么",知识库里可能有退货时限、退货条件、运费承担三个不同章节的内容。如果只取 top 1 的块,答案就会缺胳膊少腿。所以在组装 Prompt 时,要把多个相关块都放进去,并且明确要求模型综合所有参考资料回答,而不是只看第一段。
还有一个进阶的设计:把生成和检索串成一个循环,而不是一次性的"检索一次、生成一次"。第一轮检索结果如果不满足回答条件,可以触发第二轮检索,比如换一个关键词、查一下相关的 FAQ 索引,或者调用一个结构化的数据库工具。这个思路正是 Agentic RAG 的雏形,我在第四部分会展开说。对于基础 RAG 来说,先把上面三个阶段的底子打好,再考虑循环和规划也不迟。
3. 实操:搭一个最小可用的知识获取管道
3.1 文档进入管道前,强烈建议先做清洗
先动手的话,我建议你在正式搭管道之前,先花时间处理输入文档的质量问题。这一步看似不产生"代码成果",但对最终效果的影响之大,怎么强调都不过分。我见过一个项目,导入了 200 份 PDF 产品手册,检索出来的结果总是不对劲,排了一天查不出问题,最后发现是每份 PDF 的页脚公司地址都被解析出来了,模型总是被这些噪声干扰。清洗一遍之后,命中率立刻上去了。
清洗要做的事情包括:去掉页眉、页脚、水印、跳转链接;统一编码格式(特别是 PDF 转出来的中英文引号、破折号经常变成乱码);表格如果本身承载了强语义信息,最好转成 Markdown 表格或者按行组织成自然语言描述,而不是简单地把表格中的文字一行行拼出来。多栏文档必须先恢复阅读顺序,这一步在 PyMuPDF 里可以通过坐标排序实现,也可以直接用一些商业解析库。
清洗完成后,还需要做一步很多人忽略的"内容筛选"——不是所有文档都值得进知识库。重复的旧版本、已经被作废的制度文件、只有几个字的占位文档,都会成为检索时的干扰项。我的建议是先按文档的访问频率和业务重要性拉一个清单,优先处理那些用户最常问、更新最频繁的文档。知识库不是越全越好,而是越"准"越好。
3.2 分块与 Embedding 参数怎么定
搞定清洗后,进入分块和向量化环节。我直接给你一套可以复现的最小参数方案:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 分块大小 | 400 到 500 字符(中文) | 按中文字符数切,比按 token 切好控制 |
| 重叠窗口 | 50 字符 | 避免关键句子被切分 |
| 分块策略 | 优先按 Markdown 标题切,再按字符兜底 | 保留结构信息 |
| Embedding 模型 | bge-m3 或 text-embedding-3-small | 中文场景优先 bge-m3 |
| 向量库 | 小项目 FAISS,生产环境 Milvus/pgvector | 按数据量选型 |
| top_k | 5 到 8 | 边调边看命中情况 |
| 重排 | bge-reranker-v2-m3 | 在 top 50 中重排取 top 5 |
我用 Python 的 LangChain 生态给你一个可参考的索引流程,核心代码大概长这样:
from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载并清洗文档 loader = PyPDFLoader("产品手册.pdf") docs = loader.load() docs = [d for d in docs if len(d.page_content.strip()) > 20] # 过滤空页 # 2. 分块:先按结构走,超长再切割 splitter = RecursiveCharacterTextSplitter( chunk_size=450, chunk_overlap=50, separators=["\n## ", "\n### ", "\n\n", "\n"], ) chunks = splitter.split_documents(docs) # 3. 向量化并入库 embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-m3") vectorstore = FAISS.from_documents(chunks, embeddings) vectorstore.save_local("faiss_index") # 4. 检索 retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) retrieved = retriever.invoke("这个产品的保修期是多久?")跑通这条流程只需要几分钟,但它已经把 RAG 的核心链路串起来了。分块后建议你随手抽查几个块,看一眼内容是否完整、是否保留了标题信息。元数据字段(来源、标题、页码)在创建 Document 对象时就要塞进去,比如doc.metadata = {"source": "产品手册.pdf", "page": 1},后面检索和引用都要靠它。
3.3 检索参数调优与命中率自查
管道跑通之后,第一件事不是急着接 Agent,而是验一遍检索的命中率。很多人的做法是问一两个问题,看回答像不像样就认为系统可用了。这是最容易踩的坑。正确做法是:建立一个小规模的"黄金问答集",比如从真实业务里挑 30 到 50 个问题,每个问题人工标注一下正确答案应该在哪个知识块或哪份文档里。然后批量跑检索,统计正确答案是否出现在检索结果的前 K 个位置里。这个指标就是热词榜上那个 Rag Hit Rate。
我建立一个简单的自查表格:
| 用户问题 | 正确知识块 | 是否出现在 top 10 | 相似度最高值 |
|---|---|---|---|
| 新品 A 有保修政策吗 | 产品手册-新品A-保修章节 | 是 | 0.82 |
| 退货需要承担运费吗 | 售后政策-退货条款 | 否 | 0.41 |
| 差旅能住几星级酒店 | 财务制度-差旅标准-住宿 | 是 | 0.75 |
如果发现某类问题命中率很低,不要急着调大 top_k,先看检索日志里到底返回了什么。这里我有一个习惯:每次检索都把分段内容和分数打印出来,人工扫一遍,很快就能定位问题是出在分块、Embedding 还是查询词上。很多时候你会发现,检索之所以不准,不是因为模型不行,而是因为知识库里那段正确内容的表达方式和你问法之间差异太大,向量模型没能把它们关联起来。这时候可以试试在知识库里给关键内容补充别名和同义表达,或者做一层查询改写。
3.4 一个能跑通的流水线设计
从工程角度,我建议把 RAG 管道拆成独立模块,而不是写一个大函数。这样做的好处是后续每一步都能单独替换和测试。
整个流水线大致分成四层:文档接入层负责加载和清洗;索引管理层负责分块、向量化、增量和删除;检索服务层负责 query 处理、向量召回、重排;生成服务层负责 Prompt 组装、调用 LLM、输出引用。
def rag_pipeline(user_query: str, history: list[dict], retriever, llm): # 1. 改写 query search_query = rewrite_query(user_query, history) # 2. 召回+重排 candidates = retriever.invoke(search_query, k=50) results = rerank(search_query, candidates, top_k=5) # 3. 组装 Prompt context = format_context(results) # 带编号、带元数据 prompt = build_rag_prompt(context, user_query) # 4. 生成并校验 answer = llm.invoke(prompt) answer = verify_citations(answer, results) # 校验引用是否真实 return answer模块化之后,你可以在不改动其他环节的情况下,把 FAISS 换成 Milvus、把 bge-m3 换成效果更好的模型、把固定窗口分块换成语义分块。我用这套结构跑过很多业务场景,最大的体会是:前期花一点时间把边界划清楚,后期排查问题时能省下好几倍的精力。
4. 从基础 RAG 到 Agentic RAG 与知识组织
4.1 Agentic RAG:让 Agent 自己决定怎么翻资料
基础 RAG 的逻辑是"收到问题—检索一次—生成回答",这个流程对大多数简单问答是够用的。但现实中的问题往往没那么规整:用户问的可能是"B 客户的退货申请为什么被驳回",这个问题既涉及退货政策(文档),又涉及该客户的订单状态(结构化数据),还可能涉及客户历史沟通记录(聊天记录)。一个固定的检索流程很难覆盖这种多源、多步的诉求。Agentic RAG 要解决的问题,就是把"怎么获取知识"这件事本身变成一个决策过程。
放在 Agent 的架构里,RAG 不是被硬编码在固定流程里,而是注册成 Agent 可以调用的工具或者技能。Agent 根据用户的意图,决定要不要检索、用哪个知识库检索、检索结果不够时要不要换一种方式继续检索。比如它可以先查 FAQ 知识库,发现不够精确,再决定调用一个结构化数据查询工具,最后综合两部分结果给出回答。工具的描述信息通常决定了 Agent 会不会正确选用它。我自己写 Agent 工具时,会在描述里写清楚"该工具用于查找非结构化文档中的政策和流程说明,不适用于查询订单状态",这样 Agent 选错的概率会显著下降。
另一个经典的 Agentic RAG 方向是 Self-RAG,来自 2023 年底伯克利那篇论文。思路是让大模型在生成回答时,不光输出答案,还输出一些反思标记,比如"我检索到的结果是否足够支撑回答这个问题?""有没有矛盾信息?"。模型可以训练自己判断:检索结果不足就继续检索,有矛盾就指出矛盾。实际落地时,不需要完全复刻论文里的训练方案,用普通的大模型加上合适的 Prompt 也能做出类似的自我检查效果。关键是给模型一个判断依据,而不是放任它回答完就结束。
社区里已经有一些把 RAG 直接封装成服务的产品形态,比如一些 Agent 开发平台推出了 RAG as a Service 的能力,底层把文档解析、索引、检索、重排都托管了,上层 Agent 只需要调用一个接口。这种趋势对业务方是好事,省去了很多工程细节,但反过来也意味着:如果你不懂底层原理,出了问题只能干瞪眼。所以我一直建议,就算最终要用人家的托管服务,自己也要先把基础 RAG 的每一环亲手跑一遍。
4.2 GraphRAG、Ontology RAG 与 LLM Wiki:知识组织的新方向
近几年 RAG 方向上出现了很多试图解决"知识割裂"问题的进阶方案。热词榜单里的 GraphRAG、Ontology RAG、LLM Wiki,本质上都在做一件事:让知识块之间产生结构关联,而不是孤零零地躺在向量库里等着被检索。
GraphRAG 的思路是先从文档中抽取实体(人物、产品、事件、部门)和实体间的关系,构建出一张知识图谱。它的优势体现在两类问题上:一是跨文档的全局性问题,比如"我们公司过去一年所有产品的退货率趋势如何";二是关系推理类问题,比如"A 产品和 B 产品是否共用同一套售后流程"。基础 RAG 很难回答这类问题,因为答案分散在大量文档中,靠向量相似度可能永远凑不齐所有相关段落。GraphRAG 能顺着图谱里的边把相关内容关联起来。代价也明显:抽取实体的计算成本高,构建和维护周期长,中小项目慎用。
Ontology RAG 则是把领域本体(概念、属性、关系)引入检索过程。比如医疗领域,如果本体里定义了"高血压"和"血压"的层级关系,检索时就能利用这层概念关系做更精准的查询扩展。这类做法适合概念体系严格、专业术语密度高的领域,法律、医学、金融都算典型场景。但它的前提是必须先有一份质量不错的本体定义,不然概念映射本身的误差会让检索更乱。
LLM Wiki 这个方向更有意思,核心是用大模型来自动生成、修订和整理知识条目,把"文档"变成"持续更新的 Wiki 知识库"。比如一个团队的历史决策记录,可以用 LLM 定期从会议纪要中总结成结构化条目。这些条目的质量依赖于上游源头的准确性,但如果维护得当,会显著提升知识库的密度和可检索性。我个人的判断是,GraphRAG、Ontology RAG 这些方案适合作为知识组织层的进阶选项,而不是每个项目的起手式。先把基础 RAG 的索引、检索、生成做好,再评估要不要引入图谱,这才是理性的路线。
4.3 用 Hit Rate 和忠实度把管道管起来
说到评估,RAG 项目最容易犯的错误是"凭感觉调优"。今天觉得回答挺流畅,明天换个问题又拉胯了,靠肉眼判断很难稳定推进。我强烈建议从第一天起就建立一套量化评估机制,哪怕一开始只测 20 个问题。
Hit Rate 是最基础的指标,公式很简单:在测试问题集上,正确答案出现在检索结果前 K 位的次数占总问题数的比例。比如 50 个问题,答案是 50 个块,其中 42 次被检索进 top 10,那 hit rate 就是 84%。这个指标适合快速判断索引和检索环节的健康度。但要记住一个原则:hit rate 高不代表最终回答好。检索命中了,生成阶段照样可能因为 Prompt 组装不好而给出错误答案。所以还要配套评估"生成忠实度"——也就是让标注员看最终回答,判断回答里的每个关键事实是否都能在引用块中找到依据。忠实度可以手动抽样评估,也可以用更强的大模型做自动评测,让它逐条比对。
我用这套方式跑过几个知识库项目,印象最深的是:有一个项目 hit rate 已经到 95% 了,但用户仍然觉得回答不好。后来查出来是重排环节出了问题——重排后的 top 5 里确实有正确答案,但生成时被其他噪声块干扰,模型最终抽取了错误的信息。这说明检索和生成是强耦合的,不能只守着一个指标。多指标联合看,才能相对准确地定位问题。
5. 常见问题与排查技巧实录
5.1 检索结果和问题对不上,Hit Rate 老上不去
这是我被问得最多的一类问题。典型症状是:用户问"差旅报销能住什么级别的酒店",系统返回了一堆差旅制度开头的文本,但真正写了住宿标准的那一段没被检索到。遇到这种情况,先别着急换模型,按下面的顺序排查。
第一,检查分块是否损坏了语义完整性。把正确的那段内容找出来,看看它在知识库里被切成了什么样。如果一句"四星级及以下酒店"被拆成了上一块结尾和下一块开头,那模型当然检索不准。解决方法是调大 overlap,或者针对这类制度文档改成按章节标题切。第二,把单路向量检索换成混合检索。差旅标准里"星级""酒店"这些关键词本身就是很强的线索,BM25 直接命中可能比向量语义匹配更可靠。第三,检查检索阈值是否设置过严或过松。如果正确答案的相似度只有 0.5,而你的阈值设了 0.6,它直接被过滤掉了。第四,如果以上都正常,再考虑 query 改写。用户口语化的问法经常包含代词和省略,改写之后检索命中率能提升不少。
我自己的排查习惯是:先写一个脚本,把测试问题、检索到的前 20 个块、对应的相似度分数、正确答案所在的块一次性打印出来。肉眼扫一遍这四列,问题定位通常很快。这个脚本花半小时写,后面每次调优都用它,效率高很多。
5.2 回答得很流畅,但细节是编的
这类问题最让人头疼,因为表面看检索结果没毛病,回答也通顺,但仔细核对就发现"编译出来的答案"里有不存在的细节。原因通常有两个方向:生成约束太弱,或者检索上下文不够。
先查生成约束。如果你的 Prompt 只写了"请根据以下资料回答",模型很容易放飞自我。我的做法是把约束写成硬性要求:"回答内容必须完全基于参考资料;如果参考资料中没有提到相关内容,就明确回复未找到该信息,禁止根据其他知识推断。"再加上引用编号约束,几乎能杜绝凭空编造的情况。
再查上下文完整性。用户问的问题如果涉及多个知识维度,而检索只给了一个块,模型为了回答完整就会用训练记忆去补空缺。解决办法是增大 top_k 或者扩展重排候选集,让生成阶段有足够多的上下文可以引用。还有一种情况是检索到了,但 Prompt 组装时被截断了。检查一下组装之后的 Prompt,看看是否因为长度限制丢掉了关键块。
5.3 知识更新了,系统却还在答旧内容
这也是高频故障,而且越到后期越严重。第一版知识库跑通后,文档会被不断地修订、替换、作废,如果管道只有全量入库的逻辑,没有增量更新的机制,系统就会持续回答旧版本里的内容。
处理思路分三层。第一层是在元数据里增加版本号和更新时间,检索时可以根据条件过滤掉旧版本。第二层是做增量更新:文档有改动时,只重新解析、切片、向量化变更的部分,同时将旧版本对应的向量从库里删除。这个删除必须是物理删除,只做逻辑标记的话,相似度检索依然可能把它们捞出来。第三层是定期做全量校验,用文档 hash 对比检测有没有漏掉的变更。
我遇到过一种更隐蔽的情况:同一份文档在不同系统里有多个副本,其中一个更新了,另一个没更新,索引把两份都收了进去,回答的时候模型随机挑了一份。这在"知识割裂"场景里尤其常见。解决办法是在入库时制定统一的文档 ID 规范,以源系统加文档编号作为唯一标识,同 ID 的文档入库前先做内容版本对比,旧版本一律不索引。别小看这个规范,它能避免掉很多后期莫名其妙的错误答案。
6. 最后分享一点实操体会
RAG 这条知识获取管道做到最后,你会发现真正的难点不在某一个环节,而在于让整条链路耦合得足够稳。我自己跑了几个知识库项目之后的体会是:先把最简单的"解析—分块—向量化—检索—生成"循环跑通,再用 hit rate 和忠实度这类量化指标去驱动调优,比一开始就堆 GraphRAG、Agentic RAG 这些进阶方案靠谱得多。很多问题在基础管道里就已经被放大了,把基本功补上,后面的路会顺畅很多。
还有一个小技巧分享给你:在调试 RAG 的时候,把每一次检索的 query、召回块、最终回答都存在本地日志里,按天归档。没有这套日志,你遇到线上用户反馈错误答案时基本只能靠猜;有了日志,你五分钟就能还原出问题出在哪一环。RAG 不是一次性的工程,它是一条需要持续维护的管道,而这种维护能力,才是让 Agent 真正可用和可靠的关键。