第 5 章 Agent 失忆症与会话级 TODO · Agent架构实操五 开始测验

为什么上下文越长,系统提示词越没用?

十步重构任务,第三步被报错占住注意力,最初计划只出现一次却要和大量局部信息竞争。本章加一个会话级 todo_write:让模型反复提交完整计划快照,连续三轮没更新就提醒一次。
⏱ 约 12 分钟 📋 完整快照替换 🔔 三轮 Nag 提醒 +todo capability

🎯 本章导读

读完这一章,你应该能用一句话回答下面每个问题。
读完能做到
  1. 解释为什么长任务里丢的不是「目标」而是「进度」,以及为什么进度必须被显式记成结构化状态。
  2. 用 Zod 定义一份唯一输入契约,写一个「整体替换」的 TODO 快照工具。
  3. 说出「一口快照」为什么不需要回滚:校验全过才赋值,失败连新对象都不创建。
  4. 说明为什么提醒必须只进当次请求、不污染正式历史,以及按什么计数。
  5. 跑通第 5 章:让 Agent 先建立 TODO 再执行,观察提醒在第几次请求出现。
你需要先具备什么
需要说明
读完第 1—4 章会看 Agent Loop、Zod、prepare/permission/invoke、Hook 即可
TypeScript能看懂 interface、readonly、Object.freeze
Transformer 常识知道「注意力检索」大致是什么。这一章不做数学推导
建议的阅读路线
本章导读(你在这)
  ↓
① 先看一眼真实的一次运行    ← 一次任务两条关键日志
  ↓
② 先明确:长上下文不是靠后权重大
  ↓
③ Zod 契约 + 完整快照      ← 一、二
  ↓
④ 稳定 JSON + 会话状态     ← 三、四
  ↓
⑤ 系统提示只管"什么时候用" + Nag 计数  ← 五、六
  ↓
⑥ 当次注入 + 副作用标签     ← 七、八
  ↓
⑦ 失败不破坏旧快照 + 离线证明  ← 九、十
  ↓
⑧ 运行 + 实验 + 差异        ← 十一~十四
  ↓
⑨ 小结                     ← 三句话 + 七条 + 边界 + 自测
最该记住的一句 长任务里丢的不是「目标」,是「进度」——目标在开头出现过一次,进度从来没被显式记录。

📇 术语速查

本章第一次出现的词。先扫一眼,正文里遇到不认识的词回来查即可。不用背
术语一句话解释
完整快照模型每次提交的整份 TODO 状态,整体替换上一份,不是增量 diff
静态系统提示system 里一份固定文案;只告诉模型「什么时候该更新」,不携带进度数据
Nag Reminder按工具轮计数注入的「提醒你刷新 TODO」提示。只在当次请求出现
漏写即删除整体替换没有隐式 merge:漏写一项 = 删除一项
\uXXXX 转义返回的 JSON 里中文被转义(如 编写 → \u7f16\u5199),保证字节级稳定可断言
会话状态todo 快照和陈旧计数都属于「会话」,做成单例会串台
陈旧工具轮连续 N 次有工具调用但模型没有提交快照的轮次
STALE_TOOL_ROUNDS陈旧阈值,默认 3;「计数到 3」和「注入提醒」不在同一次请求

📡 先看一眼真实的一次运行

一次成功的快照提交 + 提醒到底在第几次请求出现。
一次成功的快照提交

模型调用 todo_write 后,工具把整份 TODO 以 tool result 回填,消息历史里从此就有了一份「最新进度」——这就是模型的显式记忆。

提醒到底在第几次请求出现
请求1  模型连续调用工具(读文件)            陈旧计数 0→1
请求2  模型又调工具,没提交快照            陈旧计数 1→2
请求3  模型又调工具,没提交快照            陈旧计数 2→3
请求4  beforeModel: 计数已到 3 → 注入提醒,清零   ← 第 4 次请求看到提醒
       模型看到提醒 → 调 todo_write 提交快照
为什么不是第 3 次「计数到 3」和「注入提醒」不在同一次请求:第 3 次请求时计数刚好到 3,注入发生在第 4 次请求前。

🧠 核心概念

点击展开。
1 · 长上下文不是「越靠后权重越高」

