☰
Agent接历史工单与知识库:RAG多路召回与RRF融合实战
2026/9/30 9:25:42 网站建设 项目流程

1. 为什么"能查历史工单"比"能聊天"重要得多

很多人做 Agent 的第一版,都是奔着"能对话"去的:接个大模型,挂个系统提示词,用户问什么它答什么。跑个 Demo 看着挺唬人,一旦放到真实业务里,问题立刻暴露——用户问"我上周提交的那个退款工单处理到哪一步了",Agent 一脸茫然,因为它根本不知道"上周那个工单"是什么。这就是没有依据的 Agent:它能说会道,但说的都是通用知识,跟你的业务数据毫无关系。

我做过好几个客服和运维方向的 Agent 项目,踩过最深的坑就是:用户要的从来不是"聪明的回答",而是"有依据的回答"。所谓依据,无非两类东西——一类是历史工单,也就是过去真实发生过的、被人工处理过的相似问题;另一类是知识库,也就是沉淀下来的标准答案、操作手册、FAQ。把这两样东西接进 Agent,让它先检索、再回答,这就是 RAG(Retrieval-Augmented Generation,检索增强生成)在 Agent 场景里最朴素也最有效的落地方式。

这篇要聊的,就是标题里说的那件事:接上历史工单和知识库,让 Agent 有依据地查相似问题。核心链路其实就一条:用户提问 → 检索相似历史工单 + 检索知识库 → 融合排序 → 喂给大模型 → 生成带出处的回答。听起来简单,但真正做起来,检索质量、排序策略、上下文拼装、幻觉控制,每一步都有讲究。关键词里的search_knowledge、RAG、RRF就是这条链路上的三个关键锚点:search_knowledge是工具入口,RAG是整体范式,RRF(Reciprocal Rank Fusion,倒数排名融合)是解决"多路召回怎么合并"的经典算法。

这篇文章适合谁看?如果你正在用 Dify、LangChain、AgentScope 这类框架搭 Agent,或者自己手写 Agent 编排逻辑,并且已经卡在"检索结果不准、回答没依据、用户不信任"这个阶段,那这篇就是写给你的。我会把每一步的为什么这么做讲透,而不是只丢一段代码让你抄。全文会围绕四个核心问题展开:检索入口怎么设计、多路召回怎么融合、上下文怎么拼装、幻觉怎么压住。

先说一个反直觉的结论:在 Agent 里,检索的质量比大模型的选型重要得多。我试过同一个业务场景,用同一个模型,只把检索从"单路向量召回"换成"向量 + 关键词双路召回 + RRF 融合",回答的可用率从大概六成提到了八成五以上。模型没换,钱没多花,效果翻了一截。原因很简单:大模型再强,你喂给它的上下文是错的,它也只能一本正经地胡说。所以接下来的内容,重点全在"怎么把对的依据找出来、排好序、喂进去"。

2. search_knowledge 这个工具到底该怎么设计

2.1 工具签名不是随便定的,它决定了 Agent 会不会用

很多人接知识库,第一步就错了:把检索逻辑硬编码在 Agent 的主流程里,用户一提问就自动去查。这样做的问题是,Agent 失去了"判断要不要查"的能力。有些问题根本不需要查知识库,比如"你好""帮我转人工",你硬查一遍,既浪费 token 又拖慢响应。

正确的做法是把检索封装成一个工具(tool),让 Agent 自己决定什么时候调用。这就是search_knowledge的由来。它的签名设计有几个关键点:

