Appearance
课 2 · 会话规划与 TodoWrite
本课目标
用结构化待办工具让 Agent 在多步任务中保持方向,不跑偏、不漏步。课后你会拿到一个能自主规划、逐项执行、自我纠偏的 Agent。
这一课先掌握为什么计划会被后续上下文冲掉,以及为什么要把计划单独放进 todo 工具里,不要求一上来把规划系统做成复杂的任务管理平台。
上一课让 Agent 有了记忆。但记忆解决的是"记住过去",还有一个更紧迫的问题:记住计划。
你让 Agent 做一个 10 步的任务,它往往做完前 3 步就开始即兴发挥——不是因为它笨,而是 Context Window 是有限的。随着工具调用结果不断堆积,最初的指令被推到越来越远的位置,模型"看不见"后面的步骤了。
所以这课的重点不是先记术语,而是先看清一个工程问题:如果计划只写在最开始那段 Prompt 里,它会被后面的上下文慢慢冲淡。
问题:多步任务中的漂移
让 Agent "重构这个模块:加类型标注、写文档注释、补测试",它可能:
- 完成类型标注 ✅
- 写好文档注释 ✅
- 开始"优化"你没要求的东西 ❌
- 忘了还有测试 ❌
原因不是模型能力不够,是工作记忆溢出。每次 tool call 的结果都在消耗 Context Window,原始计划逐渐淡出。
解决方案:给 Agent 一个 todo 工具
核心思路:让 Agent 把计划写进结构化状态,而不是自由文本。
TodoWrite 如何拉住多步任务
用户指令
→
拆解 todo 列表
→
一个任务进入 in_progress
→
执行当前步骤
→
更新 todo 状态
pending
→
in_progress同一时间只能有一个
→
completed
或
3 轮没更新注入 reminder 拉回计划
TodoManager
管理待办状态,核心约束:同一时间只能有一个 in_progress。这迫使 Agent 完成当前步骤再推进下一步。
typescript
interface TodoItem {
id: number
title: string
status: 'pending' | 'in_progress' | 'completed'
}
class TodoManager {
private items: TodoItem[] = []
update(items: TodoItem[]): string {
// 校验:最多一个 in_progress
const inProgress = items.filter(i => i.status === 'in_progress')
if (inProgress.length > 1) {
throw new Error('只能有一个任务处于 in_progress 状态')
}
this.items = items
return this.render()
}
render(): string {
if (this.items.length === 0) return '(暂无计划)'
return this.items.map(item => {
const icon = item.status === 'completed' ? '✅'
: item.status === 'in_progress' ? '🔄' : '⬜'
return `${icon} #${item.id} ${item.title}`
}).join('\n')
}
}todo 工具
注册为普通 tool,和其他工具一样接入 Agent:
typescript
const todoManager = new TodoManager()
const todoTool = tool({
description: `管理当前会话的任务计划。
收到复杂任务时,先调用此工具拆解为步骤。
每完成一步,更新状态。同一时间只能有一个 in_progress。`,
inputSchema: z.object({
items: z.array(z.object({
id: z.number().describe('任务编号'),
title: z.string().describe('任务描述'),
status: z.enum(['pending', 'in_progress', 'completed']),
})),
}),
execute: async ({ items }) => {
const rendered = todoManager.update(items)
return { plan: rendered }
},
})Nag 提醒:拉回跑偏的 Agent
光有 todo 工具还不够——Agent 可能忙着执行就忘了更新计划。
Nag 可以把它理解成一种“定期把 Agent 拉回当前计划”的提醒机制。解决方法是追踪 Agent 距离上次调用 todo 过了多少轮。超过 3 轮,在下一次工具返回时悄悄注入提醒。
typescript
let roundsSinceTodo = 0
const NAG_THRESHOLD = 3
function onStepFinish(event: { toolCalls?: any[] }) {
const calledTodo = event.toolCalls?.some(tc => tc.toolName === 'todo')
if (calledTodo) {
roundsSinceTodo = 0
} else {
roundsSinceTodo++
}
}
// 在 system prompt 末尾追加提醒
function getSystemPrompt(): string {
const base = `你是一个能执行多步任务的 AI 助手。
收到复杂任务时,先用 todo 工具拆解计划,然后逐步执行。
每完成一步就更新 todo 状态。同一时间只能有一个任务处于 in_progress。`
if (roundsSinceTodo >= NAG_THRESHOLD) {
return base + '\n\n<reminder>你已经连续多轮没有更新计划了,请调用 todo 工具更新当前进度。</reminder>'
}
return base
}这个机制很轻量,但效果显著:结构化计划 + 单任务约束 + 定期提醒,三者组合大幅减少 Agent 偏离。
为什么结构化状态比自由文本好
模型也可以在回答开头写一段"计划"文字,但自由文本有两个问题:
- 没有约束——模型可以随便改写、跳步、遗漏,没人校验
- 容易被冲走——随着对话推进,那段文字在 Context Window 中越来越远
结构化 todo 不同:
- 状态机制可校验(同一时间只有一个 in_progress)
- 每次调用 todo 工具,完整计划作为 tool result 回到上下文最新位置
- 应用层代码可以追踪、提醒、干预
本课产物
- ✅ TodoManager(结构化计划 + 单任务约束)
- ✅ todo 工具(拆解、追踪、更新多步任务)
- ✅ Nag 提醒机制(Agent 跑偏时自动拉回)
完整代码在 basic/examples/06-memory/02-todo/index.ts。
并入主线项目
这一课的 Demo 聚焦在会话规划本身:让 Agent 把计划写进结构化 todo 状态,并通过单一 in_progress 约束和 nag 提醒减少跑偏。
基础项目里,basic/project 承接这套能力:新增 todo 工具管理当前计划,提供 /plan 命令查看会话规划状态,并在连续多轮不更新计划时自动注入 <reminder>。
这里的 todo 是 Agent 内部的执行规划工具,不是面向用户的通用待办产品;主线当前落地的是“整包更新计划 + 查看当前计划 + 自动提醒”,不是更细粒度的任务管理系统。
试试看
bash
cd daqi-ai-agent
pnpm exec tsx basic/examples/06-memory/02-todo/index.ts重构 hello.ts:加类型标注、写文档注释、补单元测试— 观察 Agent 自动拆解计划创建一个 TypeScript 包:包含 index.ts、utils.ts 和 tests/test_utils.ts— 观察逐步执行- 如果 Agent 连续几轮没更新计划,会看到
<reminder>出现
面试追问
Q:Agent 在多步任务中容易跑偏,怎么解决?
给 Agent 一个 todo 工具,把计划写成结构化状态而不是自由文本。加两个约束:同一时间只能有一个 in_progress(强制顺序执行),超过 3 轮没更新计划就注入 reminder 提醒。结构化状态每次更新都回到上下文最近位置,不会被 Context Window 冲走。
Q:为什么不直接在 System Prompt 里写计划?
System Prompt 是静态的,不能反映执行过程中的进度变化。todo 工具的 result 每次都回到对话最新位置,相当于"刷新"了 Agent 的工作记忆。而且结构化 schema 可以做约束校验(比如不允许多个 in_progress),自由文本做不到。
Q:这个 todo 是给用户管理待办的吗?
不是。这里的 todo 是 Agent 的会话规划工具——帮 Agent 自己在复杂任务中保持方向。和用户側的"帮我记一下明天要做的事"是两个层面。前者是 Agent 内部的执行策略,后者是面向用户的功能。