HARNESS 103

TodoWrite

"没有计划的 agent 走哪算哪"

给 Agent 一个写计划、跟计划的能力。
不替它画航线,但给它一支笔和一张纸。

1

问题:多步任务中,Agent 会"迷路"

想象你让 Agent 做一个 5 步任务(比如重构一个 Python 文件:加类型标注、加 docstring、加 main guard、跑测试、修复 lint)。在没有计划工具的情况下,典型的执行过程是这样的:

❌ 没有 Todo 时的执行轨迹

1️⃣ 加类型标注 — ✅ 完成
2️⃣ 加 docstring — ✅ 完成
3️⃣ 中间又顺手改了另一个文件 — ⚠️ 跑题
4️⃣ 又回来加了部分 main guard — ⚠️ 遗漏
5️⃣ 忘了跑测试 — ❌ 跳步
6️⃣ lint 也没修 — ❌ 遗忘

✅ 有 Todo 时的执行轨迹

📋 先列计划,5 个 todo 项
1️⃣ [>] 加类型标注 → [x] 完成
2️⃣ [>] 加 docstring → [x] 完成
3️⃣ [>] 加 main guard → [x] 完成
4️⃣ [>] 跑测试 → [x] 完成
5️⃣ [>] 修复 lint → [x] 全部完成 🎉
为什么会这样?对话越长,工具结果不断填满上下文窗口,系统提示的影响力逐渐被稀释,早期步骤从模型的"注意力"中被挤出去。模型不是不努力,是记不住。Todo 列表就是它的外部记忆——一张始终在对话里可见的进度表。
2

解决方案:TodoManager + Nag Reminder

s03 加了两样东西,一个给模型用,一个管着模型:

① TodoManager — 给模型一个写计划的地方

就是一个 todo 工具。模型调用它,传入一个 todo 列表,每个 todo 有 id(编号)、text(描述)、status(状态:pending / in_progress / completed)。

TodoManager 负责验证(最多 20 项、状态只能是三种之一、同时只能有一个 in_progress)并渲染成可读格式。

② Nag Reminder — 模型忘更新时提醒它

如果模型连续 3 轮(调用 LLM + 执行工具 = 1 轮)没有碰 todo 工具,harness 会自动在下一轮的工具结果里插入一条 <reminder>Update your todos.</reminder>

这不是"系统发出了警告"——它是悄悄地出现在 user 消息里的,模型会把它当成对话的一部分,自然地响应它。

两个机制合在一起的效果:模型主动写计划 → 按计划执行 → 忘了就有人催。

3

案例:用同一个任务对比"无 Todo"和"有 Todo"

任务:"重构 hello.py:加类型标注、加 docstring、加 main guard"。我们跟踪 Agent 在有无 Todo 工具时的实际行为差异。

场景 A:没有 Todo(s02 的行为)

轮次 1:LLM 直接动手,read_file hello.py。没计划。
轮次 2edit_file 加类型标注。
轮次 3-4:又读了两个文件,"顺便"改了另一个。
轮次 5:上下文已被大量工具结果填满。系统提示里的"加 main guard"已经被挤到注意力边缘。
轮次 6:LLM 觉得"差不多了",返回 stop。— main guard 没加,docstring 写了一半。

场景 B:有 Todo(s03 的行为)

轮次 1:LLM 先调 todo,列 3 项:
[>] #1: 加类型标注   [ ] #2: 加 docstring   [ ] #3: 加 main guard
轮次 2edit_file 加类型标注 → todo 把 #1 标 completed,#2 标 in_progress。
轮次 3edit_file 加 docstring → todo 把 #2 标 completed,#3 标 in_progress。
轮次 4edit_file 加 main guard → todo 全标 completed。
轮次 5:LLM 看到 3/3 completed,返回 stop。— 全部完成,没有遗漏。
💡 区别在哪?
Todo 列表始终在对话历史里。每次 LLM 被调用时都把整个 messages 传过去,而 messages 里一直有最近一次 todo 调用的 tool_result。所以那 3 个 todo 项永远在 LLM 的注意力范围内,不会被后来的工具结果冲掉。这就是"外部记忆"的真正含义。
4

TodoManager 内部:它不是一个文件,是一个内存对象

