编码母题 / P0 / Agent 运行时 / 02

带终止条件的智能体执行循环

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度必练难度中等预计完成35–45 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

模型每轮返回最终答案或工具调用。循环应把工具结果回填消息,直到完成、取消或达到 maxSteps;拒绝无动作响应。

输入输出示例

模型连续调用工具却没有最终答案时,到 maxSteps 返回上限状态。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:带终止条件的智能体执行循环
 * 要求:模型每轮返回最终答案或工具调用。循环应把工具结果回填消息,直到完成、取消或达到
 * maxSteps;拒绝无动作响应。
 * 示例:模型连续调用工具却没有最终答案时,到 maxSteps 返回上限状态。
 */
async function runAgent({
  model,
  toolExecutor,
  messages = [],
  maxSteps = 8,
  signal,
}) {}

(async () => {
  let calls = 0;
  const model = async () =>
    ++calls === 1
      ? {
          type: "tool_calls",
          calls: [{ id: "t1", name: "clock", arguments: {} }],
        }
      : { type: "final", content: "done" };
  const result = await runAgent({
    model,
    toolExecutor: { execute: async () => ({ ok: true, output: "12:00" }) },
  });
  console.log(result.status); // 预期:"completed"
  console.log(result.output); // 预期:"done"
  console.log(result.messages[1].role); // 预期:"tool"
})().catch(console.error);

解题思路

展开思路、不变量与常见边界

关键切入点:循环控制器掌握状态和终止条件;模型只提议下一步,工具结果按调用 id 归档。

核心模型:循环状态由 messages、step 和 tool 执行结果组成;模型只做决策, ToolExecutor 控制副作用。maxSteps 是最后防线,不应代替业务终止条件。 不变量:每轮最多调用模型一次;每个 tool_call 只执行一次并产生对应结果消息; 返回 completed 时必有 final。迁移:ReAct、Plan-and-Execute、Coding Agent。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:循环控制器掌握状态和终止条件;模型只提议下一步,工具结果按调用 id 归档。
async function runAgent({
  model,
  toolExecutor,
  messages = [],
  maxSteps = 8,
  signal,
}) {
  if (
    typeof model !== "function" ||
    !toolExecutor ||
    !Number.isInteger(maxSteps) ||
    maxSteps < 1
  )
    throw new TypeError("invalid options");
  const history = [...messages];
  for (let step = 0; step < maxSteps; step += 1) {
    if (signal?.aborted)
      return {
        status: "cancelled",
        reason: signal.reason,
        messages: history,
        steps: step,
      };
    const decision = await model({ messages: [...history], step, signal });
    if (decision?.type === "final") {
      history.push({ role: "assistant", content: decision.content });
      return {
        status: "completed",
        output: decision.content,
        messages: history,
        steps: step + 1,
      };
    }
    if (
      decision?.type !== "tool_calls" ||
      !Array.isArray(decision.calls) ||
      !decision.calls.length
    )
      throw new Error("model returned no actionable decision");
    history.push({ role: "assistant", toolCalls: decision.calls });
    for (const call of decision.calls) {
      const result = await toolExecutor.execute(call, { signal });
      history.push({
        role: "tool",
        toolCallId: call.id,
        name: call.name,
        content: result,
      });
    }
  }
  return { status: "max_steps_exceeded", messages: history, steps: maxSteps };
}

// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
  let calls = 0;
  const model = async () =>
    ++calls === 1
      ? {
          type: "tool_calls",
          calls: [{ id: "t1", name: "clock", arguments: {} }],
        }
      : { type: "final", content: "done" };
  const result = await runAgent({
    model,
    toolExecutor: { execute: async () => ({ ok: true, output: "12:00" }) },
  });
  console.log(result.status); // 预期:"completed"
  console.log(result.output); // 预期:"done"
  console.log(result.messages[1].role); // 预期:"tool"
  const endless = await runAgent({
    model: async () => ({
      type: "tool_calls",
      calls: [{ id: Math.random(), name: "x" }],
    }),
    toolExecutor: { execute: async () => ({ ok: true }) },
    maxSteps: 2,
  });
  console.log(endless.status); // 预期:"max_steps_exceeded"


})().catch(console.error);

复杂度分析

时间复杂度:O(S+C),不计模型和工具。最多 S 轮循环,累计处理 C 个工具调用。

空间复杂度:O(S+C)。消息轨迹和工具结果随轮次与调用数增长。

下载原始 JS 文件