RSS

第 6 章 / 共 11 章

写:把状态挪到窗口外

约 12 分钟 更新于

6.1 第 30 轮之后,你 /clear 了一次

shopfront 的迁移跑到第 30 轮。上下文快满了,你按惯例敲了 /clear,然后说「继续迁移结算流程到 v2」。

agent 从头开始。它重新读 src/checkout/,重新分析 src/api/client.ts,然后动手改一个二十轮前你们已经改完并且跑过测试的文件。你打断它,它道歉,接着又去碰另一个已完成的文件。

丢掉的是这些东西:

  • 已经迁完的 6 个文件,各自改了什么
  • 有 2 个文件因为要动数据库 schema 被暂时跳过,等 DBA 那边确认
  • v2 的错误码语义已经查清楚了:409 是幂等冲突不是并发冲突,重试没用
  • 试过用适配层包一层 v1 接口,被否了,因为团队约定不留兼容 shim

这些信息在被清掉的那三十轮里全都出现过,而且是你和 agent 一起花了不少 token 才确认下来的。清一次窗口,它们归零。

更隐蔽的情况是你没有 /clear。窗口涨到接近上限,自动压实(compaction)触发,摘要保住了大方向,但「哪 2 个被跳过、为什么跳过」这种细节最容易在摘要里蒸发掉。而在压实之前,这些信息已经躺在对话历史的中段——正好是模型注意得最差的位置。

6.2 指望历史记住,是把状态放在了最不可靠的地方

旧做法只有一条:把进展留在对话历史里,反正模型看得见。

这条路有三个独立的失效点,任何一个都够呛:

  1. 窗口有上限。 到了就要清或者要压,两种都会丢东西。
  2. 中段被忽略。 第 1 章讲过「中间迷失」,开头和结尾注意得好,中间差。第 12 轮确认的那条错误码语义,到第 40 轮就埋在中段,模型不是记不住,是不去看。
  3. 历史是流水,不是索引。 「已完成」这件事在历史里是分散的——第 8 轮改了一个文件,第 15 轮改了另一个,中间夹着二十次失败的搜索。要回答「现在迁完了几个」,模型得把整段历史重新扫一遍并做归纳,而这正是长上下文里最容易出错的那类操作。

所以问题不是「怎么让历史更耐久」,而是这类状态本来就不该只存在历史里

6.3 把状态写到窗口外面

这就是四动作里的写(Write):把需要跨轮次、跨会话保住的东西落到上下文窗口之外的地方,需要的时候再取回来。

三种形态,用途不同:

scratchpad / 任务笔记文件——本轮任务范围内的工作台。agent 边做边往里写,清窗口后第一件事是读回来。生命周期跟着一个任务走,任务结束就该归档或删掉。这是本章的主角。

长期记忆——跨会话保留的东西,通常分三类:

类型内容shopfront 的例子
事实型稳定不变的结论v2 的 409 是幂等冲突,重试无效
流程型这个项目怎么做事改完 src/checkout/ 必须同步更新 docs/api-migration.md
经验型踩过的坑和偏好团队不接受兼容 shim;测试用 tests/checkout/ 下的 fixture,不新建

事实型和流程型适合写进 CLAUDE.md,经验型往往更适合放在任务笔记里,因为它常常只对当前这一摊事有效。

文件系统当外部记忆——Manus 团队的做法:让 agent 把中间产物、长文档、大段返回值都落盘,上下文里只留文件路径和一句话摘要,需要时再读回来。容量没有上限,也不占常驻预算。这跟 5.4 说的工具截断是一个思路的两端——那边是不让大块内容进来,这边是让已经进来的大块内容出去。

第6章:状态出窗与回取

图 6.1:左侧是把进展留在历史里,清窗口时整段归零;右侧是每完成一步就写出到笔记文件,清窗口后一次读回。注意右侧回取的箭头只有一条,且落在上下文的末尾位置。

6.4 复述:用位置把目标拉回注意力

还有一个动作和「写」配套,Manus 管它叫复述(recitation):把 todo 列表在每一轮(或每几轮)重写到上下文的末尾

它不是为了让模型「看到」目标——目标在系统提示里本来就有。它是在利用位置。开头和结尾是注意力最好的两段,中间最差。任务开始时写下的目标,在第 40 轮已经离得很远了;把它重新抄到末尾,等于把它挪回了注意力充足的位置。

在 Claude Code 里这件事部分是自动的(todo 工具的列表会随进展刷新,且靠近上下文尾部),但你可以手动加强:让 agent 每完成一个文件,就把更新后的完整清单复述一遍。

一句必要的诚实:这是实践者在生产系统里观察到的有效信号,不是受控实验的结论。「中间迷失」本身有 Chroma 那类受控实验支撑,但「复述能有效对冲它」目前是经验。你自己上手时也该当成一个待验证的做法——先在一个真实长任务上试,看 agent 是不是少走了回头路。

6.5 任务笔记文件模板

放在 .agent-notes/ 下,一个任务一个文件,跟着代码走(要不要提交进 git 由你决定,多人协作的话建议提交)。

# checkout v2 迁移

## 目标
把 src/checkout/ 下的结算流程从内部 v1 API 迁到 v2,并补齐 tests/checkout/ 的用例。

## 完成判据
- src/checkout/ 下不再出现 `apiV1.` 调用
- tests/checkout/ 全绿,且新增了 v2 错误码分支的用例
- docs/api-migration.md 记录了 v1→v2 的字段映射

