从零实现自己的agent第四期: agent的任务规划
Agent 任务规划:把 TODO 从模型脑内挪到外部状态
摘要:现在 Agent 已经能持续对话、调用工具,也有了记忆系统。但复杂任务一多,模型仍然容易跳步、重复或提前宣布完成。解决办法不是只在 prompt 里说“请先规划”,而是给它一个真正可写、可校验的外部 TODO 状态。本文拆解 update_todos 如何让任务进度变得可观察。
标签:Agent、任务规划、TODO、Tool Use、LLM 工程

会调用工具,不等于会稳定完成复杂任务
一个能调用工具的 Agent 已经很有用了。它可以读文件、跑命令、查看输出,再继续判断。但当任务变复杂时,你会看到新的问题。
比如用户说:
创建一个 demo 项目:新建目录,写 README,写 hello.py,运行验证,最后告诉我结果。
模型可能创建了文件,却忘记运行验证;也可能运行了测试,但忘了总结修改;还有时候它会在仍有步骤没做完时,直接给出“已完成”的回答。
这不是因为模型完全不会规划,而是因为计划只在模型脑内。外部程序看不到当前做到哪一步,也无法在最终回答前检查还有没有残留任务。
所以任务规划的关键,不是让模型多说几句计划,而是把计划变成运行时可维护的状态。


update_todos 的核心思想
教学示例从 build-agent-example/code/step07_plan_todolist.py 开始,主项目里对应的是 agent/tools/todo.py。
update_todos 做的事情很简单:让模型把完整任务列表写入一个外部 store。每个任务项有三个字段:
{
"id": 1,
"content": "创建 demo 目录",
"status": "pending"
}
状态只有三种:
pending → in_progress → completed
工具会限制同一时间最多只有一个 in_progress。如果模型一次传入两个进行中的任务,工具直接返回错误,让模型重新规划。

这一步的价值在于:计划不再只是自然语言,而是可写、可渲染、可校验的结构化状态。
为什么每次都传完整列表
update_todos 要求每次传入完整 todos 数组,而不是只传增量变化。这个设计看起来不够精细,但对模型很友好。
增量 patch 会引入很多边界:新增、删除、替换、移动、只改状态、合并冲突。模型很容易漏字段或改错 id。完整数组则简单得多:当前计划是什么,就把当前完整计划写回来。
TodoStore.update() 会清洗输入:
for i, t in enumerate(items, start=1):
content = (t.get("content") or "").strip()
if not content:
continue
status = t.get("status", "pending")
if status not in _VALID_STATUS:
status = "pending"
cleaned.append({
"id": t.get("id", i),
"content": content,
"status": status,
})
然后检查进行中数量:
in_progress_count = sum(1 for t in cleaned if t["status"] == "in_progress")
if in_progress_count > 1:
return "Error: 同一时间只能有一个 in_progress 任务,请重新规划。"
最后工具会返回当前列表。这个返回值很关键,因为模型下一轮能直接看到最新状态,不需要凭记忆猜自己做到哪里。
工具和 prompt 要一起工作
只提供 update_todos 工具还不够。模型不会天然知道什么时候应该用它,所以 system prompt 里要写清楚行为规则:
多步骤任务先拆计划
开始某一步前标记 in_progress
完成后标记 completed
同一时间只允许一项 in_progress
简单问答不必生成 todolist
这里体现了 Agent 工程里很重要的一条原则:
工具提供能力
prompt 规定策略
代码负责校验
只有工具,没有策略,模型可能不用。只有策略,没有工具,状态仍然不可见。只有工具和策略,没有代码校验,模型仍然可能乱填状态。
最终回答前必须检查残单
很多简单 Agent 示例在模型不再返回 tool_use 时就结束:
if message.stop_reason != "tool_use":
print(reply)
break
但有了 todolist 后,不能只相信模型自称完成。代码应该检查是否还有未完成项。
教学示例里有这样的逻辑:
unfinished = [t for t in TODOS if t["status"] != "completed"]
if unfinished:
history.append({
"role": "user",
"content": "差事尚未办妥,请按计划继续执行:\n" + render_todos(TODOS)
})
continue
如果还有 pending 或 in_progress,系统会把残单推回给模型,让它继续执行。只有全部 completed,才清空当前 TODO,避免上一轮任务污染下一轮。