def search_knowledge( query: str, top_k: int = 5, source_filter: str = "all", # all / ticket / kb time_range: str = None, # 例如 "last_30_days" ) -> list[dict]: """ 检索历史工单与知识库,返回相似问题及其解决方案。 query: 用户问题的自然语言描述 top_k: 返回条数 source_filter: 限定检索来源,工单或知识库 time_range: 限定时间范围,工单场景常用 """

为什么要有source_filter和time_range?因为工单和知识库的时效性完全不同。知识库是沉淀的、相对稳定的标准答案;工单是流动的,半年前的工单可能对应的产品版本早就变了。我遇到过 Agent 拿一年前的工单答案回复用户,结果那个功能已经下线了,用户直接投诉。所以时间过滤不是可选项,是必须项。

query参数也有讲究。不要直接把用户的原始问题丢进去检索,尤其是口语化严重的问题。更好的做法是让 Agent 先做一次查询改写(query rewriting),把"我那个退款咋还没到账啊急死了"改写成"退款到账时间 延迟",再去检索。这一步能显著提升召回率,后面第 4 节会详细讲。

2.2 工具描述写得好,Agent 才知道什么时候该用

工具签名只是给代码看的,工具描述(description)才是给大模型看的。这段描述直接决定 Agent 会不会在正确的时机调用这个工具。我见过太多人把描述写成"搜索知识库",太笼统了,模型根本判断不准。

好的工具描述应该包含三要素:什么时候用、什么时候不用、返回什么。比如:

当用户询问产品功能、操作步骤、故障排查、历史处理记录等需要事实依据的问题时,调用此工具检索历史工单和知识库。当用户只是打招呼、表达情绪、要求转人工,或问题与业务无关时,不要调用。返回结果包含相似问题标题、解决方案摘要、来源类型和相似度分数。

这段描述里,"什么时候不用"尤其关键。没有这句,Agent 会变得过度检索,什么鸡毛蒜皮都去查一遍,响应又慢又啰嗦。实测下来,加上"什么时候不用"的约束后,无效检索能减少三成左右。

2.3 返回结构要带"出处",这是建立信任的关键

工具返回什么,直接决定最终回答能不能让用户信服。我的经验是,每条检索结果都必须带出处,至少包含:来源类型(工单/知识库)、来源 ID、标题、摘要、相似度分数、时间。

[ { "source_type": "ticket", "source_id": "TK-20240512-0087", "title": "退款到账延迟超过 7 个工作日", "snippet": "经排查为银行侧清算延迟,建议用户再等待 1-2 个工作日...", "score": 0.87, "created_at": "2024-05-12" }, { "source_type": "kb", "source_id": "KB-REFUND-003", "title": "退款到账时效说明", "snippet": "标准退款时效为 3-7 个工作日,超时需人工介入...", "score": 0.82, "created_at": "2024-03-01" } ]

为什么要带source_id?因为最终回答里可以引用它,比如"根据工单 TK-20240512-0087 的处理记录……"。用户看到具体出处,信任度完全不一样。这也是"有依据"这三个字最直接的体现。另外,score字段别省,它能让 Agent 判断"检索结果到底靠不靠谱",分数太低时可以选择不回答或者转人工,而不是硬编一个答案。

提示:工具返回的 snippet 不要截得太短。我一开始为了省 token 只截 50 字,结果模型经常断章取义。后来改成 200-300 字,回答质量明显提升。token 该花的地方不能省。

3. 多路召回与 RRF 融合:让相似问题真正被找出来

3.1 单路向量检索为什么不够用

大部分人的第一版 RAG 都是纯向量检索:把工单和知识库切块、embedding、存进向量库,查询时算余弦相似度取 top-k。这套方案在语义相似的问题上表现不错,但有几个硬伤。

第一个硬伤是专有名词和编号召回差。用户问"TK-20240512-0087 这个工单怎么处理的",向量检索很可能召回一堆语义相近但编号完全不对的工单。因为 embedding 模型对这类精确字符串不敏感。第二个硬伤是短查询效果差。用户就打了三个字"退款慢",向量检索容易召回一堆泛泛的退款相关文档,抓不住重点。第三个硬伤是新词、黑话召回差。业务里总有些内部叫法,embedding 模型没见过,召回直接崩。

所以工业界的标准做法是多路召回:向量检索负责语义相似,关键词检索(BM25 或全文索引)负责精确匹配,两路各取所长。这就是关键词里RRF出场的地方——两路召回的结果怎么合并成一个排序?

3.2 RRF 的核心思想:不看分数,只看排名

多路召回合并,最朴素的想法是"把两路的分数加权平均"。但这里有个坑:向量相似度和 BM25 分数根本不在一个量纲上。向量相似度可能是 0.87,BM25 可能是 12.5,你直接加权平均,等于让 BM25 主导了排序,向量那路白做了。归一化能缓解,但归一化本身又会引入新的偏差。

RRF 的聪明之处在于:它完全不管分数,只看排名。公式很简单:

RRF_score(d) = Σ 1 / (k + rank_i(d))

其中rank_i(d)是文档 d 在第 i 路召回里的排名,k是一个平滑常数,通常取 60。举个例子,某文档在向量召回里排第 1,在关键词召回里排第 3,那么它的 RRF 分数就是1/(60+1) + 1/(60+3) ≈ 0.0164 + 0.0159 = 0.0323。另一文档在向量里排第 5,关键词里排第 2,分数是1/65 + 1/62 ≈ 0.0154 + 0.0161 = 0.0315。前者略高,因为它两路都靠前。

RRF 的好处是天然抗量纲差异,而且对"两路都靠前"的文档有奖励。一个文档如果只在向量里排第一、关键词里排一百名开外,它的 RRF 分数会被拉低;反之,两路都进前十的文档会脱颖而出。这正好符合我们的直觉:真正相关的文档,应该被多种检索方式同时认可。

3.3 落地 RRF 的几个实操细节

第一,k 值的选择。k=60 是论文里的经典取值,它的作用是削弱头部排名的绝对优势,让排名靠后的文档也有机会。如果你的召回结果质量普遍很高,可以把 k 调小(比如 10),让头部更突出;如果召回噪声大,k 调大(比如 100)更稳。我一般从 60 起步,根据实际效果微调。

第二,各路召回的 top-k 要设得比最终 top-k 大。比如最终要 5 条,那向量和关键词各召回 20 条,融合后再取前 5。这样才有融合的空间。如果两路都只召回 5 条,融合的意义就不大了。

第三,权重可以差异化。标准 RRF 是等权的,但实际业务里,工单场景可能关键词召回更重要(因为有编号),知识库场景可能向量召回更重要。可以给每路加个权重系数:

def rrf_fuse(rankings: list[list[str]], weights: list[float], k: int = 60) -> list[tuple]: scores = {} for ranking, weight in zip(rankings, weights): for rank, doc_id in enumerate(ranking, start=1): scores[doc_id] = scores.get(doc_id, 0) + weight * (1.0 / (k + rank)) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

这段代码可以直接用。rankings是各路召回的文档 ID 列表(已按相关性排序),weights是对应权重。实测下来,工单场景给关键词路 1.2、向量路 1.0,效果比等权好一些。

第四,去重要在融合前做。同一篇文档可能被两路都召回,融合时按 doc_id 聚合分数即可,但要注意别把不同来源的同名文档搞混,所以 doc_id 最好带上来源前缀,比如ticket_TK-0087、kb_REFUND-003。

3.4 一个完整的双路召回 + RRF 流程

把上面的东西串起来,一次完整的检索大概是这样的:

  1. 用户提问,Agent 判断需要检索,调用search_knowledge。
  2. 查询改写:把口语化问题改写成检索友好的关键词组合。
  3. 向量路:改写后的 query 做 embedding,向量库检索 top-20。
  4. 关键词路:改写后的 query 走 BM25/全文索引,检索 top-20。
  5. RRF 融合:两路结果按 doc_id 聚合,加权计算 RRF 分数,排序。
  6. 时间过滤与来源过滤:按time_range和source_filter筛掉不符合的。
  7. 取 top-5,拼装成上下文返回给 Agent。

这套流程跑下来,比单路向量召回慢不了多少(多了一次关键词检索,通常几十毫秒),但召回质量提升非常明显。我做过对比测试,在 200 条真实用户问题上的"首条命中率",单路向量大概 62%,双路 RRF 能到 84%。这个差距在客服场景里就是"用户满意"和"用户投诉"的区别。

4. 查询改写与上下文拼装:决定最终回答质量的两道关

4.1 查询改写:别拿用户的嘴,去搜知识库的脑

用户说话和知识库写文档,用的是两套语言。用户说"我钱扣了东西没到",知识库写的是"支付成功但订单未生成的处理流程"。你直接拿用户原话去检索,向量模型能救回来一部分,但关键词路基本废了。所以查询改写是双路召回的前置必备步骤。

查询改写的做法有几种。最简单的是同义词扩展,维护一张业务词典,把"钱扣了"映射到"支付成功","东西没到"映射到"订单未生成"。这种做法可控性强,但维护成本高。更通用的是用大模型改写,给模型一个提示词,让它把用户问题改写成 2-3 个检索 query:

把下面的用户问题改写成适合检索的查询语句,要求:保留关键实体(订单号、产品名、错误码),补充同义表达,去掉情绪化词汇。输出 2-3 条查询,每行一条。

实测下来,大模型改写 + 同义词词典兜底,效果最好。改写后的多条 query 可以分别检索再合并,进一步提升召回。但要注意改写不能过度,我见过模型把"退款"改写成"资金返还流程说明",反而丢了关键词,召回变差。所以改写提示词里一定要强调"保留原始关键词"。

还有一个细节:工单编号、订单号这类结构化信息,改写时要原样保留。这些是精确匹配的命根子,改写了就废了。可以在改写前用正则把这类 ID 抽出来,单独走一次精确检索,结果直接置顶。

4.2 上下文拼装:不是把检索结果一股脑塞进去

检索回来 5 条结果,怎么拼进提示词?很多人直接"\n".join(snippets)就完事了,这是大忌。原因有三:一是顺序混乱,模型对上下文开头和结尾的内容更敏感(所谓的"中间遗忘"现象);二是缺少结构,模型分不清哪条是工单、哪条是知识库;三是没有指令,模型不知道该拿这些内容干什么。

我的拼装模板大概长这样:

你是一个客服助手。请基于以下检索到的历史工单和知识库内容回答用户问题。 【检索结果】 [1] 来源:历史工单 TK-20240512-0087(2024-05-12,相似度 0.87) 标题:退款到账延迟超过 7 个工作日 内容:经排查为银行侧清算延迟,建议用户再等待 1-2 个工作日... [2] 来源:知识库 KB-REFUND-003(2024-03-01,相似度 0.82) 标题:退款到账时效说明 内容:标准退款时效为 3-7 个工作日,超时需人工介入... 【回答要求】 1. 优先参考相似度高的结果,若结果之间冲突,以时间更新的为准。 2. 回答中必须引用来源编号,如"根据 [1]"。 3. 若检索结果不足以回答,明确说明"暂未找到相关依据",不要编造。 4. 不要直接复制粘贴原文,用自然语言组织。 【用户问题】 我上周申请的退款到现在还没到账,怎么回事?

这个模板里有几个关键设计。编号引用让回答可追溯,用户能顺着编号去查原始工单。冲突处理规则(时间新的优先)解决了工单和知识库打架的问题。"不要编造"的明确指令是压幻觉的第一道防线。**"不要复制粘贴"**则避免回答读起来像机器拼接的。

4.3 上下文长度与"中间遗忘"的平衡

检索结果不是越多越好。我试过塞 10 条,结果模型反而抓不住重点,回答变得又长又散。5 条左右是比较舒服的量,如果业务复杂可以到 8 条,但再多就要考虑做二次精排(rerank)了。

另外,把最相关的放开头和结尾。有研究表明大模型对长上下文中间部分的信息利用率偏低,所以拼装时可以把 top-1 和 top-2 放开头,top-3 放结尾,中间放次要的。这个技巧在检索结果较多时效果明显。

还有一个容易被忽略的点:snippet 的截断位置。不要机械地按字数截,尽量在句子边界截断,否则模型看到半句话容易误解。如果用的是分块存储,检索时返回完整块比返回截断片段更好。

5. 幻觉压制与效果验证:怎么知道 Agent 真的"有依据"

5.1 幻觉的三个来源与对应压制手段

Agent 接了知识库还胡说,通常不是模型的问题,而是链路上某个环节漏了。我把幻觉来源归为三类,对应三种压制手段。

第一类:检索没召回,模型硬答。用户问的问题知识库里根本没有,但模型不甘心说"不知道",就编了一个。压制手段是设置相似度阈值:如果 top-1 的分数低于阈值(比如 0.6),直接返回"暂未找到相关依据,建议转人工",不进入生成环节。这个阈值要根据实际数据调,调太低没用,调太高会误杀。

第二类:召回了但模型没用好。检索结果明明有答案,模型却答偏了。这通常是上下文拼装的问题,或者指令不够明确。压制手段是强化指令 + few-shot 示例。在提示词里给一两个"正确引用来源"的示例,模型会模仿。

第三类:多来源冲突,模型自己选了一个。工单说 3 天,知识库说 7 天,模型随便挑了一个。压制手段是在指令里明确冲突处理规则,比如"以时间更新的为准"或"以知识库为准",别让模型自由发挥。

5.2 效果验证:别只看"感觉变好了"

做 RAG 最怕的就是"感觉变好了"却拿不出数据。我建议至少盯三个指标:

指标含义怎么测
首条命中率top-1 结果是否包含正确答案人工标注 100-200 条真实问题
引用准确率回答引用的来源是否真的支持该结论抽样人工核对
拒答率无依据时是否正确拒答构造一批知识库外的问题测试

首条命中率反映检索质量,引用准确率反映生成质量,拒答率反映幻觉控制。这三个指标一起看,才能判断整条链路是否健康。我一般每改一次检索策略或提示词,就重跑一遍这套评测,避免"改了一个地方,另一个地方退化了"。

还有一个实操技巧:把线上真实问题攒起来做回归测试集。每次迭代前跑一遍,看指标有没有掉。这个测试集不用很大,200 条就够,但一定要覆盖各种问法,包括口语化的、带错别字的、带编号的、纯情绪的。

5.3 几个我踩过的坑

坑一:把工单和知识库混在一个向量库里。一开始图省事,工单和知识库切块后塞进同一个 collection。结果检索时工单经常压过知识库,因为工单数量多、表述更口语化,跟用户问题更"像"。后来拆成两个 collection,分别检索再融合,可控性强多了。

坑二:忽略工单的状态。工单有"已解决""处理中""已关闭"等状态,只有"已解决"的工单才适合作为答案依据。我早期没过滤状态,Agent 拿了一个"处理中"的工单当答案,用户照着做发现根本不对。后来在检索时加了状态过滤,只召回已解决的。

坑三:embedding 模型和业务不匹配。通用 embedding 模型在垂直领域(比如农业、医疗)表现会打折。如果预算允许,用业务数据微调一个 embedding 模型,召回率能提升不少。预算有限的话,至少要做领域词典增强,把专有名词加进关键词路。

坑四:忘了更新索引。知识库更新了,但向量库没同步,Agent 还在用旧答案。这个坑最隐蔽,因为检索本身没问题,就是数据旧了。解决办法是建立索引更新机制,知识库变更时触发重新 embedding,或者定期全量重建。工单场景尤其要注意,新工单要能及时被检索到。

6. 把这套东西接进 Agent 框架的实操建议

6.1 Dify、LangChain、AgentScope 的接入差异

不同框架接search_knowledge的方式不太一样,但核心逻辑是通的。

Dify的知识库流水线比较成熟,内置了分段、embedding、检索。你可以直接在 Dify 里建知识库,然后在 Agent 节点里挂"知识检索"工具。但 Dify 默认的检索是单路向量,要做 RRF 双路召回,得用它的"外部知识库 API"接自己的检索服务。我一般把 RRF 逻辑写成一个独立服务,Dify 通过 API 调用,这样灵活度最高。

LangChain的灵活度最高,EnsembleRetriever原生支持多路召回 + RRF 融合,几行代码就能搭起来:

from langchain.retrievers import EnsembleRetriever, BM25Retriever from langchain_community.vectorstores import Chroma vector_retriever = Chroma(...).as_retriever(search_kwargs={"k": 20}) bm25_retriever = BM25Retriever.from_documents(docs, k=20) ensemble = EnsembleRetriever( retrievers=[vector_retriever, bm25_retriever], weights=[0.4, 0.6] )

EnsembleRetriever底层用的就是 RRF。注意weights的顺序要和retrievers对应。这套在 LangChain 里跑通后,再包成 tool 给 Agent 用就行。

AgentScope这类偏 Agent 编排的框架,重点在工具注册和消息流转。把search_knowledge注册成一个 tool,在 Agent 的 ReAct 循环里让它自主调用即可。AgentScope 2.0 提到的 "RAG as service" 思路,本质也是把检索能力服务化,Agent 按需调用。

6.2 工具调用的时机控制

Agent 什么时候调search_knowledge,是个需要调教的事。太激进,什么话都查一遍,慢且啰嗦;太保守,该查的不查,回答没依据。我的经验是在系统提示词里明确检索触发条件:

当用户问题涉及以下情况时,必须先调用 search_knowledge:询问具体操作步骤、询问历史处理记录、询问产品功能细节、报告故障或异常。当用户仅打招呼、表达感谢、要求转人工、或问题明显与业务无关时,不要调用。

再配合工具描述里的"什么时候不用",双保险。实测下来,这样能把无效检索压到很低,同时不漏掉该查的。

6.3 多轮对话里的检索上下文

单轮检索好做,多轮就麻烦了。用户第一轮问"退款慢怎么办",Agent 答了;第二轮用户说"那我要投诉呢",这时候检索 query 如果只用"那我要投诉呢",召回肯定一塌糊涂。多轮场景下,检索 query 要带上对话历史。

做法是:把最近 2-3 轮对话拼进查询改写的输入,让模型结合上下文生成检索 query。比如上面这个例子,改写后应该是"退款延迟 投诉渠道 处理流程"。这样检索才准。但要注意别把整个对话历史都塞进去,太长会稀释重点,2-3 轮足够。

6.4 成本与延迟的权衡

双路召回 + RRF + 大模型改写,链路比单路长,延迟和成本都会上去。我的实测数据是:单路向量检索约 80ms,双路 RRF 约 150ms,加上查询改写(一次小模型调用)约 300ms。整体检索环节 500ms 以内,对客服场景完全可接受。

成本上,查询改写用便宜的小模型就行,别用最贵的。embedding 和 BM25 检索本身成本很低。真正花钱的是最终生成那一步,但那一步无论你检索做不做都要花。所以检索环节的投入产出比非常高,值得做。

如果延迟实在敏感,可以做个缓存:相同或相似的 query 直接返回缓存结果。客服场景里重复问题很多,缓存命中率能到 20-30%,既省成本又降延迟。

7. 写在最后的一点个人体会

这套"历史工单 + 知识库 + RRF 融合"的方案,我在两个不同业务里落地过,最大的感受是:Agent 的智能程度,很大程度上取决于你喂给它的依据质量,而不是模型本身有多强。很多人一上来就纠结用哪个大模型,其实检索这一层没做好,换再贵的模型也是白搭。

另一个体会是,别追求一步到位。我第一版就是单路向量检索,能跑通就行;第二版加关键词路和 RRF;第三版加查询改写和状态过滤;第四版加评测体系。每加一层,效果都有可量化的提升。如果一开始就想把 RRF、rerank、query rewriting 全堆上,很容易陷在调试里出不来。

最后分享一个我常用的调试技巧:把每次检索的 query、召回结果、RRF 分数、最终回答都打日志。出问题时,顺着日志看是哪一环掉的链子——是 query 改写改歪了,还是召回没召到,还是融合排序错了,还是模型没用好。没有日志,你只能靠猜;有了日志,问题定位快得多。这个习惯帮我省了无数时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询