Todo 工具
如果你给Agent一个复杂的任务并观察它运作,你会发现它做的事情和人类在压力下做的一样:它同时启动五件事,一个都没完成,然后解释它即将做的事情。
解决办法和人类同样有效。列个清单。选一个。把它做完。划掉它。选下一个。
待办工具就是那个清单,Agent有一条规则无法反驳:一次只有一项正在进行中。
学习成果
一个todo工具,包含add、start、complete和list动作,并由内存列表中支持。多步骤任务被拆解并跟踪。单步任务则完全跳过了该工具。
快速路径
- 添加一个
todo工具,包含add、start、complete和list - 用
pending、in_progress和completed状态跟踪内存数组中的项目 - 当另一个项目已经在进行中时拒绝
start
动手练习 9.1
构建工具并验证一次激活一个的约束。
要求:
todo工具接受action枚举和可选的description和idadd创建一个带有简短生成ID、pending状态和描述的新项目start如果有其他物品被in_progress,则会拒绝。否则它会将命名的项目设置为in_progresscomplete标记了命名的项目completedlist返回多行字符串,带有状态标签
实现提示:
- 状态存在于模块范围内。同一个Agent系列共用一个列表。新一轮的开始
crypto.randomUUID().slice(0, 8)ID 就足够用于存储列表中的列表。你不需要串口计数器- 请明确拒绝信息:“正在处理:[id] 描述。先完成它。”
工具
interface TodoItem {
id: string;
description: string;
state: "pending" | "in_progress" | "completed";
}
const todos: TodoItem[] = [];
export function createTodoTool() {
return tool({
description: `Manage a task list for multi-step work.
WHEN TO USE: tasks with 3+ steps, multiple files, or dependencies between
changes. Plan once, then track progress as you go.
WHEN NOT TO USE: single-file fixes, simple questions, exploratory reads.
DO NOT USE FOR: status updates to the user (just answer them directly).`,
inputSchema: z.object({
action: z.enum(["add", "start", "complete", "list"]),
description: z.string().optional(),
id: z.string().optional(),
}),
execute: async ({ action, description, id }) => {
if (action === "add") {
const item: TodoItem = {
id: crypto.randomUUID().slice(0, 8),
description: description ?? "(unnamed)",
state: "pending",
};
todos.push(item);
return `Added: [${item.id}] ${item.description}`;
}
if (action === "start") {
const active = todos.find((t) => t.state === "in_progress");
if (active) {
return `Already working on: [${active.id}] ${active.description}. Complete it first.`;
}
const next = todos.find((t) => t.id === id);
if (next) {
next.state = "in_progress";
return `Started: [${next.id}] ${next.description}`;
}
return `No todo with id ${id}.`;
}
if (action === "complete") {
const item = todos.find((t) => t.id === id);
if (item) {
item.state = "completed";
return `Completed: [${item.id}] ${item.description}`;
}
return `No todo with id ${id}.`;
}
return todos
.map((t) => `[${t.state}] ${t.id}: ${t.description}`)
.join("\n") || "No todos.";
},
});
}单主动规则是承重部分。没有它,Agent会从前方开始所有物品,然后并行快速处理,失去对每个物品的关注。
接入主流程
const tools = {
// ...everything else
todo: createTodoTool(),
};系统提示词的Agent和护栏部分已经引导Agent走向表演。工具的WHENUSE描述告诉它什么时候先计划,而不是直接行动。
什么时候该计划,什么时候不该
| 先做计划 | 跳过计划本 |
|---|---|
| 完成任务需3步或更多步骤 | 一次文件变更,位置已知 |
| 多个文件受影响 | 一个不需要文件的简单问题 |
| 变更之间的依赖关系 | 探索阶段,尚未有明确结果 |
| 用户要求多部件功能 | 带有精确错误信息的修复 |
如果Agent为一个一句拼写错误列出待办事项清单,那说明描述过于激进。把WHEN NOT拧紧到USE。
**注意:这份名单被刻意留在记忆中**
todos 数组不会在多次运行中持续存在。这是故意的。跨场次的长期列表往往会变成一个陈旧物品的垃圾抽屉。如果你以后想要持久化,可以在会话结束时将列表快照到文件。不要带着陈旧的in_progress物品进入新游戏,因为Agent不记得它们为何开始。
动手试试
执行一个多阶段任务,观察规划过程:
bun run index.ts . "Add a 'verify' npm script that runs typecheck, lint, and tests in sequence. Then run it and report the result."你应该先看Agent 调用 todo add两三次来制定计划,然后在项目处理过程中todo start和todo complete。清单上绝不应该有两个in_progress项。
运行一个简单任务来确认Agent跳过了该工具:
bun run index.ts . "What does the cwd variable in src/sandbox-local.ts do?"Agent应该在不调用 todo的情况下回答。如果一题就成了待办事项,说明描述太急切了。
npx tsc --noEmit提交
git add src/tools.ts index.ts
git commit -m "feat(planning): add todo tool with single-active constraint"完成标准
- [ ]
todo工具有接线,可以接受add、start、complete、list - [ ] 一次只能
in_progress一件物品 - [ ] 多步骤任务被分解
- [ ] 单步任务不会触发工具
- [ ]
npx tsc --noEmit
**注意:添加依赖**
目前物品是独立的。试着给每个项目添加dependsOn: string[],列出必须先完成的项目ID。如果任何依赖仍在处理或进行中,start操作应当拒绝。现在多步任务可以表达真实的排序:“重命名函数”依赖于“找到每个调用者”。这从哪里开始让人觉得有些过头?
参考实现
见上createTodoTool。锻炼方案是同样的代码,应用到你的src/tools.ts上。