跳到正文

Todo 工具

如果你给Agent一个复杂的任务并观察它运作,你会发现它做的事情和人类在压力下做的一样:它同时启动五件事,一个都没完成,然后解释它即将做的事情。

解决办法和人类同样有效。列个清单。选一个。把它做完。划掉它。选下一个。

待办工具就是那个清单,Agent有一条规则无法反驳:一次只有一项正在进行中。

学习成果

一个todo工具,包含addstartcompletelist动作,并由内存列表中支持。多步骤任务被拆解并跟踪。单步任务则完全跳过了该工具。

快速路径

  1. 添加一个todo工具,包含addstartcompletelist
  2. pendingin_progresscompleted 状态跟踪内存数组中的项目
  3. 当另一个项目已经在进行中时拒绝start

动手练习 9.1

构建工具并验证一次激活一个的约束。

要求:

  1. todo工具接受action枚举和可选的descriptionid
  2. add创建一个带有简短生成ID、pending 状态和描述的新项目
  3. start如果有其他物品被in_progress,则会拒绝。否则它会将命名的项目设置为in_progress
  4. complete标记了命名的项目completed
  5. list 返回多行字符串,带有状态标签

实现提示:

  • 状态存在于模块范围内。同一个Agent系列共用一个列表。新一轮的开始
  • crypto.randomUUID().slice(0, 8) ID 就足够用于存储列表中的列表。你不需要串口计数器
  • 请明确拒绝信息:“正在处理:[id] 描述。先完成它。”

工具

ts
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会从前方开始所有物品,然后并行快速处理,失去对每个物品的关注。

接入主流程

ts
const tools = {
  // ...everything else
  todo: createTodoTool(),
};

系统提示词的Agent和护栏部分已经引导Agent走向表演。工具的WHENUSE描述告诉它什么时候先计划,而不是直接行动。

什么时候该计划,什么时候不该

先做计划跳过计划本
完成任务需3步或更多步骤一次文件变更,位置已知
多个文件受影响一个不需要文件的简单问题
变更之间的依赖关系探索阶段,尚未有明确结果
用户要求多部件功能带有精确错误信息的修复

如果Agent为一个一句拼写错误列出待办事项清单,那说明描述过于激进。把WHEN NOT拧紧到USE。

**注意:这份名单被刻意留在记忆中**

todos 数组不会在多次运行中持续存在。这是故意的。跨场次的长期列表往往会变成一个陈旧物品的垃圾抽屉。如果你以后想要持久化,可以在会话结束时将列表快照到文件。不要带着陈旧的in_progress物品进入新游戏,因为Agent不记得它们为何开始。

动手试试

执行一个多阶段任务,观察规划过程:

bash
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 starttodo complete。清单上绝不应该有两个in_progress项。

运行一个简单任务来确认Agent跳过了该工具:

bash
bun run index.ts . "What does the cwd variable in src/sandbox-local.ts do?"

Agent应该在不调用 todo的情况下回答。如果一题就成了待办事项,说明描述太急切了。

bash
npx tsc --noEmit

提交

bash
git add src/tools.ts index.ts
git commit -m "feat(planning): add todo tool with single-active constraint"

完成标准

  • [ ] todo工具有接线,可以接受addstartcompletelist
  • [ ] 一次只能in_progress一件物品
  • [ ] 多步骤任务被分解
  • [ ] 单步任务不会触发工具
  • [ ] npx tsc --noEmit

**注意:添加依赖**

目前物品是独立的。试着给每个项目添加dependsOn: string[],列出必须先完成的项目ID。如果任何依赖仍在处理或进行中,start操作应当拒绝。现在多步任务可以表达真实的排序:“重命名函数”依赖于“找到每个调用者”。这从哪里开始让人觉得有些过头?

参考实现

见上createTodoTool。锻炼方案是同样的代码,应用到你的src/tools.ts上。

非官方简体中文翻译 · 原课程来自 Vercel Academy