做了几个知识库检索Agent后我总结出一套能落地的设计方法论去年帮三个团队做内部知识库检索我一度以为这就是个套壳 RAG的活。向量数据库一接、文档一切、Prompt 一写完事儿。结果上线后傻眼了用户问今年的报销流程和去年有什么区别系统要么检索不到去年的流程文档要么直接胡编乱造一个答案。那时候我才意识到知识库检索 Agent 和简单的 RAG完全是两回事。这篇文章聊聊我踩过的坑以及一套真正能落地的设计思路。为什么 RAG 不够用了传统的 RAG pipeline 很简单用户查询 → 向量化 → 向量检索 → 大模型生成答案这套流程对付某某概念是什么这类问题还行。但真实场景里用户的问题长这样“对比 A 方案和 B 方案的适用场景”“这个错误代码在 3.0 版本里是怎么处理的”“帮我找一下关于 XXX 的所有讨论按时间排个序”这些问题的共同特点是它们不是单次检索能解决的。你需要理解用户意图、拆解查询、决定检索策略、甚至多次迭代。这时候Agent 的引入就不是炫技而是刚需了。知识库检索 Agent 的核心架构我画了一个简化版的架构图方便理解用户查询 ↓ [查询理解模块] —— 意图识别、query改写、实体提取 ↓ [路由模块] —— 决定去哪查、怎么查 ↓ [检索执行模块] —— 多源检索、过滤、重排序 ↓ [答案生成模块] —— 引用生成、事实校验、多轮整合 ↓ 返回结果下面一个个拆开讲。查询理解别让用户的话直接进向量库这是最基础、但也最容易被忽略的环节。用户的原始查询往往很糙。比如“那个新出的 API 怎么用”大模型看到这句话会懵哪个 API什么时候出的所以第一步要做的是 query 改写。我的做法是引入一个轻量的查询理解 AgentclassQueryUnderstandingAgent:defanalyze(self,user_query:str,chat_history:list)-QueryContext:# 1. 意图识别是搜索、对比、总结还是故障排查intentself.classify_intent(user_query)# 2. 实体提取人名、版本号、产品名、时间entitiesself.extract_entities(user_query)# 3. 指代消解把那个这个替换成具体实体resolvedself.resolve_coreference(user_query,chat_history)# 4. 生成检索 query补充上下文让 query 更完整search_queryself.generate_search_query(resolved,entities,intent)returnQueryContext(intentintent,entitiesentities,search_queriessearch_query,# 注意这里是复数后面会讲time_rangeself.infer_time_range(entities))血泪教训早期我只做了简单的指代消解结果用户连续追问时上下文一多模型就开始 hallucinate。后来我把 chat_history 做了压缩和摘要只保留跟当前问题相关的实体和结论准确率才上去。检索路由一个问题可能要去好几个地方查企业知识库从来都不是单一数据源。Wiki、Confluence、GitHub Issues、工单系统、邮件存档……每个源都有自己的检索方式和权限控制。路由模块的作用就是根据查询意图决定去哪查、用什么方式查。classRetrievalRouter:defroute(self,query_context:QueryContext)-List[RetrievalTask]:tasks[]ifquery_context.intent故障排查:tasks.append(RetrievalTask(sourcegithub_issues,strategykeyword))tasks.append(RetrievalTask(sourcerunbook,strategysemantic))elifquery_context.intent流程查询:tasks.append(RetrievalTask(sourcewiki,strategysemantic,filters{category:流程文档}))elifquery_context.intent对比分析:# 对比类问题需要查多个文档forentityinquery_context.entities:tasks.append(RetrievalTask(sourceall,strategyhybrid,queryf{entity}{query_context.search_queries[0]}))returntasks这里有个关键点不要迷信向量检索。对于故障码、ID、版本号这类精确匹配的场景BM25 或 SQL 过滤往往比语义检索更靠谱。我现在主流的检索策略是hybrid混合检索向量关键词同时查结果再融合。多跳检索复杂问题要拆成几步查这是区分高级 Agent和套壳 RAG的分水岭。用户问“张三在 Q3 负责的项目目前进度怎么样了”这个问题没法一步查到。你需要先查张三在 Q3 负责了哪些项目拿到项目列表后再查每个项目的当前进度这就是多跳检索Multi-hop Retrieval。classMultiHopRetriever:defretrieve(self,query:str,max_hops:int3)-List[Document]:collected_docs[]current_queryqueryforhopinrange(max_hops):docsself.single_retrieve(current_query)collected_docs.extend(docs)# 检查是否已经找到足够信息ifself.is_sufficient(collected_docs,query):break# 生成下一跳的查询current_queryself.generate_next_hop_query(query,collected_docs)ifnotcurrent_query:breakreturncollected_docsdefgenerate_next_hop_query(self,original_query,docs)-str:promptf 原始问题{original_query}已检索到的信息{format_docs(docs)}为了回答原始问题还缺少什么信息 请生成一个精确的检索查询。如果信息已足够返回DONE。 returnself.llm.predict(prompt)重点每一跳都要做充足性判断不然 Agent 很容易陷入无限检索。我一般用 LLM 来做这个判断同时设置最大跳数兜底。重排序与过滤检索出来的不一定都能用检索 Top-K 的结果里经常混着一些似是而非的文档。比如用户问Python 的 GIL 是什么结果里可能有一篇讲Ruby 的 GVL的文章——语义上很接近但内容完全不相关。我的做法是两道关卡第一道相关性重排序Re-rank用 cross-encoder 或 LLM 对候选文档做精细排序。cross-encoder 的效果通常比双塔模型好很多虽然慢点但值得。defrerank(query:str,candidates:List[Document])-List[Document]:pairs[(query,doc.content)fordocincandidates]scorescross_encoder.predict(pairs)fordoc,scoreinzip(candidates,scores):doc.relevance_scorescore# 按分数排序同时加一个阈值过滤return[dfordinsorted(candidates,keylambdax:x.relevance_score,reverseTrue)ifd.relevance_score0.5]第二道事实性过滤Fact-check对于关键信息我会让 LLM 做一个能否回答的判断给定以下文档片段它们是否能直接回答{question} 如果能标记为 KEEP如果不能标记为 DISCARD。这步会损失一些召回率但 precision 大幅提升。对于客服、医疗、法律这类高风险场景宁可少答也不要错答。答案生成引用比文采更重要终于到了大模型生成答案的环节。但这个环节的 Prompt 设计直接决定用户是哇好准还是这又在胡说八道吧。我的 Prompt 里有几个硬性要求必须标注引用来源请在回答中明确标注每个事实的引用来源格式为 [^1], [^2]...无法确认时明确说明如果检索到的信息不足以回答问题请直接说根据现有资料无法确定不要编造。结构化输出对于对比类、步骤类问题要求用表格或列表输出而不是大段文字。defgenerate_answer(query:str,docs:List[Document])-str:contextformat_docs_with_citation(docs)promptf基于以下检索到的文档回答用户问题。{context}用户问题{query}要求 - 如果文档中没有相关信息明确说明无法从现有资料中找到答案 - 每个关键事实都要标注引用 [^index] - 优先使用结构化格式列表、表格 - 不要包含文档中没有的信息 回答returnllm.predict(prompt)三个最痛的坑坑 1检索到了但大模型不看有时候检索结果是准确的但大模型生成答案时就是不用。常见原因是上下文太长关键信息被淹没了Prompt 里没有强调必须基于提供的文档回答文档格式混乱模型难以提取我的解法上下文控制在 4k-8k token 以内超长的做分段摘要Prompt 里加一句狠话“你只能基于提供的文档回答禁止编造”文档预处理时统一格式去掉 HTML 标签、广告、页眉页脚坑 2多轮对话里的上下文漂移用户第一轮问报销流程第二轮问那差旅呢如果直接把两轮对话拼在一起做检索很容易检索到不相关的内容。我的解法每轮都重新生成独立的 search query而不是用原始用户 query 直接检索维护一个对话状态记录已确认的关键实体检索时只对当前轮次改写后的 query 负责坑 3动态知识库的更新延迟文档更新了但向量数据库里的还是旧版本。用户拿到过时的答案体验直接崩塌。我的解法文档必须带最后更新时间生成答案时把这个时间也展示出来对于高频更新的文档缩短同步周期从每天一次改为实时在检索结果里做时间过滤优先返回最新版本写在最后设计知识库检索 Agent本质上是在做三件事让用户的问题变得可被检索让检索系统找到真正有用的信息让大模型老实说话不乱编这三件事里大模型生成答案反而是相对简单的。真正难的是前面的查询理解和检索策略。如果你刚开始做这类项目我的建议是先别急着上多跳检索把 query 改写和混合检索做好能解决 80% 的问题一定要加日志和反馈机制让用户能点这个答案有用/没用引用来源不是可选项是必须项最后技术方案永远是服务于场景的。别为了追求 Agent 的智能感把一个简单搜索能解决的问题搞成八层架构。