第 2 章 · Agent Loop 与任务规划

01 · 从一次回答到循环执行

第 1 章的 agent 只会"问一次答一次"。这一节把它升级成持续推进的执行循环,理解为什么 Coding Agent 必须是一个循环而不是单次调用。

第 1 章留下的问题

第 1 章做出的 agent 能用了:给它一个问题,它搜索文件、读取内容、给出回答。但仔细看 agent.ts,它其实是一个固定上限的循环——最多跑 5 轮工具调用就强制停止。

这带来几个问题:

上限太死板。 5 轮够回答"入口文件在哪"这种简单问题,但如果任务需要搜索 3 个关键词、读 5 个文件、再交叉对比呢?5 轮根本不够用,但又不能设太大——每多一轮就多消耗一次模型调用,token 费用和等待时间都跟着涨。

没有统计。 调了几次模型?执行了几次工具?是因为完成了才停下,还是因为到上限被强制停下的?这些信息对用户不可见,也不利于调试。

没有任务管理。 面对"帮我找到这个 bug 并给出修复方案"这样的多步骤任务,模型不知道该分几步、每步做什么。它只是反复调用工具,直到碰巧完成或被迫停止。

这一章解决这些问题。这一节先理解"为什么必须是循环",然后升级循环本身。

为什么不能只调一次模型

假设用户说:"帮我找到 runAgent 函数的所有调用点,并说明每个调用点的用途。"

如果只调一次模型,模型能做的只是猜。它没看过你的代码,不知道 runAgent 在哪个文件里、被谁调用。它要么瞎编答案,要么承认自己不知道。

正确的做法是让模型多步执行

  1. 调用 search 搜索 runAgent → 发现它在 agent.tsmain.ts 中出现
  2. 调用 read_file 读取 main.ts 中相关行 → 看到 main.ts 里 import 并调用了它
  3. 调用 read_file 读取 agent.ts 中的函数定义 → 理解它的职责
  4. 综合这些信息给出回答

每一步都是"拿到新信息 → 决定下一步做什么"。这就是一个循环,不是一个单次调用。

循环的本质:Reason-Act-Observe

把上面的过程抽象一下,每一步都是三个阶段:

Reason(推理):我看到搜索结果里有 agent.ts 和 main.ts,需要读取它们的内容
Act(行动):调用 read_file 读取 main.ts
Observe(观察):文件内容是 ...

然后拿着观察结果回到 Reason,决定下一步。这个 Reason → Act → Observe → Reason 的循环,就是 Agent Loop 的核心模式,学术界叫它 ReAct

在我们的代码里,这三步分别对应:

ReAct 阶段代码对应做了什么
Reasonmodel.chat(allMessages, allTools)模型看对话历史,决定下一步
Acttool.execute(toolCall.arguments, state)执行模型选择的工具
Observe把工具结果追加到 allMessages新信息成为下一轮 Reason 的输入

第 1 章的代码已经在做这件事了,只是没有给它一个明确的名字。这一章要做的升级是:让这个循环更智能、更可控、更可观测。

升级循环:从 MAX_TOOL_ROUNDS 到结构化结果

第 1 章的 runAgent 返回一个 string。这一章改成返回一个结构化的 AgentResult

// src/types.ts

/** 执行统计 */
interface AgentStats {
  modelCalls: number;     // 调用了几次模型
  toolCallCount: number;  // 执行了几次工具
  hitLimit: boolean;      // 是否因为达到上限而停止
}

/** Agent 运行结果 */
interface AgentResult {
  answer: string;    // 最终回复
  todos: TodoItem[]; // 执行计划(下一节展开)
  stats: AgentStats; // 执行统计
}

为什么是结构化结果而不是字符串?两个原因:

调用方需要更多信息。 main.ts 需要知道执行了多久、用了多少工具,好展示给用户。如果只返回一个字符串,这些信息就丢了。

方便测试。 测试可以断言 stats.modelCallsstats.hitLimit 等具体字段,而不是只能检查输出文本。

升级后的 agent.ts 骨架

升级后的 runAgent 大致长这样(省略计划工具部分,下一节展开):

export async function runAgent(
  state: AgentState,
  model: Model,
  tools: Tool[],
  options?: { maxIterations?: number },
): Promise<AgentResult> {
  const maxIterations = options?.maxIterations ?? 15;
  const stats = { modelCalls: 0, toolCallCount: 0, hitLimit: false };

  const systemMessage = { /* ... */ };
  const allMessages = [systemMessage, ...state.messages];

  let response = await model.chat(allMessages, allTools);
  stats.modelCalls++;

  for (let i = 0; i < maxIterations; i++) {
    if (!response.toolCalls || response.toolCalls.length === 0) {
      return { answer: response.content, todos: [], stats };
    }

    // 执行工具...
    // 把结果追加到对话历史...
    // 再次调用模型
    response = await model.chat(allMessages, allTools);
    stats.modelCalls++;
  }

  stats.hitLimit = true;
  return { answer: "未能完成任务", todos: [], stats };
}

几个关键改动:

默认上限从 5 提升到 15。 复杂任务需要更多步骤。15 次足够大多数场景,又不会让一次任务消耗过多资源。

可通过 options.maxIterations 覆盖。 测试时可以传一个很小的值来验证"达到上限"的行为,不需要真的跑 15 轮。

返回 AgentResult 调用方拿到的不只是回答,还有执行计划和统计数据。

下一节

循环的骨架升级完了。但光有循环还不够——模型在循环里不知道自己应该分几步、每步做什么。下一节引入 ReAct 模式的完整实现,让模型能先规划、再执行、最后验证。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page