这就是把“模型自觉完成”变成“运行时共同维护进度”。
普通计划文本为什么不够
你可能会问,让模型先输出一段 Markdown 计划不也可以吗?
1. 创建目录
2. 写 README
3. 写 hello.py
4. 运行验证
这种计划当然有帮助,但它仍然只是文本。模型后续可能遵守,也可能忘记。代码无法直接判断第 4 步是否真的做了,也无法自动阻止模型提前收口。
update_todos 的不同点在于,它把计划写进了运行时。程序能渲染它,能校验它,能在未完成时把它推回上下文。这就是结构化状态的价值。
凡是需要稳定维护的东西,都不要只放在自然语言里。自然语言适合理解和解释,结构化状态适合跟踪和约束。
什么样的 TODO 才好用
TODO 写得太虚,也会让规划失效。比如“处理项目问题”“完善代码”“检查一下”都不是好任务项,因为它们没有明确动作,也没有完成标准。
一个好 TODO 至少满足三点。
第一,一条只做一件事。不要把“读取文件、修改代码、运行测试”塞成一条。
第二,内容要能被工具验证。比如“运行 python hello.py 验证输出”比“确认能跑”更具体。
第三,最后要有收口项。很多任务不是做完修改就结束,还需要报告改了什么、验证了什么、有没有遗留风险。
这样写出来的计划,模型更容易按顺序推进,人也更容易从终端观察进度。
什么时候不需要规划
任务规划不是万能药。简单问答不该强制生成 todolist。用户问“什么是 tool use”,直接回答就好;如果每个问题都先拆计划,体验会变得笨重。
更适合使用 update_todos 的任务通常有这些特征:
- 多步骤;
- 涉及文件写入;
- 需要运行命令验证;
- 需要多次工具调用;
- 用户明确要求“先计划再执行”。
一个好的 Agent 应该能区分“直接回答”和“进入执行流程”。这决定了它用起来是灵活的,而不是一开口就摆出重型流程。
计划状态为什么不进 history
在主项目里,TodoStore 是一个单独的运行时状态。它不直接进入 history,也不会被记忆压缩逻辑吞掉。这个设计很重要。
history 记录的是对话和工具交互,适合给模型理解上下文;todo store 记录的是当前差事的执行状态,适合由代码检查和更新。如果把 todo 只写进 history,模型当然能看到,但程序很难可靠判断它是否完成;如果把 todo 放进独立 store,代码就能直接检查状态。
可以把它理解成两类上下文:
对话上下文:让模型知道前面说过什么
任务状态:让运行时知道当前做到哪里
这两者应该配合,而不是混在一起。模型需要看到 todo 的渲染结果,才能继续推理;代码需要保存 todo 的结构化版本,才能校验完成情况。
失败案例:模型提前收口
没有完成校验时,模型很容易提前收口。它可能已经完成了大部分工作,于是生成一句“任务已完成”,但还有一步验证没做。
这类错误很隐蔽,因为回答看起来很完整。用户不一定马上发现测试没跑,文件没检查,或者最后一步总结没有覆盖风险。
有了 update_todos 后,最终回答前可以强制检查:
还有 pending → 不准结束
还有 in_progress → 不准结束
全部 completed → 可以收口
这不是不信任模型,而是把模型不擅长稳定维护的状态交给代码。模型擅长理解任务和生成下一步动作;代码擅长检查结构化状态。这种分工,比单纯要求模型“请务必不要漏步骤”可靠得多。
自己动手验证
你可以用一个小任务测试规划工具是否生效:
创建 demo 目录;
写 README.md;
写 hello.py;
运行 hello.py;
最后总结结果。
观察终端里是否先出现完整 todo 列表。然后看每一步是否从 pending 变成 in_progress,再变成 completed。如果模型在未完成时给最终回答,运行时是否把残单推回去继续执行。
这个验证比看代码更有说服力。因为任务规划的价值不在于工具 schema 漂亮,而在于真实执行时能不能减少跳步、重复和提前结束。
prompt 里的规划规矩怎么写
update_todos 是工具,但模型什么时候调用它,仍然依赖 system prompt 里的行为规矩。规矩要写得具体,不能只写“复杂任务请先规划”。
更有效的写法是给出触发条件:
当任务需要修改文件时,先列 todo
当任务需要三步以上工具操作时,先列 todo
每次开始一个步骤前,将它标记为 in_progress
每次完成一个步骤后,将它标记为 completed
最终回答前检查是否还有 pending 或 in_progress
这些规则都能被运行时和日志验证。如果模型没有创建计划,或者完成后忘记更新状态,你能从工具调用里看出来。好的 prompt 不只是“鼓励模型认真”,而是让行为有可检查的落点。
计划粒度怎么控制
TODO 太粗会失去约束,太细又会拖慢执行。一个简单判断方法是:每条 todo 最好对应一次可以观察的阶段变化。
比如“修复 bug”太粗,因为里面包含复现、定位、修改、验证;“打开文件第 12 行”又太细,因为它只是一个工具动作,不一定代表任务阶段。更好的拆法是:
复现当前失败
定位相关实现
修改问题代码
运行验证命令
总结修改与风险
这个粒度既不会把每个工具调用都变成 todo,也能让用户看懂 Agent 现在推进到哪里。
计划不是承诺,而是工作台
很多人看到 todolist,会把它理解成模型对用户的承诺:列出来就必须一字不差执行完。但在 Agent 里,计划更像工作台。
执行过程中发现新信息,计划可以调整;某一步被证明没有必要,可以删掉或改写;遇到阻塞,也可以新增“调查替代方案”。关键是调整要显式发生,而不是模型脑内悄悄换做法。
这就是结构化计划的真正价值:不是让任务一开始就被预测完,而是让变化也能被看见。
计划和记忆的边界
前面已经有了记忆系统,为什么还要单独做 todo store?因为它们解决的问题不同。
记忆回答的是“这个 Agent 长期应该知道什么”,计划回答的是“当前这件事推进到哪里”。记忆可以跨会话存在,计划通常应该随着任务结束而清空。把临时计划写进长期记忆,会让未来上下文被旧任务污染;把长期背景塞进 todo,又会让执行列表变得臃肿。
一个简单原则是:任务结束后仍然有价值的信息,才进入记忆;只服务于当前推进过程的状态,放在计划里。这样 Agent 既不会忘掉稳定背景,也不会把每个临时步骤都背到未来。
小结
任务规划的核心,不是让模型输出一段漂亮计划,而是把任务状态外部化。
update_todos 把计划从模型脑内挪到运行时:状态可见、进度可查、错误可纠正、最终回答前可检查残单。这样 Agent 不再只靠模型自觉,而是由模型和代码共同维护任务推进。
有了计划之后,主线知道要做什么、做到哪里。但执行复杂任务时,工具输出仍然会把主 history 搞得很脏。下一步,我们需要把一些细节工作交给独立上下文处理。
视频与源码
如果你想看完整演示,可以在主页的《从零手搓 Agent》合集里按顺序观看视频版:
- 抖音:https://www.douyin.com/user/MS4wLjABAAAAk5lgbm96yoPPEoGXoY3MIp4S8voya0dzcnG0Lom5-SI?from_tab_name=main
- B站:https://space.bilibili.com/1452412374
文章里的示例代码和完整项目也放在这里:
- 📦 教学仓库:https://github.com/TheSyart/claude-agent-examples
- ⚔️ 实战项目:https://github.com/TheSyart/emperor-agent
我会持续更新 Agent 教学与实战内容。觉得有用的话,欢迎给项目点个 Star ⭐,也谢谢你一路看到这里。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)