接上历史工单和知识库这个需求,听起来好像就是“给Agent配个资料库”这么简单,但真正落地的时候会发现,问题从来不是“查不查得到”,而是“查到的东西怎么用、敢不敢信”。我做了几个企业客服场景的Agent项目之后最大的感受是:Agent能不能稳定给出可靠答案,七成取决于你喂给它的历史工单和知识库怎么收拾、怎么检索,剩下三成才轮到模型本身和Prompt设计。这篇文章就把我在实际开发中踩过的坑、试过的方法、调参的心得全部整理出来,从数据准备、检索增强到工具调用和防幻觉兜底,一条线给你讲清楚。
这整套方案的核心思路是:不指望LLM记住所有业务知识,而是让它在面对相似问题时,先去历史工单和知识库里检索证据,再基于检索到的内容组织答案。适合正在做企业级知识问答、客服工单自动处理、内部IT支持这类场景的开发者参考,哪怕你还没接触过RAG,按照文章里的步骤也能搭出一个能跑通的“有依据问答”系统。
1. 为什么必须让Agent“查”而不是“记”:需求拆解与架构选型
1.1 幻觉问题的本质:LLM不知道就是不知道
先说一个几乎所有Agent项目里都会遇到的老大难问题——幻觉。现象很熟悉:你问Agent“上次XX系统故障怎么处理的”,它给你编了一个看起来逻辑通顺、但实际从没发生过的解决方案。如果你拿这个答案发给用户,轻则被质疑专业性,重则误导用户做出错误操作,那就不是尴尬而是事故了。
幻觉的根源在于,大语言模型本质上是一个“概率补全机器”。它学习的规律是“这些词接在这些词后面比较合理”,而不是“这件事的真相是什么”。当它遇到训练数据里没有覆盖到的、或者出现频率很低的信息时,就会启动自己最擅长的“合理脑补”模式。这里有一个区分点很多人容易忽略:模型不是数据库,不像MySQL那样你能精确查到一条记录就返回那条记录。哪怕模型回答错了,它也绝不会告诉你“我不知道”,它只会给你一个“看起来很有信心”的答案。
所以对抗幻觉的第一原则是:别逼模型用“记忆”回答问题,而是给它“外挂”——历史工单和知识库。让模型从外部资料里去检索,再基于检索到的内容进行摘要和重组。这样就算模型本身的记忆里没有这个知识,它也能借助外部证据给出有出处的答案。这就像考试允许开卷,你手里有参考书,答题的正确率自然比空手裸考高很多。
1.2 RAG、微调、长上下文:三条路怎么选
想让Agent“有依据”,最常见的三个技术路线分别是RAG(检索增强生成)、微调(Fine-tuning)、长上下文直接投喂(Long Context)。我在项目里全都试过,说说各自的优缺点。
先说长上下文直接投喂。之前有段时间我很偷懒,想把几千条历史工单全部塞进Prompt里,让模型一次性读完。结果发现两个致命问题:第一,token费用高得离谱,每问一个问题都在烧钱;第二,模型实际上并不会真的“读完”几万token的文本,它对中间位置的注意力会大幅衰减。论文里叫“Lost in the Middle”,意思就是模型对长文本首尾的内容记得清楚,中间一大段基本是模糊的。你塞进去5000条工单,它真正能利用的可能就头尾几十条。这个方案对一些短文档场景勉强能用,但面对历史工单这种动辄上万条、单条几百上千字的场景,完全不现实。
再说微调。微调的本质是让模型调整自身的权重,把某些知识“内化”到模型里。这个方案的问题在于:第一,历史工单和知识库是动态变化的,今天新增一个问题,明天修改一个SOP,难道每次都重新微调?第二,微调需要高质量标注数据和时间成本,对于中小团队来说投入产出比太低。第三,也是最关键的:即使微调得很好,你依然无法控制模型“复述”出来的内容是否跟原始资料逐字一致,幻觉风险只是降低了,没有消除。
最后说RAG。RAG的思路是“不改变模型,改变模型的输入”。每次提问时,先从向量数据库或搜索引擎里召回与问题最相关的几条历史工单或知识片段,然后把它们作为参考上下文揉进Prompt里,让模型基于这些材料给出答案。它的优势非常明显:知识更新只需要替换索引里的数据,不需要重新训练模型;每个答案都有明确的资料出处,可以追溯;而且检索召回本身是一个确定性过程,可以测试、可以调优、可以解释。我现在的项目基本都是这个路线,核心就是“先查证,后回答”。
1.3 Agent+历史工单+知识库的整体协作逻辑
明确了用RAG,接下来要回答的是Agent和RAG怎么配合的问题。很多朋友理解的“RAG”是:用户问一句,后台把这个句子拿去检索,然后把结果拼进Prompt,再让模型回答。这个模式叫“单轮检索增强”,能解决一部分问题,但距离“有依据地查相似问题”还有差距。
真正做Agent化的RAG,应该让模型自己决定“要不要查”“查什么”“怎么查”。这就要用到Function Calling或者工具调用机制。举个例子:用户的问题很可能是“打印机一直报错E02,之前遇到过吗”,你直接拿这个完整句子去检索,效果可能一般。但是如果你把这个句子拆解成检索参数——问题类型“打印机报错”、故障代码“E02”、设备型号“HP LaserJet M1005”——检索出来的结果会精准得多。
所以我会把整个流程设计成:Agent先理解用户问题的意图,判断这是否需要检索历史工单/知识库,如果需要,就调用一个专门设计的search_knowledge工具,传入经过解析的关键词、分类字段、时间范围等参数。工具返回一批相似工单和知识文档,Agent再对这些内容进行总结、推理,最终给出带引用的回答。整个过程就像一个经验丰富的客服:先查系统里的历史记录,再结合当前情况给方案,而不是凭记忆随口说。
2. 数据准备:把散乱的历史工单变成高质量知识资产
2.1 工单清洗:去掉噪声,留下可检索的“答案”
我接手过的历史工单,少则几千条,多则几十万条。乍一看数据量很大,但真正清洗下来才发现,大量工单是没法直接使用的“脏数据”。这里说的脏不只是格式乱,更是内容本身没有检索价值。
最常见的几类废数据:一是内部流转信息,比如“工单转给张三处理”“李四回复:收到,正在排查”,这类信息不包含任何问题解决方案;二是原始报障描述里带了大量无关的闲聊、情绪发泄、环境信息;三是很多工单是操作记录而非解决方案,比如“重启了服务器”“更换了网线”,但没有说明“为什么要这么做”“这个操作之后现象是否消失”。如果直接把这类记录塞进知识库,Agent检索到之后也无法给出有价值的答案。
我的清洗思路是提炼“问题-原因-方案”三元组。拿到一条原始工单后,会先拆成几个字段:问题描述(用户报障时说了什么)、诊断过程(排查步骤和关键现象)、最终方案(怎么解决的)、关联标签(涉及的系统、模块、错误码)。清洗的目标不是把工单变成一篇漂亮的文章,而是把它变成一段“可以被检索到、且检索到以后可以直接回答用户”的内容。
这里有个实用小技巧:清洗不完全靠人工,我用LLM(比如GPT-4级别的模型或者开源大模型)做初步抽取,让模型把每条工单转成结构化JSON。注意抽取的时候要给一个明确的Prompt模板,比如“请从这段工单中提取:问题概述、触发条件、排查过程、解决方案、涉及系统。如果某个字段不存在,直接写无,不要编造”。跑完之后人工抽检一部分修正格式,这样清洗效率会高很多。
2.2 文本切分:chunk策略决定检索质量的上限
清洗好的工单内容要进入检索系统,面临一个绕不开的问题:怎么切分文本?向量检索和关键词检索的unit都是“一段文本”,这段文本的大小和边界直接决定了召回的质量。我见过太多人直接拿整篇工单去做Embedding,结果就是检索效果稀烂——向量只有一个,但工单里可能包含排查过程和最终方案两种完全不同类型的信息,语义混在一起,Similarity Score上不去。
切分的核心原则是“保持语义完整性”。我踩过很多坑之后总结出了一个相对实用的策略:按语义段落+长度上限双重控制。比如把一条工单按“问题描述”“诊断过程”“最终方案”这三段来切,每段控制在200~500字左右。如果某一段特别长(比如诊断过程写了2000字),再按句子或子段落切分成多个chunk,但要保证每个chunk单独看依然是完整的、可理解的。
实际工程中很多团队喜欢直接用LangChain的TextSplitter,但默认参数(比如chunk_size=500, chunk_overlap=50)不一定适合工单场景。工单数据的一个显著特点是信息密度不均衡,前半段可能是详细的问题背景,后半段可能就是结论。我会在设计chunk时额外添加一个“head”字段,就是每个chunk的第一句或前两句话,作为索引的摘要。这一步看似简单,但对提高召回率帮助特别大——因为向量模型对开头部分的语义更敏感。
2.3 向量化与元数据管理:让检索结果更精准
文本切好之后,接下来是向量化。当前主流的Embedding模型,比如OpenAI的text-embedding-3-small、开源的bge-large-zh、text2vec系列,都能把中文文本转换成密布语义信息的向量序列。选择Embedding模型时,不一定要追最新最强的模型,要看它的中文语义理解和同义改写能力。实际测试中我发现,企业对工单里的“黑话”(比如“机子卡死”“页面白屏”“内存溢出”)有不少同义词,Embedding模型如果理解不了这种映射,检索质量就会拉胯。我自己用的比较多的是bge-m3,它对中文支持不错,且支持多语言混合,关键还能在CPU上跑推理,部署成本很低。
向量化做完之后,库里的每一段文本都会对应一个向量。但向量只是一个“容器”,它本身不会告诉你这段文本属于哪个工单、涉及系统是什么、发生在什么时间。所以一定要把元数据建好。以工单为例,我的元数据设计长这样:工单ID、工单标题、问题类型(比如“打印机故障”/“网络异常”)、涉及系统/设备、错误码、解决时间、人工最终方案。这些元数据表面上看起来只是“附加信息”,但它们在三路召回和过滤中极其有用——后面讲检索的时候你就会体会到,类型字段和错误码字段可以直接缩小搜索空间,把准确率提上来不止一个档次。
这里的重点是说:知识库构建绝不只是“把文档丢进Embedding模型”,而是一个包含清洗、切分、向量化、元数据管理的完整工程。你前面越用心,后面Agent查相似问题就越准,这是一个收益随投入递减的良性循环。
3. 检索增强:让Agent能查到“相似”的问题
3.1 双路召回:关键词检索与向量检索的互补
很多人对RAG的刻板印象是“Embedding向量检索就是一切”。但实际生产环境里,只靠向量检索往往不够——至少不能只靠一种。这里讲一个我真实踩过的坑:工单里有一个很常见的字段叫“错误码”,比如E02、500、timeout。向量模型在计算“E02”和“E03”的语义时,会觉得它们“差不多”或“都很接近”,但业务上E02和E03的处理方案完全不同。一个靠向量相似度来检索的系统,能把E02的工单召回出来,很可能也会把E03的工单一起拉回来,结果就是答非所问。
所以我采用的做法是“双路召回”:向量检索+关键词检索并行,然后再把两路结果合并。关键词检索用的是传统的BM25算法(或者ES里的Match Query),它的核心是字面匹配,对错误码、型号、设备编号这类精确token的识别比向量模型靠谱得多。举个例子,用户说“Jetpack模块报错401”,BM25能精确匹配出包含“Jetpack”“401”的历史工单,而向量检索可能召回到“401未授权”相关的其他工单。两条路侧重点不同,配合使用就是一个“既懂语义、又懂关键字”的组合。
工程上怎么落地也简单:如果你用了Elasticsearch,那就用它的KNN(向量检索)和BM25原生能力做混合查询,设置权重比如0.7向量+0.3关键词;如果团队用的是向量数据库+独立搜索,也可以在代码里写一个并行检索函数,最后按相关性得分加权融合。初版可以先暴力一点,把两路Top20的结果全合并进去,让后面过滤器决定谁留下。
3.2 重排序模块:别让最强证据淹没在结果里
双路召回得到的结果是“候选集”,而候选集里真正有用的可能就那么一两段。这时候你需要一个重排序(Rerank)模块。重排序的职责是:拿用户的问题,对已经召回的候选文档逐一计算细粒度相关性,把最相关的排到最前。
我用过几种重排序方式,简单介绍一下。第一种是直接用LLM做Rerank,把用户问题和候选文档组合成一个Prompt,让模型判断“这段内容是否与问题相关,相关程度是多少”。这种方式准度高,但速度慢、token花费也高,适合对精度要求极端的场景。第二种是用专门的Rerank小模型,比如bge-reranker-base,它能在几百毫秒内对几十个候选做分数计算,效果也非常好。第三种是“启发式重排”,看起来不那么优雅,但实际很好使:给检索结果附加权重,比如“错误码精确匹配+80分”“标题关键词命中+30分”“方案字段存在+20分”“时间越近加分”,这样就能把最强证据顶到前面。
我现在的项目用的是“小模型Rerank+启发式权重”的组合。先让bge-reranker算一遍语义分数,再叠加元数据命中情况做加权修正。实际效果是:Top1的命中率会比纯向量检索提升20个百分点以上。这个环节是很多人忽略但性价比最高的部分。
3.3 参数调优:TopK、Score阈值、窗口大小的实战设置
检索环节有一堆关键参数,看起来不起眼,但其实对最终效果影响巨大。
第一个是TopK,也就是召回数量。太小了容易漏掉正确答案,太大了会把噪声也带进来。以工单场景为例,我通常设置TopK=20,也就是双路各召回20条,然后Rerank后取Top3~5作为最终上下文。这个数字不是拍脑袋拍出来的。我做过实验:Top1的准确率通常只有60%左右,Top3能到85%,Top5能到95%。但再往后,多召回的文档不仅没有提升准确率,反而因为占了Prompt的位置分散模型注意力,轻微拉低了效果。所以TopK=5是我们项目的常用配置。
第二个是Score阈值。向量检索出来的相关性分数,在不同Embedding模型下取值范围差异很大(有的是0~1,有的可能是负值)。如果只设一个硬阈值,很容易出现两种极端情况:要么把低质量的噪声全放进来,要么把一些本来可用的答案全挡在门外。我在实践中更推荐“按位次截断+动态阈值”的策略:先按排序取TopK,再做一个最低分数线,比如相似度低于0.5的直接丢弃。这个0.5也不是固定的,要根据你实际数据的分布去标定。比如你已经构建了5000条向量,可以抽样100条正样本、100条负样本,分别统计相似度分布,然后选一个能把正样本大部分留住、负样本大部分滤掉的点作为阈值。
第三个是上下文窗口大小。检索回来的段落如果太长,直接塞进Prompt会爆token,甚至被模型当成长篇大论忽略掉。我的做法是,取Rerank后的Top3~5个chunk,每个chunk截取头150~300字作为参考上下文。如果这些chunk里有一致提到的同一个方案,Agent的答案大概率就是这个方案。另外,我强烈建议把检索到的原始文档出处也拼接进上下文里,这样后续溯源和Step-by-step验证都方便。
4. 工具调用与工程化落地:Agent如何“有依据”地回答
4.1 Tool规范:一个search_knowledge函数的设计
Agent能不能正确地利用历史工单和知识库,关键在工具定义。从工程角度,我们需要给Agent暴露几个供它思考时调用的Function,其中核心的一个就是search_knowledge。这个工具的入参设计很重要,因为它决定了Agent的“策略空间”。
我设计的search_knowledge参数大致如下:
- query(必填):用户问题转换成检索词,Agent需要自己改写成简洁清晰的检索句。
- question_type(可选):问题类型,如硬件故障、网络异常、账号权限,Agent根据用户描述判断。
- error_code(可选):错误码,比如E02、500、404,如果有精确的值一定要填上。
- system(可选):涉及系统或设备,比如ERP系统、打印机型号。
- 时间范围from/to(可选):可以限定工单时间范围。
这个工具的设计逻辑是“让参数尽可能反映业务特征”。比如用户说“我的电脑连接到公司网络之后报错,提示错误代码500”,Agent会调用search_knowledge,query填“电脑连接网络报错500”,error_code填“500”,system可能填“网络设备”,这样搜索引擎召回的结果就会精准锁定在“报错500”的工单里,而不是把“500万预算案审批”这种无关工单也拉出来。
你可能会问,为什么不直接把用户的整句话拿来做query?因为在真实场景里,自然语言的句子做检索,噪声很大。比如“有没有人遇到过打印机卡纸和一直嗡嗡响的问题?在线等,急”,直接拿这句话检索,会匹配到一堆冗余词汇。让Agent先做意图理解和字段抽取,再调用工具,其实是把“语义解析”和“检索”分成两步,这是Agent化RAG和普通RAG最大的区别之一。
4.2 Prompt设计:让Agent学会用检索结果而不是背答案
有了工具还不算完,另一个极其关键的点是Prompt。Agent要能意识到“我需要先查一下知识库”,并且在拿到检索结果后能克制住自己“直接发挥”的冲动,老老实实基于内容作答。
实际开发中我会在System Prompt里明确写清楚Agent的工作流和边界,比如:
- 你是一名客户服务专家。在回答任何与产品使用、故障处理相关的问题前,你必须调用search_knowledge工具检索历史工单和知识库。
- 当检索结果中包含明确答案时,请优先基于检索内容回答,并且在回答中引用对应工单编号或知识文档标题。
- 当检索结果不包含答案或与问题不相关时,你应该告知用户“当前知识库中没有找到准确的解决方案”,并建议用户联系人工客服,而不是自己猜测。
- 回答时请使用简洁、清晰、口语化的语言,适合非技术背景的用户阅读。
Prompt还有一个细节容易忽略——Agent在生成回答时,需要看到检索结果中“哪一条和当前问题最匹配”。我在返回给Agent的上下文里,除了文档正文,还会附上Rerank的分值和来源ID。比如[Doc1](相似度0.92,来自工单20230415-001):打印机卡纸解决方案...。这样Agent就有了判断依据:优先参考高分文档,低分或明显不匹配的文档可以直接忽略,甚至能识别出“这条检索出来的内容虽然相关性一般,但里面提到了和用户问题的共同点”。
另外就是要规范“引用格式”。我通常让Agent在答案末尾输出“参考来源:工单#20230415-001”这样的格式。这不仅能提高可信度,还能为后续做自动化评测提供数据——比如你可以统计Agent回答里引用的工单是否正确,以此衡量系统的准确率。
4.3 引用溯源与兜底策略:避免幻觉的最后一层防线
就算检索和Prompt都做好了,Agent偶尔还是会“放飞自我”。这时候就需要兜底机制来拦截。我总结下来的三层防线是:
第一层,答案后验证。Agent生成回答之后,让一个独立的验证模块(或者Agent自己)重新检查一遍:回答里的关键信息是否都能在参考文档里找到对应文本片段。这相当于让Agent做一次“自我核对”——如果你发现它回答了一个文档里完全没有的内容,就把这段标记为“疑似幻觉”,并重新基于检索内容生成。
第二层,置信度判断。基于Rerank分数和最终答案与检索文本的字符串重合率来算一个“可信度”。如果可信度低于某个阈值,就什么都不做,直接降级答案为“抱歉,根据历史工单和知识库暂未找到匹配的解决方案,建议转人工”。在客服场景里,宁可不答,也不能瞎答。我真实经历里一次特别惨的教训就是:因为没做这层置信度判断,Agent把一条“测试工单”里的临时方案告诉用户,导致用户照着操作后把系统搞挂了。从此之后我所有生产环境的Agent,没有高置信度结果一律拒绝回答。
第三层,日志与分流。让Agent在回答时记录下它参考了哪些工单、知识库中哪些条目。一旦用户反馈“这个答案不对”,可以立刻根据日志回放整个推理链路,定位到底哪一步出了问题——是检索没召回正确数据、还是Rerank排错、还是Agent生成了不该有的扩展。这个可追溯性是Agent化系统的刚需,也是它比普通文档检索系统更难替代的原因之一。
三层防线都做到位之后,Agent的幻觉率基本能被控制在一个非常低的水平。说到“有依据地查相似问题”,最重要的不是“查”的动作,而是“依据”的可靠性和“回答”的谨慎度——这一点希望每个做Agent的人都能真正重视起来。
5. 常见问题与排查技巧实录
5.1 检索结果不相关?先检查你的问题拆解对不对
很多初学Agent开发的朋友反馈最多的问题就是:“我构建了知识库,检索也能跑通,但Agent回答的还是不对。”这里十有八九是检索召回出了问题,但很多人第一时间去改Prompt,这是方向性错误。
我的排查顺序是:先看Agent调用search_knowledge工具时传入的参数是否正确。比如用户说“服务器一直重启,日志里出现kernel panic错误”,Agent如果传的query是“服务器一直重启”,那漏掉的关键信息“kernel panic”就是你召唤垃圾结果的元凶。如果工具参数没问题,就看召回Top5里有没有正确文档。如果Top5里没有,就是Embedding或关键词检索的匹配能力问题;如果Top5里有,但答案还是错,那问题就出在Prompt——Agent没有正确利用文档。
这里我建议你在开发阶段加一个“链路追踪中间件”,把Agent每一步的思考过程、工具调用入参、检索到的Top5文档、Rerank分数都打印出来。别嫌丢人,这东西就是定位问题的利器。很多项目调试困难,就是因为链路是黑盒,只知道结果错,不知道错在哪一步。
5.2 知识库更新与增量同步:别让Agent用过期工单回答新问题
企业内部的知识是持续变化的:旧的工单会失效,新的方案会覆盖旧方案。如果知识库不能及时更新,Agent就会拿着两年前的老方案来指导用户,后果可想而知。我见过最典型的一次事故是,系统升级后新接口路径变了,但知识库里还存着旧路径,Agent信心满满地告诉用户旧路径,结果一连接就404。
增量同步有几个实用策略。第一,工单入库时做“去重+版本标识”。如果同一问题出现了新的工单(比如同一个错误码在新版本系统里解决方式不同),老的工单应该被标记为“旧方案”,确保检索时优先返回新方案,或者在Prompt里注明“该方案适用于旧版本系统”。第二,定期(比如每晚)跑一遍离线任务,检查知识库里的工单有没有更新,约定一个简单的规则:如果相似度极高(比如两段文本重合度超过90%)而信息有差异,那就把旧doc进入“待更新”状态。第三,上线前一定要做回归测试,拿一批固定的测试问题集,跑一轮新旧知识库的对比,看看更新是否引入了新的答案偏移。
5.3 性能和并发:能不能撑住真实访问量
Agent+RAG系统上线后,最现实的问题就是性能。一次完整问答涉及至少两次LLM调用(一次是Agent理解意图,一次是生成回答),中间还要加一次向量检索和一次Rerank。如果每个请求都这么跑一遍,并发一上来很容易被打崩。
我的经验是三步优化。第一步,缓存:高频问题(通常是同一个TOP问题集)可以把完整回答缓存起来,命中缓存直接返回,省掉所有计算成本。第二步,异步编排:Agent的第一步意图理解可以用轻量模型,只要能把用户问题转换成工具调用参数就够了;真正的答案生成再用大模型。编辑项目里这个思路还能大幅降低单请求消耗。第三步,限流和降级:给知识库检索做一个独立的服务节点,如果LLM调用失败或者超时,可以让Agent直接返回检索到的Top1文档内容作为临时答案,至少保证用户有一个可用的响应。
如果你用的是Dify或类似平台来做RAG流水线,也要注意编排里的超时设置和重试机制。很多人把整条流水线串行化,某个环节一卡就全线崩溃,正确做法是把检索环节和LLM环节拆开,必要时熔断。
5.4 实测数据参考:这套方案的效果到底怎么样
文章最后,给一个真实项目的数据供参考。我做过一个IT运维客服Agent,接入了大约3万条历史工单,包含产品故障、账号权限、网络设置三类问题。上线前用200条人工标注的测试问题做评估,纯向量RAG(不接Rerank)的Top5命中率是78%,加上关键词双路召回后提到85%,再加Rerank和元数据加权后达到94%。最终用户满意度调研里,“答案准确”一项的评分从没有系统时的3.1分提升到了4.5分(满分5分)。
但这里要特别说明:效果不是单一技术点带来的,是数据清洗、双路召回、Rerank、Agent工具调用、Prompt边界这几层共同作用的结果。单独拎出任何一块,效果都不会这么明显。这也是为什么我特别不建议直接照着网上的Demo抄一套RAG就上线。
我个人在实际项目里最大的体会是:做“有依据”的Agent,难点不在Agent,而在于你给它的“依据”靠不靠谱。与其花很多时间调Prompt,不如先把知识库的数据质量和检索链路打磨扎实。这两件事做好了,即使换一个不那么大参数的模型,效果也不会差太多。后面如果大家对工单数据清洗的Prompt模板、或者双路检索加Rerank的具体实现代码感兴趣,我可以再写一篇展开讲。