Agent 开发入门(三):为什么你的 Agent 总是在「瞎干」?试试 TodoWrite

3 阅读5分钟

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(上下文压缩)技术。