别急着上多 Agent:先把 Tool Loop 做到可停止、可恢复

0 阅读8分钟

2026-07-26-Agent-Harness-封面.png

会调用工具的 Agent 很容易演示,难的是让它知道什么时候必须停、哪些动作不能直接做,以及中断后从哪里继续。

如果这三个问题还没有确定答案,多加几个 Agent 只会把上下文、权限、成本和评测一起变复杂。本文先用一个框架无关的 JavaScript 骨架,把最大轮数、只读权限、审批暂停、检查点和取消放到模型之外。

这个循环可以演示能力,却没有回答几个生产问题:

  • 模型一直重复调用同一个工具,谁来强制停止?
  • 工具要发布文章、发消息或修改数据,谁来判断权限?
  • 进程在第 6 轮中断,能否从最近一步继续?
  • 最终答案正确,但中途访问了不该访问的数据,算不算成功?
  • 用户点击取消后,模型流和正在执行的工具会不会一起停下?

这些问题不能靠一句“请谨慎使用工具”的 Prompt 解决。Prompt 可以影响模型行为,却不是确定性的权限系统、预算器或状态机。

先分清四层:模型、Loop、Runtime、Harness

为了避免把所有能力都叫作 Agent,可以先用四层理解:

主要职责
模型根据当前上下文提出下一步动作或最终答案
Agent Loop重复“模型决策—工具执行—状态更新—是否继续”
Runtime管理有状态执行、调度、持久化、暂停与恢复
Harness围绕模型和运行时提供上下文、工具、权限、沙箱、反馈、观测与治理

这不是唯一的行业标准,而是一种实用的工程分层。LangChain 的官方产品概念也区分了 Framework、Runtime 与 Harness;OpenAI Agents SDK 的 Runner 文档则明确描述了 final_output、handoff、工具调用和 max_turns 组成的循环。它们说明的是各自产品实现,本文只提炼通用边界。

这里最重要的判断是:

模型负责提出动作,运行环境负责决定动作能不能执行、还能执行多久,以及失败后如何恢复。

一个最小可控 Loop 至少需要什么

第一版不必直接引入庞大的平台,但至少应有六类确定性控制。

1. 硬停止条件

停止条件不能只写在 Prompt 中,至少要覆盖:

  • 得到符合输出契约的最终结果;
  • 达到最大模型轮数、工具调用数、总时长或费用;
  • 用户取消;
  • 等待用户输入或人工审批;
  • 命中安全策略;
  • 出现不可重试错误;
  • 连续无进展或重复调用。

需要先统一计量单位:一次完整任务是 run,一次模型调用是 turn,一次具体工具执行是 tool call。如果只写“最多 10 步”,并行工具调用或 handoff 后很容易统计失真。

2. 工具契约

每个工具至少声明:

  • 输入、输出 Schema;
  • 可读和可写范围;
  • 是否有副作用;
  • 风险等级与审批策略;
  • 超时、重试和幂等策略;
  • 返回给模型的结构化错误;
  • 日志中需要脱敏的字段。

“搜索文档”和“删除文档”不应因为都叫文件工具而共享权限。授权应该围绕能力和影响,而不是工具名称。

3. 模型外权限

可以从一个简单分级开始:

风险例子默认策略
检索公开资料、读取允许目录允许并记录
写草稿、创建临时产物、调用付费 API限额允许或询问
发布、发消息、修改正式数据展示最终参数并审批
极高删除、支付、扩大权限、任意命令拒绝或进入专用隔离流程

审批时要展示最终执行对象和参数;批准后参数一旦变化,就要重新审批。OWASP 对提示注入的建议同样强调最小权限、外部内容隔离,以及对高影响动作加入人工介入。

4. 检查点与正式状态

不要把所有非完成情况都记成失败。最小状态可以包括:

running
completed
waiting_approval
waiting_input
failed
cancelled
budget_exceeded

检查点至少保存当前状态、配置版本、已经执行的工具、待审批动作和产物位置。写入、发送、创建等有副作用的动作还应带幂等键,恢复时先查询结果,避免重复执行。

5. 可取消与可恢复

取消信号要继续向下游传播到模型请求、工具进程和子任务,而不是只让页面上的按钮变灰。

恢复也不是“把整段聊天再发一次”。更可靠的事实来源是外部状态:任务目标、验收标准、当前步骤、配置快照、工具结果和产物索引。Anthropic 的长任务实践与 OpenAI 的 Harness Engineering 都把增量产物、可读环境和反馈回路放在重要位置。

6. 轨迹评测

只看最终答案会漏掉过程风险。至少同时看三层:

  • 结果:任务是否完成,事实是否正确;
  • 轨迹:工具选择、参数、顺序、重复调用和错误恢复是否合理;
  • 系统:延迟、成本、失败率、审批次数和恢复能力。

一个答案碰巧正确,但调用了越权工具或成本失控的运行,不能算可靠。

一个不依赖框架的最小 JavaScript 骨架

下面的代码不是完整生产 Runtime,而是把最容易遗漏的控制点放在同一个可执行骨架里。decide 可以替换成任意模型适配器,工具只暴露明确的 riskexecute

