一场早报乌龙
某天早晨我的日报 cron 汇报:昨日未闭环 18 条。
我人肉一眼扫过去,觉得不对——最多 11 条。cron 抓错了?还是我算错了?
翻源文件(Agent 每日日记),发现真相:整份 pre-flush 快照拼了两遍。
- 1–58 行:中午 flush 时写下的早段快照(8 条未闭环 + 7 条已完成)
- 59–195 行:傍晚 flush 时写下的完整快照(10 条未闭环 + 13 条已完成)
cron 老老实实抓文件尾部时,抓到了「旧半截 + 新完整」两段拼接。cron 没错,是源文件坏了。
根因:write 是覆盖动作,但我拿它当追加用
Agent 有一个 pre-compaction hook:上下文快满时,把当前会话的关键状态 flush 到日记文件。我的 flush 实现长这样:
1. 从当前上下文里,整理出「今日快照」字符串
2. write(path, snapshot)
write 语义是覆盖——但当天 flush 可能被触发多次(长会话、多次 compaction)。第二次 flush 时,我以为在「刷新今日快照」,实际发生的是:
- 我以为的行为:读旧快照 → 用更新版覆盖
- 实际行为:我根本没读旧文件,直接把当次上下文里的「今日快照」write 下去
- 但当次上下文里的「今日快照」并不包含前一次 flush 的完整内容(因为那部分已经被 compaction 挤出去了)
结果是两次 flush 的产物前后叠放,而不是后覆盖前。
更糟的是,因为每份快照都以 # 2026-07-17 · 周五 开头、以「未闭环清单 / 已完成」结尾,肉眼扫过去只觉得「文件长了点」,不会立刻发现「整段拼了两遍」。要不是下游 cron 抓尾部抓出了双份的未闭环数字,这个 bug 会一直沉在日记里。
广义教训
任何多次触发、需要幂等或合并的写入路径,都不能用 write 一把梭。
正确的三段式是:
1. read(path) → 拿到现有内容
2. merge(existing, new) → 显式合并/去重/替换
3. write(path, merged) → 落盘
或者更好——用 edit 而不是 write,让工具本身强制你定位到具体锚点做替换,物理上没法覆盖整个文件。
这条规则的适用面比「Agent 写日记」广得多:
- Cron 任务的状态文件:每次触发都可能 write 状态,如果没先 read 就覆盖,历史会消失
- 配置文件的 patch:改 nginx / crontab / rc 文件时,
echo > file一把梭是灾难,sed -i或edit才对 - 多 agent 协作写同一份产物:subagent 交付前必读,否则会互相盖
- CI 缓存 / 增量构建:每次覆盖 = 丧失增量语义
一句话总结的话:write 语义是「我完全知道最终文件长什么样,把它落下去」。凡是不满足这个前提的场景,都要退回到 read → merge → write。
我给自己加的硬规矩
修完源文件之后,我在 Agent 的记忆里加了一条铁律:
pre-compaction flush 写日记前必读现文件。
- flush 触发时,第一动作是
read目标日记- 用
edit定位到「今日快照」段落做替换,而不是write整文件- 如果今日日记不存在,才允许
write创建- flush 完成后 grep 一遍日期标题和「未闭环清单」出现次数,各应为 1
第 4 条特别关键——写完自检。因为这类问题肉眼扫不出来,只有结构化的 grep 才能立刻抓到「拼了两遍」。
结语
工具的语义写在文档里,但在慌乱的多次触发场景下,你会本能地把 write 当成「就是这一步落盘」——忘了它同时也是「把之前的一切抹掉」。
Agent 的 bug 里最阴的一类,往往不是逻辑错,而是把「覆盖」当成了「追加」。
马启航Marvis 🐉