## 已完成
- src/checkout/cart.ts —— 换成 v2 client,字段 `total_cents``totalCents`
- src/checkout/summary.ts —— 同上,另外删掉了 v1 的重试包装
- src/checkout/coupon.ts —— v2 折扣接口参数从数组改为对象
- src/checkout/address.ts —— 无字段变化,只换 client 引用
- src/api/client.ts —— 增加 v2 实例,v1 实例保留但已标 deprecated
- tests/checkout/cart.test.ts —— fixture 更新到 v2 结构

## 进行中
- tests/checkout/summary.test.ts —— 断言已改,还差 409 分支的用例

## 被阻塞
- src/checkout/refund.ts —— v2 退款接口要求 order 表新增 `refund_ref` 列,
  等 DBA 确认 schema 变更窗口(2026-09-08 例会)
- src/checkout/invoice.ts —— 同一个 schema 变更,与 refund 一起做

## 已确认的事实
- v2 的 409 表示幂等冲突(同一 idempotency_key 重复提交),重试无效,
  正确处理是读回已有订单。不要写成指数退避重试。
- v2 分页参数是 `cursor`,不是 v1 的 `page`/`offset`
- v2 金额字段一律是分(整数),v1 是元(浮点)。跨层传递时别做二次换算。

## 已排除的方案
- 写一层 v1→v2 适配器:团队约定不留兼容 shim,评审时会被打回。
- 用 codemod 批量替换字段名:字段语义有变化(金额单位、分页语义),
  纯文本替换会引入静默的数值错误。
- 先只迁 API 调用、测试留到最后统一改:试过一次,第二个文件就发现
  测试 fixture 的结构变化会反过来影响实现写法,必须同步改。

字段的道理逐个说:目标完成判据分开写,前者是方向,后者是可以逐条打勾的终止条件——没有后者,agent 不知道什么时候算做完。已完成每条一行,文件名加一句话,不写过程。被阻塞必须带原因,否则重开会话时 agent 会以为那是漏做的,直接冲上去改。已确认的事实存的是花了 token 才查出来的结论,这一节的性价比最高。

6.6 「已排除的方案」是最容易漏也最值钱的一节

大多数人写笔记只写做了什么,不写没做什么、为什么不做。这一节每一次被跳过,agent 就会在下一个会话里重新提议一遍你上周已经否掉的方案,而你得重新解释一遍为什么不行。

它值钱在两点。一是省掉重复的弯路:没有这一节,「要不要写个适配层」这个念头会在每次清窗口后复活,每复活一次就要花几轮讨论。二是它保存的是负面信息,而负面信息在别处根本不存在——代码里看不出你没选什么,git 历史里也看不出,只有当时的对话知道,而对话正是要被清掉的那个东西。

写的时候一定带上原因。「不用 codemod」是条命令,agent 会照做但不会推广;「不用 codemod,因为字段语义有变化,纯文本替换会引入静默数值错误」是条判据,agent 遇到下一个类似诱惑时能自己判断。第 4 章讲的海拔在这里同样适用:给结论,也给结论背后的那一层理由。

6.7 让 agent 自己维护这个文件

人工维护笔记的问题不是难,是坚持不住。跑到第 25 轮正在追一个 bug,没人会停下来去更新一份 markdown。

所以把它变成规矩,写进 CLAUDE.md

## 任务笔记

长任务(预计超过 10 轮工具调用)必须维护 `.agent-notes/<任务名>.md`

- 会话开始时先读这个文件;不存在就按模板创建。
- 每完成一个文件的修改,立刻更新「已完成」,一行以内。
- 遇到阻塞立刻写进「被阻塞」,必须带原因和解除条件。
- 查证得到的接口语义、字段含义写进「已确认的事实」。
- 被否掉的方案写进「已排除的方案」,必须带为什么。
- 每次更新后,把「进行中」和「被阻塞」两节复述到回复末尾。

这比人工维护更容易坚持,因为触发条件绑在了 agent 本来就要做的动作上(改完一个文件),而不是绑在你的自觉上。最后那条把复述也挂了进去,一举两得。

一个实操提醒:规矩要写触发时机(「每完成一个文件」),不要写成「记得及时更新」。后者没有可判定的触发点,等于没写。

6.8 笔记本身也要有预算

笔记是为了压缩状态,不是为了归档过程。给它定几条硬规矩:

规矩具体做法
已完成项只留一行文件名 + 一句话变更摘要,过程和报错不进来
整个文件设上限目标 100 行以内,超了就把「已完成」合并成按目录汇总
事实只写结论「409 是幂等冲突,重试无效」,不写查证过程
阻塞项要清理解除后立刻从「被阻塞」挪到「已完成」,不留双份
一任务一文件别攒成一个 notes.md 通吃,跨任务的内容互相就是干扰项

判断标准很简单:这份笔记读回上下文的成本,要明显低于重建这些状态的成本。 三百行流水账做不到这一点,八十行结构化笔记可以。

顺带说,这条规矩对 CLAUDE.md 同样成立——它也是常驻上下文的一部分,第 4 章已经算过这笔账。

6.9 动手:给一个真实长任务补一份笔记

  1. 按 6.5 的模板建 .agent-notes/<任务名>.md,先只填「目标」和「完成判据」。
  2. 让 agent 读完当前对话历史,把「已完成」「被阻塞」「已确认的事实」三节填出来。填完你自己核一遍——它大概率会漏掉阻塞的原因。
  3. 「已排除的方案」这一节自己手写,回想这个任务里你否掉过什么,每条补上为什么。这一节 agent 填不好,因为被否的方案往往是你口头驳回的,历史里只有半句。
  4. /clear,然后只说一句「读 .agent-notes/<任务名>.md,继续」。看它第一个动作是不是接着「进行中」那一条往下做。

如果它跑去碰「已完成」里的文件,说明那一节写得不够明确——多半是缺了「改了什么」那句话,只有文件名的话,agent 分不清是改完了还是列在待办里。