假设用户对客服 Agent 说:

请帮我取消订单 123

模型很快返回一段结构化结果:

{
  "name": "cancel_order",
  "arguments": {
    "orderId": 123
  }
}

日志里已经出现 cancel_order,看起来 Agent 做出了正确决定。可此时查询订单系统,订单 123 仍然是“待发货”。

这不是模型调用失败。恰恰相反,模型已经完成了它在这一轮中的工作:根据用户请求和工具描述,生成一个结构化的 Tool Call。问题在于,提出动作和执行动作不是一回事。

接下来还有一串没有发生的事情:

  • 谁确认订单 123 属于当前用户?
  • 谁校验订单是否仍可取消?
  • 谁判断这项操作是否需要人工审批?
  • 谁真正调用订单服务?
  • 如果订单已经发货或服务超时,谁处理失败?
  • 工具返回结果后,谁决定回复用户还是继续调用其他工具?

Tool Calling 只建立了模型与应用之间的协议接口。要把这个调用请求变成已经发生的业务动作,应用必须接住模型输出,执行工具,再把真实结果送回模型。模型若继续产生新的 Tool Call,这个过程还要重复。

上一篇讨论了为什么模型能力不等于企业生产力。本篇沿着其中的“任务闭环层”继续向内拆:一次模型调用,究竟如何变成一段可以执行、暂停、恢复和结束的 Agent Run?

一、Tool Calling 只完成了“提出动作”

1. 模型返回的是结构化决策

向模型声明工具时,应用通常会提供工具名称、用途说明和参数结构。模型根据当前输入判断是否使用工具;如果需要,它返回工具名称与参数,而不是直接进入订单数据库执行 SQL。

以取消订单为例,模型看到的可以是这样一份工具定义:

const cancelOrder = {
  name: "cancel_order",
  description: "取消尚未发货且属于当前用户的订单",
  parameters: {
    type: "object",
    properties: {
      orderId: { type: "number" }
    },
    required: ["orderId"]
  }
};

工具描述让模型知道“有哪些动作可以选择”,参数 Schema 约束它“应该用什么格式提出动作”。模型返回的 namearguments 仅仅是一份结构化请求;真正的函数调用必须由应用侧(Client Tool)接管并执行。OpenAI 与 Anthropic 的 Tool Calling 文档都把这个边界写得很清楚:模型产生调用,应用执行代码,再将 Tool Result 返回模型。12

模型在一轮中也不一定只返回 Tool Call。根据 API 和应用约定,它还可能返回最终文本、结构化业务结果,或者多个工具调用。Runtime 必须检查实际输出,而不能假设每次响应都是可以直接展示给用户的答案。

2. 应用把请求变成真实动作

一个 Tool Call 从模型出来后,至少要经过四类处理:

参与者输入负责事项输出不负责事项
Model当前 Context、工具定义生成最终回答或下一步结构化调用Final Output / Tool Call不直接保证业务动作成功
RuntimeModel Output、Run State解析、校验、选择分支、维护循环执行指令或 Run Result不替代业务系统完成动作
Policy / ApprovalTool Call、用户与风险信息判断允许、拒绝或暂停审批Policy Decision不生成业务结果
Tool Executor已通过校验的参数调用订单服务、数据库或外部 APITool Result不决定完整任务是否结束

回到订单案例。Runtime 首先要确认 cancel_order 是已注册工具,orderId 符合参数 Schema;Policy 再结合当前用户、订单归属与动作风险判断是否放行;Tool Executor 最后才向订单服务发出取消请求。

这个边界也解释了为什么不能简单地说“模型会操作数据库”。对于 Client Tool,动作由用户应用执行;对于部分 Hosted Tool 或 Server Tool,则由模型服务提供方的运行时执行。执行位置可以不同,但都不是“模型权重本身直接连接外部系统”。2

3. 一次完整往返包含两次模型交互

Client Tool 的典型过程不是一次请求,而是五个步骤:1

cancel_order Model 应用 / Runtime 用户 cancel_order Model 应用 / Runtime 用户 请取消订单 123 输入 + cancel_order 定义 Tool Call(orderId=123) 校验参数、权限与审批策略 执行取消订单 Tool Result(取消成功) 原输入 + Tool Call + Tool Result 订单 123 已取消 最终答复

