TodoWrite
让 AI 给自己写待办清单
一、原理:LLM 的 working memory 局限
LLM 处理多步骤任务有个特别的弱点——它会"忘"。
不是真的忘——LLM 没有"记忆"概念。它的"记忆"是 message 历史。但当历史长起来之后,LLM 的注意力会"散开"——它能看到所有内容,但真正记得在做什么、做到哪一步、还剩什么这件事会失准。
具体表现:
表现 1:任务断章
用户让 LLM 做 5 件事:"修 auth、加 logging、改 README、跑测试、commit"。LLM 做完 1、2 后开始 3——做着做着把它做成了"还要改 docs"——任务漂移了。原计划的 4、5 被忘了。
表现 2:循环回头
LLM 在跑长任务中突然"想起来"应该先做某件事——回去做。但回去做的时候发现已经做过了——又往前继续。这种"原地打转"是常见现象。
表现 3:完成度自报失真
LLM 完成 3/5 任务时跟你说"已完成所有任务"——它真的觉得做完了。这是 1.4 章讲的"谄媚倾向"的延伸——LLM 倾向"声称完成"而不是"确认完成"。
这三个表现都跟一件事相关——LLM 缺乏外部化的状态追踪。它的"任务进度"全在 message 历史里——但 message 历史既冗长又分散,LLM 自己读起来不容易抓重点。
解决方案是给 LLM 一个外部待办清单。
TodoWrite 就是这样的工具。它让 LLM 写一个 markdown 格式的待办清单——每项任务有 status(pending、in_progress、completed)。LLM 在每个 turn 开始时看到这个清单、做完任务后更新状态。
这个清单是 LLM 的"外部 working memory"——它显式化任务进度。LLM 不再需要从冗长 message 历史里推断"我做到哪了"——清单直接告诉它。
效果立竿见影:
- 任务漂移减少(清单上不在的事 LLM 不会做)
- 循环回头消失(已完成项不会再做)
- 完成度自报准确(必须勾掉所有项才算完)
这是个用工具补 LLM 认知短板的经典案例。LLM 缺什么——给它什么。
二、案例:opencode 的 TodoWrite 设计
打开 packages/opencode/src/tool/todowrite.txt,description 长达 44 行——是 opencode 工具里第二长(仅次于 task.txt)。
为什么 TodoWrite 的 description 这么长?因为它不是"被调用的工具"——它是"被 LLM 主动使用的工具"。LLM 不会被外部触发去 TodoWrite,是 LLM 自己决定何时用。
这意味着 LLM 要学会的不只是"怎么调"——还要学会"何时调"。description 必须教 LLM 在合适时机主动使用这个工具。
我们看几个设计细节。
细节 1:明确的状态机
TodoWrite 的核心是 4 个状态:pending、in_progress、completed、cancelled。
转换规则:
- pending → in_progress(开始做)
- in_progress → completed(做完了)
- in_progress → cancelled(决定不做了)
- pending → cancelled(直接放弃)
明确状态机让 LLM 有"工作方式的脚本"——它知道每个任务的当前状态、下一步是什么。
细节 2:限制 in_progress 只有一个
description 强调:
"in_progress: Currently working on (limit to ONE task at a time)"
这条规则避免 LLM "同时开 5 个任务"——结果一个都没做完。强制一次专注一项。
这跟人类工作方式一致——多任务实际效率低。LLM 也是。
细节 3:实时更新而非批量
description 里强调:
"Mark completed only after actually completing the work, not preemptively."
每个 task 真做完了再标 completed。不要先把所有的都标 completed 再做——那是骗自己。
细节 4:全量替换而非增量
TodoWrite 的接口是"传入完整清单"——每次 LLM 调用都传整个 todo list。
为什么不是"add a task"、"update task 3 to in_progress"这种细粒度 API?因为细粒度让 LLM 容易出 race condition——它以为状态是 X,实际是 Y。
全量替换让 LLM 每次都"看到完整清单 → 写新完整清单"——没有"我以为是什么样"的歧义。
实现上 opencode 在每次 TodoWrite 调用时完整覆盖之前的清单——这是设计的选择。
细节 5:没有 TodoRead 工具
这是个有意思的设计——opencode 有 TodoWrite 但没有 TodoRead。
为什么?因为 LLM 每个 turn 开始时,opencode 会自动把当前 todo list 注入到 context——LLM 不需要主动读。
这种"自动注入"让 LLM 时刻看到当前进度——不需要它"想起来去查"。
但同时,TodoWrite 必须是 LLM 主动写——它要决定什么时候更新清单。
读自动 + 写主动——这种不对称的设计让 LLM 不会忘记"还有任务"(自动看到),但又能控制"什么时候完成"(自己确认)。
细节 6:示例驱动学习
description 里有完整使用示例——什么场景应该用、清单应该长什么样、状态应该怎么转。
一个典型示例:
User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!
Assistant: Creates todo list with the following items:
- Create dark mode toggle component in Settings page
- Add dark mode state management (context/store)
- Implement CSS-in-JS styles for dark theme
- Update existing components to support theme switching
- Run tests and build process, ensure they pass Begins working on the first task
这个示例展示了:用户提出复杂多步骤需求 → LLM 主动 TodoWrite 创建清单 → 立刻开始第一项。
LLM 看到这个示例就学会"哦,复杂需求来了我应该先 TodoWrite"——不是"先 read 文件、再写代码、再回头才想起来 TodoWrite"。
细节 7:保留用户原话
description 里有一条特别的规则:
"Preserve the user's original request verbatim — don't paraphrase commands"
如果用户说"修 auth bug",LLM 应该 todo 写"修 auth bug"——不要改写成"修复授权模块的缺陷"。
为什么?因为重写会引入误解——LLM 自己改写成的语义可能跟用户原意不一致。保留原话避免这种误解。
细节 8:跟 plan 阶段的关系
opencode 有 plan mode(1.8 章讲过)。在 plan mode 下 LLM 写 plan file,里面有"实施步骤"。这些步骤跟 TodoWrite 的清单是什么关系?
opencode 的设计是——plan file 是"高层方案",TodoWrite 清单是"立刻要做的具体步骤"。两者有重叠但不完全一样:
- Plan:架构决定、文件级别的改动方向
- Todo:实施时的具体任务,按顺序执行
进入 build mode 后,LLM 通常会基于 plan 创建 todo 清单——把 plan 的高层方案分解成可执行的 todo 项。
三、设计启示:用工具补 LLM 的认知短板
这一章的核心论点:TodoWrite 是个特殊工具——它不操作外部世界,它操作 LLM 自己的认知。
这种"自我操作工具"是 AI Agent 设计里一个被低估的模式。它的本质是——给 LLM 一个外部脚手架。LLM 自己记不住的、容易漏的、容易忘的——用工具显式化出来。
如果你做 AI 产品,下面几条原则有用:
1. 识别 LLM 的认知短板
观察 LLM 在你的产品场景里"经常出错"的模式:
- 多步骤任务忘了某一步?→ TodoWrite 这种清单工具
- 长 session 忘了用户的偏好?→ Preferences 工具(让 LLM 主动记忆)
- 跨 turn 的"上下文断片"?→ Notes 工具(让 LLM 留 memo 给自己)
- 工具调用历史模糊?→ 自动注入工具调用 summary
每个短板对应一个外部脚手架工具。
2. 自我操作工具的接口要简单
TodoWrite 只有一个参数——todos(清单)。简单到 LLM 不可能用错。
如果你设计自我操作工具有 10 个参数——LLM 会困惑。保持简单。
3. 全量替换而非增量
类似 TodoWrite,自我操作工具最好是"传入完整状态 → 完整替换"。避免增量更新的复杂度(race condition、状态不一致)。
4. 自动注入相关信息
LLM 写的清单/笔记/偏好——下次 turn 开始时自动注入到 context。不要让 LLM 主动 read——它会忘。
opencode 没有 TodoRead 是这种设计的体现。
5. 让 LLM 自然学会用
description 里给丰富示例——LLM 通过模仿学会"何时该用"。不要写复杂规则——LLM 在长 prompt 里会忽略。
6. 状态机要明确
每个自我操作工具应该有明确状态。TodoWrite 是 4 态。你的工具可能是 2 态(draft/sent)或者 3 态(new/active/archived)。
明确状态让 LLM 有清晰的操作指引。
7. 限制并发避免冲突
TodoWrite 的"只能一个 in_progress"是个约束。这种约束帮 LLM 聚焦。
如果你的工具支持并发——给个上限(最多 3 个 active 项),不要让 LLM "全部开起来"。
8. UI 端可视化
LLM 的自我操作要让用户看见——todo 清单在 UI 上显示、preferences 列在 settings 里。
让用户感受到"AI 在自我组织"——这是产品信任的来源。用户看到 AI 写清单、勾完成——会觉得"AI 在认真做"。如果这一切藏在后台,用户感受不到。
最后一个观察。TodoWrite 这种"自我操作工具"是 AI Agent 2024 年才普及的设计模式。Claude Code 推 TodoWrite、Cursor 有类似的 Plan view、Devin 有 Plan + Steps——这是个跨产品的共识。
为什么这两年才有?因为 LLM 的能力到一个程度后,"长任务能力"成了主要瓶颈。早期 LLM 连单步都做不好——没人在乎"它会不会忘任务"。现在 LLM 能跑 50 步——任务管理成了关键问题。
预计未来 1-2 年会有更多"自我操作工具"涌现——Memory 工具(让 LLM 长期记忆)、Reflection 工具(让 LLM 反思决策)、Goal 工具(让 LLM 显式化目标)。
理解 TodoWrite 这个模式,你就有了设计这一类工具的框架——识别 LLM 短板、给外部脚手架、状态机明确、自动注入相关信息、用户能看见。
下一章 3.10 是第 3 篇的收尾——我们看 opencode 15 个工具是怎么形成一个决策网的。description 里"何时用别的工具"是这个决策网的核心。