一、引言

换模型的评测跑完,手里是一张分类别的准确率表。常见的样子是:一级类别判得相对准,二级类别不行,而且各个二级类别之间差距非常大。第一反应往往是模型不行,换个更强的就好。我们从小模型 A 换到中等规模的模型 B 后,二级类别整体涨了十几个百分点,但类别之间的差距还是几十个点,低的还是低。换模型抬的是整体水位,抹不平类别之间的差距,这部分只能靠 PE。

直觉的做法有两种:一种是把错例丢给模型,让它自己改 prompt;另一种是出一条错就往 prompt 里打一个补丁,补一条规则或者加一条例子。这两种我们都试过,效果都不好,下面详细展开。

二、踩过的坑

坑一:让模型自己归因

一个常见做法是把判错的 case 直接丢给模型,让它自己改 prompt。用最强的模型试过几次,效果不好,改完一批错另一批。这时候要先问自己一个问题:作为更懂业务的人,我知道错在哪吗?能给模型指方向吗?如果我都不知道,那其实是在对模型许愿。

改不出来是必然的。模型看到的只有「这条判错了」,它不了解这个类别为什么这么设计,不了解金标标注时的暗逻辑,不知道金标和 prompt 冲突时该听谁的,也不知道哪些边界是业务方裁定过的。缺了这些信息,再强的模型也只能对着 prompt 里的相关词改,改的是表面症状,不治本。

人介入归因的做法是:先找出混淆矩阵里的 top 10 混淆对,导出错误 case 逐条归因,大致能分成三类:

  1. 模型确实没按定义判
  2. 金标有问题(标错了、金标和口径打架)
  3. 定义有问题(定义过宽或过窄)

直接基于第一类 case 改 prompt,通常就会有一定效果,这一轮我们在这里拿到了大约 5 个点。第二、三类需要人裁定,争议大的要拉业务方一起定,都调整好后,PE 才又见成效。

归因还会找出一类模型自己不容易发现的错。意图识别会给模型几轮对话历史帮它理解当前 query。比如上一轮用户想办某件事,助手回复说自己办不了、建议去找某个人,用户接着问「怎么联系他」,模型会判成「咨询联系方式」。单看这句没判错,但和产品目标冲突:上一轮助手的回复本身就是错的,用户的真实诉求还是办那件事,不该顺着错误回复往下判。之前 prompt 里也写了要结合上下文识别意图,但模型归因的时候不会觉得自己没看上下文,所以这种错很难靠模型自己找出来。针对性的修改是在 prompt 里要求只看用户说了什么,忽略助手的回复,从用户的话里提炼意图。

坑二:类别本身定义不清、互相耦合

调 PE 会发现另一个现象:有的类别不管怎么改,准召、F1 都不达标。归因时能明确感知到,是类别的边界耦合了。模型分不清,换个真人来也不一定分得清。

我们这波最典型的是「问规则」和「问自己的情况」。后者是问已经发生在自己身上的事,比如「我的额度为什么变少了」。前者是问规则本身的内容和口径,比如「什么情况下会调额度、多久恢复」。写在纸上像是两个类别,但用户一句「到时间了为什么还没恢复」,既在问自己的情况,也在问规则的时限,很难在两者之间分清。我们改了几版,召回还是在四成左右。最后的处理是把「问自己的情况」合并进了「问规则」:首先这类问法本身就同时问两件事,硬拆不自然;其次从下游执行动作看,两类的差异也不大,合并后对下游回复链路没有影响。

设计类别的时候就要检查它和相邻类别有没有耦合,耦合的类别不要指望 prompt 去分,从定义源头修才是根本办法。判断标准也简单:如果你自己对着二十条 query 都要犹豫,模型不可能稳定。

坑三:出 badcase 就堆 few-shot

早期出一条错就往 few-shot 里加一条同款,后来做了一组规则版和 few-shot 版的 A/B,规则版全面领先,字数还少了四分之一,具体数据和原因在上一篇里写过,这里不重复。这轮真正新的教训是写法:每个类别一段定义,边界条款内嵌在定义里写清「不算什么、算什么」;通用规则留 4 条跨类别的判法;few-shot 压到 13 条,单轮的只放 unknown 边界这类定义说不清的,多轮的展示上下文补全;不再靠加例子修单点问题。

Anthropic 在《Effective context engineering for AI agents》里说的也是这个:

Teams will often stuff a laundry list of edge cases into a prompt in an attempt to articulate every possible rule the LLM should follow for a particular task. We do not recommend this. Instead, we recommend working to curate a set of diverse, canonical examples that effectively portray the expected behavior of the agent.

它反对的是往 prompt 里堆一长串边缘 case 去穷举规则,推荐的是精选一组典型的例子。跟我们的优化方向吻合。

坑四:调 prompt 只关注精度,不关注成本

最开始用小模型 A,看中的是延时和成本,准确率差一点没关系,用户 query 相对收敛,正则能兜住大部分,流到 LLM 的都是长尾。后来需求扩张,正则兜不住那么多了,只能靠模型自己判准,所以重新做了选型,按延时、成本、效果选了模型 B。

有人会问,模型 B 的原价比模型 A 贵好几倍,看成本怎么会选它。答案是它有前缀缓存。按供应商定价,命中缓存的输入 token 和未命中的价差超过一个数量级。意图识别的 prompt 九成以上是固定模板,理论上大部分 token 都能走缓存价。但选型评测跑完算成本时发现,缓存 token 占比只有 75%,折算下来比模型 A 贵一半,要到 85% 以上才能打平。效果是上去了,钱多花了一半。

原因在 prompt 的版式。线上模板是「任务、类别树、边界规则、上下文、输出格式、few-shot、当前输入」,对话历史夹在中间。缓存按前缀逐 token 比对,历史一变,后面的输出格式和 few-shot 虽然一字没改,也全部按原价重算。改法是把会变的挪到最后:「任务、类别定义、通用规则、输出格式、few-shot、对话历史、当前输入」,固定模板在前,历史和 query 在末尾,这样每次只有历史和 query 这一百来 token 按原价。

版式改完,PE 侧能做的就做完了,但光这一步不够,还要工程侧配合。两边一起做完,线上缓存命中到了 90%,过了打平线,比模型 A 更便宜,效果也更好。

三、总结

这轮 PE 做完,二级类别准确率涨了二十多个百分点。想高效地把效果提上去,前提是对自己的项目足够了解:判错的 case 自己逐条归因,日常看线上 trace,这两件事省不掉,丢给模型自己改是在许愿。归因清楚了,该改 prompt 的改 prompt,该合并的定义合并掉,边界写进定义,例子不要太多,典型即可。另外,换模型和调 prompt 都不能只看精度,成本、效果、延时要一起算,选最合适的,不是最强的。