第一轮 Model Call 产生动作请求;应用执行工具;第二轮 Model Call 读取 Tool Result,才生成“订单已取消”的最终答复。如果工具返回“订单已经发货”,模型也应基于这个事实调整回答,而不是继续宣称取消成功。

所以,接入 Tool Calling 不自动等于得到一个 Agent。 它只是让模型能够用结构化格式提出动作。应用还需要执行动作、回传结果,并处理模型可能继续提出的下一个动作。

二、从一次往返到 Agent Run,中间多了一个循环

1. 一次 Model Call 不等于一次 Agent Run

Model Call 的边界很清楚:应用组装 Context,调用模型,取得一次输出。Agent Run 的边界则更长:从收到任务开始,经历若干次模型调用、工具执行与状态更新,直到运行完成、暂停、失败或被取消。

OpenAI Agents SDK 把一个 Run 描述为持续到“真实停止点”的循环:调用模型,检查输出;有 Tool Call 就执行后继续,有 Handoff 就切换处理者,没有后续工具工作且得到最终回答时才返回结果。3 Anthropic 对 Agent 的概括更直接:LLM 根据环境反馈在循环中使用工具。4

因此,一次 Run 里可能发生:

Model Call #1 → 查询订单
Tool Result  → 订单存在,尚未发货
Model Call #2 → 取消订单
Tool Result  → 需要人工审批
Pause        → 等待负责人批准
Resume       → 执行取消
Model Call #3 → 生成最终答复
Complete

如果只记录最后一条文本,我们会丢掉真正决定任务结果的过程:模型为什么调用某个工具、工具返回什么、动作是否经过审批、运行从哪里恢复。

2. 最小循环是“决策—执行—反馈—更新”

把具体 SDK 的类名拿掉,最小 Agent loop 只有两个反复发生的核心步骤:

  1. Model Call:模型根据当前 Context 返回 Final Output 或 Tool Call。
  2. Tool Execution:应用执行工具,把 Tool Result 交给下一轮。

Runtime 负责两者之间的分支、状态与边界:

Final Output

Tool Call

循环回流

校验失败

执行异常

取消或超限

读取 State 并组装 Context

Model Call

模型输出

Complete

校验 Tool 与参数

是否需要审批

保存 State 并 Pause

Tool Execution

记录 Tool Result 并更新 State

Fail 或受控修复

Cancel / Fail

这里刻意不用“模型思考了什么”解释循环。工程上真正能够记录和复现的是 Model Request、Model Response、Tool Call、Tool Result、State Update、Approval 和 Error。模型内部如何形成输出,不影响 Runtime 对这些公开事件的处理。

3. 环境反馈让下一步建立在事实上

模型产生 cancel_order 时,只能说明它根据已有 Context 判断取消动作可能合适。真实环境可能返回完全不同的结果:

  • 订单取消成功;
  • 订单已经发货,不能取消;
  • 订单不属于当前用户;
  • 订单服务暂时不可用;
  • 动作超过自动处理额度,需要人工审批。

这些结果会改变下一步路径。取消成功后可以生成确认信息;已经发货时要解释限制或转入退货流程;服务超时可能进入受控重试;需要审批时则保存现场并暂停。

Agent 的动态性不是来自“模型可以自由发挥”,而是来自下一步路径由模型输出和环境反馈共同决定。环境结果为模型提供 ground truth,Runtime 则保证这个结果以正确格式进入下一轮。4

4. Planning 可以存在,但不是固定方框

复杂任务可能需要显式计划。例如,一个采购 Agent 可以先输出待办列表,再逐项查询库存、比较供应商并发起审批。计划也可以保存在 State 中,执行后更新完成状态。

但在取消订单这类短任务里,模型完全可以每轮根据当前 State 与 Tool Result 选择下一步,不必先调用独立 Planner。现有主流实现也没有共同要求每个 Agent 都必须配置一个 Planning 组件:有的把它视为模型行为,有的用结构化计划实现,有的通过 Orchestrator 或 Graph 显式编排。