Transformer 根据内容动态计算注意力,不能简单理解为「后面 token 一定比前面重要」。但长任务有三个现实问题:

  • 工具输出不断增长,早期目标要与更多内容竞争注意力;
  • 相似日志和局部错误反复出现,模型更围绕眼前问题继续生成;
  • 最初计划只描述「要做什么」,却没持续提供「做到哪一步」。

系统提示写「不要忘记计划」只能表达原则,不能保存任务状态。本章思路:把计划变成结构化状态,通过工具结果把最新快照放回消息流。

能力边界 TODO 能降低偏航概率,不能证明任务完成。最终结果仍要由测试/静态检查等可观察验收确认。
2 · 完整快照,不是增量命令

没有 todo_add/update/remove 三组增量接口,只有一个整体替换工具。完整快照更适合模型:

  • 当前状态只由最后一次成功调用决定,易解释和重放;
  • 不需要处理「更新一个已不存在的索引」的状态机;
  • tool result 本身包含后续推理需要的全部计划;
  • 校验完成后只一次赋值,失败不产生半更新状态。

第一步完成后,模型不能只提交「第一项已完成」,要再次提交三项完整状态。

3 · Zod:先 trim 再 min(1)
content: z.string()
  .transform(c => c.trim())
  .pipe(z.string().min(1, "todo content must not be empty")),
status: z.enum(["pending","in_progress","completed"]),

如果直接对原字符串 min(1)," " 会通过长度检查。先 trim 再验证,它会稳定失败。两层 .strict() 都拒绝未知字段。最多 50 项。

同源 ToolDefinition 持有这个 schema,Registry 既用它生成 OpenAI JSON Schema,也用它在 dispatch 前 safeParse。模型看到的契约和 handler 收到的契约同源。
4 · 状态属于会话,不是模块

TodoTracker 不能做成模块级单例,否则同进程创建两个 Agent 时,第二个会话会看到第一个的计划。组合根在每次 buildAgent() 时 new 一个 tracker,同一个实例既提供工具 handler,也作为工具轮观察者传给 Loop。

进程退出后状态消失,本章不做磁盘持久化(第 12 章项目 Task 才解决跨会话持久化)。

5 · Nag Reminder 按工具轮计数

一个 assistant 消息可含多个 tool call。同一轮读三个文件,提醒计数应增加一次不是三次。规则三条:

  1. 没有工具调用,不计数;
  2. 本轮只要出现 todo_write,计数归零;
  3. 本轮有工具但没 todo_write,计数加一。

达到 3 后,beforeModel() 返回一条 system guidance 并立即清零。注意「达到三轮」和「注入提醒」不是同一个请求:

模型请求上一轮工具有 reminder轮后计数
10
2read_file1
3glob2
4shell重置后重新计数
6 · reminder 只进当次请求,不污染历史

Loop 调用模型前读取 guidance,只用于构造当前 ModelRequest,没有 push 进 history:

messages: [systemMessage(prompt), ...history, ...observerGuidance]

好处:提醒靠近当前生成位置能发挥提示作用;不会永久堆积,后续请求不会看到一串过期 reminder。

recordToolRound 在所有 tool result 配对写入历史后才记录工具名——位置不能乱移,否则会打断 OpenAI 消息协议。

7 · 失败不破坏旧快照

模型提交非法状态(status:"working"),路径是:JSON.parse → schema.safeParse 失败 → invalid_arguments → handler 未调用 → #todos 引用和值都不变。

状态赋值只有一个入口,只接收经 Zod 验证的输入,没有回滚问题。测试还保存更新前的数组引用,确认失败后仍是同一个快照对象。

8 · write effect ≠ 磁盘写审批

todo_write 的 effect 是 "write"(改变会话内状态),但第 3 章审批规则精确匹配 write_file 和 edit_file,不是粗暴拦截所有 write effect。所以更新 TODO 不弹磁盘写入审批,但仍过统一权限决策并写审计。

区分 副作用类别描述行为性质,具体策略决定哪些行为需要人工确认。

▶️ 交互演示:TODO 快照 + Nag 计数器

点待办项循环状态,或点按钮模拟「调用其他工具」/「调用 todo_write」,看 Nag 计数器到 3 时如何只在下一次请求注入 reminder。