const TERMINAL = new Set([
  "completed",
  "waiting_approval",
  "waiting_input",
  "failed",
  "cancelled",
  "budget_exceeded",
]);

export async function runAgent({
  goal,
  decide,
  tools,
  checkpoint,
  signal = new AbortController().signal,
  limits = { maxTurns: 8, maxToolCalls: 12, maxMs: 30_000 },
}) {
  const state = {
    runId: crypto.randomUUID(),
    status: "running",
    goal,
    turn: 0,
    toolCalls: 0,
    startedAt: Date.now(),
    events: [],
  };

  const finish = async (status, extra = {}) => {
    Object.assign(state, extra, { status, finishedAt: Date.now() });
    await checkpoint(structuredClone(state));
    return state;
  };

  await checkpoint(structuredClone(state));

  try {
    while (!TERMINAL.has(state.status)) {
      if (signal.aborted) return await finish("cancelled");
      if (Date.now() - state.startedAt >= limits.maxMs) {
        return await finish("budget_exceeded", { reason: "max_time" });
      }
      if (state.turn >= limits.maxTurns) {
        return await finish("budget_exceeded", { reason: "max_turns" });
      }

      state.turn += 1;
      const action = await decide({
        goal,
        state: structuredClone(state),
        signal,
      });

      if (action.type === "final") {
        if (typeof action.output !== "string" || action.output.length === 0) {
          throw new Error("final output does not match schema");
        }
        return await finish("completed", { output: action.output });
      }

      if (action.type !== "tool") {
        throw new Error(`unsupported action: ${action.type}`);
      }

      const tool = tools[action.name];
      if (!tool) {
        state.events.push({
          type: "tool_rejected",
          turn: state.turn,
          name: action.name,
          reason: "tool_not_found",
        });
        await checkpoint(structuredClone(state));
        continue;
      }

      if (tool.risk !== "read") {
        return await finish("waiting_approval", {
          pendingAction: {
            name: action.name,
            input: action.input,
            risk: tool.risk,
          },
        });
      }

      if (state.toolCalls >= limits.maxToolCalls) {
        return await finish("budget_exceeded", {
          reason: "max_tool_calls",
        });
      }

      state.toolCalls += 1;
      const result = await tool.execute(action.input, { signal });
      state.events.push({
        type: "tool_result",
        turn: state.turn,
        name: action.name,
        status: result.status,
      });
      await checkpoint(structuredClone(state));
    }

    return state;
  } catch (error) {
    return await finish("failed", {
      error: error instanceof Error ? error.message : String(error),
    });
  }
}

这段代码有意不做三件事:

  1. 不允许模型直接改变 risk
  2. 不在恢复时自动重放副作用动作;
  3. 不把异常堆栈和工具原文全部塞回模型上下文。

真实项目还要补上参数 Schema 校验、单次调用超时、有限重试、幂等键、持久化存储、敏感字段脱敏、重复调用检测,以及经过校准的评测集。

第一版为什么通常不需要多 Agent

Anthropic 的工程建议区分了由代码预设路径的 Workflow 和由模型动态决定过程的 Agent,并建议从简单、可组合的模式开始。

多 Agent 会额外引入:

  • 上下文如何传递;
  • 不同 Agent 的工具权限如何隔离;
  • handoff 后谁对最终结果负责;
  • 费用和轮数如何跨 Agent 统计;
  • 循环嵌套后如何停止和恢复;
  • 评测到底针对单个角色还是完整轨迹。

如果一个单 Agent 加三个只读工具都无法稳定通过真实问题集,增加角色数量通常只是放大不确定性。只有在角色隔离、独立权限或并行研究确实带来价值时,再引入 handoff 或子 Agent。

最小落地顺序

如果现在有一个“能调用工具”的 Demo,可以按这个顺序补齐:

  1. 写清任务、成功标准和失败状态;
  2. 给所有工具补 Schema、副作用与风险等级;
  3. 在模型外加入最大 turn、工具数、时长和取消;
  4. 高影响动作改为 waiting_approval,不直接执行;
  5. 每个有意义步骤后写检查点;
  6. 为结果、轨迹、成本和安全建立同一套回归样本;
  7. 稳定后再决定是否需要专用 Runtime、多 Agent 或平台化。

真正的工程分水岭,不是模型能不能调用更多工具,而是系统能否明确回答:

这次运行做了什么、为什么能做、何时必须停、失败后从哪里继续。

事实、验证与边界说明

  • 官方事实:OpenAI Agents SDK 的 Runner 循环、max_turns 行为;LangChain 文档中的 Framework / Runtime / Harness 产品分类;Anthropic 对 Workflow 与 Agent 的区分;OWASP 对提示注入、最小权限和人工介入的建议。
  • 工程归纳:本文的四层模型、风险分级、最小状态集和落地顺序,是对多份官方资料的综合,不是统一行业规范。
  • 代码验证:示例为框架无关骨架,已使用 Node.js 22.16.0 完成语法校验;未连接真实模型和生产工具。
  • 个人实践边界:本文不声称已经在“水獭比特技术知识助手”或其他生产项目中完成部署、压测和长期运行。
  • 待验证项:不同 Runtime 的恢复语义、复杂审批流、并行工具和多 Agent 成本,需要在具体项目中重新验证。

官方参考