更准确的说法是:Planning 是一种可以显式化的运行行为或编排模式,不是最小 Agent 必须拥有的独立组件。 如果系统展示计划,应展示模型明确输出的计划或应用记录,而不是把生成的解释当作模型真实内部推理的完整披露。

三、循环为什么离不开 State?

循环解决了“结果回来后继续调用”的流程问题,但它无法回答两个更关键的工程问题:每一轮应该带上哪些信息? 以及 中断后如何从断点原样恢复?

若每次都把完整历史、所有工具结果塞给模型,Context 会越来越长,敏感信息也可能被不必要地暴露。若什么都不保存,下一轮又会失去任务进度。要拆解这对矛盾,需要区分 Context、State、Session / Thread 和 Memory。下面是本文采用的工作定义,不是某一家厂商的统一术语体系。

1. Context 是本轮模型真正看见的信息

Context 是一次 Model Call 实际收到的信息集合,通常包括 Instructions、经过选择的消息、可用工具定义、与当前决策有关的 State,以及按需检索出的资料。

它不是数据库中所有可用数据,也不等于完整会话历史。Context Window 有容量限制,更重要的是,混入大量无关信息会稀释当前任务真正需要的信号。Context Engineering 的工作,就是决定每一轮把哪些信息交给模型。5

OpenAI Agents SDK 也明确区分 Conversation History 与 Run Context:前者会进入模型,后者可以只供应用代码和工具使用。6 当前用户的数据库连接、鉴权对象和内部日志句柄属于 Runtime 依赖,没有必要因为它们“与运行有关”就发送给模型。

2. State 保存任务事实与恢复位置

State 是应用维护的任务事实与运行进度。取消订单时,它可以包含:

interface AgentState {
  runId: string;
  status: "running" | "paused" | "completed" | "failed" | "cancelled";
  turnCount: number;
  events: Array<ModelEvent | ToolEvent>;
  pendingApproval?: {
    call: ToolCall;
    decision?: "approved" | "rejected";
  };
  lastError?: string;
}

State 中既有可能进入下一轮 Context 的信息,例如最近一次 Tool Result;也有只供 Runtime 使用的信息,例如重试次数、审批决定和内部错误。State 是运行拥有的数据,Context 是本轮选择给模型看的数据。

cancel_order 等待审批时,应用应保存 Pending Tool Call、当前轮次和已有事件。审批完成后从这份 State 恢复同一次 Run,而不是把“批准了”伪装成一个全新的用户问题。官方 Agent Runtime 对审批流程也采用“中断并返回可恢复 State”的模式。78

3. Session / Thread 组织连续运行

Session 或 Thread 更接近一个组织容器或定位标识:它把多次调用、消息历史和 Checkpoint 归到同一条连续交互中。不同框架的具体语义并不相同,不能把 OpenAI Session 与 LangGraph Thread 当成完全相同的 API。

可以用一个简单关系理解:

名词作用
State是某个时刻保存了什么
Checkpoint是 State 在特定步骤的快照
Session / Thread帮助 Runtime 找到属于同一连续交互的历史与快照

LangGraph 通过 Checkpointer 保存 Thread 内的 Graph State,用于对话延续、人工介入和故障恢复;OpenAI Agents SDK 则可以通过 Session、Conversation ID 或 Response ID 延续不同类型的会话状态。39

4. Memory 保存未来可能再用的信息

Memory 通常指跨步骤或跨会话保留、并在未来按需取回的信息。例如:

  • “用户偏好短信通知”可以进入长期 Memory;
  • “用户常用收货地址”可以在授权后跨会话读取;
  • “订单 123 正等待取消审批”则应属于当前 Run State。

信息被写入 Memory,不代表模型下一轮自动知道它。外部 Memory 必须经过检索、筛选并放入当前 Context,才能直接影响本轮输出。Anthropic 对 Agent Context 的讨论,以及 LangGraph 对 Checkpointer 与 Store 的区分,都体现了这一点:前者处理当前 Thread 的状态连续性,后者保存跨 Thread 的应用数据。59

四者的关系可以画成:

按需检索筛选

裁剪/摘要压缩

提取本轮相关事实

更新当前任务进度

