1. 先讲清楚需求Agent查相似问题到底解决什么痛点我做过不少客服场景的 Agent 项目接到这个需求的第一反应不是上大模型跑一下而是先跑到业务现场看了半小时坐席操作。当时那家公司的客服团队每天要处理四百多张工单新员工培训周期是四周很多老员工离职后工单直接变成死数据。坐席接到一个用户问题比如发票开了但收不到邮件新人的常规操作是先查邮件发送日志再问网络组再查发票系统搞半天发现这个case一个月前就有人处理过——当时是邮件模板里链接带了个过期域名。你看问题不是没有数据而是数据散落在历史工单里没人会用。用户提的标题是接上历史工单和知识库让 Agent 有依据地查相似问题本质上就是想把这层经验沉淀固化下来Agent 先检索历史相似工单和知识库条目再基于检索结果去回答你这个问题和某某工单类似当时的处理办法是什么。我理解这个需求的核心价值有三块缩短坐席处置时间把翻历史工单、问老员工这个动作从半小时缩短到几秒钟。保证回答有依据大模型直接回答会一本正经地编答案接上检索之后每句话都能对到具体的工单或知识片段出问题可以追责溯源。沉淀组织经验知识库不是一次性建完而是每次工单处理完自动回写闭环。所以这篇文章我不打算只讲概念我会把从数据准备、检索策略、Agent 编排到上线验证的完整链路拆开讲。你不需要懂很深的算法跟着步骤能把这个功能搭起来。2. 历史工单和知识库的数据处理脏数据决定匹配上限这一步是最容易被低估的。很多人上来就把 Excel 里的工单导出拼成一段文本塞给大模型结果检索出来的内容驴唇不对马嘴。原因很简单原始工单里夹杂着大量无效信息。客服对话里会有亲这边帮您查询一下请稍等不好意思让您久等了这类寒暄话还有系统自动追加的日志片段、时间戳、操作记录。这些东西全部进向量库检索的噪声就特别大。2.1 工单清洗与字段抽取我处理工单数据的第一步是字段梳理一般工单系统里会包含这些字段工单编号、创建时间、关闭时间、坐席 ID用户描述原始问题处理过程中间交流记录结论/处理结果最终的解决方法关联的产品模块或故障类型标签真正有用的是用户描述和结论这两块。其中用户描述用于构建检索时的 query 匹配样本结论用于给 Agent 生成回答提供事实依据。而处理过程内容太多且含对话噪声在构建初期可以先不纳入。清洗规则我一般按这几条走去除非业务字段时间戳、无意义的系统编号、重复的回执信息。压缩连续空白和换行避免分段时产生大量空片段。把用户描述 结论组合成一条标准记录例如问题发票已开具但客户邮箱一直收不到发票邮件。 处理结果排查发现邮件模板中的链接使用了已过期域名更新为新的域名后重新发送客户正常收到。清洗完的数据需要去重。同一用户反复提单、同一个问题开了三张工单这些情况在历史数据里非常常见。我的做法是对用户描述做一次向量化计算互相之间的相似度高于 0.95 的合并保留一条因为后续做评估集的时候重复样本会干扰指标计算。2.2 知识库的分类与片段切分知识库和工单数据不一样。工单是问题-结果的短文本知识库往往是产品文档、FAQ、操作手册篇幅长、层级多。喂给检索前必须切分。切分粒度没有标准答案我通常按主题完整 长度可控来控制一段最少 200 字、最多 800 字太长会稀释语义太短会丢失上下文。比如一篇产品文档里讲如何配置邮件服务器下面包含SMTP 设置发件人认证常见报错几个小节。如果整个文档直接切片Agent 查用户收到邮件反复退信时可能匹配到的是文档开头的环境说明而不是常见报错里的具体解决办法。我的做法是按目录层级切每个 H2 或 H3 对应一个 snippet并把标题拼进片段内容里。处理完的片段可以加上元数据后面检索时能根据元数据过滤字段示例用途source_type工单 / 知识库 / 手册来源区分product发票系统产品线过滤ticket_id / doc_id工单号 / 文档编号回答引用标注tags邮件、域名、退信关键词检索辅助2.3 向量化和索引构建文本切完之后就是向量化。向量化这一步的选择比很多人想象的重要。我见过项目在一开始就迷信用最贵的 embedding 模型结果效果不好还特别慢。实际中我按这个思路选中文场景优先用兼容中文和代码的模型如果历史工单里大量包含报错日志、SQL、JSON 片段那模型对代码语义的理解能力就非常关键。我当时验证过一个现象有些通用 embedding 模型把接口返回 500和网关超时映射到离得很远的位置但实际业务里它们指同一个问题——所以不能只依赖通用 embedding必须配合关键词权重。这部分放到检索章节细讲。向量化之后要解决索引问题。如果数据量在两三万条以内直接用向量数据库或者支持向量的开源组件就行不需要上分布式方案。我记得当时用的方案是把向量存进带 HNSW 索引的表里查询的时候 topK 取 20 条检索延迟大概在 50ms 到 100ms足够用。这里有个经验索引构建完一定要抽样验证分布。随机抽 100 条工单拿用户描述去检索人工看召回的前 10 条有没有同场景的工单如果多次出现问题明明很像但召回一堆不相关条目的情况多半是切分粒度或字段抽取有问题先不要急着调模型回去看数据。3. 检索策略相似问题匹配从像到准数据准备好了Agent 拿到一个用户问题后要做什么我把它拆成三步重写查询、混合检索、重排序。这个链路决定了 Agent 能不能有依据地查相似问题。3.1 查询重写口语问题变成搜索句式用户不会按照知识库的写法提问。真实对话里出现的是我这个发票怎么一直开不出来啊而知识库里写的是发票开具失败原因排查。这两句话直接做向量匹配效果通常一般因为语义上有距离。所以需要先做一次查询重写。我用的是 LLM 改写把用户问题改写成一个适合检索的陈述句或关键词组合。比如上面的问题改写为发票开具失败 原因 排查。查询重写有几个细节要注意保持原问题的实体词不丢不能只做压缩。改写后的 query 用作用户问题的补充不要替换原文而是多路检索时各用各的后面再融合结果。改写速度要快用一个小模型或快速模型即可不需要走主 Agent 模型。3.2 混合检索向量 关键词双通道只靠向量检索的坑前面提过。实际项目中我更常用混合检索向量召回 关键词召回。关键词召回怎么做基于 ES 的分词检索或者用简单的 BM25 算法把 query 里的实体词拆出来去匹配元数据里的 tags 和标题字段。为什么要混合我用一个真实例子说明。某工单的问题描述是导入订单一直显示失败报错超时知识库里有一条标题是批量导入订单超时处理方案。这两句话的语义和关键词都高度重合向量能匹配、关键词也能匹配属于理想情况。但还有些 case 是客户想改收货地址但订单已发货知识库里没有写改地址只有订单发货后不能修改订单信息。这时向量匹配能命中关键词匹配则完全失效。反过来也有问题里全是专业名词和产品名比如PDA 扫描枪扫码后数据不上传如果 embedding 模型对PDA和扫描枪的领域含义理解弱向量匹配会跑偏但关键词召回能通过PDA这个词精确命中文档里专门讲 PDA 的章节。所以混合检索的必要性就在这里。我的融合策略是 RRFReciprocal Rank Fusion对两个通道各自的排序结果给分然后按分数融合。实现代码很短# 伪代码RRF 分数融合 def rrf_score(rank, k60): return 1.0 / (k rank) # 对向量召回 topN 和关键词召回 topM 打分 scores defaultdict(float) for rank, doc_id in vector_topk: scores[doc_id] rrf_score(rank) for rank, doc_id in keyword_topk: scores[doc_id] rrf_score(rank) final_ranking sorted(scores.items(), keylambda x: -x[1])RRF 的好处是不需要精细调参两个通道只要某个通道排进前列就能贡献排名分。实际项目中我基本都是先跑通 RRF再根据效果决定要不要上训练式的重排模型。3.3 粗排到精排重排序与阈值混合检索出来的 top20 条仍然不够准。我观察到的一个典型现象向量召回的前几条常常是整体语义相似的但它们可能是同一个模板里的不同变体真正解决问题的结论藏在第 8 条之后。这时候需要重排序rerank。重排序的本质是基于查询和候选内容逐对打分比双塔向量更精细。成本高一些但只需要对 top20 条打分速度仍然可控。实现上有成熟的 cross-encoder 模型可以直接调用我当时处理的是中文工单实测下来用 cross-encoder 比单纯向量排序提升明显尤其是在问题描述很短、解释很长的场景下。重排之后要设置召回阈值。这一步经常被忽略不设阈值就会导致另一个大问题没有相似问题的时候Agent 会强行从最不相关的检索结果里编答案。我在生产系统里见到过这种事故用户问一个知识库根本没覆盖的新功能问题Agent 却从旧工单里找了一个看起来有点像的答案回复了出去造成误导。我的处理方案是对重排后的 top1 计算一个匹配分数低于阈值则判定为未检索到相似问题。Agent 收到这个信号后走兜底话术当前知识库未覆盖您的问题我已将问题转人工处理。阈值在离线评估集上做调优用标注好的正负样本画 PR 曲线选兼顾召回率和准确率的点。阈值这个参数不是玄学是可以被评估的。我会在后面的上线验证章节展开讲。3.4 检索链路在 Agent 里的角色把检索策略讲完如果你的项目只是一个检索服务 一段 Prompt那其实已经不缺什么了。但既然标题说的是让 Agent有依据地查那 Agent 的编排也很关键。Agent 的核心能力是决定什么时候去查、查完之后怎么用检索只是它的工具之一。4. Agent 编排让模型学会调用工具而不是背答案很多人做一个客服问答 Agent的时候习惯把所有内容都塞进 system prompt 里让大模型记住全部知识。这个做法在小范围演示没问题放到生产环境立刻爆。知识库几万条工单你塞不进上下文就算塞得进去token 成本也扛不住。真正可行的做法是把知识库和历史工单变成 Agent 的检索工具让模型在需要时主动调用。4.1 工具定义与 Function Calling先定义一个检索工具大模型通过工具调用去执行检索。这个工具在 OpenAI 格式里叫 function在开源框架里往往叫 tool。定义大致是这样{ name: search_similar_issues, description: 在历史工单和知识库中检索相似问题返回处理方案和参考来源, parameters: { type: object, properties: { query: { type: string, description: 检索用的标准问题描述尽量包含核心症状和产品名 }, top_k: { type: integer, description: 返回的候选条数默认5 } }, required: [query] } }工具描述里我得交代清楚参数的意义尤其是top_k。我的经验是Agent 模型对 tool 的参数赋值有时并不准尤其当用户的问题特别长、带多段描述时模型可能不知道应该截取哪一段。所以我在工具提示里加了尽量包含核心症状和产品名避免模型把一长段对话原文都塞进去。4.2 工作流设计先查询、再生成Agent 的一次完整回答流程我建议按顺序规定死而不是完全放开让模型自己想。完全放开的 Agent 在客服场景很容易跑偏比如模型跳过检索直接凭训练知识作答这就失去了有依据的意义。我采用的是固定编排 一次检索的工作流用户问题 - 查询重写 - 调用检索工具 - 组装上下文 - 生成回答第一步对用户原始问题做一个快速改写把口语化句子压缩成搜索句式。第二步调用上面定义的检索工具传改后的 query取回 top5 候选。第三步把这 top5 候选组装成上下文限定长度按候选来源拼接标注来源标签【工单#100234】问题: 发票已开具但客户收不到邮件 处理结果: 排查发现邮件模板链接为过期域名更新域名后正常发送 【知识库-邮件故障排查】...第四步生成回答时专门加一条 Prompt 约束只允许基于上下文内容作答上下文找不到答案时必须明说未找到相似问题记录。这一步因为工作在系统侧完成了查资料的动作大模型实际上在被约束的范围内做总结提炼幻觉空间被压缩到最小。有人可能觉得这样多了一步工具调用延迟高不高实测下来检索耗时几十毫秒加上工具调用的一个来回总体延迟增加不多比起无检索的大模型直接答在客服场景可以接受而且方案收益远大于这点延迟成本。4.3 上下文组装与引用标注组装上下文时有个细节不要把 top5 全部塞进去。有些候选相似度并不高全部塞给大模型会影响判断还可能让模型被噪声带偏。我通常只取重排后 top3最多 top5而且每条候选要限制长度长工单要截断到三百字以内的核心结论部分。同时给每个候选条目标注来源编号要求模型在回答的末尾列出引用来源。比如以上结论参考自工单#100234、知识库《邮件故障排查》。这一步的价值在于可追溯业务人员看到 Agent 的结论之后能一键打开原文核对这是企业内部工具能不能获得信任的分水岭。我在项目里还发现一个现象坐席对 Agent 的信任度很大程度取决于它敢不敢说不知道。如果 Agent 永远给答案但答案总出错坐席很快就会放弃使用。反过来如果 Agent 有依据地引用并明确标出不确定的部分坐席会更愿意参考。前面讲过的阈值兜底机制在这里的价值就完全体现出来了。5. 上线前的评估与持续补齐从 Demo 到生产环境Demo 阶段检索效果不错不代表放到生产环境就能用。我见过太多项目死在这一步演示时专门挑几条好命中的问题跑给领导看一上线真实流量进来问答质量立刻崩掉。5.1 构建评估集与指标我把评估集分成两类。一类是历史工单复现随机抽最近三个月已办结的工单拿用户原始问题去检索看能否命中原工单或同类工单。一类是真实用户长尾问题整理客服群里用户各种五花八门的说法因为这才是新问题的主要来源。评估指标上我关注三个Top5 召回率相似工单出现在前 5 条里的比例反映检索能力。引用准确率Agent 引用来源时来源是否真的支持它的回答。这个需要人工抽检。兜底触发率触发未找到相似问题的比例这个指标太低说明可能乱答太高说明知识覆盖不足。5.2 持续补齐机制工单闭环回写前面提到工单清洗这里我想强调闭环回写的重要性。让 Agent 有依据地查相似问题如果知识库不持续更新Agent 的依据会越来越陈旧。我见过的方案里比较有效的做法是每张办结工单打标后自动进入待向量化队列。每天凌晨跑一次增量索引新工单进入检索池。每周人工抽检一批新检索样本把误召回的数据从库里摘掉或修正。为什么需要摘除误召回因为工单里也会混入一些特例甚至处理错误的工单。比如某次用户退不了款是因为坐席操作失误工单结论写的是重试后成功这类工单流入知识库后续遇到相似问题就会给出错误的引导。所以除了增量写入一定要设计质量淘汰机制。这类机制可以用简单的标记 - 人工审核 - 下架流程先跑起来等工单量大了再考虑自动化质量评分。5.3 一个典型的现场故障与排查链路说到问题排查我分享一个具体案例这比抽象的警告更直观。当时系统上线后某类问题频繁出现检索错误用户问域名解析失败网页打不开检索结果里排第一的是如何修改域名解析记录的知识片段当时从产品语义上和问题高度相关但对坐席来说用户是需要一个排障结论不是配置操作文档。真实场景里这两个东西在向量空间里距离很近但业务目的完全不同。排查链路是这样走的先看检索排序发现 top1 是配置文档top5 里混着大量配置类文档。于是查召回来源分布发现故障排障类工单占比远低于配置操作类文档。回到数据侧分析发现配置类文档普遍篇幅短、标题包含很多关键词比如解析、域名跟 query 重合度高而排障类工单里用户口语描述中关键词少。处理方法是调整数据源权重检索结果融合时给排障工单的分数加权同时在元数据里加问题类型字段重排时优先展示故障排查类候选。这个问题暴露出来的核心教训是检索做的是字面和浅层语义匹配业务意图的区分度必须靠数据分层和加权调优来做。这也是为什么我强调上线后要持续做抽检而不是一劳永逸。6. 一些可以继续扩展的方向如果你按上面的步骤把这个历史工单 知识库检索 Agent搭起来了你已经解决了核心问题。不过有几个扩展方向我觉得值得提前想清楚它们会影响到后期维护成本。多轮对话处理用户问完第一个问题再追问那这个故障会影响多久这时候 Agent 应该结合上一轮检索到的工单内容来回答而不是重新检索一次。可以用会话内记忆把历史工具调用结果保留在上下文里一起拼接给生成模型。相似案例的对比展示如果坐席端不需要直接给用户最终答复而是辅助人工检索结果可以按相似度 方案类型分组展示甚至用聚类方式把同一类问题的多个工单折叠成一条展示减少人工阅读负担。权限隔离历史工单可能包含不同客户的敏感信息多层出租的场景按租户隔离检索池是边界条件这必须在架构上预留否则后期数据权限整改成本很高。这些方向不一定要上线第一天就做全但是想清楚边界能避免后面翻工。我在实际项目里体会最深的一点是这类 Agent 项目真正的分水岭不在大模型选型而在数据工程和评估机制。模型效果差别不大但数据清洗、切分策略、检索加权这些环节每差一点最后呈现的效果天差地别。所以如果你想复现这个项目我建议从周一的脏工单数据开始动手而不是从选模型开始。先把 100 条真实工单清洗好、检索出 10 个像样的结果后面所有环节才有优化的基础。