Agent 处理短任务时,可以直接读取、修改和验证。但任务一旦包含十几个步骤,新的报错和工具输出会不断进入上下文,模型很容易只顾眼前问题,忘记原始目标中尚未完成的部分。
TodoWrite 的作用不是让模型“思考得更多”,而是把计划转换成运行时能够保存、展示和校验的结构化状态。
用户目标
↓
模型调用 todo_write
↓
Harness 校验并保存计划
↓
模型执行普通工具
↓
持续更新 pending → in_progress → completed它没有增加读写文件或运行命令的能力,增加的是进度可见性和任务恢复线索。
计划为什么要放在模型之外
自然语言中的“我准备先做 A,再做 B”很容易被后续消息淹没,也不方便程序判断当前进度。结构化 Todo 则可以被:
- 用户直接检查;
- Harness 校验状态是否合法;
- UI 渲染为进度列表;
- reminder 机制检测是否长期未更新;
- 会话恢复或外部存储系统持久化。
这不是要求暴露模型的私有推理过程。Todo 应记录用户可理解的任务、状态和结果标准,而不是逐字保存内部思维链。
最小状态模型
当前课程为每个任务保留两个字段:
{
"content": "运行测试并修复失败",
"status": "pending"
}状态只有三种:
pending:尚未开始;in_progress:当前正在处理;completed:已经完成。
TodoManager 持有整个任务数组,todo_write 每次提交一份新数组,用新列表替换旧列表。它更接近“提交当前计划快照”,而不是逐条发送增量事件。
这种设计很简单,但调用者必须把仍然有效的旧任务一并传回。若只提交当前任务,其他任务会从计划中消失。
更新前先验证,再替换
一个可靠的更新函数不应边验证边修改正式状态。更安全的顺序是:
def update(todos):
validated = validate_all(todos)
self.items = validated
return self.render()先生成完整的 validated 列表,所有项目都通过后再替换 self.items。这样某一项非法时,原有计划不会停留在“前半部分已经更新”的中间状态。
当前教学实现检查:
- 输入必须是列表;
- 每项必须是对象;
content不能为空;status必须属于三个允许值;- 同一时刻最多一个任务是
in_progress; - 一次最多提交 20 项。
“最多一个进行中任务”体现的是串行专注模型。若系统支持多个并发子 Agent,就可能需要允许多个 in_progress,并增加负责人或执行 ID。状态约束应服务于实际执行模型,而不是机械照搬。
20 项上限同样只是课程中的防膨胀策略。真实系统可以按界面容量、上下文成本和任务复杂度配置,并在计划过大时引导模型拆分里程碑。
Schema 和运行时校验缺一不可
工具定义通过 JSON Schema 告诉模型参数结构:
{
"name": "todo_write",
"input_schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"maxItems": 20,
"items": {
"type": "object",
"properties": {
"content": {"type": "string", "minLength": 1},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
},
},
"required": ["content", "status"],
},
}
},
"required": ["todos"],
},
}Schema 能减少模型生成错误,但不能替代 handler 内的验证。调用可能来自旧客户端、测试、手工请求或绕过 schema 的其他入口,运行时仍应把输入当作不可信数据。
课程还兼容字符串形式的 Todo:先尝试 json.loads,再尝试 ast.literal_eval。literal_eval 只解析 Python 字面量,不会像 eval 那样执行任意表达式,但如果接口契约本来就要求数组,生产 API 通常更适合拒绝字符串,保持单一输入格式。
render() 只负责展示
状态存储和展示应分开。render() 读取 self.items,把状态映射为标记,并计算完成数量:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]",
}[todo["status"]]
done = sum(todo["status"] == "completed" for todo in self.items)在 Python 中,布尔值可以参与整数求和,因此每个 True 贡献 1,最终得到已完成数量。
渲染层可以换成图标、终端颜色或网页组件,但不应借此修改任务状态。模型也不应从终端截断后的预览反推真实列表;Harness 保存的结构化状态才是事实源。
Reminder 是软约束,不是调度器
当前课程统计“连续多少个工具调用轮次没有使用 todo_write”:
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>",
}
)
rounds_since_todo = 0这里的“三轮”指包含工具调用的 Agent Loop 轮次,不是三个工具,也不是三次用户对话。一次响应即使调用多个普通工具,计数通常也只增加一次;只要其中用了 todo_write,计数就会清零。
提醒作为文本 block 与工具结果一起回传模型。它能提高模型更新计划的概率,但不能保证模型一定照做,也不能证明任务真的完成。
更成熟的系统可以根据风险采用不同强度:
- 软提醒:提示模型更新计划;
- 协议约束:没有合法计划时拒绝执行高风险工具;
- 工作流状态机:由运行时决定哪些步骤可以开始;
- 人工检查点:关键步骤完成后等待用户批准。
不要把提示词层面的 reminder 当作事务、权限或调度保证。
content、text 和消息层级
在 Messages API 风格的消息中,字段名取决于所在层级和 block 类型:
{"role": "user", "content": [...]}
{"type": "text", "text": "Update your todos."}
{"type": "tool_result", "tool_use_id": call_id, "content": output}外层 message 使用 content 承载 block 数组;文本 block 使用 text;工具结果 block 使用 content 并通过调用 ID 与对应的 tool_use 关联。
不宜把某个 SDK 版本中 block 类型的数量写成稳定知识。API 和 SDK 会演进,代码应依赖当前官方类型定义,并只处理自身实际支持的 block。
这份状态还缺少什么
课程中的 TodoManager 是进程内单例,适合演示,却不是完整任务系统:
- 进程退出后计划丢失;
- 多个会话可能共享或覆盖同一份状态;
- 整体替换没有版本号,存在并发丢失更新;
completed由模型声明,不代表测试或验收真的通过;- Todo 没有稳定 ID、依赖关系、负责人、时间戳或失败原因。
如果要把它升级为产品能力,可以逐步加入:
- 按会话或任务 ID 隔离状态;
- 持久化与恢复;
- 乐观锁或版本号;
- 稳定 Todo ID 和变更事件;
- 完成条件及验证证据;
- 取消、阻塞、失败等状态;
- 大任务拆分和子任务依赖。
但字段越多,模型正确维护状态的成本也越高。先确定用户需要观察和控制什么,再扩展模型。
建议覆盖的测试
TodoWrite 至少应验证:
- 空列表和正常状态流转;
- 非列表输入、非对象项目和空内容;
- 未知状态及大小写规范化策略;
- 多个
in_progress是否被拒绝; - 超过配置上限的计划;
- 某项非法时旧状态保持不变;
- 更新是否按约定整体替换;
- reminder 的计数、清零和注入位置;
- 一轮多个工具调用时的计数语义;
- 会话隔离、并发更新和进程恢复;
- UI 截断不影响真实结构化状态;
completed是否需要外部验证证据。
小结
TodoWrite 的关键价值,是把易被上下文冲淡的自然语言计划变成 Harness 可保存、校验和展示的状态。它能帮助模型维持方向,也让用户看见当前进度。
但 Todo 仍然只是计划记录:模型写下 completed 不等于任务已经通过验收,三轮 reminder 也不是强制调度。真正可靠的 Agent 系统还需要权限控制、验证命令、持久化、并发隔离和必要的人工检查点。