s03 的 TodoManager 不是写到磁盘的——它就是一个 Python 对象,存在内存里。这意味着:每次运行 s03,todo 列表都是空的。(后面 s07 才会把任务持久化到磁盘。)

TodoManager 做了三件事

① 验证:每次模型调 todo,TodoManager 检查数据是否合法——最多 20 项、每项必须有 text、status 只能是 pending/in_progress/completed、同时只能有一个 in_progress。如果模型传了不合法的数据(比如两个 in_progress),直接报错。

② 存储:把验证通过的 todo 列表存到 self.items。注意每次 update 是全量替换,不是增量追加。模型每次调 todo 都要传完整的列表。

③ 渲染:把 todo 列表变成人类可读的文本格式,作为 tool_result 返回给 LLM:

[>] #1: 添加类型标注
[ ] #2: 添加 docstring
[ ] #3: 添加 main guard
(0/3 completed)

为什么强制"同一时间只有一个 in_progress"?

这是 s03 最精妙的设计决策之一。如果允许多个 in_progress,模型会"同时做 5 件事",每件事都做一半。强制顺序聚焦 = 做完一个,再做下一个。这是用数据结构约束行为。

另外:TodoManager 只有 40 行代码。一个类 + 两个方法(update + render)。不需要数据库,不需要队列——就是内存里存一个列表。大道至简。

5

Nag Reminder:不更新计划就催你

模型有时候会"忘记"更新 todo。比如连续 4 轮都在做实际工作,没调 todo 工具。这时 harness 会介入:

Nag 的工作流程

Agent Loop 里有一个 rounds_since_todo 计数器:

① 每轮循环结束前,检查这轮模型有没有调 todo 工具。
② 调了 → 计数器归零。
③ 没调 → 计数器 +1。
④ 计数器 ≥ 3 → 在 tool_result 消息里注入一条 reminder 文本

# 注入 nag 的逻辑(在 agent_loop 中,每轮末尾) rounds_since_todo = 0 if used_todo else rounds_since_todo + 1 if rounds_since_todo >= 3: results.append({"type": "text", "text": "<reminder>Update your todos.</reminder>"})

关键设计:reminder 不是"系统警告",它是悄悄塞进对话的

注意注入的 reminder 是 {"type": "text"},被塞在 results 列表里,和其他 tool_result 一起以 role: "user" 进入 messages。模型看到的是:

{"role": "user", "content": [
  {"type": "tool_result", ...},
  {"type": "text", "text": "<reminder>Update your todos.</reminder>"},
]}

模型把它当成对话的自然一部分来响应,通常会紧接着调一次 todo 来更新状态。这个设计比"系统发警告"自然得多——不会打断模型的思路,只是轻轻拍一下肩膀。

6

代码拆解:s03 的三个新东西

A

TodoManager 类

一个 Python 类,40 行。接收模型传来的 items 列表,验证、存储、渲染。和 s02 的 dispatch map 一样简单——加一个工具,加一个 handler 函数。

class TodoManager: def update(self, items): # 验证: max 20, status 必须合法, 只能一个 in_progress for item in items: if item["status"] not in ("pending","in_progress","completed"): raise ValueError(...) self.items = validated return self.render() # 返回可读的文本格式
B

dispatch map 加一行(又是这个模式)

todo 工具和 bash、read_file 一样注册。handler 就是一个 lambda 调用 TODO.update()。

