题记:一条纠偏指令,应该有适用范围、生效时刻,也应该有人能把它撤下来。

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. 编辑不是覆盖,是撤回后再发布⌗

控制台的“编辑”没有直接更新原记录的正文。

存储层在同一事务中做两件事:

  1. 撤回旧笔记,记录原因与时间。
  2. 创建新笔记,沿用匹配串和已有信号关联,作者改为人类维护者。

结果是旧正文仍然能查,新正文获得新的 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、校准作者迁移与控制台组件,以及校准接口、回合行为和访客策略测试。文中的测试描述来自源码断言规格,不是本次另行执行的测试结果或模型行为评测。