Agent 工程体系 · 第 3/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 运行循环:观察、决策、行动、反馈、状态与终止
Agent 不是“一次调用模型并返回文本”,而是一个持续推进的运行过程:模型读取当前状态,提出决策;运行时执行决策中的动作;外部环境返回结果;运行时把结果写回状态;模型再基于更新后的状态继续判断,直到任务完成、需要人工介入,或触发某种终止条件。
OpenAI 对 Agent 运行循环的概括是:调用当前 Agent 的模型、检查模型输出、执行工具调用或切换到其他专员、在没有更多工具工作时返回结果。Anthropic 也将 Agent 描述为“LLM 根据环境反馈循环使用工具”的系统。两者共同指向一个核心事实:Agent 的可靠性主要取决于循环如何管理状态和边界,而不只是模型本身的回答质量。 (developers.openai.com)
一、先区分:工作流、模型调用和 Agent 运行循环
1. 一次模型调用不是 Agent
一次模型调用可以表示为:
其中:
- 是输入上下文;
- 是模型;
- 是模型输出。
如果输出只是最终文本,那么调用结束后系统就停止。这种方式适合分类、摘要、改写和简单问答。
但 Agent 的任务通常不能由一个输出直接完成。例如:
查询订单状态,如果订单已支付但未发货,则创建客服工单,并向用户说明工单编号。
这个任务至少涉及:
- 查询订单;
- 解释查询结果;
- 判断是否满足创建工单的条件;
- 调用创建工单工具;
- 读取工单创建结果;
- 组织最终回复。
模型无法凭空知道订单状态,也不能把“应该创建工单”直接当成“工单已经创建”。因此,Agent 必须在模型和环境之间多次往返。
2. 固定工作流和 Agent 的差异
固定工作流把步骤写在程序中:
查询订单 -> 判断状态 -> 创建工单 -> 返回结果
Agent 则可能让模型动态决定:
用户目标
-> 模型判断需要查询订单
-> 工具返回结果
-> 模型判断还需要查物流
-> 工具返回结果
-> 模型决定创建工单
-> 工具返回结果
-> 模型输出最终答案
Anthropic 将前者称为 Workflow,将后者称为 Agent:Workflow 的路径由预先编写的代码决定,Agent 则由 LLM 动态决定处理过程和工具使用方式。对于步骤明确、成功路径稳定的任务,固定工作流更容易测试;对于无法提前确定步骤数量和工具顺序的开放任务,Agent 循环才有价值。代价是更高的延迟、费用和错误累积风险。 (anthropic.com)
二、运行循环的形式化模型
把一次 Agent 执行抽象成离散时间系统。
设:
- :第 个时刻的运行状态;
- :Agent 在当前状态中观察到的输入;
- :模型决策函数;
- :模型产生的动作;
- :外部环境或工具执行器;
- :动作执行结果;
- :状态更新函数;
- :终止判断函数。
一次循环可写成:
如果 ,继续下一轮;如果为真,则运行终止。
这里的“观察”不是一定要调用视觉模型或浏览器。它指的是:把当前运行所需的事实组织成模型可以消费的上下文。可能包括:
- 用户目标;
- 历史消息;
- 上一轮模型输出;
- 工具执行结果;
- 当前任务进度;
- 预算和截止时间;
- 错误信息;
- 等待人工审批的事件。
“行动”也不只意味着调用工具。模型可能产生三类不同结果:
- 最终回答;
- 一个或多个工具调用;
- 向其他 Agent 转交任务。
运行时必须先解释模型输出,再决定状态如何变化。不能把所有模型输出都当作文本,也不能把模型提出的工具调用当作工具已经成功执行。
三、循环状态不是聊天记录
1. 历史记录与运行状态的差异
聊天记录通常回答:
到目前为止,用户和模型说过什么?
运行状态还必须回答:
当前正在执行哪个动作?哪些动作已经成功?哪些动作失败?是否正在等待审批?剩余预算是多少?恢复时从哪里继续?
因此,一个更完整的状态可以表示为:
RunState {
run_id: "run_123",
status: "running",
objective: "查询订单并在需要时创建工单",
current_agent: "support_agent",
messages: [...],
facts: {
order_id: "O-1001",
payment_status: "paid",
shipment_status: "not_shipped"
},
pending_actions: [],
completed_actions: [...],
failed_actions: [],
step_count: 3,
token_usage: {
input: 4200,
output: 900
},
deadline: "2026-09-01T10:30:00+08:00",
termination_reason: null,
version: 7
}
其中 messages 是模型上下文的一部分,但不是全部状态。facts、pending_actions、completed_actions 和预算字段都可能影响下一次决策,却未必适合原样放进对话历史。
2. 状态必须能解释下一步
一个有效状态至少要满足:
也就是说,给定持久化后的 ,系统应能重新判断下一步应该做什么,而不依赖只存在于某个进程内存里的变量。
反例是:
last_tool_result = result
# 进程重启后 last_tool_result 丢失
如果下一次模型调用需要依赖 last_tool_result,恢复后的 Agent 就无法区分:
- 工具从未执行;
- 工具已经执行但结果未写回;
- 工具执行成功但进程在写回前崩溃;
- 工具执行失败并且应该重试。
这四种情况对应完全不同的恢复动作。
3. 状态需要版本号
多个 Worker 或恢复任务可能同时操作同一个运行。状态应带有单调递增的版本号:
version = 7
写入时使用乐观并发控制:
UPDATE agent_runs
SET state_json = :new_state,
version = version + 1
WHERE run_id = :run_id
AND version = :expected_version;
如果更新行数为零,说明另一个执行者已经推进了状态,当前执行者不能盲目覆盖。否则可能出现:
- Worker A 执行工具;
- Worker B 从旧状态恢复;
- Worker B 覆盖 A 写入的工具结果;
- Agent 重复执行工具。
这不是模型错误,而是状态并发控制缺失。
四、事件和状态:循环如何被真正推进
状态描述“现在是什么”,事件描述“发生了什么”。
一个事件通常包含:
{
"event_id": "evt_0007",
"run_id": "run_123",
"sequence": 7,
"type": "tool.completed",
"action_id": "act_0003",
"tool_name": "create_ticket",
"input": {
"order_id": "O-1001",
"reason": "paid_but_not_shipped"
},
"output": {
"ticket_id": "T-9001"
},
"status": "succeeded",
"occurred_at": "2026-09-01T10:02:11+08:00"
}
常见事件类型包括:
run.started
model.started
model.completed
tool.requested
tool.started
tool.completed
tool.failed
approval.requested
approval.accepted
approval.rejected
run.paused
run.cancel_requested
run.cancelled
run.completed
run.failed
run.terminated
事件和状态的关系可以写成:
其中 Reduce 是确定性的状态折叠函数。只要事件顺序和内容不变,最终状态就应一致。
flowchart LR
A[当前状态 S_t] --> B[观察 Observe]
B --> C[模型回合 Model Turn]
C --> D{模型输出类型}
D -->|最终答案| E[完成状态]
D -->|工具调用| F[行动 Action]
D -->|需要审批| G[暂停状态]
F --> H[工具回合 Tool Turn]
H --> I[工具结果事件]
I --> J[写回状态 Update]
J --> A
G --> K[审批事件]
K --> J
关键路径是:
模型输出工具调用
-> 记录 tool.requested
-> 执行工具
-> 记录 tool.completed 或 tool.failed
-> 将结果写入模型上下文和业务状态
-> 重新调用模型
如果省略最后一步,工具调用只是孤立的副作用,模型并不知道动作是否成功。
五、模型回合和工具回合不是同一件事
1. 模型回合
模型回合是运行时向模型提交上下文并接收模型输出的过程:
输入:用户目标 + 当前状态 + 历史事件
输出:最终文本、工具调用、转交或其他控制结果
模型回合的结果是“决策”,不是事实。模型可能说:
{
"type": "tool_call",
"name": "create_ticket",
"arguments": {
"order_id": "O-1001"
}
}
这只代表模型请求调用工具,不代表工单已经创建。
2. 工具回合
工具回合是运行时执行动作并接收外部结果:
输入:工具名 + 结构化参数
输出:成功结果、业务拒绝、超时或异常
例如:
{
"type": "tool_result",
"name": "create_ticket",
"status": "succeeded",
"data": {
"ticket_id": "T-9001"
}
}
只有工具回合成功并且结果被写回,系统才获得新的环境事实。
3. 为什么必须严格区分
如果程序在模型刚产生工具调用后就直接返回:
model_output = call_model(state)
if model_output.type == "tool_call":
return model_output
那么用户得到的可能只是:
我将为您创建工单。
但工单可能尚未创建,甚至参数校验已经失败。
正确的因果顺序是:
模型决定创建工单
≠ 工单已经创建
工具成功返回工单编号
= 环境确认工单已经创建
这也是 Agent 必须从环境获得“ground truth”的原因:工具结果、代码执行结果或其他外部观测,才是判断进度的依据。 (anthropic.com)
六、动作结果必须写回:否则循环不会学习
1. 写回的两层含义
动作结果写回至少有两层:
写回对话上下文
把工具结果作为下一次模型输入的一部分:
assistant: 调用 create_ticket(order_id="O-1001")
tool: ticket_id="T-9001"
模型据此才能生成:
工单 T-9001 已创建。
写回业务状态
同时把可查询的事实写入结构化状态:
{
"facts": {
"ticket_id": "T-9001",
"ticket_status": "created"
},
"completed_actions": [
{
"action_id": "act_0003",
"type": "create_ticket",
"result_ref": "evt_0007"
}
]
}
只写聊天记录,不写结构化状态,会导致重复执行检测、审计和恢复困难;只写结构化状态,不写模型上下文,则下一轮模型无法看到完整因果链。
2. 结果写回的事务边界
工具调用和状态写回通常不在同一个数据库事务中,因为工具可能是外部 HTTP 服务。于是会出现经典故障窗口:
1. 调用支付工具成功
2. 进程在写回数据库前崩溃
3. 恢复后无法确认支付是否成功
4. Agent 重试支付
因此,具有副作用的工具需要幂等键:
idempotency_key = run_id + ":" + action_id
服务端收到同一个幂等键时,应返回第一次执行的结果,而不是再次产生副作用。
不能简单依赖:
if action_id in completed_actions:
skip()
因为 completed_actions 可能在第一次调用成功后尚未持久化。更稳妥的设计是:
- 生成稳定的
action_id; - 在执行前持久化
tool.requested; - 向工具传递幂等键;
- 工具返回后持久化
tool.completed; - 恢复时根据事件判断动作处于“已完成”“未开始”还是“结果未知”。
“结果未知”不能自动等同于“失败”。
七、完整算例:查询订单并创建工单
设用户提出:
查询订单 O-1001。如果已经付款但还没有发货,就创建客服工单,并告诉我工单编号。
初始状态:
status = running
step_count = 0
facts = {}
pending_actions = []
第 1 轮:模型决定查询订单
模型观察到用户提供了订单号,但没有订单状态,于是产生:
{
"type": "tool_call",
"action_id": "act_0001",
"tool": "get_order",
"arguments": {
"order_id": "O-1001"
}
}
此时状态应先记录:
step_count = 1
pending_actions = ["act_0001"]
注意,pending_actions 表示“已请求但尚未得到结果”,不是“已成功”。
第 1 个工具回合:订单查询成功
工具返回:
{
"order_id": "O-1001",
"payment_status": "paid",
"shipment_status": "not_shipped"
}
运行时写回:
facts.payment_status = paid
facts.shipment_status = not_shipped
pending_actions = []
completed_actions += act_0001
并把工具结果放入下一次模型上下文。
第 2 轮:模型根据事实决定创建工单
现在模型看到:
payment_status = paid
shipment_status = not_shipped
它判断条件成立,产生:
{
"type": "tool_call",
"action_id": "act_0002",
"tool": "create_ticket",
"arguments": {
"order_id": "O-1001",
"reason": "paid_but_not_shipped"
}
}
状态变化:
step_count = 2
pending_actions = ["act_0002"]
第 2 个工具回合:工单创建成功
工具返回:
{
"ticket_id": "T-9001",
"status": "created"
}
状态变化:
facts.ticket_id = "T-9001"
facts.ticket_status = "created"
pending_actions = []
completed_actions = ["act_0001", "act_0002"]
第 3 轮:模型输出最终答案
模型产生文本:
订单 O-1001 已付款但尚未发货。我已创建客服工单,编号为 T-9001。
运行时此时才可以将状态置为:
status = completed
termination_reason = final_output
完整轨迹是:
run.started
model.completed(tool_call=get_order)
tool.requested(act_0001)
tool.completed(act_0001)
model.completed(tool_call=create_ticket)
tool.requested(act_0002)
tool.completed(act_0002)
model.completed(final_answer)
run.completed
这个例子说明,Agent 的“步数”不能只按模型调用次数计算,也不能只按工具调用次数计算。系统必须明确自己的计数单位。常见做法是把一次“模型决策加随后处理”称为一个 Agent step,而把模型请求、工具执行、审批等待分别记录为事件。
八、终止条件:完成不是唯一出口
终止条件是判断运行是否可以停止的规则。可以把它表示为:
1. 成功终止
成功终止通常需要同时满足:
- 模型产生最终答案;
- 当前没有待执行工具调用;
- 必要的业务不变量成立;
- 结果已经持久化。
不能仅凭“模型没有调用工具”判断成功。例如模型可能输出普通文本:
订单已经退款。
但系统没有任何退款工具事件,说明这只是未经验证的声明。
可以定义业务级成功条件:
Success(S) =
final_output_exists
AND pending_actions == []
AND required_fact("refund_id")
AND refund_event.status == "succeeded"
2. 最大步数
最大步数是最基本的安全阀:
它能防止模型因误解任务而无限循环,但不能保证任务质量。最大步数过小,任务可能在最后一个必要动作前被截断;过大,则会增加延迟和费用。
达到上限时,不应伪装成成功,而应记录:
status = terminated
termination_reason = max_steps
partial_result = current_state
然后由上层决定是:
- 返回部分结果;
- 转人工;
- 允许用户继续;
- 使用更强模型重试;
- 进入补偿流程。
3. Deadline
Deadline 是绝对时间边界,而不是“再执行几步”。设:
它解决的是单次工具调用很慢、模型服务排队或网络阻塞的问题。每次执行模型和工具前都应检查剩余时间,并把剩余时间传递给下游。
例如:
总截止时间:10:30:00
当前时间:10:29:52
HTTP 工具超时:10 秒
此时不能再无条件发起一个 10 秒请求。运行时应把超时时间限制为剩余预算,例如 7 秒,并在超时后进入明确的失败或暂停状态。
4. Token 和费用预算
Token 预算控制模型上下文和输出规模;费用预算控制实际资源消耗。两者相关但不等价:
- 上下文很长,Token 可能很多,但调用模型价格可能较低;
- 工具调用次数不多,但单个外部服务可能产生高费用;
- 重试会增加 Token 和工具费用;
- 并行调用会降低墙钟时间,却可能增加总成本。
可以用累计预算表示:
当:
运行时应停止继续扩展任务,而不是让模型自行决定是否“值得继续”。
5. 循环检测
最大步数只能限制长度,不能识别“重复做同一件事”。循环检测需要定义动作指纹:
例如以下动作可能具有相同指纹:
{"tool": "search", "arguments": {"query": "退款规则"}}
{"tool": "search", "arguments": {"query": "退款规则"}}
如果连续多轮出现相同动作,且环境状态没有变化,则说明循环没有产生新信息。
但不能把所有重复都视为错误。例如轮询任务可能合理地重复查询:
查询任务状态 -> 等待 5 秒 -> 再次查询任务状态
此时应把“等待时间”“任务状态变化”和“最大轮询次数”纳入判定条件。循环检测的核心不是动作是否重复,而是:
重复动作是否带来了新的可观察状态,或者是否有明确的单调进展。
九、失败、暂停和取消必须分开
1. 失败
失败表示系统无法按当前路径完成运行,例如:
- 模型调用失败;
- 工具参数校验失败;
- 工具返回业务错误;
- 工具超时;
- 状态持久化失败;
- 触发最大步数;
- 触发预算限制。
失败还应区分可重试和不可重试:
可重试:
- 网络超时
- 临时限流
- 下游 503
不可重试:
- 参数非法
- 权限不足
- 订单不存在
- 业务规则拒绝
模型输出的错误信息也不能直接作为重试依据。重试策略应由运行时根据错误类别决定。
2. 暂停
暂停表示运行没有失败,只是等待外部事件:
- 人工审批;
- 用户补充信息;
- 异步工具回调;
- 定时等待;
- 配额恢复。
暂停状态应保存继续执行所需的快照:
status = paused
pause_reason = approval_required
pending_actions = ["act_0005"]
state_snapshot = "..."
审批通过后,应从原状态恢复,而不是发起一条新的用户消息。否则可能产生:
- 步数重新计数;
- 工具调用丢失;
- 对话历史重复;
- 服务端 continuation ID 不一致。
OpenAI Agents SDK 当前文档也明确区分了暂停运行和新回合:审批或取消后继续执行时,应使用保存的状态恢复,而不是把它当成新的用户轮次。 (developers.openai.com)
3. 取消
取消表示有人或某个系统主动要求停止:
用户点击停止
任务超时
租户撤销授权
服务关闭
取消不是异常,也不等于工具失败。运行时至少需要三个阶段:
running
-> cancel_requested
-> cancelled
cancel_requested 表示系统已经收到取消请求,但当前工具可能尚未安全停止。只有确认当前动作不再继续,或者已经进入可接受的终态后,才能写成 cancelled。
对于有副作用的工具,取消尤其复杂:
1. 用户请求取消
2. create_payment 已经发送到支付服务
3. 本地还没有收到响应
此时不能因为本地取消就断言支付没有发生。正确处理通常是:
- 记录取消请求;
- 等待工具返回或通过幂等查询确认;
- 将结果标记为成功、失败或未知;
- 必要时执行补偿动作。
十、可持久化执行轨迹:把运行变成可恢复事实
1. 轨迹不是日志
普通日志主要服务于排查:
calling tool create_ticket
可持久化执行轨迹则必须支持:
- 恢复;
- 审计;
- 重放;
- 去重;
- 统计;
- 解释为什么终止;
- 判断副作用是否已经发生。
因此轨迹中的事件不能只记录一句字符串,而应包含结构化输入、输出、状态和关联 ID。
2. 推荐的最小轨迹结构
{
"run_id": "run_123",
"sequence": 12,
"event_id": "evt_0012",
"type": "tool.completed",
"action_id": "act_0007",
"tool_name": "create_ticket",
"request": {
"order_id": "O-1001",
"reason": "paid_but_not_shipped"
},
"response": {
"ticket_id": "T-9001"
},
"status": "succeeded",
"attempt": 1,
"started_at": "2026-09-01T10:02:10+08:00",
"completed_at": "2026-09-01T10:02:11+08:00",
"idempotency_key": "run_123:act_0007"
}
必须特别保存:
run_id:一次运行的身份;sequence:事件顺序;action_id:动作身份;attempt:第几次尝试;status:请求、执行中、成功、失败或未知;idempotency_key:副作用去重依据;started_at和completed_at:时延和 Deadline 诊断依据。
3. 事件日志与当前快照
只保存所有事件,恢复时逐条重放,审计能力强,但长任务恢复慢;只保存当前快照,恢复快,但难以解释中间发生了什么。
工程上常用组合:
事件轨迹:完整事实
状态快照:快速恢复
快照版本:对应最后一个已折叠事件
例如:
snapshot.last_sequence = 100
events = 101, 102, 103, ...
恢复时先加载快照,再重放 101 之后的事件。
4. 一个最小的本地执行器
下面的代码不依赖 LLM 服务,使用假的模型和工具演示核心语义。它展示了三个关键点:
- 模型输出工具调用时,不能立即完成;
- 工具结果必须写回历史和状态;
- 只有模型输出最终答案时,运行才成功结束。
from dataclasses import dataclass, field
from typing import Any
@dataclass
class State:
messages: list[dict[str, Any]] = field(default_factory=list)
facts: dict[str, Any] = field(default_factory=dict)
completed_actions: set[str] = field(default_factory=set)
step_count: int = 0
status: str = "running"
def fake_model(state: State) -> dict[str, Any]:
"""根据当前状态模拟模型决策。"""
if "order" not in state.facts:
return {
"type": "tool_call",
"action_id": "get_order:O-1001",
"name": "get_order",
"arguments": {"order_id": "O-1001"},
}
order = state.facts["order"]
if (
order["payment_status"] == "paid"
and order["shipment_status"] == "not_shipped"
and "ticket" not in state.facts
):
return {
"type": "tool_call",
"action_id": "create_ticket:O-1001",
"name": "create_ticket",
"arguments": {
"order_id": "O-1001",
"reason": "paid_but_not_shipped",
},
}
return {
"type": "final",
"text": f"工单已创建,编号为 {state.facts['ticket']['ticket_id']}。",
}
def fake_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
"""根据工具名模拟外部环境。"""
if name == "get_order":
return {
"order_id": arguments["order_id"],
"payment_status": "paid",
"shipment_status": "not_shipped",
}
if name == "create_ticket":
return {
"ticket_id": "T-9001",
"status": "created",
}
raise ValueError(f"unknown tool: {name}")
def run_agent(max_steps: int = 5) -> State:
state = State()
state.messages.append({
"role": "user",
"content": "查询订单 O-1001,必要时创建客服工单。",
})
while state.status == "running":
if state.step_count >= max_steps:
state.status = "terminated:max_steps"
break
state.step_count += 1
decision = fake_model(state)
if decision["type"] == "final":
state.messages.append({
"role": "assistant",
"content": decision["text"],
})
state.status = "completed"
break
action_id = decision["action_id"]
if action_id in state.completed_actions:
# 已经成功执行过的动作不能再次产生副作用
state.status = "failed:duplicate_action"
break
state.messages.append({
"role": "assistant",
"tool_call": decision,
})
try:
result = fake_tool(decision["name"], decision["arguments"])
except Exception as exc:
state.messages.append({
"role": "tool",
"name": decision["name"],
"error": str(exc),
})
state.status = "failed:tool_error"
break
state.messages.append({
"role": "tool",
"name": decision["name"],
"content": result,
})
state.completed_actions.add(action_id)
if decision["name"] == "get_order":
state.facts["order"] = result
elif decision["name"] == "create_ticket":
state.facts["ticket"] = result
return state
result = run_agent()
print(result.status)
print(result.facts)
print(result.messages[-1])
预期输出类似:
completed
{
'order': {
'order_id': 'O-1001',
'payment_status': 'paid',
'shipment_status': 'not_shipped'
},
'ticket': {
'ticket_id': 'T-9001',
'status': 'created'
}
}
{'role': 'assistant', 'content': '工单已创建,编号为 T-9001。'}
这个示例中的 fake_model 不是生产模型接口,而是为了让循环可确定地运行。真实系统还需要在每个事件落盘后更新快照,并处理进程崩溃、重复投递、超时和并发恢复。
十一、流式输出不等于运行已经完成
流式输出只是把模型生成过程或运行事件逐步交给调用方消费,并不改变 Agent 的核心循环。
用户可能已经看到:
我正在查询订单……
但此时:
- 模型回合可能尚未结束;
- 工具调用可能尚未执行;
- 工具结果可能尚未写回;
- 运行可能随后失败;
- 最终答案可能尚未生成。
因此,流式系统应区分:
可展示事件
运行完成事件
运行失败事件
运行暂停事件
不能在收到第一段文本时就把任务标记为成功。OpenAI 的运行文档也强调,流式运行需要等待完成后再把结果视为稳定;如果运行因审批或取消中断,应基于保存的状态继续。 (developers.openai.com)
十二、状态机视角:把隐藏控制流显式化
一个生产级 Agent 至少可以抽象为以下状态:
created
-> running
-> waiting_tool
-> running
-> waiting_approval
-> running
-> completed
running
-> failed
-> cancelled
-> terminated
事件触发转移:
| 当前状态 | 事件 | 下一状态 |
|---|---|---|
created |
run.started |
running |
running |
tool.requested |
waiting_tool |
waiting_tool |
tool.completed |
running |
waiting_tool |
tool.failed |
failed 或 running |
running |
approval.requested |
waiting_approval |
waiting_approval |
approval.accepted |
running |
waiting_approval |
approval.rejected |
cancelled 或 failed |
running |
model.completed(final) |
completed |
| 任意非终态 | cancel_requested |
cancelled |
running |
budget_exceeded |
terminated |
状态机的价值不在于画图,而在于阻止非法转移。例如:
completed -> running
通常是非法的;如果用户想继续对话,应创建新的 turn,或者使用明确的 continuation 机制,而不是修改已完成运行的历史。
十三、常见错误及其表现
错误一:把模型的计划当成执行结果
表现:
模型说“已发送邮件”,但没有 send_email 成功事件。
诊断方法是检查业务副作用是否有对应的工具完成事件,而不是检查最终文本。
错误二:工具失败后继续假装成功
表现:
工具返回 403
模型仍输出“工单创建成功”
运行时应把工具失败作为模型可见的反馈,或者直接终止;不能让最终输出绕过工具事实。
错误三:重试没有幂等性
表现:
用户收到两封邮件;
支付服务出现两笔扣款;
同一个订单创建多个客服工单。
诊断时要对照 action_id、attempt 和工具侧幂等键,判断是模型重复请求、Worker 重复消费,还是超时后的未知结果重试。
错误四:把暂停当成失败
表现:
等待人工审批的任务被标记为 failed;
用户审批后系统重新创建了一次任务。
暂停应保存可恢复状态,审批结果应作为事件追加,而不是重新构造一条新的用户输入。OpenAI Agents SDK 将中断结果建模为 interruptions 加 state,其中状态快照用于后续恢复。 (developers.openai.com)
错误五:只保存最终答案
表现:
最终答案说“任务失败”,但无法知道失败在模型调用、工具参数、网络还是业务规则。
最终答案是用户界面数据,不是完整运行轨迹。生产系统通常还需要保存工具调用、交接、守卫、原始模型响应和用量等更细粒度记录,以支持审计和调试。 (developers.openai.com)
十四、实现取舍:简单循环与复杂编排
最小 Agent 循环可以只有一个 while:
while not should_stop(state):
decision = model(state)
result = execute(decision)
state = update(state, result)
但生产环境通常还需要加入:
- 事件持久化;
- 状态版本控制;
- 工具幂等;
- 超时和取消;
- 重试分类;
- 人工审批;
- Token 和费用计量;
- 循环检测;
- 轨迹查询;
- 运行恢复。
这些机制不应一开始就隐藏在复杂框架之后。Anthropic 建议优先使用简单、可组合的实现,并理解框架底层的提示词、模型响应和工具调用,否则抽象层会增加调试困难。 (anthropic.com)
OpenAI Agents SDK 当前文档也把运行循环、状态延续、审批暂停、流式消费和结果快照分别作为运行时概念:结果不仅包含最终输出,还可能包含历史、最后一个 Agent、响应延续标识、待审批项和可恢复状态。 (developers.openai.com)
真正需要明确的不是“是否使用框架”,而是下面这条控制边界:
模型负责提出决策
运行时负责解释决策
工具负责改变外部环境
事件系统负责记录事实
状态机负责决定是否继续
终止器负责阻止无界运行
当这几种职责混在一起时,系统会把“模型说了什么”“工具做了什么”“状态记住了什么”和“任务是否完成”混为一谈。Agent 运行循环的核心,就是把这些因果关系逐步分离,再通过事件和状态重新连接起来。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent、工作流与普通程序:自治边界、确定性和正确选型
- 下一篇:Agent 状态机设计:节点、事件、守卫、转移和可恢复执行
- 延伸:Agent 终止与预算:最大步数、Deadline、Token、费用和循环检测
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论