| 你需要先具备 | 说明 |
|---|---|
| 读完第 1—4 章 | 会看 Agent Loop、Zod、prepare/permission/invoke、Hook 即可 |
| TypeScript | 能看懂 interface、readonly、Object.freeze |
| Transformer 常识 | 知道「注意力检索」大致是什么。这一章不做数学推导 |
本章导读(你在这)
↓
① 先看一眼真实的一次运行 ← 一次任务两条关键日志
↓
② 先明确:长上下文不是靠后权重大
↓
③ Zod 契约 + 完整快照 ← 一、二
↓
④ 稳定 JSON + 会话状态 ← 三、四
↓
⑤ 系统提示只管"什么时候用" + Nag 计数 ← 五、六
↓
⑥ 当次注入 + 副作用标签 ← 七、八
↓
⑦ 失败不破坏旧快照 + 离线证明 ← 九、十
↓
⑧ 运行 + 实验 + 差异 ← 十一~十四
↓
⑨ 小结 ← 三句话 + 七条 + 边界 + 自测很多人以为长上下文问题出在「开头被遗忘」。Transformer 的注意力核心是检索:能否检索到,取决于两点——信号强度(内容本身够不够「显著」)和显式记录(有没有被写进消息流)。进度之所以丢,不是位置靠前,而是它根本没有作为独立信号存在过。
todo_write 只有一个输入:整份采用列表。schema 用 z.strictObject + z.array + 各项 .refine 校验:status 只能是 pending / in_progress / done。用 transform 保证语义归一(如去空白后不能为空)。只保留这一个入口,模型就没法把「更新进度」理解成别的。
const TodosSchema = z.strictObject({
todos: z.array(TodosItem),
});
// 模型必须提交整份 TODO,缺一项 = 删一项
整体替换没有隐式 merge:模型这次提交 3 项,下次只交 1 项,另外 2 项就没了。「漏写即删除」是刻意的——半更新比不更新更危险,至少模型能看见结果一致。校验全部通过后才做一次赋值,所以不存在半更新,失败也不需要回滚。
readonly 只管编译期,运行时靠 Object.freeze。实验二里严格模式会抛 TypeError,快照值不变。工具结果必须是对模型「有用而且能复读」的完整快照:content 返回整份 {"todos":[...]},中文转成 \uXXXX(如 编写 → \u7f16\u5199)。字节级稳定让离线测试能精确断言,模型也能把结果原样喂回下一步。
{"todos":[{"content":"\u7f16\u5199 README","status":"done"}]}
todo 快照和陈旧计数都是会话状态。如果做成 export const tracker = new TodoTracker() 的单例,两个不同会话跑同一个进程就会串台:A 会话的「三连陈旧」会触发 B 会话的提醒。状态必须从组合根按会话创建并注入。
system 里一份固定文案只说明:有 todo_write 工具、它的入参是完整快照、什么时候该调用。它不携带任何进度数据(带了就破坏静态前缀 / KV Cache)。提醒计数按 assistant 消息算:一条 assistant 消息里三个工具调用只算一轮。计数到 3 和注入提醒不在同一次请求;注入后立刻清零。
STALE_TOOL_ROUNDS = 3
// 连续 3 个陈旧工具轮 → 下次请求注入提醒 → 立即清零
提醒加进 ModelRequest(本轮给模型的 messages),但不 push 进 #history。否则提醒会在轨迹里累积成过期提示,第二条任务轮到时就变成噪音。在 result.history 里搜不到 Keep the TODO list current,正是这个契约的证明。
request.messages.push(reminderSystemMessage) // 当次请求
// history.push(...) ← 这里没有
todo_write 的 effect 是 write,但它只改会话内状态,不碰磁盘。规则精确匹配工具名而不是粗暴按 effect 匹配,所以 todos 更新不弹审批框;审计里有记录(任何最终决定都进审计),但无关磁盘安全。
handler 里先校验、后赋值。模型提交 {"todos":[...]} 中有非法项时,Zod 在 prepare 阶段就拦下:handler 一次都没跑,旧快照原样保留(用 toBe 断言,失败路径连新对象都没创建)。
// 失败路径:handler 不跑、无副作用、旧快照 isEqual/to 不变
if (prepared.error) return prepared.error; // invalid_arguments
| 测试文件 | 验证内容 |
|---|---|
todos.test.ts | Zod 契约、整体替换、冻结、会话隔离、按 round 计数、提醒注入时机 |
ch05-*.test.ts | 端到端:提醒出现在正确的模型请求、不进历史、失败不破坏旧快照 |
请求1 模型连续调用工具(读文件) 陈旧计数 0→1
请求2 模型又调工具,没提交快照 陈旧计数 1→2
请求3 模型又调工具,没提交快照 陈旧计数 2→3
请求4 beforeModel: 计数已到 3 → 注入提醒,清零 ← 第 4 次请求看到提醒
模型看到提醒 → 调 todo_write 提交快照
Set-Location code + npm ci。
npm run test:ch05 预期 14 个文件 109 个测试。
prompt 写明「先建立完整 TODO」。
看模型复述的完整 JSON(中文是 \uXXXX)。
agent-tutorial -- run --chapter 5。
npm run test:ch05 # 预期 14 files / 109 tests
npm run ch05 -- --prompt "先建立完整 TODO,再读取 README.md 并总结运行和验证步骤"
# stderr 有 [Hook]/[Permission];模型是否先调 todo_write 取决于模型本身,
# 系统提示是引导,不是强制门控。不调就把 prompt 写更明确。
npm run ch05 -- --prompt "先用 todo_write 建立三项计划,然后原样告诉我 todo_write 返回的完整文本"
# 模型复述出的是一整份 {"todos":[...]},中文是 \uXXXX 形式
| 现象 | 原因 / 处理 |
|---|---|
| 模型压根不调用 todo_write | 系统提示只是引导。把 prompt 写明「先建立完整 TODO」;本章不强制门控 |
| 模型没按提醒刷新 | 提醒只在当次请求出现,不进历史;靠模型调用 todo_write 才算「听进去了」 |
invalid_arguments | Zod 在 prepare 拦下;旧快照没变,handler 没跑 |
| 改了 STALE_TOOL_ROUNDS 后测试红 | todos.test.ts 断言默认 3;改完记得改回 |
npm run test:ch05 执行 14 个测试文件、109 个测试。四个值得亲手做的实验:
| 实验 | 预期 | 学到什么 |
|---|---|---|
| 一 · 撞一次 invalid_arguments 提交非法 todo,断言旧快照没变 | 用 toBe 而非 toEqual——失败路径连新对象都没创建 | Zod 在 prepare 拦下,#writeTodos 没跑 |
| 二 · 改已保存的快照 严格模式改冻结对象 | 抛 TypeError,快照值不变(需 @ts-expect-error) | readonly 只管编译期,Object.freeze 管运行时 |
| 三 · STALE_TOOL_ROUNDS 改成 1 让模型连续读几个文件 | 几乎每次请求都带提醒 | 阈值不能太小——提醒本身占注意力,变成新噪音。记得改回 3 |
| 四 · 一轮多个调用只算一轮 | 第一个 beforeModel 不提醒 | 按 assistant 消息计数,不按调用个数 |
| # | 结论 | 在哪一节 |
|---|---|---|
| 1 | 整体替换没有隐式 merge:漏写一项 = 删除一项 | 二 |
| 2 | 校验全部通过后才做一次赋值,不存在半更新,也不需要回滚 | 二、九 |
| 3 | readonly 只管编译期,运行时要靠 Object.freeze | 二 |
| 4 | 快照和陈旧计数都是会话状态,做成单例会串台 | 四 |
| 5 | 按 assistant 消息计数,一轮三个调用只算一轮 | 六 |
| 6 | 「计数到 3」和「注入提醒」不在同一次请求;注入后立刻清零 | 六 |
| 7 | 提醒进 ModelRequest,不进 #history | 七 |
| 已经有 | 还没有 | 说明 |
|---|---|---|
| 会话内 TODO 快照 | 磁盘持久化、跨会话恢复 | 进程退出即消失。第 9 章做文件记忆 |
| 单一整体替换工具 | 依赖关系、blockedBy、并发认领 | 第 12、17 章做任务 DAG |
| 固定「三轮」阈值 | 官方那种「完成 3 项却没 verification 就提醒」 | 刻意选固定轮数,让离线测试可确定性断言 |
| 一条固定文案的提醒 | 按任务内容动态生成 | 不做。动态文案无法精确断言 |
| 提醒只注入当次请求 | 提醒进入正式历史 | 不做。会在轨迹里累积过期提示 |
| effect: write 参与决策 | 用 effect 粗暴匹配审批规则 | 规则精确匹配工具名 |
| 单一会话的 tracker | 全局共享的 tracker | 不做。见第四节串台示例 |