1. 引言:LLM 无状态性与外部记忆需求
我们人类在处理复杂任务时,通常会在脑海里维持一份心理清单(checklist),或者干脆写在便签纸上,方便一步步对照执行。这样即使事情再多再杂,也不容易遗漏。
大语言模型却完全不同------它在本质上是无状态的,每一次 API 调用都是一次独立的推理,模型并不会自动"记住"上一轮说过什么、做过什么。虽然我们可以把整个对话历史反复塞给模型,让它看起来好像保持了上下文,但这种记忆完全是被动的(因为对话历史是大模型自己按概率生成的,所以天然不可控),还受限于上下文窗口的长度。
特别是当模型需要执行多步骤任务时(例如"先重构模块 A,再更新测试,最后运行构建"),问题就暴露出来了:
- 模型在几轮对话之后就可能忘掉还没完成的任务;
- 一旦触发对话压缩(compact),旧的上下文就被丢弃;
- 用户无法直观地看到 AI 当前做到哪一步;
- 子 Agent 和主线程的任务状态还会互相干扰。
为了解决这些问题,Claude Code 引入了 TodoWrite 工具。它的本质,就是为 AI Agent 提供一个外部工作记忆 ------一个独立于对话历史、可读可写、还能自动提醒的代办任务管理工具,同时也让用户获得了透明的进度感知。
2. TodoWrite 工具的设计理念
2.1 任务参数的设计
TodoWrite 工具本质是一个大模型工具,所以我们需要对其进行工具定义,然后在请求大模型的时候将其带上,大模型再自主判断是否需要使用 TodoWrite,需要使用 TodoWrite 的时候就生成代办任务列表(todoList)。Claude Code 规定了每个任务对象强制包含以下三个字段,缺一不可:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
content |
string | 任务内容,祈使句 | "修复登录页的样式问题" |
status |
enum | 任务状态 | "pending" / "in_progress" / "completed" |
active_form |
string | 进行时描述,用于 UI 展示 | "正在修复登录页样式" |
为什么需要同时提供 content 和 active_form?因为当任务处于 in_progress 状态时,面板上应展示动态进行时文本(如"正在...",而不是静态的祈使句),给用户更自然的进度感知。
2.2 状态机与约束
上一小节中讲到的 status 字段就包含了以下三种状态:
scss
pending ──(开始)──→ in_progress ──(完成)──→ completed
↑ │
└────(放弃/重置)───────┘ (允许,但不推荐)
上述三个状态,形成一个有限的、有方向的状态机,让每个 todo 项的生命周期都清晰可追踪。这也相当于给用户一个"进度条",用户不需要翻对话历史去猜 AI 在干嘛。TodoWrite 会渲染成一个实时面板,当前在做什么(in_progress)、还剩什么(pending)、做完了什么(completed),一目了然。就像外卖 App 上的"商家正在备餐 → 骑手正在配送"。
同时最佳实践要求同一时间只有一个任务处于 in_progress。这也符合人类的工作习惯------一次只做一件事,避免上下文切换混乱。
3. TodoWrite 在 Agent 中的工作流程
有了清晰的设计理念和工具定义,接下来我们就在前面实现的基础 Agent Loop 中实现它。看看 TodoWrite 如何与模型协作,形成一套完整的任务管理机制。
3.1 工具定义
通过前面的知识学习,我们知道要给大模型添加工具能力时,需要遵循一套标准的流程:"工具定义 → 工具实现 → 注册到 Agent"。
同样地我们要给 Agent 添加 TodoWrite 的能力,就得先进行大模型的工具定义,根据上一小节对 TodoWrite 工具的设计理念的了解我们对 TodoWrite 的工具定义如下:
python
# ---------- 工具定义 ----------
tools = [
# ── 原有工具 ──────────────────────────────────────────────
# 省略...
# ── TodoWrite 工具 ─────────────────────────────────────────
{
"type": "function",
"function": {
"name": "todo_write",
"description": (
"更新当前会话的待办清单。主动用于跟踪多步骤任务的进度。\n\n"
"## 何时使用\n"
"1. 任务需要 3 个或更多步骤时\n"
"2. 用户明确要求使用待办清单时\n"
"3. 用户提供了多个任务(编号或逗号分隔)时\n\n"
"## 何时不使用\n"
"1. 只有单个简单任务时\n"
"2. 纯信息咨询时\n\n"
"## 状态说明\n"
"- pending:未开始\n"
"- in_progress:进行中(一次只限一个)\n"
"- completed:已完成\n\n"
"每个任务必须提供 content(祈使句,如\"修复登录bug\")和 "
"active_form(进行时,如\"正在修复登录bug\")。\n\n"
"完成所有任务后清单会自动清空。"
),
"parameters": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"description": "更新后的完整任务清单",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "任务内容,祈使句,如\"修复登录bug\""
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "任务状态"
},
"active_form": {
"type": "string",
"description": "任务的进行时描述,如\"正在修复登录bug\""
}
},
"required": ["content", "status", "active_form"]
}
}
},
"required": ["todos"]
}
}
}
]
经过上述工具定义,大模型在调用 TodoWrite 工具时,会输出以下格式的数据:
json
// 一次典型的 todo_write 调用
{
"todos": [
{
"content": "修复登录页的样式问题",
"status": "in_progress",
"active_form": "正在修复登录页样式"
},
{
"content": "更新单元测试",
"status": "pending",
"active_form": "正在更新测试用例"
}
]
}
同时为了避免 token 浪费以及对话压缩的影响,这些任务数据不能反复进上下文窗口,需要保存在一个外部独立的全局变量里,而对话历史中只有工具调用的记录和返回的确认信息。
3.2 状态隔离与会话管理
我们上面已经讲到了 TodoWrite 的状态并不存储在对话历史中。所以我们根据 Claude Code 的实现原理模拟一个全局 _app_state 字典,按 session_id 存储不同的待办清单:
python
import uuid
─── TodoWrite 状态管理 ──────────────────────────────────────────────
# 模拟 appState,按 session_id 隔离 todo 清单
_app_state = {
"todos": {} # { session_id: [ TodoItem, ... ] }
}
# 当前会话 ID(每次进程启动生成一个,模拟 getSessionId())
_SESSION_ID = str(uuid.uuid4())
def get_session_todos(session_id: str) -> list:
"""获取指定 session 的 todo 列表(只读副本)。"""
return list(_app_state["todos"].get(session_id, []))
def set_session_todos(session_id: str, todos: list) -> None:
"""写入指定 session 的 todo 列表。"""
_app_state["todos"][session_id] = todos
这样大模型调用 TodoWrite 工具的时候,就会调用 set_session_todos 函数存储任务数据,通过 get_session_todos 函数读取具体任务数据。
为什么需要会话隔离?
在实际的 Claude Code 或类似工具中,用户可能同时打开多个工作区(例如不同的项目目录),或者同时与多个子 Agent 对话。如果所有会话共用一个清单,任务状态就会互相污染。通过 session_id 隔离,每个会话拥有一份独立的外部记忆,互不干扰。
3.3 TodoWrite 工具实现
有了状态管理的支撑,我们就可以实现 TodoWriteTool 类了。它的核心职责是:接收模型生成的待办清单,校验后持久化到会话隔离的外部存储,并给用户和模型分别提供可视化的反馈。实现如下:
python
class TodoWriteTool:
"""
- 写入新状态 → 全部完成时自动清空
- 按 session_id 隔离不同会话的清单
- 返回可视化面板
"""
STATUS_ICON = {
"pending": "⬜",
"in_progress": "🔄",
"completed": "✅",
}
def execute(self, todos: list, session_id: str = _SESSION_ID) -> str:
# 1. 校验每条 todo 的字段
for item in todos:
if not item.get("content", "").strip():
return "❌ 每条任务的 content 不能为空"
if item.get("status") not in ("pending", "in_progress", "completed"):
return f"❌ 非法状态: {item.get('status')!r}"
if not item.get("active_form", "").strip():
return "❌ 每条任务的 active_form 不能为空"
# 2. 全部完成时自动清空(对应原版 allDone 逻辑)
all_done = all(t["status"] == "completed" for t in todos)
new_todos = [] if all_done else todos
# 3. 写新状态
set_session_todos(session_id, new_todos)
# 4. 打印可视化 todo 面板
# 注意:原版通过消息历史反向遍历检测 todo_write 调用,无需手动重置计数器
self._print_panel(todos)
# 5. 返回给模型的提示文本(以下英文是 Claude Code 源码的提示)
return (
"Todos have been modified successfully. "
"Ensure that you continue to use the todo list to track your progress."
)
def _print_panel(self, new_todos: list) -> None:
"""在终端打印结构化的 todo 面板,便于用户实时感知进度。"""
print("\n\033[35m╔══════════════════════════════════╗")
print("║ 📋 TODO 任务清单 ║")
print("╚══════════════════════════════════╝\033[0m")
if not new_todos:
print("\033[32m 🎉 所有任务已完成,清单已自动清空!\033[0m\n")
return
for idx, item in enumerate(new_todos, 1):
icon = self.STATUS_ICON.get(item["status"], "❓")
label = item["active_form"] if item["status"] == "in_progress" else item["content"]
color = (
"\033[32m" if item["status"] == "completed" else
"\033[33m" if item["status"] == "in_progress" else
"\033[0m"
)
print(f" {icon} {color}{idx}. {label}\033[0m")
# 进度统计
total = len(new_todos)
completed = sum(1 for t in new_todos if t["status"] == "completed")
in_prog = sum(1 for t in new_todos if t["status"] == "in_progress")
print(f"\n\033[36m 进度: {completed}/{total} 已完成"
f"{f' | 当前进行中: {in_prog}' if in_prog else ''}\033[0m\n")
实现要点解读:
- 强校验
模型生成的内容可能不符合 JSON Schema(例如漏掉active_form),此时直接返回错误信息,模型会看到并重新生成合法调用。这比静默失败更健壮。 - 自动清空
当模型将所有任务标记为completed时,存储层直接写入空列表。用户不需要额外操作,UI 面板会消失,符合"做完即消失"的心理预期。 - 与外部记忆的交互
所有状态读写都通过get_session_todos/set_session_todos完成,对话历史中只保留工具调用的记录和返回的确认字符串,不受上下文窗口压缩的影响。 - 可视化面板
_print_panel使用 ANSI 颜色码在终端输出结构化表格,并实时显示进度。用户一眼就能知道当前正在做什么(in_progress项会显示"正在..."),以及还剩多少任务。 - 返回给模型的提示
返回的英文提示源于 Claude Code 官方实现,它明确告诉模型"清单已更新,请继续使用它跟踪进度"。这有助于模型在后续轮次中维持对清单的元认知。
集成到 Agent Loop
在 agent_loop 中,当模型调用 todo_write 时,只需实例化工具并执行:
python
def agent_loop(messages: list):
while True:
# 省略...
for tool_call in msg.tool_calls:
# 省略...
elif fn_name == "todo_write":
result = todo_tool.execute(
todos=args.get("todos", []),
session_id=_SESSION_ID
)
print(f"✅ {result}\n")
# 省略...
这样,TodoWrite 就完整地嵌入了 Agent 的执行体系。在 Agent Loop 中,当模型返回 tool_calls 且函数名为 todo_write 时,执行器会调用 TodoWriteTool.execute(),并将结果(一个确认字符串)作为 tool 角色的消息追加到对话历史中。这样,模型就能"感知"到清单已经更新,并据此规划下一步行动。
用户则通过实时面板获得透明的进度感知,从而解决引言中提到的"遗忘任务、压缩丢失、进度不可见"等一系列痛点。
3.4 测试演示
让我们通过一个具体的例子,看看 TodoWrite 如何改变 Agent 的行为。首先我们在根目录下创建一个 test.txt 的文件,写入如下内容:
我是程序员 Cobyte
我正在写《AI Agent 基础知识入门实践》教程
接着启动上述程序,输入:请帮我完成以下4件事:1. 读取 test.txt 文件内容,2. 统计文件的字数,3. 作者是谁,4. 作者正在做什么。
代码中的 _print_panel 方法会在每次 todo_write 调用时,向终端输出如下的结构化任务面板:
╔══════════════════════════════════╗
║ 📋 TODO 任务清单 ║
╚══════════════════════════════════╝
✅ 1. 读取 test.txt 文件内容
✅ 2. 统计文件的字数
🔄 3. 正在查找作者
⬜ 4. 描述作者正在做什么
进度: 2/4 已完成 | 当前进行中: 1
通过这个面板让 AI 的内部工作记忆"外显"出来,这样用户就不需要翻看冗长的对话历史,一眼就能掌握进度。
4. 自动提醒机制
TodoWrite 的核心价值不仅在于"写入",更在于系统能够主动提醒 大模型还有未完成的任务。这个机制在 Claude Code 中通过 todo_reminder 附件实现,完全不需要用户干预。
4.1 触发条件与阈值
在 Claude Code 泄露的源码中分别设置了 _TURNS_SINCE_WRITE 和 _TURNS_BETWEEN_REMINDERS 两个阈值来控制 todo_reminder 的自动提醒,分别解决"何时该提醒"和"多久提醒一次"的问题。
_TURNS_SINCE_WRITE
表示距离模型上一次主动调用todo_write工具 已经过去了多少轮对话(以 assistant 消息为计数单位)。
如果这个数字小于设定的阈值,说明模型刚刚更新过待办清单,还在积极跟进任务,系统就不需要打扰它。只有当模型连续多轮对话都没有更新清单(超过了这个阈值),系统才认为它可能"走神了",允许触发提醒。_TURNS_BETWEEN_REMINDERS
表示距离上一次注入todo_reminder提醒消息 已经过去了多少条消息(无论 user 还是 assistant)。
如果这个数字小于阈值,说明刚刚提醒过不久,系统会安静等待,避免每轮都催、惹人烦。只有超过这个阈值,才允许再次注入提醒。
只有当两个条件同时满足 ,并且待办清单中存在未完成的任务(status != "completed")时,系统才会自动注入一条提醒消息。
4.2 反向遍历计数算法
Claude Code 的一个精妙的设计是:计数不依赖任何外部状态,而是每次需要判断时反向遍历对话历史,动态计算两个值:
turns_since_last_todo_write:从最近一次包含todo_write工具调用的 assistant 消息到现在,经过的"不含 todo_write 的 assistant 消息"数量turns_since_last_reminder:从最近一条_is_todo_reminder标记的消息到现在,经过的消息数量
这种无状态计数方式避免了同步问题,也简化了系统架构。即使对话被压缩、恢复,或者状态在多个地方被修改,计数逻辑依然正确。
4.3 提醒消息的构建与注入
当触发条件满足时,系统会构建一条类似下面的消息
vbnet
The TodoWrite tool hasn't been used recently. If you're working on tasks
that would benefit from tracking progress, consider using the TodoWrite
tool... Make sure that you NEVER mention this reminder to the user.
Here are the existing contents of your todo list:
1. [pending] 修复登录页样式
2. [in_progress] 更新单元测试
3. [completed] 安装依赖
关键要点:
- 消息
role为"user",确保模型能够正常接收(因为部分 API 对 system 消息位置敏感) - 添加
_is_todo_reminder: True内部标记,供下一次反向遍历时识别 - 明确要求模型永远不要向用户提及这个提醒------这是系统级的隐形辅助
也就是将其包装成一个带有 _is_todo_reminder: True 标记的 user 角色 消息,追加到 messages 列表末尾。为什么用 user 而不是 system?因为某些 OpenAI 风格 API 对 system 消息的位置敏感(要求必须在最前面),而 user 可以安全地插在任何位置。
4.4 实现 todo_reminder 自动注入
根据前面对 Claude Code 的自动提醒机制的深入了解,本质就是为了在 Agent 运行时动态判断"模型是否遗忘了待办清单",并适时给予温和提醒,我们需要实现一套无状态的自动提醒机制 。核心思路是:每次调用 LLM 之前,反向扫描对话历史,计算两个轮次计数,若都超过阈值且存在未完成任务,则自动向消息列表尾部追加一条"隐形"的提醒消息。
接下来我们实现这个功能,具体实现如下:
python
# ─── todo_reminder 自动注入 ───────────────────────────────────────────
#
# 对应 Claude Code attachments.ts 中的 getTodoReminderAttachments() 逻辑:
# 计数器不依赖外部状态,而是每次调用时反向遍历消息历史动态计算。
#
# 消息历史中的 todo_reminder 以特殊 user 消息表示:
# { "role": "user", "_is_todo_reminder": True, "content": "..." }
# 工具调用消息(assistant 含 tool_calls)对应 Claude Code 的 type='assistant'。
# 阈值配置(对应 attachments.ts 中的 TODO_REMINDER_CONFIG)
_TURNS_SINCE_WRITE = 1 # 距上次 todo_write 超过 N 轮才提醒
_TURNS_BETWEEN_REMINDERS = 2 # 距上次 reminder 超过 M 轮才再次提醒
def _has_todo_write_call(msg) -> bool:
"""
判断一条 assistant 消息是否包含 todo_write 工具调用。
对应原版:message.content.some(b => b.name === 'TodoWrite')
"""
# openai SDK 返回的 ChatCompletionMessage 对象
if hasattr(msg, "tool_calls") and msg.tool_calls:
return any(tc.function.name == "todo_write" for tc in msg.tool_calls)
# 已序列化为 dict(追加到历史后再遍历时)
if isinstance(msg, dict) and msg.get("role") == "assistant":
for tc in (msg.get("tool_calls") or []):
if isinstance(tc, dict):
if tc.get("function", {}).get("name") == "todo_write":
return True
else:
if tc.function.name == "todo_write":
return True
return False
def _is_todo_reminder_msg(msg) -> bool:
"""判断一条消息是否为之前注入的 todo_reminder 附件。"""
return isinstance(msg, dict) and msg.get("_is_todo_reminder") is True
def get_todo_reminder_turn_counts(messages: list) -> tuple[int, int]:
"""
反向遍历消息历史,动态计算两个计数值。
对应 attachments.ts 中的 getTodoReminderTurnCounts():
- turns_since_last_todo_write:从最近一次含 todo_write 的 assistant 轮次到现在
经过的"不含 todo_write 的 assistant 轮次"数量
- turns_since_last_reminder:从最近一次 todo_reminder 注入到现在经过的消息数量
返回 (turns_since_last_todo_write, turns_since_last_reminder)
"""
turns_since_write = 0
turns_since_reminder = 0
found_write = False
found_reminder = False
for msg in reversed(messages):
role = msg.get("role") if isinstance(msg, dict) else getattr(msg, "role", None)
# ── 计算 turns_since_last_todo_write ─────────────────────
# 只有 assistant 消息才参与计数(与原版一致)
if not found_write and role == "assistant":
if _has_todo_write_call(msg):
# 找到含 todo_write 的轮次,停止计数(该轮本身不计入)
found_write = True
else:
turns_since_write += 1
# ── 计算 turns_since_last_reminder ───────────────────────
# 找到最近一次 todo_reminder 注入消息,停止计数(该条本身不计入)
if not found_reminder:
if _is_todo_reminder_msg(msg):
found_reminder = True
else:
turns_since_reminder += 1
if found_write and found_reminder:
break
# 若历史中从未出现过 todo_write / reminder,
# 计数值已累积到消息总数,足够大,可正常触发提醒
return turns_since_write, turns_since_reminder
def build_todo_reminder_message(todos: list) -> str:
"""
构建 todo_reminder 附件的消息文本。
对应 attachments.ts 中 case 'todo_reminder' 的逻辑。
"""
todo_items = "\n".join(
f"{idx + 1}. [{todo['status']}] {todo['content']}"
for idx, todo in enumerate(todos)
)
# 以下提示词来自于 Claude Code
message = (
"The TodoWrite tool hasn't been used recently. "
"If you're working on tasks that would benefit from tracking progress, "
"consider using the TodoWrite tool to track progress. "
"Also consider cleaning up the todo list if it has become stale and no longer matches "
"what you are working on. Only use it if it's relevant to the current work. "
"This is just a gentle reminder - ignore if not applicable. "
"Make sure that you NEVER mention this reminder to the user"
)
if todo_items:
message += f"\n\nHere are the existing contents of your todo list:\n\n[{todo_items}]"
return message
def get_todo_reminder_attachments(
messages: list,
session_id: str = _SESSION_ID,
) -> list[dict]:
"""
判断是否需要注入 todo_reminder,若需要则返回包含一条提醒消息的列表,
否则返回空列表。纯函数,不维护任何外部状态。
对应 attachments.ts 中的 getTodoReminderAttachments(messages, toolUseContext):
- 条件1:工具集中存在 todo_write(本实现直接跳过此检查,调用方保证)
- 条件2:消息历史非空
- 条件3:turns_since_write >= 阈值 AND turns_since_reminder >= 阈值
- 条件4:todo 清单中存在未完成任务
返回的消息带有 _is_todo_reminder=True 标记,便于下次计数时识别。
"""
if not messages:
return []
todos = get_session_todos(session_id)
# 条件4:清单中存在未完成任务
if not any(t["status"] != "completed" for t in todos):
return []
turns_since_write, turns_since_reminder = get_todo_reminder_turn_counts(messages)
if turns_since_write < _TURNS_SINCE_WRITE or turns_since_reminder < _TURNS_BETWEEN_REMINDERS:
return []
reminder_text = build_todo_reminder_message(todos)
# 以带标记的 user 消息注入,对用户透明(isMeta: true 的等价实现)
# 使用 role="user" 以便 OpenAI API 接受(system 消息夹在中间可能被部分模型忽略)
reminder_msg = {
"role": "user",
"content": f"[todo_reminder --- 仅供模型参考,请勿向用户提及]\n{reminder_text}",
"_is_todo_reminder": True, # 内部标记,下次反向遍历时用于识别
}
# 终端侧打印调试信息
print(
f"\033[90m[todo_reminder 已注入] "
f"turns_since_write={turns_since_write} "
f"turns_since_reminder={turns_since_reminder}\033[0m"
)
return [reminder_msg]
从代码中可以看到,提醒机制由以下几个函数协同完成:
| 函数 | 职责 |
|---|---|
_has_todo_write_call(msg) |
判断一条 assistant 消息是否包含 todo_write 工具调用 |
_is_todo_reminder_msg(msg) |
判断一条消息是否为之前注入的 todo_reminder(通过 _is_todo_reminder 标记) |
get_todo_reminder_turn_counts(messages) |
反向遍历消息历史,返回 (turns_since_write, turns_since_reminder) |
build_todo_reminder_message(todos) |
根据当前待办清单构造提醒文本(包含任务列表) |
get_todo_reminder_attachments(messages, session_id) |
核心决策函数:检查所有条件,若需提醒则返回包含一条提醒消息的列表 |
inject_todo_reminder(messages, session_id) |
调用上述函数,若需要则直接修改 messages 列表(追加提醒消息) |
这些函数都不依赖任何外部可变状态(除了通过 get_session_todos 读取清单),因此可以在任意时刻安全调用。
4.5 集成到 Agent Loop 的入口点
根据 Claude Code 的实践,提醒注入需要在 两个关键时机 发生:
入口1:用户输入后、调用 LLM 之前
diff
if __name__ == "__main__":
print(f"\033[90m会话 ID: {_SESSION_ID}\033[0m")
history = [
{"role": "system", "content": SYSTEM}
]
while True:
# 省略...
history.append({"role": "user", "content": query})
+ # ── 入口1:用户输入后、调用模型前注入 todo_reminder ────────
+ # 对应 processUserInput.ts:504:用户每发一条消息时触发。
+ # 通过反向遍历消息历史动态计算计数,无需维护外部状态。
+ inject_todo_reminder(history, session_id=_SESSION_ID)
final_answer = agent_loop(history)
# 省略...
在接收用户输入后,追加到 history,然后立即尝试注入提醒
入口2:每轮工具循环结束后、再次调用 LLM 之前
diff
def agent_loop(messages: list, session_id: str = _SESSION_ID):
while True:
# 省略...
for tool_call in msg.tool_calls:
# 省略...
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": fn_name,
"content": result
})
+ # ── 入口2:每轮工具循环结束后注入 todo_reminder ──────────────
+ # 对应 query.ts:1580:本轮所有工具执行完毕,准备下一轮 LLM 请求前。
+ inject_todo_reminder(messages, session_id=session_id)
+ # 然后继续 while True 循环,再次调用 LLM
这两个入口点覆盖了 Agent 运行时所有"即将与 LLM 对话"的时刻,确保模型永远不会在没有提醒的情况下长时间遗忘清单。
当提醒消息被注入后,模型会在下一轮生成时看到它。由于提醒中明确要求 "NEVER mention this reminder to the user" ,模型通常会:
- 静默地意识到还有未完成的任务;
- 主动调用
todo_write更新清单(例如标记某个任务为in_progress,或完成一个任务); - 继续推进工作。
而一旦模型调用了 todo_write,消息历史中就会出现新的含 todo_write 的 assistant 消息,从而重置 turns_since_last_todo_write 计数器,提醒机制自动暂停。这就形成了一个 "遗忘 → 提醒 → 更新 → 清零 → 继续遗忘" 的闭环。
4.6 终端调试输出
为了模拟大模型的遗忘,我们在每轮工具循环结束后注入 todo_reminder 之前模拟添加几轮大模型的回复的内容:
diff
def agent_loop(messages: list, session_id: str = _SESSION_ID):
while True:
# 省略...
for tool_call in msg.tool_calls:
# 省略...
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": fn_name,
"content": result
})
+ for i in range(4):
+ messages.append({
+ "role": "assistant",
+ "content": '模拟普通回复'
+ })
# ── 入口2:每轮工具循环结束后注入 todo_reminder ──────────────
# 对应 query.ts:1580:本轮所有工具执行完毕,准备下一轮 LLM 请求前。
inject_todo_reminder(messages, session_id=session_id)
然后我们再启动我们的程序,输入以下内容: 请帮我完成以下4件事:1. 读取 test.txt 文件内容,2. 统计文件的字数,3. 作者是谁,4. 作者正在做什么。
我们发现在终端出现了我们在程序里设置的提醒内容。

