作者在语言学习应用的生产环境中使用廉价LLM,经历了四轮prompt优化仍无法杜绝「过度纠正」问题,最终选择用解析器验证输出来兜底。提供了模型输出不可靠时的工程思路。
我在一个小型生产应用的核心流程中运行着一个 LLM:Mening 每天帮助语言学习者批改作文。从单元经济学角度,日常调用落在了一个便宜、快速的模型层级上,而输出会写入数据库,所以它必须遵守一份契约:严格的 JSON、封闭的错误类别集合、用学习者的 UI 语言解释、不编造"修改"。
便宜的层级在这个任务上确实做得很好。它在遵循任务相关规则方面却一塌糊涂。这篇文章讲的是我在接受标题中那条内部规则之前爬过的升级阶梯,以及教会我这一点的两场真实战斗的数据。
中文学习者一直在收到一种特定的非纠正:模型会把 了 插入本来就正确的句子,然后解释为什么学习者"需要"它。母语者核查表明原句是正确的。
我在提示词阶梯上一级一级往上爬,每次部署后观察线上流量:
判断式规则("只标记真正的错误")。
不应该标记什么的具体示例。
把规则移到系统提示词更靠后的位置,接近生成点。
用大写字母写成的绝对规则,不容例外。
四次迭代之后,模型依然如故。不是每次都犯——这比每次都犯更糟糕,因为看起来像是修好了,直到它再次出问题。
真正有效的修复不是措辞。解析边界现在会在任何数据到达数据库之前先剔除两类编辑:noop 编辑("错误"字符串和"正确"字符串相同)和纯粹的 了-插入编辑。几行 Go 代码,此后零回退。提示词依然礼貌地请求;但解析器不在乎模型有没有听。
契约规定:用用户的 UI 语言解释错误(比如俄语),绝不用目标语言(比如芬兰语)解释。真实用户报告收到了芬兰语解释。当然,这是间歇性的。
在动任何东西之前我先测量了:用同样的七份真实提交,21 次运行,完全相同的生产请求形态。6/21 的回复漂移了(29%),而且漂移是全有或全无的——模型为整个 JSON 承诺一种语言。
然后我测量了所有人最先想到的修复方案:
Temperature 设为 0.0:仍然 5/21 漂移。Temperature 不是那个杠杆——硬币翻转不是采样噪声,而是指令输给了载荷。当输入以芬兰语文本为主时,"用俄语回答"就会褪色。
把语言规则移到系统提示词末尾:3/21。好些了,但仍然有问题。
把规则作为用户轮次的最后一行重复,在学习者的文本之后:0/21。
这是这篇文章里最便宜的教训:系统提示词离生成点很远,对于小模型来说,距离很重要。必须存活的规则应该放在用户轮次里,紧挨着与它对抗的数据。
但 21 次运行的样本上 0/21 并不是保证,所以边界还是加了一个守卫。offScriptExplanations 检查响应中每条解释的书写系统是否与 UI 语言的正字法匹配。故意设计得很笨:
func offScriptExplanations(correction *Correction, uiLang string) bool {
// Deliberately dumb on purpose
uiScript := scriptOf(uiLang)
for _, exp := range correction.Explanations {
if scriptOf(exp.Language) != uiScript {
return true
}
}
return false
}
它是书写系统检查,不是语言识别——不需要模型调用,没有依赖,只是 Unicode 范围。
它只在 UI 和目标书写系统不同的时候触发(西里尔文 UI 对拉丁目标)。同书写系统的语言对是惰性的,因为这种情况下书写系统检查分不清两种语言,而猜测正是守卫变成 bug 的途径。
它要求整个响应都偏离书写系统才会行动。俄语解释中引用的芬兰语片段绝不能触发它。
当它触发时,纠正会运行一次修复重试,并明确指出违规内容。如果重试仍然是错的,错误语言的答案会被保留而不是丢弃——用户需要眯眼看一个纠正,胜过没有纠正。守卫应该降级,而不是摧毁。
模型报告的每个错误都落在六个封闭类别中,受到两重强制——SQLite 中的 CHECK 约束,以及解析边界处的验证。集合之外的任何东西都会被拒绝,模型编造的任何字段都会被丢弃。
最后一部分原来是一份额外的安全属性。模型输出是系统中唯一的不可信输入,解析器相应地对待它:没有工具调用可以被劫持,没有额外字段可以用来 smuggle 指令,没有硬性 token 上限。针对这个管道的提示词注入基本上无处可去,不是因为提示词说"忽略注入",而是因为边界只接受它期望的形状。
现在,当一个便宜模型违反规则时,我按顺序这样做:
用具体的名称重写规则,而不是抽象("用俄语,绝不用芬兰语"比"用 UI 语言"更好)。
把它移到系统提示词的末尾。
作为用户轮次的最后一行重复,在数据之后。
用真实输入测量——20 次运行告诉你的比任何提示词审查都多。
如果即使很少见仍然泄露:停止提示。在解析边界强制执行它,如果输出可以挽救则进行修复重试,如果不能则有一条降级路径。
前四级降低失败率。只有第五级把它设为零,而恰好达到第五级的规则正是那些"很少见"也不可接受的规则。
提示词是一份请求。解析器是一份契约。
这个应用来自 mening.app——每日写作练习,记住你一直重复犯的错误。它的记忆方面的技术细节写在 error memory under the hood 中。