我对“超体”这个项目代号很有感情。它是我参与搭建的一个企业内部智能问答系统——把产品文档、历史工单、FAQ、技术公告全部收进知识库,用户以自然语言提问,系统直接给出有依据的答案。前六篇系列文章聊了架构、数据管道、部署这些“从0到1”的事,这一篇我们不得不说最难啃的部分:效果优化。
说实话,模型接口调通只需要一个下午,但要把回答质量从“偶尔靠谱”打磨到“稳定可靠”,靠的是检索、提示词、工具这三个环节一起发力。我把它叫“三管齐下”,不是修辞,是真的同一时间需要动三条线。这篇就把我们最近这一轮完整的优化过程复盘出来,适合正在做RAG应用、智能客服、知识库问答的开发者参考。
1. 整体思路拆解:为什么必须三管齐下
做AI应用的同学应该都有过这种经历:模型回答不准,第一反应是改提示词,改来改去发现效果时好时坏;又怀疑是模型不行,换个更强的模型,结果还是答非所问;最后才醒悟过来,问题可能出在喂给模型的内容压根不对。
我们早期就吃过这种亏。团队里几个人围着提示词调了两个星期,把系统提示词写得像法律条文那么严密,但回答质量提升仍然有限。后来做了一次完整的问题归因分析,把线上回答错误的样本一个个拉出来看,发现真正的根因分散得很开:有的是知识库压根没召回相关文档,有的是检索到了但排序不对,有的是模型没理解输出格式要求,还有的是需要调外部工具才能回答的问题,但模型根本不会调工具。
这次分析之后,我认识到一个问题:对于知识问答类应用,效果优化天然由三个维度构成:
| 维度 | 解决的问题 | 侧重点 |
|---|---|---|
| 检索质量 | 模型能否拿到正确资料 | 召回率、排序准确性、分块合理性 |
| 提示词设计 | 模型能否正确使用资料 | 指令清晰度、约束强度、输出格式 |
| 工具调用 | 模型能否完成系统之外的动作 | 工具定义质量、调度逻辑、容错机制 |
这三个维度其实是层层递进的关系。检索是粮草,提示词是战术,工具是装备。粮草没送到,战术再精妙也只能瞎比划;粮草送到了但战术混乱,拿到正确材料也讲不出正确答案;而工具调用则决定了系统能不能从“只会说”进化到“能做事”。
我建议每个遇到效果问题的团队都先做一次归因分析,不要急着改东西。把出错样本分三类:是检索不到?是指令理解偏了?还是功能上需要工具?分类清楚再动手,效率会高很多。后面几个章节我详细说我们在每个维度做了什么、踩了什么坑。
2. 检索优化:从“找得到”到“找得准”
检索是RAG系统的地基。模型再聪明,如果知识库没有把该用的资料送上来,一切都是白搭。我们围绕检索做了四件事:混合检索、分块策略调整、重排链路、查询改写。
2.1 混合检索选型:向量检索和关键词检索必须配合
做知识库问答的时候,很多人以为向量检索就够了,这是个常见误区。向量检索擅长捕捉语义相似,比如用户问“电脑开不了机怎么办”,知识库里有一条“设备无法正常启动的排查步骤”,语义上确实相近,向量能匹配上。但用户还常常直接用产品名称、型号、错误码这些精确词提问,比如问“EB-502错误怎么解决”,这时候关键词检索的精确匹配往往比向量更可靠。
我们的做法是同时跑两条召回链路:向量召回和BM25关键词召回,然后把两路结果合并。早期版本只用了向量召回,结果就是很多含具体型号、编号的问题明明知识库里有答案,系统却说“未找到相关信息”。后来加上关键词召回,这类问题的召回率肉眼可见地提升。
两路结果合并之后,排序就变成了一个问题。可以直接按分数加权,也可以把合并后的结果交给重排模型。我们当时的做法是:向量和BM25各自取Top 20,合并去重,然后统一进入重排环节。
2.2 分块策略:过大过小都会出问题
分块是我认为检索优化里最容易被低估的环节。很多团队把文档随便切一切就扔进知识库,我建议拿回答错误样本倒推检查分块是否合理。
我们测试过三种分块大小:256 token、512 token、1024 token,overlap取50到100不等。结论是,过大的分块(1024 token以上)虽然上下文信息完整,但噪音太多,检索时容易因为一块中包含太多无关内容而拉低相关性分数;过小的分块(256以下)则容易把一句话的逻辑截断,比如表格、代码块被切得七零八落。
最终我们采用了一种半结构化的方式:先按文档本身的章节标题切分,再对切出来的长段落做二次分块,每个块控制在350到500 token之间。同时保留元数据,比如文件名、章节路径、页码,这些信息在后续重排和引用展示中非常有用。
另一个经验:表格类内容必须特殊处理。把表格原样塞进向量库,检索效果很差。我们把表格转成“键值对描述文本”,比如“型号:X300,最大负载:500公斤,电源:AC 220V”,这样语义向量才能有效编码。处理之后,关于设备参数类问题的准确率提升非常明显。
2.3 重排(Rerank):打通“最后一公里”
召回阶段拿回来的Top 20结果,相关性排序是粗糙的。向量分数高不等于真的匹配用户问题。我们接了一个重排模型对Top 20结果重新打分,再取前5到8条作为上下文送入模型。这一步是投入产出比最高的一个改动。
举个实际例子:用户问“设备在低温环境下能不能正常工作”,向量召回的第一名是一条关于设备工作湿度范围的文档,相关但不完全对题;重排模型把关于温度适应性的文档提到最前面,最终回答质量立刻不一样了。重排模型通常比嵌入模型的区分能力更强,它能更好地捕捉“问题与文档之间到底是不是真的对口”这种关系。
当然,重排也有成本。多一层模型调用,时长和费用都会增加。我们的处理是只在Top 20结果上做重排,控制候选规模;另外离线把重排模型量化部署,线上延迟增加控制在可接受范围内。
2.4 查询改写:让检索理解用户的真实意图
用户提问题不一定规范,有的人说“怎么登录”,有的人说“我去,登不进去了”,还有的人一句话里糅杂了好几个问题。直接拿原始问题去检索,效果经常不理想。我们加了一个查询改写层:在检索之前,先让模型对用户问题做一次轻量加工。
加工动作包括这几类。第一是意图归一化,比如把“你们这个怎么用”改写成“XX产品功能使用说明”。第二是拆解复合问题,比如“怎么登录和改密码”拆成两个子查询,分别检索再合并结果。第三是补充业务词,比如把“那个东西坏了”补充成“设备故障报修流程”,这需要结合产品上下文。
查询改写还有一个隐秘的好处:能帮助处理同义词和口语化表达。关于改写模型,我们用的是轻量模型,在一次搜索链路里增加几十毫秒延迟,但检索准确率提升很明显。如果不用查询改写,就必须在检索端建同义词表和别名库,维护成本更高。
3. 提示词工程:把“会说话”变成“说准确的话”
检索优化之后,模型拿到的资料基本是靠谱的了,下一个瓶颈就在提示词。很多团队对提示词的理解停留在“把要求写详细一点”,这不够。提示词需要像软件一样结构化设计、版本管理、回归测试。
3.1 系统提示词的结构化设计
我们的系统提示词经历了从“一段话”到“一个模块化模板”的演进。最早的提示词是一大段自然语言描述,后来发现改起来非常痛苦:今天加一个格式要求,明天加一个语气要求,全挤在一段话里,模型很容易忽略细节,也很容易把不同要求搞混。
重构之后,我们把系统提示词拆成四个模块:
- 角色定义:明确系统是什么身份,面对什么用户群体。
- 任务说明:告诉模型在本次交互中要完成什么任务。
- 输出约束:给出硬性要求,比如不能编造、必须引用参考内容、不知道就直说。
- 输出格式:定义答案的JSON结构或文本样式。
每个模块之间用明确的标记分隔。下面是我们模板的简化版:
你是某产品知识库的智能助手,面向终端用户解答产品使用与故障排查问题。 任务: 1. 基于以下参考内容回答问题。 2. 如果参考内容不足以作答,明确回复“当前知识库中暂无相关信息”。 硬性约束: - 不得编造参考内容中没有的事实。 - 回答必须包含至少一条参考内容来源编号。 - 语气简洁专业,不使用网络流行语。 参考内容: [1](内容...) [2](内容...) 用户问题: (用户输入...)纯文本模板会导致输出格式变化,为了稳定解析,我们最终把输出部分改成了JSON格式,用类似的指令让模型固定输出answer和citations两个字段。这样下游就可以直接用结构来对接,不用再猜模型输出的边界。结构化改造之后,上游应用侧的解析故障基本清零。
3.2 事实清单法:一根降低幻觉的强心针
这是我这轮优化里最推荐的一个技巧。与其让模型直接生成回答,不如让它先提取“支持性事实”,再基于这些事实组织回答。
具体做法是:在提示词里要求模型第一步先阅读参考内容,列出所有与问题相关的事实条目,每条事实必须带上来源编号;第二步基于列出来的事实完成最终回答。这样强制模型在生成答案之前,先做一轮“证据确认”。
从心理学角度说,这个技巧的本质是让模型先建立一块“工作记忆板”,后续生成时会围绕这些事实来组织语言,而不是漫无边际地发挥。我们做了AB对比测试:未使用事实清单前,虚构内容占错误样本比例挺高的;使用后,这部分错误明显降低。代价就是响应稍微变慢一点,多了一次中间推理,但正确率提升了,这个代价是值得的。
3.3 参数调优:温度、TopP、最大长度都有讲究
提示词不只是文字,采样参数也归属于这个维度。不同场景对随机性的需求不一样。
问答场景,我们用的温度是0.2,TopP是0.6左右,这能保持回答的稳定性,同时不至于把话说得太死。如果是头脑风暴类工具,温度可以开到0.8甚至1.0。但知识库问答一定不要追求“花哨”,稳定准确是第一位的。
最大长度也值得关注。如果设置太短,模型可能答到一半就截断了;太长又增加延迟。我们是根据历史回答的平均长度来设定的,留了50%的冗余,同时配合“回答尽量精简”的指令,把大部分回答控制在400字以内。
另外一个细节是:不要在提示词里堆砌“非常重要”“一定要”这类程度副词。模型对这些词的敏感度并不稳定,过度使用反而降低指令的系统性权重。用明确的硬性约束词和编号条目,比情绪化强调管用得多。
3.4 提示词的版本管理与回归测试
提示词和代码一样,需要版本管理。我们一开始用文本文件直接改,后来经常出现“这个版本效果好,但改不回去了”的尴尬情况。后来我们把提示词纳入统一配置中心,每个模板都有版本号、修改人和变更记录。
更重要的是维护一套回归测试集。我们整理了一批覆盖不同难度的测试问题,包括简单检索题、复合推理题、拒答类问题、需要工具联动的问题,总共60多条。每次修改提示词,先跑一遍回归测试集,对比答案质量评分,再决定是否上线。虽然不能保证发现所有问题,但能拦住大部分明显退化。
在实际操作中,我们还会把回归测试的评分做成一个简单的看板,记录每次改动的得分变化,这样一段时间后翻看记录,能够判断哪些提示词改动是真正有效的。
4. 工具调用:让模型从“会说”进阶到“能做事”
检索和提示词解决的是“怎么把话说对”,但很多真实用户需求不只是问答——他们需要系统查出订单状态、查询实时数据、触发流程工单。这类问题必须靠工具调用解决。我们在工具调用上的优化踩的坑也不少,核心是四项:工具定义质量、容错重试、多工具编排、可观测性。
4.1 Function Calling的工具定义:参数描述才是灵魂
大模型平台都支持函数调用,定义看起来很简单:一个函数名、一段描述、几个参数。但实际效果差异非常大,关键在于描述写得够不够细。我们最早的工具定义很粗糙,比如查订单工具就写“根据订单号查询订单详情”,模型经常传错参数格式。
后来我们参考了一个很朴素的原则:把工具描述当作写在操作手册里的说明来写,参数必须包括类型、取值范围、示例值。比如订单号参数,明确写“18位数字字符串,示例:202408280001234567”,模型传参的准确率立刻上去了。
下面是我推荐的一种工具定义结构:
{ "name": "query_order_status", "description": "根据用户提供的订单号查询订单当前状态。仅在用户明确给出订单号时调用;订单号通常是18位数字串。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,18位数字,例如202408280001234567", "minLength": 18, "maxLength": 18 } }, "required": ["order_id"] } }还有一个细节:如果你的模型服务商支持工具调用的“强制禁用”开关,一定要在非必要场景里关掉工具调用,否则模型有时候会自作主张发明工具参数。我们有过一次模型虚构了一张未存在的电子券编码去调验证接口的教训,后来加了参数校验和强制校验开关才堵住漏洞。
4.2 工具调用结果的解析与容错重试机制
工具调用是模型行为,不是程序代码,它天生有不确定性。所以要按“可能会出错”来设计整套流程。我们设计了三层防线。
第一层是参数前置校验,在发给工具之前,先用代码校验参数格式,不符合预期就直接返回“参数缺失或格式错误”,不走工具逻辑。第二层是工具返回结果的统一包装解析,不管下游系统返回什么格式,都转成统一JSON结构,防止后续提示词拼接时报错。第三层是重试机制:如果工具返回错误或模型第一轮产生幻觉,我们允许整个链路重试一次,第二次调用时把第一次的错误信息作为反馈传给模型。
举一个真实场景:用户问“帮我查一下订单7123到哪了”,模型第一次调用工具时把订单号识别成“7123”,前置校验发现长度不足,系统将错误信息反馈给模型,模型重新提取出完整订单号“7123889900123456”,第二次调用成功。如果没有这个重试机制,用户得到的就是一次失败的答复。容错重试整个流程成本不高,但对用户体验改善相当大。
4.3 多工具编排:不要让模型一张嘴同时调十个工具
当系统里有十几个工具时,模型经常出现选择困难。特别是同时收到“查天气”和“查温度”这种功能近似的工具时,模型可能会选错,或者干脆把上一个工具的结果拿来胡答。
我们做了一次工具数量收敛和归类。把底层相似的API合并成一个大工具,比如“查询业务数据”统一收口,内部再做路由分发。同时引入一个意图分类前置工具:先让模型判断用户的问题属于哪一类场景,再只暴露这一场景下需要的候选工具,这样大大减少了误选概率。
还有一种情况:一个用户问题需要连续调用多个工具,比如“我家设备离线了,帮我查一下设备信息并且重新下发指令”,需要先查设备列表,再调用下发指令工具。这种多跳工具调用我们用了一个简单的编排逻辑:在系统层定义一个极简的状态机,仅允许“查询类工具”成功后,才可调用“操作类工具”,并且操作类工具必须引用查询结果中的真实设备ID。千万别让模型自由发挥。
4.4 日志与全链路追踪:没有观测就没有优化
工具调用链路比纯问答复杂得多,没有日志和追踪,基本等同于抓瞎。我们把一次完整的会话请求串起来,加上一个request_id,从用户问题、检索结果、重排结果、模型提示词、工具调用参数到工具返回结果,全部写入结构化的追踪日志里。
这个日志的价值在于,每次线上回答质量出问题,我们都能按request_id把整条链路拉出来逐段排查,快速判断是检索错了、提示词没约束住,还是工具侧返回了脏数据。我们甚至把日志做成一张简单的看板,能看到工具调用的成功率、平均耗时、最常见的失败原因,用数据驱动下一步优化。
5. 常见问题与排查技巧实录
结合我们这一轮实战,整理了5类出现频率最高的问题及排查思路。
| 问题现象 | 可能根因 | 建议排查手段 |
|---|---|---|
| 模型回答“知识库中无信息”,但知识库里明明有 | 检索召回失败,向量与关键词都没命中 | 检查分块大小、检索TopK数量,打印召回结果逐条看相关性 |
| 模型答非所问,内容跟问题不完全搭边 | 检索到了相关文档,但重排未把真正对题的排到前面 | 人工核对Top 5里是否存在正确文档;检查重排模型是否生效 |
| 同一个问题多次问,答案不一致 | 温度过高或上下文构造不稳定 | 降温度到0.2以内,检查检索结果是否每次稳定 |
| 模型编造工具参数,调用错误工具 | 工具描述不清晰、参数约束缺失 | 细化工具描述和参数示例,加入参数前置校验 |
| 工具调用成功但用户仍不满意 | 工具返回结构过于复杂,模型没能正确解析 | 统一工具返回格式,返回结果里只保留模型需要的字段 |
先说第一个问题,这也是最迷惑人的。一次用户问“怎么申请发票”,知识库里有明确的发票申请流程文档,但系统说查不到。拉日志一看,发现文档被分块之后,发票流程被切碎在两段内容里,单看任何一段都不算和“申请发票”高度相关,两条的向量分数都偏低,重排后也没进Top 5。最后把那个文档改成按章节标题做结构化分块,问题就消失了。这个案例告诉我们,知识库文档的分块质量直接影响检索结果,不能只调参数。
第二个排查技巧是“看Top 5”。我觉得排查RAG类问题最有效的手段之一,就是直接看检索链路返回的Top 5内容到底是什么。如果Top 5里压根没有正确答案,那是召回问题;如果正确答案在Top 5里但重排后掉出去了,那是重排问题。这招能把问题定位范围缩小一大半。
第三个问题是版本灰度。修改提示词或检索参数后,不能全量上线直接改,最好先让新配置只作用于部分流量。我们用了一个简单的AB分流方案,线上保留新旧两个配置版本各跑一部分流量,观察一两天的用户反馈和错误率,再决定是否全量切换。
6. 关于效果优化的几点体会
这一轮实践下来,我最大的体会是:效果优化没有银弹,必须系统化推进。只调提示词、只加工具、只优化检索,都不能覆盖所有问题类型。三个维度是互相咬合的齿轮,缺一个整体转不动,只有一个转得再快也没用。
另外,建议每个团队都沉淀一套“效果评测集”。不要等到线上出问题才临时找几个问题测试,平时就要把典型问题、边界问题、易错问题积累起来,每次改动自动回归。没有评测集的优化,基本是在碰运气。
最后分享一个小技巧:优化过程中,每个改动最好只改一个变量。我见过一些团队一次改了分块、重排、提示词三个地方,结果效果变好了,但根本说不清是哪个改动起了作用。我们后来严格自律:一次只改一个维度,跑完回归看效果,再改下一个。虽然慢一点,但每一步都走得明白,积累的经验也能沉淀下来复用。
下一轮我们打算在这套体系上加更多画像类的问答策略、多轮对话记忆优化。这次先写到这里,如果你也在做类似系统,希望这些踩坑记录能帮你少走一些弯路。