Agent 最怕的不是不会写代码:一个 TodoWrite 如何让它不跑偏?

本文是「从零理解 Claude Code:20 个 Agent Harness 机制」系列的第 5 篇。

源码仓库:shareAI-lab/learn-claude-code

本文基于开源仓库学习整理,具体实现以仓库代码为准。

让 Agent 改一行代码,很多时候看不出它和普通聊天有什么区别。

真正拉开差距的,是任务开始变长以后,就像下面这样的复杂任务:

text 复制代码
把项目里的 Python 文件统一改成 snake_case,
运行测试,
修复测试失败,
最后检查有没有遗漏。

这类任务表面上由几步操作组成,实际却不是线性执行的。

以重命名为例。Agent 改完几个文件后运行测试,测试报出导入错误。它接着定位报错、修改路径、再跑一轮测试。此时它的每一步都在解决一个真实问题,但任务的重心已经发生了偏移:原本要完成的是一次全量命名规范化,现在却逐渐变成了"把眼前的测试修到通过"。

为什么会出现这样的问题呢?其实原因很简单。

Agent 在循环里拿到的是一串局部反馈:某个文件存在、某次替换成功、某条命令报错、某组测试失败。每一条反馈都会告诉它下一步可以做什么,却不会自动告诉它:

  • 原始目标还剩多少没有完成;
  • 当前这一步服务于哪一项任务;
  • 测试通过以后,是否真的满足了最初的验收条件;
  • 现在应该继续排错,还是回去检查那些还没处理的文件。

换句话说,工具调用给 Agent 提供了下一步行动所需的信息,但没有维护整个任务目前处于什么状态。

没有这份状态,Agent 很容易变成一个只对最近反馈做反应的系统。它能解决局部问题,却不一定能把局部动作收敛到全局完成。

todo_write 补上的正是这一层。

它不负责执行重命名、修改导入或运行测试。它负责把任务拆成可追踪的状态:哪些步骤还没开始,哪一步正在进行,哪些结果已经得到验证。

这样,Agent 每次从工具结果里抬头时,都有一份明确的参照,而不是只靠上下文里较早的一句用户需求,判断自己接下来还该做什么。

一、复杂任务为什么容易跑偏

前几篇里,Agent 的工作方式大致是:

text 复制代码
模型判断下一步
→ 调用工具
→ 读取结果
→ 再判断下一步

这种一来一回的循环,处理眼前的小问题当然很好用。

读完一个文件,决定要不要继续搜;改完一处代码,跑一下测试;命令报错了,再根据输出找原因。每一步都有明确反馈,模型也容易接上下一步。

但任务一长,问题就变了。

重命名文件和修测试并不是两件各做各的事。文件名改了,导入路径、配置、文档里的引用都可能受影响;测试通过以后,还得回头确认有没有漏掉的文件。

这时,Agent 面对的不再是一连串独立操作,而是一件需要持续对照目标、不断回头检查的事。

以统一文件命名并修复测试这个问题为例,过程里通常会混着下面几类工作:

阶段 Agent 需要关注什么
盘点 哪些文件不符合命名规则
修改 文件名、导入路径、引用位置是否同步更新
验证 测试是否通过
收尾 是否仍有遗漏,是否引入新问题

如果没有显式计划,Agent 很容易被眼前的工具输出牵着走。

测试失败后,它会优先处理报错;处理报错后,又会继续跟进新的错误。每一步都有道理,但所有局部动作加起来,未必还能回到最初的任务目标。

这有点像出门办业务。

出门前,脑子里一直想着到了窗口该怎么说、要办哪些手续、排队要等多久。一路上也都在想这件事。结果真到了地方,才发现身份证忘带了。

Agent 做复杂任务时也会遇到类似情况。它不断根据最新的报错、文件内容和命令输出往下走,很容易把精力都放在眼前的问题上,却漏掉最初任务里那些还没完成的条件。

所以人出门办事会提前看一眼证件和材料清单。todo_write 做的也是这件事。

它把一段自然语言任务,变成一份会随着执行过程更新的待办状态。Agent 修完一轮测试后,不需要只根据最新的报错决定下一步,它还能回头看一眼:文件命名都检查完了吗?配置和文档引用处理了吗?最后的遗漏检查做了吗?

二、TodoWrite 不做事,它只把计划写下来

这一章新增的工具叫 todo_write

它不能读文件,不能改代码,也不能执行命令。它只接收一个待办列表,并保存当前状态。

一次调用大致长这样:

json 复制代码
[
  {"content": "扫描不符合 snake_case 的文件", "status": "in_progress"},
  {"content": "更新文件名和相关导入", "status": "pending"},
  {"content": "运行测试并修复失败", "status": "pending"},
  {"content": "检查是否有遗漏", "status": "pending"}
]

每一项只有两部分:

字段 含义
content 这一步具体要做什么
status 当前进度:pendingin_progresscompleted

工具在程序里把这份列表保存到 CURRENT_TODOS,并打印到终端。