通过以上实现与验证结果,todo_reminder 可以说是一个完全自动、无外部状态、低骚扰 的遗忘防护网,与 TodoWrite 工具配合,共同构成了 AI Agent 可靠的外部工作记忆系统。
即便长时间对话会触发上下文压缩(compact)。压缩后,旧的消息被丢弃,但 TodoWrite 生成的任务仍然保留在内存中。当新的对话轮次开始时,系统会再次通过 todo_reminder 机制将当前清单注入模型输入------模型拿到清单后,便能"想起"未完成的工作,继续执行。
这相当于一个不依赖对话历史的外部记忆恢复点,极大提升了长会话的可靠性。
5. 总结:TodoWrite 作为 AI Agent 的"外部工作记忆"
TodoWrite 模式从根本上解决了 LLM 的遗忘问题。通过三个核心设计:
- 写入通道:模型可以显式更新状态
- 自动提醒:系统在模型遗忘时主动注入清单
- 可视化面板:用户实时感知进度
它让 AI Agent 从"一次一推理的无状态函数"变成了"能够持续跟踪多步骤任务的工作伙伴"。这个模式不仅适用于 Claude Code,也可以被集成到任何基于 LLM 的 Agent 系统中------只需一个工具定义、一个状态存储、以及一个智能的提醒触发器。
如果你正在构建一个需要执行复杂任务的 AI Agent,不妨借鉴 todoWrite 模式,给你的模型配一个可靠的外部工作记忆。
我是程序员Cobyte,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。