先看一个场景:用户对客服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约束它“应该用什么格式提出动作”。模型返回的name与arguments仅仅是一份结构化请求;真正的函数调用必须由应用侧(Client Tool)接管并执行。OpenAI与Anthropic的Tool Calling文档都把这个边界写得很清楚:模型产生调用,应用执行代码,再将Tool Result返回模型。12
模型在一轮中也不一定只返回Tool Call。根据API和应用约定,它还可能返回最终文本、结构化业务结果,或者多个工具调用。Runtime必须检查实际输出,而不能假设每次响应都是可以直接展示给用户的答案。
2. 应用把请求变成真实动作
一个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
3. 一次完整往返包含两次模型交互
Client Tool的典型过程不是一次请求,而是五个步骤:1
sequenceDiagram
participant U as 用户
participant A as 应用 / Runtime
participant M as Model
participant T as cancel_order
U->>A: 请取消订单 123
A->>M: 输入 + cancel_order 定义
M-->>A: Tool Call(orderId=123)
A->>A: 校验参数、权限与审批策略
A->>T: 执行取消订单
T-->>A: Tool Result(取消成功)
A->>M: 原输入 + Tool Call + Tool Result
M-->>A: 订单 123 已取消
A-->>U: 最终答复
第一轮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只有两个反复发生的核心步骤:
- Model Call:模型根据当前Context返回Final Output或Tool Call。
- Tool Execution:应用执行工具,把Tool Result交给下一轮。
Runtime负责两者之间的分支、状态与边界:
flowchart LR
%% 样式定义
classDef runtime fill:#e8f4ff,stroke:#2478e5
classDef model fill:#f0f9eb,stroke:#67c23a
classDef tool fill:#fdf6ec,stroke:#e6a23c
classDef endnode fill:#fef0f0,stroke:#f56c6c
A["读取 State 并组装 Context"]:::runtime
B["Model Call"]:::model
C{模型输出}
D["Complete"]:::endnode
E["校验 Tool 与参数"]:::tool
F{是否需要审批}
G["保存 State 并 Pause"]:::runtime
H["Tool Execution"]:::tool
I["记录 Tool Result 并更新 State"]:::runtime
J["Fail 或受控修复"]:::endnode
K["Cancel / Fail"]:::endnode
%% 主线正向链路,从上至下分层排布避免交叉
A --> B
B --> C
%% 分支1:直接结束
C -- Final Output --> D
%% 分支2:工具调用链路
C -- Tool Call --> E
E --> F
F -- 是 --> G
F -- 否 --> H
H --> I
I -.循环回流.-> A
%% 异常分支统一右下排布,不横穿主线
E -- 校验失败 --> J
H -- 执行异常 --> J
A -- 取消或超限 --> K
这里刻意不用“模型思考了什么”解释循环。工程上真正能够记录和复现的是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
四者的关系可以画成:
flowchart LR
%% 样式分类
classDef memory fill:#e1f5fe,stroke:#0288d1
classDef session fill:#f3e5f5,stroke:#7b1fa2
classDef state fill:#fff8e1,stroke:#f57c00
classDef runtime fill:#f1f8e9,stroke:#388e3c
classDef model fill:#fef2f2,stroke:#dc2626
%% 数据源
M[(Long-term Memory)]:::memory
H[(Session History)]:::session
T[(Current State)]:::state
%% 中间流程节点
S[Context Builder]:::runtime
C[Model Context]:::runtime
L[Model Call]:::model
O[Model Output]:::model
R[Tool Result]:::state
U[State Update]:::runtime
%% 数据流走向(分层排布,杜绝连线交叉)
M -- 按需检索筛选 --> S
H -- 裁剪/摘要压缩 --> S
T -- 提取本轮相关事实 --> S
S --> C
C --> L
L --> O
O --> R
R --> U
%% 状态回流 & 长期记忆落库分支
U -.更新当前任务进度.-> T
U -- 识别长期有效数据,异步写入 --> 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 | 用户或系统主动终止 | 是 | 通常需显式重开 | 用户撤回取消请求 |
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放弃控制。运行时掌握审批、资源限制、超时和取消,才能让模型决策进入一个可管理的系统。
stateDiagram-v2
direction LR
[*] --> Running : 启动Agent Run
%% 主循环迭代
Running --> Running : Tool Result → 组装上下文继续执行
%% 人工审批暂停分支
Running --> Paused : 需要人工审批
Paused --> Running : 审批通过/驳回,恢复执行
%% 模型主动正常结束
Running --> Completed : Model 返回 Final Output
%% 异常失败分支
Running --> Failed : 执行异常 / 内容拦截 / 资源超限(超时/预算耗尽)
%% 外部强制终止(用户撤回、连接断开等系统取消信号)
Running --> Cancelled : 用户撤回 / 上游断连 / 主动取消信号
%% 全部终止态收敛至结束
Completed --> [*]
Failed --> [*]
Cancelled --> [*]
五、用最小代码复原一个Agent Runtime
前面分别拆开了Tool Calling、执行循环、State和生命周期。把它们放回同一段代码,可以更清楚地看到Agent SDK的run()隐藏了什么。
下面的TypeScript示例不绑定具体模型或框架。callModel、executeTool、sa veState和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;sa veState: (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.sa veState(state);return state;}// 恢复时先处理暂停中的原 Tool Call,避免让模型重复生成。if (state.pendingApproval) {const pending = state.pendingApproval;if (!pending.decision) {state.status = "paused";await deps.sa veState(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.sa veState(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.sa veState(state);return state;}if (deps.requiresApproval(decision)) {state.pendingApproval = { call: decision };state.status = "paused";await deps.sa veState(state);return state;}const result = await deps.executeTool(decision);await appendToolResult(state, decision, result);await deps.sa veState(state);}state.status = "failed";state.lastError = `maxTurns exceeded: ${deps.maxTurns}`;await deps.sa veState(state);return state;} catch (error) {state.status = "failed";state.lastError =error instanceof Error ? error.message : "unknown runtime error";await deps.sa veState(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.sa veState(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的一部分。框架降低了实现成本,但不会消除这些机制。出现重复调用、状态丢失、越权执行或无法恢复时,排查仍然要回到几个基本问题:
- 模型实际返回了什么?
- Runtime选择了哪个分支?
- 工具是否真正执行,结果是否正确回填?
- State在哪一步更新和持久化?
- Run为什么继续、暂停或结束?
理解循环不是为了重复造一个Agent SDK,而是为了知道框架在替我们承担什么,以及系统出错时应该从哪里找证据。
总结:Agent的核心不是“拥有工具”,而是“运行得起来”
回到订单123。模型返回cancel_order时,只提出了动作;Runtime还要校验参数与权限,在风险边界前暂停审批,调用订单服务,并把真实结果交给下一轮。State保存这段过程,让Run可以暂停和恢复;最大轮次、错误处理和取消信号则防止它无限运行或越过系统边界。
由此可以给出本文的工作定义:
这一定义是根据多家官方运行机制做出的工程归纳,不是行业唯一标准。它强调的也不是组件数量,而是四件事能否连起来:模型决策、外部执行、状态更新和边界控制。
这篇文章可以先带走三个判断:
- Tool Calling只是协议接口,不是完整Agent。
- Agent loop的核心,是Model Call与Tool Execution根据环境反馈反复推进。
- State与Runtime边界决定这个循环能否受控、可恢复地运行。
下一篇会在这个最小执行循环之上,继续讨论Skill、Workflow、MCP等能力如何接入,又该如何编排。
Footnotes
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
