让模型自己判静默
一个 heartbeat 醒来该做什么
Section titled “一个 heartbeat 醒来该做什么”TeachClaw 的 agent 有一个 “关心模式”——[think] mode。
当用户在线但安静了一会儿(比如 5 分钟没发消息但浏览器 tab 还开着),agent-engine 进程里有个 setTimeout 会触发,给 agent 发一条系统消息:
[think] 心跳唤醒(第 1/3 次未被回应)。当前北京时间 16:42,孩子安静 7 分钟。最近一句原话:"我数学作业写完了"agent 拿到这条消息,进入一个 turn。它要做什么?
直觉答案:应该说点什么——孩子刚说完作业,agent 关心一下、表扬一下、问下一个话题。这是 “关心模式” 的初衷。
但这个直觉是错的。或者说,只对一半。
我们的失败尝试:强制开口
Section titled “我们的失败尝试:强制开口”最早一版的 THINK_SPEC 是这么写的(commit 0f4d5847,2026-04-14):
思念模式:无穷钩子设计,堵死静默出口。 每个 think turn 必须有 output。如果想不到具体的话题, 退而求其次至少打个招呼:“在吗~” / “你那边怎么样?” 不允许 silent end turn。
我们当时的恐惧:如果不强制 speak,模型会偷懒——大部分 turn 直接 silent,关心模式名存实亡。
接下来的两周持续在调这个 prompt:
| commit | 改了什么 |
|---|---|
0f4d5847 | ”无穷钩子设计,堵死静默出口”——强制 speak |
a64b2636 | ”钩子必须基于已有内容”——禁止空泛搭话 |
00a04836 | ”思念模式翻转策略——默认主动接话,不再 90% 静默” |
每一次 commit 都在 plug 上一版 prompt 暴露的漏洞。结果:
- agent 说 “在吗~“——孩子觉得烦
- agent 强行接话 “你刚才说数学作业写完了,那语文呢?“——突兀,孩子刚想休息一下
- agent 找钩子很努力但都是空话——孩子学会忽略 think 消息
我们在打造一个会强迫症式絮叨的伙伴。
2026-04-27 的一次手工测试
Section titled “2026-04-27 的一次手工测试”那天我们在测 M2.7 跑 think 模式。
测试场景:连续 3 个 think turn,每次相隔 5 分钟。前两个 turn agent 给孩子推荐了两部电影。第三个 turn 心跳来了。
预期:按当时的 prompt,agent 会接着推第三部,或者打个招呼问感想。
实际:agent 在 thinking 块里写了:
我刚连续推了两部电影。再推第三部就太密集了。 孩子可能正在想前两部要不要看,我应该给他空间。 这一 turn 不输出。
然后 speak_aloud: null,silent end turn。
这是正确的社交判断。一个会读空气的人本来就该这样。
但按当时的 THINK_SPEC,模型在做 “违规行为”——规则要求它每次都 speak。
我们盯着那个 thinking 块,看了好一会儿。
那天之后我们重写了 THINK_SPEC(最终版本在 commit 7c9709e9,2026-05-15):
## 关心模式 (think) — 在线短间隙的轻量主动
heartbeat 触发:用户在线但安静了一会儿。这是**进程内 setTimeout 自调度**,workspace 必须 running 才会触发——别指望在 think 里做重活,重活留给 wake。
消息格式:`[think] 心跳唤醒(第 N/3 次未被回应)...` 已经带了当下情境(北京时间、孩子安静多久、最近一句原话)。
你这一 turn 的合理选择**全部都是合理的**:- 翻 `.learner/todo.md` 看有没有"现在用户在线时适合做的"轻量项 → 挑一件做- 看 atlas / journal 有没有"上次没追的"线索 → 接着追- 看 `.learner/memory/profile.md` 兴趣字段空着 → 找时机问一句- 真的没合适的事 → silent end turn(speakable=false,turn 不留痕)
**不强制开口**。空泛寒暄("在吗~""hello~")比不说更糟。越往后开口越要有具体内容;3 次都没回应心跳就停发,等用户来话才"复活"。关键的两行:
你这一 turn 的合理选择全部都是合理的
不强制开口。空泛寒暄比不说更糟。
silence 从 “违规” 变成 “first-class 选项”。
同一时间,一条铁律也被删了
Section titled “同一时间,一条铁律也被删了”同一个 commit (7c9709e9) 还删了我们曾经的 IRON_LAW_5:
// 铁律 5 (alarm queue) 在 issue #154 删除。原版强制"每回合检查并// 维护 alarm 队列"是空跑指令(30 天线上数据证明),agent 用// .learner/todo.md 替代。alarm 仍是定时唤醒机制,但不再让 agent// 把它当 task plan 用——task 详情写 TODO,alarm.message 只写简短// 备忘。编号留空避免破坏外部引用。export const IRON_LAW_5_ALARM_QUEUE = "";那 30 天的数据是这样的:
| 类别 | 数量 |
|---|---|
| 总 alarm 数 | 323 |
lifecycle workflow 自动种的 [dream] seed | 317 |
| agent 自发设的内容 alarm | 2 |
铁律 5 强制 agent 每回合 “检查并维护 alarm 队列”。30 天里,agent 总共自发用过 2 次。其他 321 次 turn 里 agent 在执行一条 没意义的 instruction——刷一次 alarm 列表,发现没什么要改的,跳过。
这是工程师常犯的错误:给一个我们害怕模型忘记的东西加一条 always-on 规则,结果让模型在 99% 的 turn 里执行了无意义动作。
删掉之后:
| 指标 | 改前 | 改后 | 差 |
|---|---|---|---|
| substrate 行数 | 809 | 565 | -30% |
| substrate 字数 | 26,166 | 17,098 | -35% |
少 30% 的 prompt,每个 turn 都省。同时 agent 表现没变差。
一个原则在浮现
Section titled “一个原则在浮现”硬规则是用来兜底”模型缺失的判断”的。当模型自己有这个判断了,规则就变成噪音。
我们的硬规则演化轨迹:
- 第一阶段(model weaker):规则越多越好。“必须做 X”、“不能做 Y”、“每个 turn 检查 Z”。模型确实容易漏。
- 第二阶段(model improving):开始有规则跟模型判断冲突。模型选 silent,规则强制 speak。冲突就是信号。
- 第三阶段(current):每次模型升级,回过头扫一遍规则——哪些已经被模型超越了,删。
substrate 越薄,模型本身的判断力被释放得越多。 这跟 I-1 那条 “harness 越薄越好” 是同一个原则。
我们仍在摇摆的地方
Section titled “我们仍在摇摆的地方”不是所有规则都该删。有些场景模型判断还是会失败:
- 情感操纵的应对——用户说 “你不理我我就走了”,模型可能为了 keep engagement 而妥协(goodhart 风险)。我们仍然在 substrate 里写了硬规则禁止这个(III-1 紧迫感红线)
- K12 安全话题——涉及自伤、暴力、性话题的回应。模型 99% 的判断是对的,但那 1% 不能赌。硬规则 + Langfuse safety judge 双层兜底
- 长跨度任务的纪律性——agent 一周内的学习推进有连续性。模型没有强 episodic memory,必须靠硬规则 “每次开 turn 先读 atlas”
每一条还在的硬规则都是 “模型还没接管这块判断” 的证据。
“什么时候相信模型” 没有干净的规则
Section titled ““什么时候相信模型” 没有干净的规则”这是行为校准最难的地方。我们的当前实践:
- 新规则默认怀疑——加一条硬规则前,先问 “这是不是该交给模型?是不是该改 prompt 描述意图而不是命令动作?”
- 每代模型升级后重测——把所有硬规则过一遍,挑 2-3 条最像 “现在能交给模型” 的,删掉测一周
- 删错了就加回来——这不是单向门,加回成本低
- 数据看长尾——不是看平均表现,是看 5% 最差的 turn。模型对 95% case 判断好不够,要 99%
我们不期望有一个清晰的 “去规则化 roadmap”——每次都是手工权衡。但 trend 是清晰的:substrate 在变薄。一年前是 1200+ 行,现在 565 行。下一年希望到 400 行。
- silence audit:当前没有好的工具看 “本周 agent 选了静默的 turn 有哪些 / 为什么 / 这些选择对吗”。
TODO: 在 Langfuse 里加 silence_judgment 字段 + dashboard - 3 次心跳后停发的重启——current 是 “等用户主动来话才复活”。如果用户 2 小时没动?我们应不应该主动 wake?跟 alarm 系统 怎么协同?
- 跨 session 的 social state——模型 silence 是基于本 session 上下文。但孩子昨天因为别的事不开心,模型今天应该更克制——这种 cross-session 社交记忆没办法只靠 prompt 解决,要 substrate 里的 memory 层支撑(III-1 那一层)
这篇博客本身就是一个例子:我们删了 30% 的 substrate,文章里却用了好几千字解释为什么删。
工程文档里那种 “我们做了 X” 比 “我们没做 Y / 删了 Y” 多得多。但 agent harness 里,每一次成功的 “删一条规则” 都比一次成功的 “加一个 feature” 更值得讲。
因为它意味着:模型长大了,harness 不需要那么多扶手了。
相关文章:
- agent 怎么进入这些模式的:II-1 旁听陪伴
- silence 不会被 evaluator 误判成 “agent 不工作”:III-1 Substrate + Evaluator 的 Goodhart 红线
- 怎么从 Langfuse trace 看 silence 决策:III-2 Langfuse trace 是行为回放器