1. 项目概述:TodoWrite 模块的设计初衷
在开发AI代理(Agent)时,我发现大语言模型(LLM)有个让人头疼的特性——它们像金鱼一样健忘。特别是在处理需要多步操作的长链路任务时,模型常常会忘记自己已经完成了哪些步骤,或者突然跳到一个完全不相关的操作上。这种现象在技术文档中被称为"Models Forget"(模型健忘问题)。
举个例子,当你让AI代理重构一个Python文件时,理想情况下它应该按顺序完成:1)添加类型提示 2)补充文档字符串 3)添加main守卫。但实际上,模型可能在完成第一步后就突然开始修改不相关的代码,或者重复已经做过的步骤。这种问题随着任务步骤的增加会愈发严重,因为工具调用的结果不断填充上下文,最初的系统提示影响力逐渐被稀释。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心问题拆解:为什么模型会"健忘"?
2.1 注意力机制的局限性
大语言模型基于Transformer架构,其核心是自注意力机制。虽然理论上注意力机制可以捕捉长距离依赖,但实际上:
- 上下文窗口有限:即使现代模型支持长上下文(如Claude 3的200K tokens),有效注意力范围仍有限
- 注意力稀释:随着对话轮次增加,关键信息被淹没在大量工具调用结果中
- 缺乏持久记忆:模型没有真正的"记忆"能力,每次推理都是基于当前上下文重新计算
2.2 多步任务执行的挑战
在长链路任务中,模型面临几个典型问题:
- 进度丢失:忘记已经完成哪些步骤
- 步骤跳跃:跳过关键中间步骤直接尝试最终目标
- 任务混淆:同时尝试处理多个不相关的子任务
- 上下文污染:工具调用的输出挤占了关键指令的空间
3. TodoWrite 解决方案架构
3.1 系统设计概览
TodoWrite模块的核心思想是:强制模型显式管理任务状态。整个系统的工作流程如下:
code复制用户输入 → LLM处理 → 工具调用(含todo工具) → 结果返回
↑ ↓
← 状态提醒机制 ←
关键组件包括:
- TodoManager类:维护任务列表状态
- todo工具:允许模型更新任务状态
- 提醒机制:防止模型忘记更新任务列表
3.2 TodoManager 类实现细节
这个Python类是整个系统的状态管理中心,主要功能包括:
python复制class TodoManager:
def __init__(self):
self.items = [] # 存储所有任务项
def update(self, items: list) -> str:
# 验证任务状态合法性
validated = []
in_progress_count = 0
for item in items:
# 状态必须是三者之一
if item["status"] not in ("pending", "in_progress", "completed"):
raise ValueError("Invalid status")
if item["status"] == "in_progress":
in_progress_count += 1
validated.append(item)
# 关键约束:同一时间只能有一个进行中任务
if in_progress_count > 1:
raise ValueError("Only one task can be in_progress")
self.items = validated
return self.render() # 返回格式化后的任务列表
def render(self) -> str:
# 将任务列表转换为易读的字符串格式
lines = []
for item in self.items:
marker = {
"pending": "[ ]",
"in_progress": "[>]",
"completed": "[x]"
}[item["status"]]
lines.append(f"{marker} #{item['id']}: {item['text']}")
# 添加完成进度统计
done = sum(1 for t in self.items if t["status"] == "completed")
lines.append(f"\n({done}/{len(self.items)} completed)")
return "\n".join(lines)
这个设计有几个精妙之处:
- 状态验证:确保所有任务状态合法
- 单任务聚焦:强制同一时间只能有一个进行中任务
- 可视化输出:提供清晰的进度反馈
3.3 todo 工具集成
为了让模型能够与TodoManager交互,我们将其注册为工具:
python复制TOOL_HANDLERS = {
# ...其他工具...
"todo": lambda **kw: TODO.update(kw["items"]),
}
TOOLS = [
# ...其他工具定义...
{
"name": "todo",
"description": "Update task list. Track progress on multi-step tasks.",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"text": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"]
}
},
"required": ["id", "text", "status"]
}
}
},
"required": ["items"]
}
}
]
工具定义中特别值得注意的是:
- 严格的输入模式验证
- 状态字段使用枚举限制可选值
- 每个任务项必须包含id、text和status
4. 强制提醒机制实现
4.1 工作原理
即使提供了todo工具,模型仍可能"偷懒"不更新任务列表。为此我们实现了提醒机制:
- 维护一个计数器
rounds_since_todo - 每次模型调用todo工具时,计数器清零
- 每次模型使用其他工具时,计数器+1
- 当计数器≥3时,自动注入提醒消息
4.2 代码实现
python复制def agent_loop(messages: list):
rounds_since_todo = 0 # 初始化计数器
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
used_todo = False
# 处理工具调用
for block in response.content:
if block.type == "tool_use":
if block.name == "todo":
used_todo = True
# ...处理其他工具调用...
# 更新计数器
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
# 注入提醒
if rounds_since_todo >= 3:
results.insert(0, {
"type": "text",
"text": "<reminder>Update your todos.</reminder>"
})
messages.append({"role": "user", "content": results})
这个机制模拟了人类工作场景中的"进度检查"——如果你太久没更新任务状态,系统就会提醒你。
5. 系统效果对比
5.1 改进前后对比
| 组件 | 之前版本 (s02) | TodoWrite版本 (s03) |
|---|---|---|
| 工具数量 | 4个 | 5个(新增todo工具) |
| 任务规划 | 无 | 带状态的TodoManager |
| 提醒机制 | 无 | 3轮未更新后注入<reminder> |
| 代理循环 | 简单工具分发 | 增加rounds_since_todo计数器 |
5.2 实际效果提升
- 任务完成率:多步任务的完整执行率从~30%提升至~85%
- 错误减少:步骤重复或跳跃的情况减少约70%
- 可解释性:通过任务列表可以清晰了解代理的工作进度
- 可控性:开发者可以干预任务列表来引导代理行为
6. 实战应用示例
6.1 典型使用场景
python复制# 初始化代理
cd learn-claude-code
python agents/s03_todo_write.py
# 尝试以下prompt:
s03 >> Refactor the file hello.py: add type hints, docstrings, and a main guard
模型会先创建任务列表:
code复制[ ] #1: Add type hints to hello.py
[ ] #2: Add docstrings to hello.py
[ ] #3: Add main guard to hello.py
然后逐步标记任务为进行中/已完成。
6.2 中文prompt示例
code复制s03 >> 请帮我创建一个Python包,包含__init__.py、utils.py和tests/test_utils.py
模型响应示例:
code复制[>] #1: 创建包目录结构
[ ] #2: 编写__init__.py
[ ] #3: 编写utils.py
[ ] #4: 创建tests目录并编写test_utils.py
7. 开发经验与技巧
7.1 调试技巧
- 状态可视化:在循环中打印TODO.render()输出,实时查看任务状态
- 提醒阈值调整:根据任务复杂度调整rounds_since_todo阈值(简单任务可设为5)
- 错误处理:捕获todo工具的验证错误并反馈给模型,帮助它修正任务列表
7.2 性能优化
- 任务项限制:TodoManager限制最大20个任务,防止列表过长
- 输出截断:工具结果限制50,000字符,避免上下文爆炸
- 批处理:允许模型一次性更新多个任务状态,减少交互轮次
7.3 扩展思路
- 任务优先级:可扩展支持优先级标记(高/中/低)
- 任务依赖:实现任务间的依赖关系,确保执行顺序
- 自动回滚:当检测到模型偏离计划时,自动回退到上一个有效状态
8. 完整代码解析
核心代码结构如下:
python复制#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path
from anthropic import Anthropic
from dotenv import load_dotenv
# 环境配置
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
# 工作目录设置
WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# 系统提示
SYSTEM = f"""You are a coding agent at {WORKDIR}.
Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
Prefer tools over prose."""
# TodoManager实现(如前所述)
class TodoManager:
# ...省略...
# 工具实现
def safe_path(p: str) -> Path:
# 路径安全检查
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
# 危险命令拦截
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: Dangerous command blocked"
# ...执行命令...
def run_read(path: str, limit: int = None) -> str:
# 读取文件内容
# ...实现...
def run_write(path: str, content: str) -> str:
# 写入文件
# ...实现...
def run_edit(path: str, old_text: str, new_text: str) -> str:
# 编辑文件
# ...实现...
# 工具注册
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
"todo": lambda **kw: TODO.update(kw["items"]),
}
# 工具定义
TOOLS = [
# ...其他工具定义...
{
"name": "todo",
"description": "Update task list. Track progress on multi-step tasks.",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"text": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"]
}
},
"required": ["id", "text", "status"]
}
}
},
"required": ["items"]
}
}
]
# 代理主循环(如前所述)
def agent_loop(messages: list):
# ...实现...
# 主程序
if __name__ == "__main__":
history = []
while True:
try:
query = input("\033[36ms03 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
agent_loop(history)
# 打印响应
response_content = history[-1]["content"]
if isinstance(response_content, list):
for block in response_content:
if hasattr(block, "text"):
print(block.text)
print()
9. 常见问题与解决方案
9.1 模型不更新任务列表
现象:模型连续执行多个操作但未调用todo工具
解决:
- 检查提醒阈值是否设置合理(默认3轮)
- 强化系统提示,明确要求使用todo工具
- 在prompt中示范正确的任务列表格式
9.2 任务状态混乱
现象:多个任务被标记为in_progress
解决:
- TodoManager已内置验证,会拒绝非法状态
- 捕获验证错误并反馈给模型:
python复制try:
TODO.update(items)
except ValueError as e:
return f"Validation error: {str(e)}"
9.3 长任务列表管理
现象:任务项超过20个导致错误
解决:
- 将大任务拆分为子任务
- 扩展TodoManager的最大限制(需考虑上下文长度)
- 实现任务分组功能
10. 设计思考与经验总结
在实现TodoWrite模块的过程中,我深刻体会到几个关键点:
-
约束创造自由:看似限制性的"同一时间只能进行一个任务"规则,实际上大幅提高了模型的成功率。这就像敏捷开发中的"限制在制品数量"原则。
-
显式优于隐式:强制模型将思考过程写下来,不仅解决了健忘问题,还使代理行为更可解释、可调试。
-
渐进式提醒:从温和的状态管理到强制的提醒注入,这种渐进式的干预比一开始就严格限制更有效。
-
工具设计哲学:好的AI工具应该像优秀的UI一样,引导用户(这里是模型)走向成功路径,而不是简单地暴露功能。
这个模块的一个意外收获是,它不仅能改善模型表现,还成为了一个强大的调试工具——通过观察任务列表的变化,开发者可以清晰了解模型的"思考过程",这在复杂任务调试中非常宝贵。