当前完整快照(每次提交都是整体替换)

    陈旧计数:0 / 3 (本轮工具: )
    🔔 [system reminder] Keep the TODO list current. Call todo_write with the complete task snapshot when the plan changes. (只进这次请求,不写进 history,且立即清零)

    🔑 一句话总结

    把容易丢失的计划状态从长上下文抽出来 → 变成模型每次都能稳定看到的工具结果

    显式状态优于隐式期望。系统提示是原则,不能保存「当前进度」;todo_write 把状态变成可校验、可断言、可替换的数据契约。

    🚀 运行第 5 章

    前置条件和前几章一样。下面几步,每步都给出预期结果。
    第 0–1 步 · 环境与离线测试
    Set-Location 'F:\笔记\Agent实操\code'
    npm ci
    npm run test:ch05
     Test Files  14 passed (14)
          Tests  109 passed (109)

    那行 配置错误: Missing required settings: ... 仍是被断言的预期输出(config.test.ts 验证缺配置时 CLI 返回退出码 2),不是失败。

    第 2–4 步 · 先规划再执行、观察快照、统一入口
    npm run ch05 -- --prompt "先建立完整 TODO,再读取 README.md 并总结运行和验证步骤"
    # stderr 有 [Hook]/[Permission];模型是否先调 todo_write 取决于模型本身,
    # 系统提示是引导,不是强制门控。不调就把 prompt 写更明确。
    
    npm run ch05 -- --prompt "先用 todo_write 建立三项计划,然后原样告诉我 todo_write 返回的完整文本"
    # 模型复述出整份 {"todos":[...]},中文是 \uXXXX 形式
    
    npm run agent-tutorial -- run --chapter 5 --prompt "先建立完整 TODO,再读取 README.md 并总结运行和验证步骤"
    常见报错排查表
    现象原因 / 处理
    模型压根不调用 todo_write系统提示只是引导。把 prompt 写明「先建立完整 TODO」;本章不强制门控
    模型没按提醒刷新提醒只在当次请求出现,不进历史;靠模型调用 todo_write 才算「听进去了」
    invalid_argumentsZod 在 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. 长任务里丢的不是「目标」,是「进度」——目标在开头出现过一次,进度从来没被显式记录。
    2. 所以本章把进度做成结构化状态:模型每次提交完整快照,快照作为最新的 tool result 回到消息流。
    3. 模型忘记更新时,运行时按「连续三个陈旧工具轮」注入一条只进当次请求的提醒。
    一定要记住的七条
    #结论在哪一节
    1整体替换没有隐式 merge:漏写一项 = 删除一项
    2校验全部通过后才做一次赋值,不存在半更新,也不需要回滚二、九
    3readonly 只管编译期,运行时要靠 Object.freeze
    4快照和陈旧计数都是会话状态,做成单例会串台
    5按 assistant 消息计数,一轮三个调用只算一轮
    6「计数到 3」和「注入提醒」不在同一次请求;注入后立刻清零
    7提醒进 ModelRequest,不进 #history
    本章代码边界(明确「还没做什么」)
    已经有还没有说明
    会话内 TODO 快照磁盘持久化、跨会话恢复进程退出即消失。第 9 章做文件记忆
    单一整体替换工具依赖关系、blockedBy、并发认领第 12、17 章做任务 DAG
    固定「三轮」阈值官方那种「完成 3 项却没 verification 就提醒」刻意选固定轮数,让离线测试可确定性断言
    一条固定文案的提醒按任务内容动态生成不做。动态文案无法精确断言
    提醒只注入当次请求提醒进入正式历史不做。会在轨迹里累积过期提示
    单一会话的 tracker全局共享的 tracker不做。见第四节串台示例
    检查你是否真的读懂了(不看文章回答)
    1. 模型提交 3 项计划,下一次只提交 1 项,另外 2 项会怎样?
    2. 为什么快照要输出 编写 而不是直接输出中文?
    3. TodoTracker 做成单例 export const 会出什么问题?至少说两个。
    4. 一条 assistant 消息里有 3 个工具调用,计数加几?
    5. 提醒在第几次模型请求出现?为什么不是第 3 次?
    6. 在 result.history 里能搜到 Keep the TODO list current 吗?为什么?
    7. todo_write 的 effect 是 write,为什么不弹审批框?审计里有东西吗?

    QA 测验