识别长期有效数据,异步写入

Long-term Memory

Session History

Current State

Context Builder

Model Context

Model Call

Model Output

Tool Result

State Update

概念保存什么典型生命周期是否直接对模型可见订单案例
Context本轮推理所需的选定信息一次 Model Call当前请求、可用工具、最近 Tool Result
State当前任务事实、进度和控制信息一次 Run,可持久化恢复按需选择待审批调用、轮次、执行结果
Session / Thread连续交互的历史与状态定位多次调用或多个 Run其中部分可进入 Context同一客服会话或任务线程
Memory未来可能复用的偏好、事实或经验跨 Run / 跨 Session否,需取回后进入 Context用户通知偏好

这个区分的价值不在术语本身,而在数据边界:哪些信息必须让模型看到,哪些只应由 Runtime 保管,哪些需要跨会话保存。

四、谁决定继续、暂停与失败?

模型可以返回 Final Output,也可以建议下一步动作,但 Agent Run 的生命周期不能只交给模型决定。应用还要处理审批、错误、预算、超时和用户取消。

本文用 Continue、Complete、Pause、Fail 和 Cancel 描述这些状态。它们是便于解释 Runtime 的工程归纳,不是所有 SDK 共同采用的标准枚举。

状态典型触发条件是否终态是否可恢复订单案例
Continue获得 Tool Result,需要再次调用模型查询订单成功,继续判断能否取消
Complete得到最终输出,且没有后续工具工作通常不再继续取消成功并生成答复
Pause等待审批、用户信息或外部事件等待负责人批准取消
Fail不可恢复错误、校验拦截或达到限制视补偿策略而定参数无效、重试耗尽
Cancel用户或系统主动终止通常需显式重开用户撤回取消请求
1. Complete:运行结束不自动等于业务成功

在最小循环里,模型返回 Final Output 且没有更多 Tool Call,可以作为 Run 的停止点。3 但“模型不再调用工具”和“业务目标已经成功”不是永远等价。

假如订单服务返回“已经发货,无法取消”,模型可以正确解释原因并结束 Run。此时运行本身正常完成,取消订单这一业务目标却没有达成。生产系统仍需要独立的任务结果或业务校验,不能只用 finalOutput !== null 统计成功率。

2. Pause:需要外部决定时保存现场

取消、退款、发布、删除等有副作用的动作,常常不能由模型输出直接触发。Runtime 可以让模型继续提出动作,但在执行前根据 Policy 暂停。

暂停时应返回待处理事项和可恢复 State。审批人批准或拒绝后,应用从同一份 State 继续:批准则执行原 Tool Call;拒绝则把拒绝结果记录为 Tool Result,让模型决定如何回复用户。这个过程中不需要重新让模型生成一次取消请求,也不应把 Pause 当成运行失败。78

3. Fail:失败处理不能藏在无限重试里

Agent loop 会放大错误处理的重要性,因为每一轮都有新的失败入口:

  • 模型输出无法解析;
  • 工具名称不存在或参数不合法;
  • Guardrail 阻止输入、输出或动作;
  • Tool Executor 超时;
  • 外部服务返回业务错误;
  • 运行达到最大轮次、重试次数或预算。

maxTurns 不是性能优化,而是一条基本控制边界。没有它,模型和工具可能在相同结果之间反复往返。工具失败也不能一律自动重试:查询类动作通常可以安全重试,取消订单等有副作用的动作必须先设计幂等键和结果核验,否则一次网络超时后的自动重试,可能因缺乏幂等键而造成重复执行(如重复扣款或重复取消)。

4. Cancel:应用必须保留强制停止权

用户撤回请求、上游连接断开、运行超时或预算耗尽时,Runtime 都需要能够终止循环。模型可以生成“任务已经完成”,也可以建议停止,但应用仍应保留独立的取消信号。

这也是“Agent 自主执行”的边界:自主意味着模型可以在授权范围内动态选择下一步,不意味着 Runtime 放弃控制。运行时掌握审批、资源限制、超时和取消,才能让模型决策进入一个可管理的系统。

启动Agent Run

Tool Result → 组装上下文继续执行

需要人工审批

审批通过/驳回,恢复执行