text 复制代码
## Current Tasks
[◐] 扫描不符合 snake_case 的文件
[ ] 更新文件名和相关导入
[ ] 运行测试并修复失败
[ ] 检查是否有遗漏

看上去只是多了一份清单。

但这份清单的作用不在于给用户看,而在于让 Agent 的决策过程有一个可见的锚点。

模型每次调用 todo_write 时,待办内容和状态都会进入对话历史。后续继续推理时,它不只看到最近一次报错,也能看到任务进行到了哪一步,还有哪些事情没做。

三、计划真正有用的地方,是状态会变化

很多人第一次看到待办工具,会觉得它只是把 Prompt 里的要求重新列一遍。

如果列表只写一次,后面从不更新,那确实没什么价值。

TodoWrite 的重点是状态变化。

一个正常的执行过程应该像这样:

text 复制代码
扫描文件:in_progress
      ↓
扫描完成:completed
      ↓
更新导入路径:in_progress
      ↓
运行测试:in_progress
      ↓
检查遗漏:completed

例如,Agent 用 glob 找到了所有 Python 文件后,不应该只继续调用下一个工具,还应该把第一项标记为 completed,把"更新文件名和导入"改成 in_progress

这个动作很小,但它在告诉模型两件事:

  1. 已经完成了什么;
  2. 当前应该把注意力放在哪里。

尤其是在测试失败之后,这个状态很重要。

测试报错会把 Agent 拉进细节里,但待办列表仍然挂着"检查是否有遗漏"。当它修完报错后,更容易回到全局任务,而不是直接结束。

下面这张图把待办状态和工具执行之间的关系画出来了。

四、这其实是在给 Agent 保存一份工作记忆

这里有一个实现层面的细节需要区分。

CURRENT_TODOS 保存的是当前进程中的待办数据,用于展示和维护状态;它本身不参与模型推理。模型能够在后续步骤中参考计划,是因为调用 todo_write 时提交的待办内容会作为工具调用记录保留在消息历史中,并随下一轮请求再次进入模型上下文。

因此,TodoWrite 不是给模型增加长期记忆,也不会自动保证模型始终遵循计划。

它提供的是一份显式的、可更新的任务状态:任务被拆成哪些步骤,当前进行到哪里,还有哪些步骤尚未完成。

这份状态仍由模型维护,因而可能出现两类问题:任务拆分本身不合理,或者模型在缺少验证的情况下过早将某项标记为 completed

所以,待办列表的价值不在于形式上的"列计划",而在于每一项都应对应一个可以检查的完成条件。

待办项不能只写成一个笼统动作,因为模型很难据此判断何时算完成。

比如"扫描文件"只描述了动作,没有说明扫描后要得到什么结果。更合适的写法是:确认项目中不存在不符合 snake_case 的 Python 文件。这样,Agent 不仅知道要搜索,还知道结束前需要验证搜索结果。

"修复测试"也有同样的问题。测试失败的原因可能很多,Agent 如果没有范围约束,可能会不断修改无关代码。写成 运行 pytest,并处理本次重命名引入的导入错误 会更明确:它需要运行哪项验证,处理的是哪一类问题,哪些问题不属于这一步的范围。

五、只靠 System Prompt,为什么还不够

这一章的系统提示词明确要求:

text 复制代码
在开始多步骤任务前,先使用 todo_write 制定计划,并在执行过程中更新状态。

但 Prompt 里的要求并不总能被模型稳定执行。

复杂任务跑了几轮后,最新的文件内容、报错日志和工具结果会不断进入上下文。模型可能暂时忘了更新待办,也可能觉得眼前的操作更紧急。

因此,教学代码里加了一个简单的提醒机制。

如果连续三轮没有调用 todo_write,程序会在下一次请求模型前,把下面这条消息插入上下文:

text 复制代码
<reminder>Update your todos.</reminder>

逻辑可以概括为:

text 复制代码
连续三轮未更新待办
→ 程序注入提醒
→ 模型再次看到任务状态要求
→ 更新待办后,计数归零

这不是一个聪明的规划器。

它只是一个护栏:当 Agent 长时间埋头调用工具时,提醒它回头看一眼计划。

也要说明一点,连续三轮这个阈值是仓库为了教学加上的机制,不是一个通用的最佳实践。真实任务里,提醒频率应该和任务复杂度、工具类型、上下文长度有关。

例如,连续三次读取文件不更新待办,可能值得提醒;连续三次为了同一个 Bug 修改并运行测试,频繁打断反而会影响效率。

因此,提醒策略本身也需要设计,不能机械地每三轮提醒一次,这个只是一个比较笼统的做法,实际上还是要根据不同的业务场景选择不同的提醒策略。

六、TodoWrite 加到 Agent 里,主循环几乎不用重写

从代码结构看,这一章新增的仍然只是一个普通工具:

text 复制代码
todo_write
→ run_todo_write
→ CURRENT_TODOS