TOOL_HANDLERS = { "bash": ..., "read_file": ..., "write_file": ..., "edit_file": ..., "todo": lambda **kw: TODO.update(kw["items"]), # ← 就这一行 }
💡 又一次证明 dispatch map 模式的威力——加工具不用改循环。
C

Agent Loop 里的两个改动

Agent Loop 本体多了两样:① 追踪这轮有没有用 todo(used_todo 标记);② 连续3轮没用就注入 nag。

def agent_loop(messages): rounds_since_todo = 0 # ← 新增:计数器 while True: response = client.messages.create(...) # ... 工具执行 ... used_todo = False # ← 新增:本轮的标记 for block in response.content: if block.type == "tool_use": if block.name == "todo": used_todo = True # 这轮碰了 todo rounds_since_todo = 0 if used_todo else rounds_since_todo + 1 if rounds_since_todo >= 3: # ← 新增:3 轮没碰就催 results.append({"type": "text", "text": "<reminder>..."})
7

s02 vs s03:什么变了

组件s02s03
工具数量45 (+todo)
规划能力TodoManager 带状态追踪
Nag 注入3 轮后注入 reminder
Agent Loop简单分发+ rounds_since_todo + nag
Dispatch Map加一行,模式不变
外层 REPL完全不变
8

从 s03 带走的三个核心认识

01

外部记忆 > 依赖模型记性

模型不是不努力,是记不住。Todo 列表作为外部记忆,始终在对话历史中可见,不会被后来的工具结果冲掉。

02

用数据结构约束行为

"同一时间只能一个 in_progress"不是建议,是硬约束。好的 harness 设计用数据验证来引导模型的执行模式,而不是靠提示词"建议"。

03

被动注入 > 主动打断

Nag reminder 不是弹窗警告,而是悄悄塞进对话的文本。模型把它当成对话的自然部分来响应。Harness 的干预应该是"不显眼"的。

9

落地案例:TodoWrite 在真实 AI 产品中的应用

学完 TodoWrite 之后,你会发现几乎所有处理复杂任务的 AI Agent 都有某种"计划追踪"机制。以下是三个真实案例。

📋 场景一:Claude Code 的 TodoWrite 工具(你天天在用的)

你平时用 Claude Code 时看到的那个任务列表——[ ] pending[>] in_progress[x] completed——就是 TodoWrite。它就是 s03 的代码跑在 Claude Code 里的样子。

[x] #1: 添加登录页面组件
[x] #2: 实现表单验证逻辑
[>] #3: 对接后端认证 API
[ ] #4: 添加错误处理和提示
[ ] #5: 写单元测试
(2/5 completed)

下次 Claude Code 列出 Todo 时,你就能"看到背后":它调了 todo 工具 → TodoManager.update() 验证了数据 → 渲染成这个格式 → 作为 tool_result 注入了对话。如果 Claude 连着几轮没更新进度,你可能会在对话里看到它突然"自己提醒自己"更新 todo——那就是 nag reminder 在工作。

🤖 场景二:SWE-agent — 学术界的编码 Agent

SWE-agent 是普林斯顿大学开源的编码 Agent,在 SWE-bench 基准测试中名列前茅。它的核心设计之一就是规划:

开局:Agent 先查看 issue 描述和代码库结构,生成一个"plan"——列出需要修改的文件和修改思路。
执行:Agent 按 plan 逐项执行,每完成一步就更新进度。
反思:如果某一步失败了,Agent 回到 plan 重新评估。

SWE-agent 的论文里有一个关键数据:带了 plan 的 Agent 在复杂任务上的成功率比无 plan 的高出 30%+。这和 s03 的设计直觉完全吻合——没有计划的 Agent 走哪算哪。

🧠 场景三:AutoGPT / BabyAGI — 任务分解 Agent

AutoGPTBabyAGI 是 2023 年引爆 AI Agent 热潮的两个开源项目。它们的核心机制就是 TodoWrite 的强化版:

用户给一个大目标("帮我研究新能源汽车市场并写一份报告")。
Agent 自动拆成子任务:搜索资料 → 阅读关键文章 → 提取数据 → 写大纲 → 写正文 → 排版。
每个子任务就是一个 todo(pending → in_progress → completed)。
优先级排序:Agent 不仅追踪进度,还会动态调整优先级——如果某个子任务发现新线索,会插入新的 todo。

AutoGPT 和 BabyAGI 都因为"上下文管理"问题而难以稳定工作——这正是 s04-s06 要解决的问题。你学到的 TodoWrite 是它们任务管理机制的最小原型。

核心迁移:TodoWrite = 让 Agent 在长任务中不迷路。不管是 Claude Code 修 bug、SWE-agent 重构代码、还是 AutoGPT 写市场报告——任务越大,plan 越重要。你现在可以把"帮我做 X"升级为"先帮我列出完成 X 需要做的步骤,然后我们一步一步来"。这就是从普通用户到会用 Agent 的用户的跃迁。

s03 有了规划能力,但单个 Agent 遇到大任务还是会超上下文。
s04:怎么把大任务拆成子任务,每个在独立的上下文里执行?

s04: Subagent →