Model 返回 Final Output

执行异常 / 内容拦截 / 资源超限(超时/预算耗尽)

用户撤回 / 上游断连 / 主动取消信号

Running

Paused

Completed

Failed

Cancelled

五、用最小代码复原一个 Agent Runtime

前面分别拆开了 Tool Calling、执行循环、State 和生命周期。把它们放回同一段代码,可以更清楚地看到 Agent SDK 的 run() 隐藏了什么。

下面的 TypeScript 示例不绑定具体模型或框架。callModelexecuteToolsaveStateapprovalPolicy 由外部注入,循环只负责编排它们。为突出主线,本示例假定每轮单次 Tool Call;实际生产环境需额外扩展支持并行多工具调用与结果聚合。

type RunStatus =
  | "running"
  | "paused"
  | "completed"
  | "failed"
  | "cancelled";

type ToolCall = {
  type: "tool_call";
  callId: string;
  name: string;
  arguments: Record<string, unknown>;
};

type ModelDecision =
  | { type: "final"; content: string }
  | ToolCall;

type AgentEvent =
  | { type: "user"; content: string }
  | { type: "model"; decision: ModelDecision }
  | { type: "tool"; callId: string; result: unknown };

type PendingApproval = {
  call: ToolCall;
  decision?: "approved" | "rejected";
};

type AgentState = {
  runId: string;
  status: RunStatus;
  turnCount: number;
  events: AgentEvent[];
  pendingApproval?: PendingApproval;
  finalOutput?: string;
  lastError?: string;
};

type RuntimeDependencies = {
  callModel: (events: AgentEvent[]) => Promise<ModelDecision>;
  executeTool: (call: ToolCall) => Promise<unknown>;
  requiresApproval: (call: ToolCall) => boolean;
  saveState: (state: AgentState) => Promise<void>;
  signal?: AbortSignal;
  maxTurns: number;
};

async function appendToolResult(
  state: AgentState,
  call: ToolCall,
  result: unknown
) {
  state.events.push({
    type: "tool",
    callId: call.callId,
    result
  });
}

async function runAgent(
  state: AgentState,
  deps: RuntimeDependencies
): Promise<AgentState> {
  state.status = "running";

  try {
    while (state.turnCount < deps.maxTurns) {
      if (deps.signal?.aborted) {
        state.status = "cancelled";
        await deps.saveState(state);
        return state;
      }

      // 恢复时先处理暂停中的原 Tool Call,避免让模型重复生成。
      if (state.pendingApproval) {
        const pending = state.pendingApproval;

        if (!pending.decision) {
          state.status = "paused";
          await deps.saveState(state);
          return state;
        }

        if (pending.decision === "rejected") {
          await appendToolResult(state, pending.call, {
            ok: false,
            reason: "rejected_by_reviewer"
          });
        } else {
          const result = await deps.executeTool(pending.call);
          await appendToolResult(state, pending.call, result);
        }

        state.pendingApproval = undefined;
        await deps.saveState(state);
        continue; // 工具结果已回填,重新进入循环调用模型生成后续回复
      }

      state.turnCount += 1;
      const decision = await deps.callModel(state.events);
      state.events.push({ type: "model", decision });

      if (decision.type === "final") {
        state.finalOutput = decision.content;
        state.status = "completed";
        await deps.saveState(state);
        return state;
      }

      if (deps.requiresApproval(decision)) {
        state.pendingApproval = { call: decision };
        state.status = "paused";
        await deps.saveState(state);
        return state;
      }

      const result = await deps.executeTool(decision);
      await appendToolResult(state, decision, result);
      await deps.saveState(state);
    }

    state.status = "failed";
    state.lastError = `maxTurns exceeded: ${deps.maxTurns}`;
    await deps.saveState(state);
    return state;
  } catch (error) {
    state.status = "failed";
    state.lastError =
      error instanceof Error ? error.message : "unknown runtime error";
    await deps.saveState(state);
    return state;
  }
}

async function review(
  state: AgentState,
  decision: "approved" | "rejected",
  deps: RuntimeDependencies
) {
  if (!state.pendingApproval) {
    throw new Error("No pending approval");
  }

  state.pendingApproval.decision = decision;
  await deps.saveState(state);
  return runAgent(state, deps);
}

