别把模型的坏习惯,写成代码里的永久判词

题记:一条纠偏指令,应该有适用范围、生效时刻,也应该有人能把它撤下来。
1. 今天的观察,别写成明天的定律⌗
发现某一类模型总有一种讨人嫌的习惯,最顺手的修法是什么?
如果模型属于某家族:
给上下文追加一段纠偏说明
这很有效率。匹配条件与说明文本放在一起,运行时判断一下就能用。但它也把两种寿命不同的东西焊死了:运行机制需要长期稳定,对模型习惯的判断却应该允许过期。
模型升级、渠道变化、任务分布变化,都可能让原先合理的提醒变成噪音。此时维护者需要回答的已经不只是“这段代码有没有执行”,而是:
- 现在有哪些纠偏说明?
- 这个模型实际会读到哪几条?
- 谁加的,为什么还留着?
- 自动化能不能撤回一条人类明确保留的说明?
如果答案只能从条件分支里找,这不是运行时配置,只是一份穿着代码外套的行为备忘录。
这次改动把一段按模型家族追加的硬编码说明,迁入已有的校准笔记存储,并补上控制台与作者边界。值得讨论的不是多了一个编辑框,而是纠偏从隐含判断变成了可管理的对象。
2. 一条笔记,不只有正文⌗
CalibrationNote 的结构很朴素:
| 信息 | 作用 |
|---|---|
match、body | 匹配谁,告诉它什么 |
author | 人类维护者或自动分诊 |
signalId | 可选的改进信号来源 |
status | 生效中或已撤回 |
| 创建时间、撤回时间、撤回原因 | 留下生命周期痕迹 |
这不是把每句话都升级成一套工作流。它只补齐几个实际需要回答的问题。
尤其是 author:这里的作者不是展示用的署名,而会影响自动化能否撤回笔记。
正文也仍然是一段自然语言指令,不是可执行策略语言。把它存进数据库,不会让模型必然服从,更不会让提示词获得工具确认门没有授予的权限。
可管理,不等于可强制执行。 这条界线不划清,控制台越漂亮,误解越容易发生。
3. 预览不是模拟模型,是复用同一把尺子⌗
匹配机制没有引入分类器,也没有新增规则语言。其核心可以直接写出来:
const identity = `${providerId} ${model}`.toLowerCase()
const hits = activeNotes.filter(note => identity.includes(note.match))
笔记写入时,匹配串会去掉首尾空白并转为小写。运行时把渠道标识和模型名拼在一起,再做子串匹配。
优点是好懂,代价也是好懂:短匹配串可能命中过多目标;同一模型通过不同渠道提供时,渠道名也会参与匹配。这里没有“更具体的规则覆盖更宽泛的规则”,命中的生效笔记会一起进入上下文。
因此,预览不是装饰。
控制台选择一个 provider 后,服务端读取当前有效配置中的模型名,再调用运行时使用的同一个 forModel。前端不自己抄一份匹配算法,也不要求维护者手填一个可能已经过期的模型名。
当前有效模型配置
│
▼
渠道标识 + 模型名
│
▼
forModel ────── 生效笔记
│
├── 控制台:展示命中列表
└── 运行时:渲染进会话上下文
它能回答“按当前配置,会匹配哪些笔记”。它不能预测“模型读完会不会听话”,也不是完整提示词预览。
这种克制很重要:确定性的选择问题交给代码,概率性的行为问题留给评估。 不要把前者包装成后者已经解决。
4. 编辑不是覆盖,是撤回后再发布⌗
控制台的“编辑”没有直接更新原记录的正文。
存储层在同一事务中做两件事:
- 撤回旧笔记,记录原因与时间。
- 创建新笔记,沿用匹配串和已有信号关联,作者改为人类维护者。
结果是旧正文仍然能查,新正文获得新的 ID。它保留了版本痕迹,但没有因此变成完整的版本控制系统:当前结构并没有专门的前后版本链接字段。
事务在这里保护一个具体问题:假如新正文不合法,不能出现“旧说明撤掉了,新说明却没加上”的半成品状态。
接口测试对此有明确规格:先添加一条有效笔记,再提交超长正文的编辑请求;请求被拒绝后,原笔记仍能被匹配到。另一个测试检查编辑后的新 ID、旧记录的撤回原因与时间,以及预览缓存失效后的命中结果。
这比“保存成功,列表刷新了”更接近真正的语义:修改的是未来使用的版本,不是把过去的正文擦掉。
撤回同样不是物理删除。历史列表仍展示已撤回笔记,运行时只筛选生效笔记。
5. 自动纠偏不能把人类决定一起纠掉⌗
如果系统既能自动生成校准笔记,又能自动清理笔记,那么作者身份必须进入权限判断。
否则会出现一个很别扭的循环:人类刚把指令改明确,自动化下一轮认为它“不再需要”,又把它撤了。系统表面上在自我改进,实际上在跟操作者拔河。
这里采用的是不对称权限:
| 操作 | 控制台的人类维护者 | 自动分诊服务 |
|---|---|---|
| 新增笔记 | 写入人类作者身份 | 默认写入分诊作者身份 |
| 编辑已有笔记 | 撤回旧项,创建人类版本 | 此次没有增加对应编辑入口 |
| 撤回分诊笔记 | 可以 | 可以 |
| 撤回人类笔记 | 可以 | 拒绝 |
实现里有两个值得注意的小地方。
第一,控制台新增接口由服务端固定写入人类作者身份,不信任请求体传来的 author。测试故意提交自动分诊作者字段,返回记录仍然是人类作者。
第二,人类编辑自动分诊笔记后,新笔记保留原来的信号关联,但转为人类作者。此后,自动分诊服务不能撤回它。
保留来源,不等于继续授予来源方处置权。 一条建议被人类接管后,来源仍然有解释价值,控制权却已经变化。
边界也不能夸大:撤回限制落实在自动分诊服务入口,而不是数据库里一条对所有调用者都生效的作者权限策略。控制台接口的访问边界还依赖服务端既有身份与访问控制;访客策略测试明确拒绝了笔记列表、预览和写入路由。一个 author 字段本身不是身份认证系统。
6. “马上生效”应该翻译成下一回合⌗
笔记存储缓存生效集合,写入后使缓存失效。运行时在准备回合上下文时,按当前模型读取命中笔记,渲染为校准段落。
这意味着控制台可以立即反映修改,匹配模型的下一回合可以读到新文本;它不意味着正在执行的回合会被中途改写。
运行时已有的上下文分段机制还会对渲染后的校准文本计算指纹,把结果放进段 key。校准内容变化因此能够参与上下文基线的变化判断。
这也说明了两个层次不能混为一谈:
- 笔记记录保存作者、状态和撤回原因,服务于管理。
- 上下文指纹描述实际渲染给模型的文本,服务于运行时分段。
如果只改变管理元数据,没有改变渲染文本,校准文本指纹不会因此变化。反过来,正文变化会影响指纹,即使维护者觉得只是“换了一种说法”。
这是一种合理分工。运行时不需要猜测两句话是否语义等价,管理界面也不该靠一个 hash 来解释谁改过什么。
7. 迁出硬编码,不代表行为范围原封不动⌗
这次迁移还有一个容易被“配置化”三个字盖过去的取舍。
旧实现通过家族判断追加说明。新迁移没有把所有旧匹配条件逐一翻译成通用规则,而是为三个具体渠道标识写入笔记;相应测试也逐一检查这些迁移目标。
因此,这不是覆盖范围完全等价的重构。
它把“属于这个家族就自动继承”改成了一组明确的数据项。新渠道不会仅仅因为被旧家族判断识别,就自动获得同一段说明;但子串匹配的实际覆盖仍要以预览为准。
这是从隐式继承转向显式维护:少一点自动覆盖,多一点维护责任。
测试提供的证据也应该按层次读:
| 测试规格 | 能证明什么 |
|---|---|
| 迁移目标与作者断言 | 指定笔记被写入,并带人类作者身份 |
| 回合上下文断言 | 指定目标读到新校准段落,旧硬编码标题不再出现 |
| 编辑与回滚断言 | 新旧记录和失败请求符合生命周期语义 |
| 分诊撤回限制断言 | 自动分诊服务不能撤回人类笔记 |
| 访客策略断言 | 相关路由不向访客开放 |
这些都不等于模型行为已经改善。测试检查的是选择、注入和控制边界,没有提供提问率下降、任务成功率提升或跨模型一致性的行为评测。
没有这样的评测数据,就不要给一次配置管理改动配一张想象中的效果图。
8. 进步不在编辑器,而在谁有权改口⌗
相比“发现习惯,追加提示词”,这个设计多做的事情其实很有限:
- 让命中范围能够被直接查看。
- 让编辑与撤回留下历史。
- 让人类接管成为自动化必须尊重的状态。
- 让生效时刻落在明确的回合边界。
它没有解决指令冲突,没有证明纠偏效果,也没有把自然语言变成强制策略。
但它让一个重要问题从代码角落里走了出来:模型为什么读到这条说明,以及谁有权让它不再读到。
对可自我改进的 Agent,这不是管理后台的边角功能。自动化可以提出判断,不应该因此获得无限期维持或撤销判断的权力。
模型的习惯会变。系统应该允许纠偏说明跟着变,而不是每次都要先把它从代码里的永久判词中挖出来。
代码依据:落地版本 e43f2c8 的 src/improve/calibration.ts、src/improve/service.ts、src/runtime/turn-runner.ts、src/server/calibration-routes.ts、校准作者迁移与控制台组件,以及校准接口、回合行为和访客策略测试。文中的测试描述来自源码断言规格,不是本次另行执行的测试结果或模型行为评测。