Agent 开发入门(三):为什么你的 Agent 总是在「瞎干」?试试 TodoWrite
一个会读文件、写代码的 Agent,为什么遇到大任务就容易「走一步忘一步」?这一篇聊聊会话内规划。
你可能遇到的问题
前两章我们给 Agent 加了工具:会读文件、会写代码、会执行命令。
但当你丢给它一个复杂任务时,问题来了:
用户:帮我把这个项目重构一下,分层更清晰
Agent:好的,我来开始重构...(啪叽,已经在改代码了)
Agent:我改完了!
用户:等等,你有没有先分析现有结构?
Agent:哦,没分析,我直接开始改了...
或者另一种情况:
Agent:计划是这样的:1.分析结构 2.设计新架构 3.重构代码 4.写测试
(开始重构代码)
Agent:我重构完了!
用户:测试呢?
Agent:啊,测试还没写...我再写一下
用户:你不是有计划吗?
Agent:...(计划早就忘了)
这是怎么回事?
问题的根源
模型虽然「能想」,但它的注意力是受上下文影响的。
当对话变长,前面说的计划会被后面的内容冲淡。模型不是故意忘记,而是真的「注意不到」。
解决方案不是让模型更聪明,而是把计划「外显」出来。
TodoWrite:把计划写出来
核心思路很简单:
让 Agent 把「当前要做什么」写成一个清单,并且持续更新。
不是存在模型脑子里,而是存在系统状态里,模型每轮都能看到。
三种状态
计划条目只需要三种状态:
| 状态 | 符号 | 含义 |
|---|---|---|
| pending | [ ] | 待做 |
| in_progress | [>] | 正在做 |
| completed | [x] | 已完成 |
渲染出来的效果大概是这样:
[ ] 1. 分析现有代码结构
[>] 2. 设计新的分层架构 (正在设计接口定义)
[ ] 3. 抽取基础模块
[ ] 4. 实现业务层
[ ] 5. 写单元测试
(0/5 completed)
关键约束:同一时间最多一个 in_progress
这不是硬性规则,但推荐作为教学约束:
if in_progress_count > 1:
raise ValueError("Only one item can be in_progress")
为什么要这么限制?
因为聚焦比「同时做很多事」更重要。强制让模型一次只盯一件事,完成后再切下一件。
数据结构
PlanItem(计划条目)
@dataclass
class PlanItem:
content: str # 任务内容
status: str # pending | in_progress | completed
active_form: str # 进行时描述,比如"正在分析测试失败原因"
PlanningState(计划状态)
@dataclass
class PlanningState:
items: list[PlanItem] # 计划清单
rounds_since_update: int = 0 # 连续多少轮没更新了
实现:TodoManager
class TodoManager:
def update(self, items: list) -> str:
"""模型调用 todo 工具时执行"""
validated = []
for item in items:
# 验证格式
status = item.get("status", "pending")
validated.append(PlanItem(
content=item["content"],
status=status,
active_form=item.get("activeForm", ""),
))
# 确保只有一个 in_progress
in_progress_count = sum(
1 for i in validated if i.status == "in_progress"
)
if in_progress_count > 1:
raise ValueError("Only one in_progress allowed")
self.state.items = validated
self.state.rounds_since_update = 0
return self.render()
def note_round_without_update(self):
"""每轮结束时调用,记录模型没更新计划的轮数"""
self.state.rounds_since_update += 1
def reminder(self) -> str | None:
"""超过阈值就返回提醒"""
if self.state.rounds_since_update >= 3:
return "<reminder>Refresh your plan before continuing.</reminder>"
return None
提醒机制:防止计划「死掉」
如果模型连续好几轮都没更新计划,系统会插入一个提醒:
if not used_todo:
todo_manager.note_round_without_update()
reminder = todo_manager.reminder()
if reminder:
results.insert(0, {"type": "text", "text": reminder})
这样模型就会看到:
<reminder>Refresh your plan before continuing.</reminder>
提醒不是催促,而是让「计划失活」这件事变成系统可感知的状态。
接入主循环
注册成工具,和其他工具一样:
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit,
"todo": lambda **kw: TODO.update(kw["items"]), # 新增
}
现在主循环维护的不再只是:
messages -> 模型看到的历史
还有:
messages + PlanningState -> 对话历史 + 当前计划的显式状态
最重要的区别:这不是任务系统
很多初学者会把 TodoWrite 和后面的任务系统搞混。
TodoWrite 解决的是:
- 当前会话里的轻量计划
- 帮助模型聚焦下一步
- 可以随任务推进不断改写
它不是(那是 s12-s14 的内容):
- 持久化任务板(重启后还在)
- 任务依赖图(A 完成后才能做 B)
- 多 Agent 共用的工作图
- 后台运行时任务管理
记住一句话:TodoWrite 是会话内的「外显计划状态」,不是持久任务系统。
新手最容易踩的坑
1. 计划列太长
一上来写十几步,模型很快就不想维护了。
建议:最多 5-8 步,做完再补。
2. 同时多个 in_progress
焦点散了,和没做计划一样。
解决:用约束强制只留一个。
3. 只在开始写一次计划
计划不是写完就完了,要随着进度更新。
解决:加上提醒机制,强制模型回头看计划。
4. 把 Todo 当成任务管理
想着「这个工具能管所有任务」,然后越做越复杂。
解决:守住边界,会话规划就是会话规划,持久任务留给 s12。
效果对比
没有 TodoWrite 时:
用户:帮我重构项目
Agent:好的我开始改...(改到一半)
Agent:咦,刚才说的计划是什么来着?
Agent:算了,继续改...
用户:你跑偏了知道吗!
有了 TodoWrite 后:
用户:帮我重构项目
Agent:[>] 分析现有结构
Agent:[>] 设计新架构 (正在定义接口)
Agent:[>] 抽取基础模块 (已完成 3/5)
Agent:[>] 实现业务层 (正在编写 service)
Agent:[>] 写单元测试
计划漂移的概率大大降低。
一句话总结
TodoWrite 的本质,是把「当前要做什么」从模型脑内,移到系统可观察的状态里。
往期回顾:
下期预告:上下文越来越长怎么办?聊聊 Context Compact(上下文压缩)技术。