指出prompt中"问vs猜"的决策从未被显式约束,模型升级后可能从生成JSON转向生成澄清性 prose,导致JSON解析失败被下游代码误判为正常。
KeyError: 'action',或者 JSON 解析失败——当你收到的回复以"Just to confirm — which order did you mean?"开头。Prompt 没有变,模型变了,模型对于"欠规范请求是否值得一个问题还是一个猜测"有了自己的私密判断。
症状及其产生的错误
以一个支持分诊助手为例。它的 prompt 描述了四个操作——refund、cancel、escalate、reply——并要求返回一个 JSON 对象,指定其中一个操作及其参数。客户写道"cancel it",但没有提供订单号,而账户下有三个未完成的订单。
在旧模型上,回复是一句话询问是哪个订单。你的代码捕获了解析失败,回退到"需要人工"分支,这种行为在两年间阴差阳错地正确运行着。在新模型上,回复是有效的 JSON,指定 cancel 并附上最近一个订单 ID——从上下文中编造出来的。没有报错。订单被取消了。客户来问你你才知道。
反过来同样频繁发生,而且动静更大。一个始终收到 JSON 的流水线开始收到澄清性的 prose,解析器抛出异常,而 on-call 工程师看到 JSONDecodeError 激增,却找不到任何 deploy 可以怪罪。两种情况都是同一个缺陷:ask-versus-guess 的决策从未被写下来,所以它从来就不是你拥有的东西。
为什么平衡发生了移动
你的 prompt 里没有任何东西把它固定住。Prompt 描述了四个操作是什么、输出应该长什么样;但没有说当输入不能确定一个操作时该怎么办。那个空白被模型训练后的默认行为填补了——关于 helpfulness 和 caution 的默认值,这些默认值是按模型调优的。它们不会被暴露为参数,也不会被记录成一个数字,而且它们会再次移动。
这就是 prompt 可移植性失败的一般形态:你依赖但从未指定的行为。修复方法绝不是找到恢复旧默认值的措辞,而是完全消除对任何默认值的依赖。
将决策建模为一个 schema 字段
关于何时提问的 prose 指令在每个模型上都弱执行,因为它们与 prompt 中的其他一切竞争。Schema 由你的验证器强制执行,而验证器没有意见。将两种结果建模为 discriminated union,这样"提问"就成为一个一等公民的、可机器检验的结果,而不是解析失败:
{
"type": "object",
"required": ["status"],
"oneOf": [
{
"properties": {
"status": { "const": "clarify" },
"question": { "type": "string", "maxLength": 200 },
"missing": {
"type": "array",
"items": { "enum": ["order_id", "amount", "reason"] },
"minItems": 1
}
},
"required": ["status", "question", "missing"],
"additionalProperties": false
},
{
"properties": {
"status": { "const": "act" },
"action": { "enum": ["refund", "cancel", "escalate", "reply"] },
"arguments": { "type": "object" }
},
"required": ["status", "action", "arguments"],
"additionalProperties": false
}
]
}
现在调用代码根据数据分支,而不是根据解析是否成功:
const out = validate(response); // throws on schema violation
if (out.status === "clarify") {
return askUser(out.question, out.missing);
}
return execute(out.action, out.arguments);
两件事发生了变化,而这些是再多的 prompt 重写都无法实现的。澄清回复不再是一种异常,所以更频繁地提问的模型会优雅地降级,而不是 paging 某个人。而更少提问的模型再也无法偷偷蒙混过关,因为 act 分支需要必须通过验证的参数。
将必须填充的槽位命名出来
Schema 使两种结果明确化;但它还没有说明哪种是正确的。这是一个策略决策,它应该作为枚举的前置条件出现在 prompt 中,而不是作为形容词。替换掉"如果你不确定就提问"这种形式的指令——这种指令要求模型对一个它并不具备的置信度进行内省——改为列出输入中必须存在的 fact 列表:
只有在所选操作的每个必需槽位都出现在对话中时,
才能返回状态 "act"(逐字引用或由客户明确说明):
refund -> order_id, amount
cancel -> order_id
escalate -> reason
reply -> (无)
如果任何必需槽位缺失,返回状态 "clarify",
在 "missing" 中列出缺失的槽位,并仅就这些槽位提问。
不要从账户历史、最近性或仅存在一个合理候选这一事实
来推断某个槽位。
最后一句话是承重的那一句。"仅有一个合理候选"正是产生错误取消的推理,而且除非被告知不要这样做,否则模型就会做这样的推理。这条指令是可执行的,因为 missing 在 schema 中:一个对缺失槽位进行操作的模型必须产生一个你的验证器可以根据同一个槽位表用代码检查的参数对象。
注意这不是什么。这不是置信度阈值。要求模型给出一个数字置信度并据此做门控,只是把未指定的判断往下移动了一层而没有移除它。槽位在输入中是可观察的;置信度不是。
捕捉下一次迁移的夹具集
构建一组故意欠规范的输入,每个都标有期望的状态,以及对于 clarify 情况,期望的缺失列表。十二个足够起步,应该包括:一个没有任何槽位的请求;一个包含每个槽位的请求;一个槽位存在但模糊的请求("大的那个");一个槽位出现在较早轮次而不是最新一轮的请求;一个客户命名了不存在订单的请求;以及一个两个操作都合理的请求。
报告两个数字,而不是一个。ask rate 是整个集合中返回 clarify 的比例。slot precision 是在 clarify 情况中,missing 与标签完全匹配的频率。ask rate 移动几个点的迁移通常是可以接受的;slot precision 移动则是模型问错了东西,这在客户看来是无能而不是谨慎。在迁移前对当前模型运行这两个指标,这样你就有了一个不是凭记忆的基线,并看看需要多大的样本来判断一个变化是真实的。
将夹具与 prompt 和 schema 放在同一个仓库中,并让标签成为代码审查的一部分。槽位表、schema 枚举和夹具标签是同一策略的三条陈述,它们一旦放在不同地方就会开始漂移——在 prompt 中添加了新操作但没有夹具也没有槽位行,这是这个修复在落地六个月后最常见的衰减方式。一个廉价的守卫是一个测试,它从 prompt 文件中读取槽位表并断言 schema 枚举中的每个操作都出现在其中,这在构建时失败而不是在客户面前失败。
值得说明的一个边界:本文讨论的是确实下不能确定答案的输入。当输入是完整的但模型仍然遗漏了一个规则时,失败是另一种情况——参见嵌套指令失去保真度,那是通过重构规则来修复的,而不是通过添加 clarify 路径。
模型迁移对深度嵌套或条件指令的影响
为什么同一个 prompt 在每个模型上表现不同
模型迁移后输出变长了(或变空了)