这段代码没有实现具体模型与订单服务,却保留了最小 Runtime 的关键机制:

  1. ModelDecision 把 Final Output 与 Tool Call 变成显式分支。
  2. while 循环让 Tool Result 能够进入下一轮 Model Call。
  3. AgentState 保存事件、轮次、审批点与运行状态。
  4. pendingApproval 让 Run 从同一 Tool Call 恢复,避免重新生成动作。
  5. maxTurnsAbortSignalcatch 提供失败与取消出口。

为了突出主线,示例省略了 Tool Registry、Schema Validation、幂等键、分布式锁、超时重试、Checkpoint 版本和 Trace。这些不是可有可无的细节,而是生产化 Runtime 需要继续补齐的能力;本篇只证明它们应该接入执行循环的哪个位置。

OpenAI Agents SDK、LangChain / LangGraph 等框架会替我们封装模型适配、Tool Dispatch、循环、Session、Checkpoint、审批或 Trace 的一部分。框架降低了实现成本,但不会消除这些机制。出现重复调用、状态丢失、越权执行或无法恢复时,排查仍然要回到几个基本问题:

  • 模型实际返回了什么?
  • Runtime 选择了哪个分支?
  • 工具是否真正执行,结果是否正确回填?
  • State 在哪一步更新和持久化?
  • Run 为什么继续、暂停或结束?

理解循环不是为了重复造一个 Agent SDK,而是为了知道框架在替我们承担什么,以及系统出错时应该从哪里找证据。

总结:Agent 的核心不是“拥有工具”,而是“运行得起来”

回到订单 123。模型返回 cancel_order 时,只提出了动作;Runtime 还要校验参数与权限,在风险边界前暂停审批,调用订单服务,并把真实结果交给下一轮。State 保存这段过程,让 Run 可以暂停和恢复;最大轮次、错误处理和取消信号则防止它无限运行或越过系统边界。

由此可以给出本文的工作定义:

Agent 是一个由 Runtime 组织的应用级执行系统:模型根据当前 Context 产生下一步决策,应用执行动作并更新 State,再依据环境反馈持续推进,直到任务完成、暂停、失败或取消。

这一定义是根据多家官方运行机制做出的工程归纳,不是行业唯一标准。它强调的也不是组件数量,而是四件事能否连起来:模型决策、外部执行、状态更新和边界控制。

这篇文章可以先带走三个判断:

  1. Tool Calling 只是协议接口,不是完整 Agent。
  2. Agent loop 的核心,是 Model Call 与 Tool Execution 根据环境反馈反复推进。
  3. State 与 Runtime 边界决定这个循环能否受控、可恢复地运行。

下一篇会在这个最小执行循环之上,继续讨论 Skill、Workflow、MCP 等能力如何接入,又该如何编排。



  1. OpenAI, “Function calling”, https://platform.openai.com/api/docs/guides/function-calling (official-doc) ↩︎ ↩︎

  2. Anthropic, “Tool use with Claude”, https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview (official-doc) ↩︎ ↩︎

  3. OpenAI, “Running agents”, https://platform.openai.com/api/docs/guides/agents/running-agents (official-doc) ↩︎ ↩︎ ↩︎

  4. Anthropic, “Building effective agents”, https://www.anthropic.com/engineering/building-effective-agents (official-blog) ↩︎ ↩︎

  5. Anthropic, “Effective context engineering for AI agents”, https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents (official-blog) ↩︎ ↩︎

  6. OpenAI, “Agent definitions”, https://platform.openai.com/api/docs/guides/agents/define-agents (official-doc) ↩︎

  7. OpenAI, “Results and state”, https://platform.openai.com/api/docs/guides/agents/results (official-doc) ↩︎ ↩︎

  8. OpenAI, “Guardrails and human review”, https://platform.openai.com/api/docs/guides/agents/guardrails-approvals (official-doc) ↩︎ ↩︎

  9. LangChain, “LangGraph persistence”, https://docs.langchain.com/oss/python/langgraph/persistence (official-doc) ↩︎ ↩︎

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