它和 read_filewrite_file 一样,最终通过 TOOL_HANDLERS 找到对应实现。

Agent Loop 不需要知道待办列表长什么样,也不需要负责渲染进度。

循环只多维护了一个计数器:

python 复制代码
rounds_since_todo += 1

if block.name == "todo_write":
    rounds_since_todo = 0

当计数达到阈值时,循环向消息列表追加提醒。

这也延续了前几篇的思路:

  • 工具分发负责找到能力;
  • Hooks 负责挂载扩展逻辑;
  • Agent Loop 负责推进模型与工具之间的循环;
  • TodoWrite 负责保存任务状态。

每层只做自己的事,后面加功能时才不会把所有东西重新揉进一个函数。

七、计划不是执行顺序表,而是一份可修订的承诺

计划最容易被误解成开始前一次性列好,然后agent照着做就行了。

但是真实开发不是这样的。

当 Agent 扫描完项目结构后,可能发现任务比预想多了一步:重命名文件会影响配置文件、文档链接,或者动态导入逻辑。

这时应该更新计划,而不是继续按旧计划执行。

例如原来只有三项:

text 复制代码
扫描文件
→ 重命名文件
→ 运行测试

扫描后发现还有文档引用,计划就应该变成:

text 复制代码
扫描文件
→ 重命名文件并更新导入
→ 更新文档和配置引用
→ 运行测试
→ 检查遗漏

TodoWrite 的价值不在于让 Agent 严格服从第一版计划,而在于让计划变化本身变得可见。

如果计划改变了,用户能看到;模型后续也能看到。相比把所有判断都藏在连续的工具调用里,这种方式更容易追踪,也更容易纠偏。

我们用一张图片来理解一下:

八、跑一下这章代码

进入仓库目录后执行:

bash 复制代码
python s05_todo_write/code.py

可以试这个任务:

text 复制代码
Refactor s05_todo_write/example/hello.py:
add type hints, docstrings, and a main guard.

观察几个地方:

  1. Agent 的第一个工具调用是不是 todo_write
  2. 它有没有把任务拆成可执行步骤;
  3. 开始处理某一步时,是否标记为 in_progress
  4. 完成后,是否及时改成 completed
  5. 连续几轮不更新待办时,是否收到提醒。

更复杂一点,也可以试:

text 复制代码
Create a Python package under s05_todo_write/example/demo_pkg,
including __init__.py, utils.py, and tests/test_utils.py.

重点不是待办列表写得漂不漂亮,而是 Agent 在创建文件、编写测试、运行验证的过程中,能不能持续更新状态,并在结束前检查有没有未完成项。

小结

TodoWrite 不扩展 Agent 的执行能力。它不会增加读写文件、执行命令或调用外部服务的权限,而是为当前任务维护一份显式状态。

在复杂任务中,模型会持续接收文件内容、命令输出和报错信息。待办列表将任务目标、当前进度和未完成事项以结构化形式保留在上下文中,使后续决策不只依赖最新一轮工具反馈。

因此,规划工具的作用不仅是让 Agent 在执行前生成一段计划文本,而且还为多步骤任务提供可更新的进度记录和完成检查依据。

下一篇将讨论子 Agent。

待办列表适合管理一段对话内可以完成的多步骤任务。但当任务规模继续扩大,例如需要同时分析多个模块、分别修改和验证时,单个 Agent 的上下文仍然会变得拥挤。此时可以将部分工作拆分给独立的子 Agent,在隔离的上下文中完成后再汇总结果。

相关推荐
Kel1 小时前
Node.js 没那么复杂
人工智能·node.js·全栈
茶马古道的搬运工1 小时前
Qoder 多角色协同开发:用 Custom Agent 搭一条软件生产线
人工智能
xd1855785551 小时前
睡眠质量评估 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
武汉唯众智创1 小时前
基于大语言模型的心理咨询数字人:从0到1构建一个能“共情“的AI心理陪伴助手
人工智能·数字人·ai心理健康·校园心理健康解决方案·ai无感监测·具身智能计算技术·智能体通信技术
minhuan1 小时前
大模型上下文工程核心策略解析:窗口管理、消息编排、记忆压缩与检索增强应用实践21.8
人工智能·大模型应用·大模型上下文工程·上下文窗口管理·上下文消息编排·上下文记忆压缩
love530love1 小时前
【排障实录】GPT Desktop (Codex) 开启 WSL 智能体模式后无法启动?手把手教你修复
人工智能·windows·gpt·agent
何时梦醒1 小时前
# ⚛️ React 19 + TypeScript 深度学习笔记 —— 从组件化思维到 WebGPU 端侧 AI 落地(续)
人工智能·react.js
小刘学技术1 小时前
AI人工智能中的类别不平衡问题:成因、影响与解决方案
开发语言·人工智能·python·机器学习
阿拉雷️1 小时前
部署实战】Docker + AI Agent:让AI一键部署Spring Boot到服务器,从打包到上线只要一条指令
人工智能·spring boot·docker