Claude Code 03:TodoWrite 规划与长任务防迷航

在掌握了 Agent 核心循环与文件操作工具后,Agent 已经能够完成“读取文件 A 并修复某行报错”这类简单的单步任务。

但一旦进入真实软件工程场景——例如“为系统增加 JWT 鉴权功能”,任务通常涉及读取配置、编写鉴权中间件、修改登录路由、补充单测并更新文档。在多达十几轮的工具调用后,没有规划层的 Agent 会频繁出现令人沮丧的**“迷航现象”**:

  1. 重复劳动:反复读取同一个文件,甚至覆盖刚刚写好的逻辑;
  2. 遗漏子任务:改完中间件后就宣告任务完成,完全忘记了单测与文档;
  3. 目标漂移(Context Drift):被中间出现的某个调试小报错带偏,顺藤摸瓜去改无关库,彻底偏离了最初的核心需求。

人类工程师在面对复杂工程时,第一反应是列出 Checklist。大模型同样需要一张动态更新的任务清单。

本文将实现一个极简的 Todo 规划层,揭示为什么必须强制“同时只能有一个进行中任务”,并拆解 Claude Code 内部从 V1 内存 Todo 到 V2 磁盘 Task 系统的技术演进。


规划层核心设计:TodoManager

我们通过一个专用的 TodoManager 来维护当前会话的任务看板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from dataclasses import dataclass
from typing import List, Literal, Optional

Status = Literal["pending", "in_progress", "completed"]

@dataclass
class TodoItem:
id: str
content: str
status: Status = "pending"

class TodoManager:
def __init__(self):
self.todos: List[TodoItem] = []
self._rounds_since_update: int = 0

def add(self, content: str) -> str:
new_id = str(len(self.todos) + 1)
self.todos.append(TodoItem(id=new_id, content=content, status="pending"))
self._rounds_since_update = 0
return new_id

def update(self, todo_id: str, status: Status) -> str:
for item in self.todos:
if item.id == todo_id:
# 黄金法则:同时只能存在一个进行中的任务
if status == "in_progress":
for other in self.todos:
if other.status == "in_progress":
other.status = "pending"

item.status = status
self._rounds_since_update = 0
return f"已将任务 [{todo_id}] 状态更新为: {status}"
return f"Error: 找不到 ID 为 {todo_id} 的任务。"

def render_board(self) -> str:
"""格式化输出任务看板"""
if not self.todos:
return "当前无任务清单。"

icons = {"pending": "○ 待处理", "in_progress": "◐ 进行中", "completed": "● 已完成"}
lines = [f"[{item.id}] {icons[item.status]} - {item.content}" for item in self.todos]
return "\n".join(lines)

def check_attention_drift(self) -> Optional[str]:
"""主动防迷航检查:若连续 3 轮工具调用未更新 Todo,注入警告"""
self._rounds_since_update += 1
if self._rounds_since_update >= 3:
uncompleted = [t for t in self.todos if t.status != "completed"]
if uncompleted:
return f"\n[系统提醒] 你已连续 3 轮未更新任务状态。当前仍有 {len(uncompleted)} 个任务未完成,请核对当前焦点!"
return None

为什么强制“同时仅有一个进行中任务”?

模型在长上下文中缺乏人类的专注力分配机制。如果允许模型将 3 个任务同时标记为 in_progress,它在生成代码时就会在多个目标之间反复横跳,无法形成连贯的原子操作。
强制互斥机制(in_progress 唯一性)像一副思维轨道,迫使模型每次只能聚焦解决当前的单一最小闭环。


集成进 Agent 工具栈

将 Todo 操作作为独立工具暴露给模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
todo_mgr = TodoManager()

TOOL_HANDLERS["todo_write"] = lambda **kw: handle_todo_write(kw)

def handle_todo_write(args: dict) -> str:
action = args.get("action")
if action == "add":
tid = todo_mgr.add(args["content"])
return f"成功添加任务,ID 为 {tid}。\n当前看板:\n{todo_mgr.render_board()}"
elif action == "update":
res = todo_mgr.update(args["id"], args["status"])
return f"{res}\n当前看板:\n{todo_mgr.render_board()}"
elif action == "view":
return todo_mgr.render_board()
return f"未知 action: {action}"

在系统指令中明确约束:“面对涉及 2 步以上的任务,第一步必须调用 todo_write 拆解任务,并在完成每个步骤后立即更新状态”。


生产源码探秘:V1 内存 Todo 的局限与废弃

在早期版本的 Claude Code 源码中,TodoWriteTool 就是按照上述全量快照思路实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/tools/TodoWriteTool.ts(V1 历史源码逻辑)
export class TodoWriteTool {
async call({ todos }, context) {
const appState = context.getAppState()
const todoKey = context.agentId ?? getSessionId()

// 每次更新必须传入整个 todos 数组的全量覆盖快照
const allDone = todos.every(t => t.status === 'completed')
const newTodos = allDone ? [] : todos

context.setAppState(prev => ({
...prev,
todos: { ...prev.todos, [todoKey]: newTodos }
}))

return { content: [{ type: 'text', text: renderTodoList(newTodos) }] }
}
}

V1 方案的致命缺陷

随着 Claude Code 面向大规模复杂项目演进,V1 的全量快照模式暴露出了严重的物理瓶颈:

  1. 全量覆盖易丢数据:模型在更新第 5 个任务时,偶尔会漏传第 2 个任务,导致任务被意外删除;
  2. 无法跨会话持久化:所有的 Todo 都存留在 Node.js 进程内存(appState)中,用户按 Ctrl+C 退出后,整个进度灰飞烟灭;
  3. 不支持任务依赖:无法表达“必须等任务 A 的单测跑通后,才能开始任务 B”这种 DAG 依赖关系;
  4. 无法支持多智能体并发:当多个 Agent 协同工作时,内存快照的相互覆写会导致严重的竞态冲突。

因此,Claude Code 在新架构中已经将 TodoWriteTool 标记为 Deprecated(已弃用),全面转向了基于文件持久化的 Task 工具族(V2)。


架构演进:V1 Todo vs V2 磁盘 Task

在 V2 架构下,每个任务对应磁盘上的一个持久化 JSON 文件,具备唯一的 UUID、状态时间戳以及 blockedBy: string[] 字段。任务变成了真正的独立实体,不再受限于单次模型上下文的存活周期。


总结

Todo 规划机制给 Agent 带来的改变是质的飞跃:

  1. 提供持续外部反馈:通过任务看板,模型始终清楚“我已经做了什么,我现在正在做什么,下一步要做什么”;
  2. 单进行中约束:严格限制同一时刻只能推进一个原子操作,消除多线程脑裂;
  3. 走向持久化:认识到全量内存快照的局限,为后续构建基于磁盘的持久化任务图埋下伏笔。

然而,规划层只解决了“不迷路”的问题。当某个子任务需要读取大量代码或执行数十次报错探索时,主 Agent 的上下文仍然会被这些无用的中间过程撑爆。

下一篇我们将探讨上下文防污染的核心利器:Subagent 子智能体与独立上下文隔离。