程云来 / 杭州
返回文章·Agent 与 Harness
·约 6 分钟

思想|TodoWrite,清单写着完成,事情就做完了吗?

让多步工作的进度成为可读取、可修正的状态,同时分清计划声明、真实执行与停止条件。


输出是 42,哪一步还没做?

上一课已经能检查请求、询问权限、记录工具结果。现在把用户问题再说完整一些:

先读 main.py 和 expected.txt,运行之前问我;获准后运行并核对输出,报告实际结果,用清单记录进度。

假设源码是 print(6 * 7),预期文件里写着 42。模型读了源码,拿到一次程序输出,便回答“输出 42”。这个回答看起来没错,却没有说明预期文件是否读过、有没有完成核对。

原问题和完整工具历史都还在,但读者需要逐条翻找,才能分辨做了什么、还有什么没做。我们希望把当前工作拆成可查看的条目:读取源码、读取预期、运行并核对。每做一步,都能修正同一份计划,而不是在长对话里不断追加“接下来我要……”的文字。

这就是本课增加 TodoWrite 的理由:把计划变成 Harness 能保存、展示和重新提供给模型的数据。它有助于观察和修正漏项,并不保证模型一定正确执行。

模型写计划,Harness 保存状态

本课每项只有三个字段:id 标识条目,content 说明要做什么,status 使用 pending、in_progress、completed。ID 为后续核对提供同一条目的身份,状态表示当前声明;例如“运行并核对”可以保持 pending,直到运行获准并取得结果。

模型通过 todo_write(items=[…]) 提交完整列表。Harness 检查整份输入,保存成功后返回当前清单文本。这个工具只改内存计划;执行 Python 仍要调用 run_main,仍受前几课的权限和 Hook 约束。

SUBMITTED PLAN1 completed2 in_progress3 pendingVALIDATE ALLRENDER TEXTREADY TO COMMITSTAGINGCURRENT PLAN1 completed2 pending3 pending提交列表校验与准备宿主清单
模型提出整份新计划

模型已经读取源码,现在请求把读取预期输出设为进行中;这次更新由 todo_write 提交,尚未改动宿主清单。

todo_write(items=[…])
  1. 模型提交完整清单,状态是进度声明。
  2. 所有条目校验后才一次替换;失败保留旧清单。
  3. 下一次模型请求重新提供当前计划,原问题和工具历史保留。
  4. completed不执行动作,也不能证明任务完成,仍需检查工具证据。

图按一次计划交接推进。切到“整份拒绝”看旧状态是否被保留,再切到“声明全完成”:即使列表格式完全正确,也没有因此启动程序。真实函数、校验分支与请求组装见实践篇。

对于只有一步的请求,额外列清单可能多出一次工具调用。任务包含多个可遗漏的动作、需要展示进度或中途修正路线时,这种显式状态才更有价值。本例选择三个小动作,是为了让读者能完整核对一次更新。

先检查整份,再替换一次

假设旧清单是“源码已读,预期待读,运行待做”。模型提交的新列表中,第二项和第三项都成了 in_progress。本课约定同时最多一项进行中,这次更新应该失败。

如果边检查边覆盖旧清单,第一项先写成功,最后一项才出错,宿主就留下了一份既不是旧计划、也不是有效新计划的中间状态。更容易理解的写法是先在局部变量里收集候选条目,全部通过后,再一次替换 state["items"]。

输入情况本课处理更新后的当前清单
整份有效接受并替换使用新列表
重复 ID、未知状态、两项进行中拒绝并返回理由保留旧列表
空列表明确清空空列表
新列表省略旧条目整份替换被省略的条目移除

这是本册选择的最小契约。它便于解释原子提交,也要求模型每次都带上仍需保留的条目。若任务由多人或多个进程同时编辑,就需要版本检查、逐项更新或并发控制,不能把这份无锁内存列表直接当成共享任务系统。

清单保存了,下一轮怎样读到?

保存与重新读取是两件事。todo_write 的成功反馈会进入历史,但经历多次更新以后,历史里会同时出现旧清单和新清单。当前究竟采用哪一份,应由宿主保存的状态决定。

本课在每次模型调用之前复制完整历史,把当前清单附在首条用户问题的副本后。原问题保留,工具请求和对应结果原样保留;附加内容只属于本次请求,不再写回原历史。下一次调用重新生成,因此不会层层叠加同一条提醒。

下面的独立实验只演示“修改请求副本不影响原记录”:

PyPython · 浏览器本地运行
未运行
from copy import deepcopy

messages = [{"role": "user", "content": "读取文件,再运行并核对。"}]
plan = "运行并核对:pending"
prepared = deepcopy(messages)
prepared[0]["content"] += "\n当前清单:" + plan
print(messages[0]["content"])
print(prepared[0]["content"])
输出

点击行号设置断点,再点“调试”;也可以直接运行。

代码只读;执行发生在浏览器中,不会发送到博客服务器。

在修改 prepared 的行设置断点,可以对照两份文本。原问题只有一行,本次输入多出当前清单。这段代码没有调用模型;正式实现的 prepare_messages 会保留完整工具历史,并从宿主状态读取最近一次成功提交。

课程参考还演示了按固定轮数发出计划更新提醒。本课没有加入这个计数器:每轮提供当前状态已经足够观察清单如何参与判断;“模型多久应更新一次”和“怎么把当前计划带进去”是两个不同的设计问题。

全部 completed,也可能一次都没跑过

计划状态是模型提交的内容。只要格式合法,本课允许把任务全部标成 completed,也允许改回 pending。这方便修正计划,却不能验证声明是否属实。

验证“程序运行了”要看 run_main 的实际结果;验证“输出与预期一致”还要看预期文件内容与程序输出。TodoWrite 的返回只证明清单更新成功。把一项改成 completed,不会生成退出码,也不会代替运行许可。

停止同样需要分开看。用户拒绝运行以后,模型可以回答“本次未执行”,此时循环为 answered,运行项仍是 pending。达到模型调用上限会返回 call_limit,即使清单还没更新完也要交回控制权。我们没有让“全部标记完成”成为退出条件,也没有因为存在未完成项就强行续跑。

如果应用需要机器确认完成,就要为具体任务增加可检查的证据,例如测试命令、文件断言或人工验收,并明确哪个组件验证。进度表帮助你找到要检查的地方;真正的完成结论仍从这些证据产生。

回到开头,看到 42 后还可以追问:预期文件读了吗?核对动作完成了吗?清单让遗漏更容易被看见,工具记录则让回答有据可查。两份信息一起使用,才不会把一个完成标记当成工作的结果。

资料核对于 2026-10-19:课程 TodoWrite提供显式计划与提醒的教学路线;官方任务追踪文档说明结构化任务工具与进度展示。产品工具的默认可用性会随模型和版本变化,本课实现自己的 todo_write,不依赖 Claude Code 内部任务系统。