ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent接历史工单与知识库:RAG多路召回与RRF融合实战

Agent接历史工单与知识库:RAG多路召回与RRF融合实战 1. 为什么能查历史工单比能聊天重要得多很多人做 Agent 的第一版都是奔着能对话去的接个大模型挂个系统提示词用户问什么它答什么。跑个 Demo 看着挺唬人一旦放到真实业务里问题立刻暴露——用户问我上周提交的那个退款工单处理到哪一步了Agent 一脸茫然因为它根本不知道上周那个工单是什么。这就是没有依据的 Agent它能说会道但说的都是通用知识跟你的业务数据毫无关系。我做过好几个客服和运维方向的 Agent 项目踩过最深的坑就是用户要的从来不是聪明的回答而是有依据的回答。所谓依据无非两类东西——一类是历史工单也就是过去真实发生过的、被人工处理过的相似问题另一类是知识库也就是沉淀下来的标准答案、操作手册、FAQ。把这两样东西接进 Agent让它先检索、再回答这就是 RAGRetrieval-Augmented Generation检索增强生成在 Agent 场景里最朴素也最有效的落地方式。这篇要聊的就是标题里说的那件事接上历史工单和知识库让 Agent 有依据地查相似问题。核心链路其实就一条用户提问 → 检索相似历史工单 检索知识库 → 融合排序 → 喂给大模型 → 生成带出处的回答。听起来简单但真正做起来检索质量、排序策略、上下文拼装、幻觉控制每一步都有讲究。关键词里的search_knowledge、RAG、RRF就是这条链路上的三个关键锚点search_knowledge是工具入口RAG是整体范式RRFReciprocal 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.87BM25 可能是 12.5你直接加权平均等于让 BM25 主导了排序向量那路白做了。归一化能缓解但归一化本身又会引入新的偏差。RRF 的聪明之处在于它完全不管分数只看排名。公式很简单RRF_score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档 d 在第 i 路召回里的排名k是一个平滑常数通常取 60。举个例子某文档在向量召回里排第 1在关键词召回里排第 3那么它的 RRF 分数就是1/(601) 1/(603) ≈ 0.0164 0.0159 0.0323。另一文档在向量里排第 5关键词里排第 2分数是1/65 1/62 ≈ 0.0154 0.0161 0.0315。前者略高因为它两路都靠前。RRF 的好处是天然抗量纲差异而且对两路都靠前的文档有奖励。一个文档如果只在向量里排第一、关键词里排一百名开外它的 RRF 分数会被拉低反之两路都进前十的文档会脱颖而出。这正好符合我们的直觉真正相关的文档应该被多种检索方式同时认可。3.3 落地 RRF 的几个实操细节第一k 值的选择。k60 是论文里的经典取值它的作用是削弱头部排名的绝对优势让排名靠后的文档也有机会。如果你的召回结果质量普遍很高可以把 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, start1): scores[doc_id] scores.get(doc_id, 0) weight * (1.0 / (k rank)) return sorted(scores.items(), keylambda x: x[1], reverseTrue)这段代码可以直接用。rankings是各路召回的文档 ID 列表已按相关性排序weights是对应权重。实测下来工单场景给关键词路 1.2、向量路 1.0效果比等权好一些。第四去重要在融合前做。同一篇文档可能被两路都召回融合时按 doc_id 聚合分数即可但要注意别把不同来源的同名文档搞混所以 doc_id 最好带上来源前缀比如ticket_TK-0087、kb_REFUND-003。3.4 一个完整的双路召回 RRF 流程把上面的东西串起来一次完整的检索大概是这样的用户提问Agent 判断需要检索调用search_knowledge。查询改写把口语化问题改写成检索友好的关键词组合。向量路改写后的 query 做 embedding向量库检索 top-20。关键词路改写后的 query 走 BM25/全文索引检索 top-20。RRF 融合两路结果按 doc_id 聚合加权计算 RRF 分数排序。时间过滤与来源过滤按time_range和source_filter筛掉不符合的。取 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-00872024-05-12相似度 0.87 标题退款到账延迟超过 7 个工作日 内容经排查为银行侧清算延迟建议用户再等待 1-2 个工作日... [2] 来源知识库 KB-REFUND-0032024-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, k20) 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 改写改歪了还是召回没召到还是融合排序错了还是模型没用好。没有日志你只能靠猜有了日志问题定位快得多。这个习惯帮我省了无数时间。
返回列表