假设用户对客服 Agent 说:

请帮我取消订单 123。
模型很快返回一段结构化结果:
{"name": "cancel_order","arguments": {"orderId": 123}}
日志里已经出现 cancel_order,看起来 Agent 做出了正确决定。可此时查询订单系统,订单 123 仍然是“待发货”。
这不是模型调用失败。恰恰相反,模型已经完成了它在这一轮中的工作:根据用户请求和工具描述,生成一个结构化的 Tool Call。问题在于,提出动作和执行动作不是一回事。
接下来还有一串没有发生的事情:
Tool Calling 只建立了模型与应用之间的协议接口。要把这个调用请求变成已经发生的业务动作,应用必须接住模型输出,执行工具,再把真实结果送回模型。模型若继续产生新的 Tool Call,这个过程还要重复。
上一篇讨论了为什么模型能力不等于企业生产力。本篇沿着其中的“任务闭环层”继续向内拆:一次模型调用,究竟如何变成一段可以执行、暂停、恢复和结束的 Agent Run?
向模型声明工具时,应用通常会提供工具名称、用途说明和参数结构。模型根据当前输入判断是否使用工具;如果需要,它返回工具名称与参数,而不是直接进入订单数据库执行 SQL。
以取消订单为例,模型看到的可以是这样一份工具定义:
const cancelOrder = {name: "cancel_order",description: "取消尚未发货且属于当前用户的订单",parameters: {type: "object",properties: {orderId: { type: "number" }},required: ["orderId"]}};
工具描述让模型知道“有哪些动作可以选择”,参数 Schema 约束它“应该用什么格式提出动作”。模型返回的 name 与 arguments 仅仅是一份结构化请求;真正的函数调用必须由应用侧(Client Tool)接管并执行。OpenAI 与 Anthropic 的 Tool Calling 文档都把这个边界写得很清楚:模型产生调用,应用执行代码,再将 Tool Result 返回模型。12
模型在一轮中也不一定只返回 Tool Call。根据 API 和应用约定,它还可能返回最终文本、结构化业务结果,或者多个工具调用。Runtime 必须检查实际输出,而不能假设每次响应都是可以直接展示给用户的答案。
一个 Tool Call 从模型出来后,至少要经过四类处理:
| 参与者 | 输入 | 负责事项 | 输出 | 不负责事项 |
|---|---|---|---|---|
| Model | 当前 Context、工具定义 | 生成最终回答或下一步结构化调用 | Final Output / Tool Call | 不直接保证业务动作成功 |
| Runtime | Model Output、Run State | 解析、校验、选择分支、维护循环 | 执行指令或 Run Result | 不替代业务系统完成动作 |
| Policy / Approval | Tool Call、用户与风险信息 | 判断允许、拒绝或暂停审批 | Policy Decision | 不生成业务结果 |
| Tool Executor | 已通过校验的参数 | 调用订单服务、数据库或外部 API | Tool Result | 不决定完整任务是否结束 |
回到订单案例。Runtime 首先要确认 cancel_order 是已注册工具,orderId 符合参数 Schema;Policy 再结合当前用户、订单归属与动作风险判断是否放行;Tool Executor 最后才向订单服务发出取消请求。
这个边界也解释了为什么不能简单地说“模型会操作数据库”。对于 Client Tool,动作由用户应用执行;对于部分 Hosted Tool 或 Server Tool,则由模型服务提供方的运行时执行。执行位置可以不同,但都不是“模型权重本身直接连接外部系统”。2
Client Tool 的典型过程不是一次请求,而是五个步骤:1
sequenceDiagramparticipant U as 用户participant A as 应用 / Runtimeparticipant M as Modelparticipant T as cancel_orderU->>A: 请取消订单 123A->>M: 输入 + cancel_order 定义M-->>A: Tool Call(orderId=123)A->>A: 校验参数、权限与审批策略A->>T: 执行取消订单T-->>A: Tool Result(取消成功)A->>M: 原输入 + Tool Call + Tool ResultM-->>A: 订单 123 已取消A-->>U: 最终答复
第一轮 Model Call 产生动作请求;应用执行工具;第二轮 Model Call 读取 Tool Result,才生成“订单已取消”的最终答复。如果工具返回“订单已经发货”,模型也应基于这个事实调整回答,而不是继续宣称取消成功。
所以,接入 Tool Calling 不自动等于得到一个 Agent。 它只是让模型能够用结构化格式提出动作。应用还需要执行动作、回传结果,并处理模型可能继续提出的下一个动作。
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
如果只记录最后一条文本,我们会丢掉真正决定任务结果的过程:模型为什么调用某个工具、工具返回什么、动作是否经过审批、运行从哪里恢复。
把具体 SDK 的类名拿掉,最小 Agent loop 只有两个反复发生的核心步骤:
Runtime 负责两者之间的分支、状态与边界:
flowchart LR%% 样式定义classDef runtime fill:#e8f4ff,stroke:#2478e5classDef model fill:#f0f9eb,stroke:#67c23aclassDef tool fill:#fdf6ec,stroke:#e6a23cclassDef endnode fill:#fef0f0,stroke:#f56c6cA["读取 State 并组装 Context"]:::runtimeB["Model Call"]:::modelC{模型输出}D["Complete"]:::endnodeE["校验 Tool 与参数"]:::toolF{是否需要审批}G["保存 State 并 Pause"]:::runtimeH["Tool Execution"]:::toolI["记录 Tool Result 并更新 State"]:::runtimeJ["Fail 或受控修复"]:::endnodeK["Cancel / Fail"]:::endnode%% 主线正向链路,从上至下分层排布避免交叉A --> BB --> C%% 分支1:直接结束C -- Final Output --> D%% 分支2:工具调用链路C -- Tool Call --> EE --> FF -- 是 --> GF -- 否 --> HH --> II -.循环回流.-> A%% 异常分支统一右下排布,不横穿主线E -- 校验失败 --> JH -- 执行异常 --> JA -- 取消或超限 --> K
这里刻意不用“模型思考了什么”解释循环。工程上真正能够记录和复现的是 Model Request、Model Response、Tool Call、Tool Result、State Update、Approval 和 Error。模型内部如何形成输出,不影响 Runtime 对这些公开事件的处理。
模型产生 cancel_order 时,只能说明它根据已有 Context 判断取消动作可能合适。真实环境可能返回完全不同的结果:
这些结果会改变下一步路径。取消成功后可以生成确认信息;已经发货时要解释限制或转入退货流程;服务超时可能进入受控重试;需要审批时则保存现场并暂停。
Agent 的动态性不是来自“模型可以自由发挥”,而是来自下一步路径由模型输出和环境反馈共同决定。环境结果为模型提供 ground truth,Runtime 则保证这个结果以正确格式进入下一轮。4
复杂任务可能需要显式计划。例如,一个采购 Agent 可以先输出待办列表,再逐项查询库存、比较供应商并发起审批。计划也可以保存在 State 中,执行后更新完成状态。
但在取消订单这类短任务里,模型完全可以每轮根据当前 State 与 Tool Result 选择下一步,不必先调用独立 Planner。现有主流实现也没有共同要求每个 Agent 都必须配置一个 Planning 组件:有的把它视为模型行为,有的用结构化计划实现,有的通过 Orchestrator 或 Graph 显式编排。
更准确的说法是:Planning 是一种可以显式化的运行行为或编排模式,不是最小 Agent 必须拥有的独立组件。 如果系统展示计划,应展示模型明确输出的计划或应用记录,而不是把生成的解释当作模型真实内部推理的完整披露。
循环解决了“结果回来后继续调用”的流程问题,但它无法回答两个更关键的工程问题:每一轮应该带上哪些信息? 以及 中断后如何从断点原样恢复?
若每次都把完整历史、所有工具结果塞给模型,Context 会越来越长,敏感信息也可能被不必要地暴露。若什么都不保存,下一轮又会失去任务进度。要拆解这对矛盾,需要区分 Context、State、Session / Thread 和 Memory。下面是本文采用的工作定义,不是某一家厂商的统一术语体系。
Context 是一次 Model Call 实际收到的信息集合,通常包括 Instructions、经过选择的消息、可用工具定义、与当前决策有关的 State,以及按需检索出的资料。
它不是数据库中所有可用数据,也不等于完整会话历史。Context Window 有容量限制,更重要的是,混入大量无关信息会稀释当前任务真正需要的信号。Context Engineering 的工作,就是决定每一轮把哪些信息交给模型。5
OpenAI Agents SDK 也明确区分 Conversation History 与 Run Context:前者会进入模型,后者可以只供应用代码和工具使用。6 当前用户的数据库连接、鉴权对象和内部日志句柄属于 Runtime 依赖,没有必要因为它们“与运行有关”就发送给模型。
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
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
Memory 通常指跨步骤或跨会话保留、并在未来按需取回的信息。例如:
信息被写入 Memory,不代表模型下一轮自动知道它。外部 Memory 必须经过检索、筛选并放入当前 Context,才能直接影响本轮输出。Anthropic 对 Agent Context 的讨论,以及 LangGraph 对 Checkpointer 与 Store 的区分,都体现了这一点:前者处理当前 Thread 的状态连续性,后者保存跨 Thread 的应用数据。59
四者的关系可以画成:
flowchart LR%% 样式分类classDef memory fill:#e1f5fe,stroke:#0288d1classDef session fill:#f3e5f5,stroke:#7b1fa2classDef state fill:#fff8e1,stroke:#f57c00classDef runtime fill:#f1f8e9,stroke:#388e3cclassDef model fill:#fef2f2,stroke:#dc2626%% 数据源M[(Long-term Memory)]:::memoryH[(Session History)]:::sessionT[(Current State)]:::state%% 中间流程节点S[Context Builder]:::runtimeC[Model Context]:::runtimeL[Model Call]:::modelO[Model Output]:::modelR[Tool Result]:::stateU[State Update]:::runtime%% 数据流走向(分层排布,杜绝连线交叉)M -- 按需检索筛选 --> SH -- 裁剪/摘要压缩 --> ST -- 提取本轮相关事实 --> SS --> CC --> LL --> OO --> UR --> U%% 状态回流 & 长期记忆落库分支U -.更新当前任务进度.-> TU -- 识别长期有效数据,异步写入 --> M
| 概念 | 保存什么 | 典型生命周期 | 是否直接对模型可见 | 订单案例 |
|---|---|---|---|---|
| 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 | 用户或系统主动终止 | 是 | 通常需显式重开 | 用户撤回取消请求 |
在最小循环里,模型返回 Final Output 且没有更多 Tool Call,可以作为 Run 的停止点。3 但“模型不再调用工具”和“业务目标已经成功”不是永远等价。
假如订单服务返回“已经发货,无法取消”,模型可以正确解释原因并结束 Run。此时运行本身正常完成,取消订单这一业务目标却没有达成。生产系统仍需要独立的任务结果或业务校验,不能只用 finalOutput !== null 统计成功率。
取消、退款、发布、删除等有副作用的动作,常常不能由模型输出直接触发。Runtime 可以让模型继续提出动作,但在执行前根据 Policy 暂停。
暂停时应返回待处理事项和可恢复 State。审批人批准或拒绝后,应用从同一份 State 继续:批准则执行原 Tool Call;拒绝则把拒绝结果记录为 Tool Result,让模型决定如何回复用户。这个过程中不需要重新让模型生成一次取消请求,也不应把 Pause 当成运行失败。78
Agent loop 会放大错误处理的重要性,因为每一轮都有新的失败入口:
maxTurns 不是性能优化,而是一条基本控制边界。没有它,模型和工具可能在相同结果之间反复往返。工具失败也不能一律自动重试:查询类动作通常可以安全重试,取消订单等有副作用的动作必须先设计幂等键和结果核验,否则一次网络超时后的自动重试,可能因缺乏幂等键而造成重复执行(如重复扣款或重复取消)。
用户撤回请求、上游连接断开、运行超时或预算耗尽时,Runtime 都需要能够终止循环。模型可以生成“任务已经完成”,也可以建议停止,但应用仍应保留独立的取消信号。
这也是“Agent 自主执行”的边界:自主意味着模型可以在授权范围内动态选择下一步,不意味着 Runtime 放弃控制。运行时掌握审批、资源限制、超时和取消,才能让模型决策进入一个可管理的系统。
stateDiagram-v2direction LR[*] --> Running : 启动Agent Run%% 主循环迭代Running --> Running : Tool Result → 组装上下文继续执行%% 人工审批暂停分支Running --> Paused : 需要人工审批Paused --> Running : 审批通过/驳回,恢复执行%% 模型主动正常结束Running --> Completed : Model 返回 Final Output%% 异常失败分支Running --> Failed : 执行异常 / 内容拦截 / 资源超限(超时/预算耗尽)%% 外部强制终止(用户撤回、连接断开等系统取消信号)Running --> Cancelled : 用户撤回 / 上游断连 / 主动取消信号%% 全部终止态收敛至结束Completed --> [*]Failed --> [*]Cancelled --> [*]
前面分别拆开了 Tool Calling、执行循环、State 和生命周期。把它们放回同一段代码,可以更清楚地看到 Agent SDK 的 run() 隐藏了什么。
下面的 TypeScript 示例不绑定具体模型或框架。callModel、executeTool、saveState 和 approvalPolicy 由外部注入,循环只负责编排它们。为突出主线,本示例假定每轮单次 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 的关键机制:
ModelDecision 把 Final Output 与 Tool Call 变成显式分支。while 循环让 Tool Result 能够进入下一轮 Model Call。AgentState 保存事件、轮次、审批点与运行状态。pendingApproval 让 Run 从同一 Tool Call 恢复,避免重新生成动作。maxTurns、AbortSignal 和 catch 提供失败与取消出口。为了突出主线,示例省略了 Tool Registry、Schema Validation、幂等键、分布式锁、超时重试、Checkpoint 版本和 Trace。这些不是可有可无的细节,而是生产化 Runtime 需要继续补齐的能力;本篇只证明它们应该接入执行循环的哪个位置。
OpenAI Agents SDK、LangChain / LangGraph 等框架会替我们封装模型适配、Tool Dispatch、循环、Session、Checkpoint、审批或 Trace 的一部分。框架降低了实现成本,但不会消除这些机制。出现重复调用、状态丢失、越权执行或无法恢复时,排查仍然要回到几个基本问题:
理解循环不是为了重复造一个 Agent SDK,而是为了知道框架在替我们承担什么,以及系统出错时应该从哪里找证据。
回到订单 123。模型返回 cancel_order 时,只提出了动作;Runtime 还要校验参数与权限,在风险边界前暂停审批,调用订单服务,并把真实结果交给下一轮。State 保存这段过程,让 Run 可以暂停和恢复;最大轮次、错误处理和取消信号则防止它无限运行或越过系统边界。
由此可以给出本文的工作定义:
这一定义是根据多家官方运行机制做出的工程归纳,不是行业唯一标准。它强调的也不是组件数量,而是四件事能否连起来:模型决策、外部执行、状态更新和边界控制。
这篇文章可以先带走三个判断:
下一篇会在这个最小执行循环之上,继续讨论 Skill、Workflow、MCP 等能力如何接入,又该如何编排。
OpenAI, “Function calling”, platform.openai.com/api/docs/gu… (official-doc) ↩ ↩2
Anthropic, “Tool use with Claude”, docs.anthropic.com/en/docs/bui… (official-doc) ↩ ↩2
OpenAI, “Running agents”, platform.openai.com/api/docs/gu… (official-doc) ↩ ↩2 ↩3
Anthropic, “Building effective agents”, www.anthropic.com/engineering… (official-blog) ↩ ↩2
Anthropic, “Effective context engineering for AI agents”, www.anthropic.com/engineering… (official-blog) ↩ ↩2
OpenAI, “Agent definitions”, platform.openai.com/api/docs/gu… (official-doc) ↩
OpenAI, “Results and state”, platform.openai.com/api/docs/gu… (official-doc) ↩ ↩2
OpenAI, “Guardrails and human review”, platform.openai.com/api/docs/gu… (official-doc) ↩ ↩2
LangChain, “LangGraph persistence”, docs.langchain.com/oss/python/… (official-doc) ↩ ↩2