Agent 工程体系 · 第 18/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 反思与验证:Critic、Verifier、规则检查和停止边界
Agent 的核心循环不是“让模型多想几次”,而是让系统在每次行动后获得新的环境事实,判断当前状态是否仍然满足任务约束,并决定继续、修正、请求人工介入还是终止。
一个可执行的 Agent 通常包含四类能力:
- 规划:决定下一步做什么;
- 行动:调用工具改变或查询外部环境;
- 反思:解释刚才发生了什么、哪里可能有问题;
- 验证:依据独立证据判断结果是否满足要求。
其中,Critic 主要回答“这一步或这个候选结果有什么问题”,Verifier 主要回答“结果是否满足可判定条件”。二者都不是“再调用一次模型”这么简单。若没有清晰的检查对象、证据来源和停止边界,反思只会变成额外的文本生成,验证则可能只是模型对自己答案的重复确认。
Anthropic 将 Agent 描述为由模型动态决定流程和工具使用的系统,而不是沿固定代码路径执行的工作流;同时强调,Agent 在执行过程中需要从工具结果、代码执行等环境反馈中取得“ground truth”,并通过最大迭代次数等停止条件保持控制。(anthropic.com) OpenAI 的 Agents SDK 文档也将 Agent 定义为能够规划、调用工具、协作并保留状态以完成多步工作的应用,并区分了由应用自行控制循环的 Responses API 与由 SDK 管理 Agent 循环的 Agents SDK。(developers.openai.com)
一、先建立正确的对象模型
1. 什么是 Agent 反思
**反思(reflection)**是 Agent 对当前执行轨迹、最近一次行动或候选结果进行解释和诊断的过程。
它至少包含三个输入:
- 当前任务目标;
- 已发生的执行轨迹;
- 最近获得的观察结果。
输出通常不是最终答案,而是结构化诊断:
{
"status": "needs_revision",
"findings": [
{
"type": "missing_evidence",
"claim": "退款金额为 120 元",
"evidence": "订单查询结果中未返回退款金额"
}
],
"next_action": "query_refund_detail",
"confidence": 0.86
}
反思关注的是:
- 当前路径是否偏离目标;
- 哪个假设被观察结果否定;
- 哪个步骤失败;
- 是否遗漏必要条件;
- 下一次行动应该如何调整。
因此,反思本质上是一个诊断与修正机制,而不是最终正确性的证明。
2. 什么是 Critic
Critic 是负责发现问题、提出反馈或指出改进方向的评审组件。
Critic 的典型问题是:
“这个计划、工具调用、代码补丁或答案有哪些缺陷?”
它可以评审:
- 计划是否覆盖任务要求;
- 工具选择是否合理;
- 参数是否完整且类型正确;
- 当前步骤是否重复;
- 结果是否包含未经证实的推断;
- 生成内容是否违反格式、安全或业务约束。
Critic 的输出可以是自然语言,也可以是结构化问题列表。但 Critic 的结论通常是“发现风险”或“建议修改”,不必承担最终放行责任。
例如:
{
"verdict": "revise",
"issues": [
{
"severity": "high",
"location": "step_2",
"reason": "查询使用了用户输入的模糊姓名,没有使用唯一用户 ID"
}
],
"suggested_fix": "先调用 resolve_user_id,再按 ID 查询订单"
}
Critic 可以是模型,也可以是程序:
- LLM Critic:适合检查语义、完整性、推理漏洞;
- 静态分析器:适合检查代码、SQL、JSON Schema;
- 业务规则引擎:适合检查金额、权限、状态机;
- 测试执行器:适合检查行为;
- 人工审核:适合处理高风险和不可形式化的问题。
3. 什么是 Verifier
Verifier 是负责根据明确判定条件,对候选结果进行验收的组件。
Verifier 的典型问题是:
“这个结果是否满足验收条件?”
它的输入应尽量接近可验证事实,而不是只接收模型的一段解释。例如,对于“完成退款”这个任务,Verifier 不应仅检查 Agent 是否输出了“退款成功”,而应查询支付系统的真实状态:
{
"order_id": "O1001",
"refund_status": "SUCCEEDED",
"refund_amount": 12000,
"currency": "CNY",
"provider_ref": "RF20260901001"
}
然后根据规范判断:
order_id 匹配
refund_status == SUCCEEDED
refund_amount == expected_amount
currency == CNY
provider_ref 存在
Verifier 更接近验收测试、后置条件检查或安全门。它的输出应该是:
{
"passed": true,
"checks": {
"order_match": true,
"status_succeeded": true,
"amount_match": true,
"currency_match": true,
"provider_reference_present": true
}
}
关键区别是:
| 组件 | 主要问题 | 输出含义 | 是否应直接决定放行 |
|---|---|---|---|
| Critic | 哪里可能有问题 | 反馈、缺陷、修正建议 | 通常不单独决定 |
| Verifier | 是否满足验收条件 | 通过、失败、未知 | 可以作为放行门 |
| 规则检查 | 是否违反硬约束 | 允许、拒绝、暂停 | 通常直接决定 |
| 测试执行器 | 行为是否符合预期 | 测试结果、日志、差异 | 可以作为证据 |
| 人工审核 | 人是否接受该风险 | 批准、拒绝、补充信息 | 可作为最终授权 |
Critic 发现问题,Verifier 判断条件,规则检查执行硬约束。
二、为什么“反思”不能替代“验证”
设任务目标为 ,Agent 在时刻 的内部状态为 ,调用工具或执行动作 ,环境返回观察结果 。
状态转移可以表示为:
其中:
- :当前任务状态,包括已知事实、未完成子目标、历史轨迹和风险状态;
- :下一步行动,例如搜索、查询、写文件或提交变更;
- :工具或环境返回的结果;
- :状态更新过程。
Agent 的计划器根据状态产生动作:
其中 是策略, 是目标。
验证器则判断当前状态是否满足完成条件:
这里必须允许 unknown。如果证据不足,不能把“无法判断”当成“通过”。
反思器可以产生诊断:
其中 可能包含缺陷、风险、原因和建议动作。但 本身不等价于 。
反例:模型自评的循环确认
用户要求:
“确认订单 O1001 已退款,并给出退款流水号。”
Agent 第一次调用支付工具失败,工具返回:
{
"status": "TIMEOUT",
"message": "request timed out"
}
模型随后生成:
“支付系统可能已处理成功,但响应超时。根据上下文判断退款应该已经完成。”
如果再让一个 Critic 评审这段话,它可能输出:
“该回答表达了不确定性,建议补充说明。”
即使 Critic 说“没有明显问题”,也不能证明退款成功。因为没有新的环境事实,信息集没有增加:
重新生成解释不会改变支付系统的真实状态,也不会增加退款流水号。此时正确动作应是:
- 使用幂等键重新查询退款状态;
- 若查询仍失败,进入
unknown; - 不向用户声称完成;
- 必要时转人工或等待异步回调。
反思可以指出“超时导致状态不确定”,但只有独立查询或可信事件记录才能验证结果。
三、四层检查模型:从硬约束到语义评审
生产 Agent 不应只配置一个“检查器”。更稳定的做法是将检查分成四层。
1. 结构检查
结构检查判断输出是否满足机器可解析的格式。
例如:
- JSON 是否合法;
- 必填字段是否存在;
- 字段类型是否正确;
- 枚举值是否合法;
- 数组长度是否满足要求;
- 工具参数是否符合 Schema。
结构检查通常不需要 LLM。
from dataclasses import dataclass
from typing import Any
@dataclass
class CheckResult:
passed: bool
reason: str
def check_refund_request(data: Any) -> CheckResult:
if not isinstance(data, dict):
return CheckResult(False, "必须是 JSON 对象")
required = {"order_id", "amount", "currency", "idempotency_key"}
missing = required - data.keys()
if missing:
return CheckResult(False, f"缺少字段: {sorted(missing)}")
if not isinstance(data["order_id"], str) or not data["order_id"]:
return CheckResult(False, "order_id 必须是非空字符串")
if not isinstance(data["amount"], int) or data["amount"] <= 0:
return CheckResult(False, "amount 必须是正整数,单位为分")
if data["currency"] != "CNY":
return CheckResult(False, "当前流程只允许 CNY")
return CheckResult(True, "结构检查通过")
这里金额使用整数分,而不是浮点数。若直接使用 float,可能出现:
0.1 + 0.2 == 0.30000000000000004
这不是 Agent 特有问题,但 Agent 会把模型生成的金额、工具参数和业务状态串联起来,因而更容易把普通的数据表示错误扩大成实际扣款或退款错误。
2. 规则检查
规则检查判断是否违反明确业务约束。
例如:
def check_refund_policy(
*,
order_status: str,
paid_amount: int,
requested_amount: int,
already_refunded: int,
user_can_refund: bool,
) -> CheckResult:
if not user_can_refund:
return CheckResult(False, "用户无退款权限")
if order_status not in {"PAID", "PARTIALLY_REFUNDED"}:
return CheckResult(False, f"订单状态 {order_status} 不允许退款")
remaining = paid_amount - already_refunded
if requested_amount > remaining:
return CheckResult(
False,
f"退款金额超过可退金额: requested={requested_amount}, remaining={remaining}"
)
return CheckResult(True, "业务规则检查通过")
规则检查的特点是:
- 条件明确;
- 结果可重复;
- 不依赖模型的措辞;
- 失败时通常不能由模型自行“解释通过”。
例如,订单实际已支付 100 元、已退款 80 元,本次请求退款 30 元。无论模型如何评价客户意图,规则结果都应是失败:
模型可以选择“请求人工处理”或“改为退款 20 元并询问用户”,但不能直接绕过规则。
3. 事实验证
事实验证判断系统状态是否已经达到目标。
假设目标是:
则 Verifier 执行:
def verify_refund(
*,
order_id: str,
expected_amount: int,
payment_record: dict,
) -> CheckResult:
if payment_record.get("order_id") != order_id:
return CheckResult(False, "支付记录中的订单号不匹配")
if payment_record.get("status") != "SUCCEEDED":
return CheckResult(
False,
f"退款状态不是 SUCCEEDED: {payment_record.get('status')}"
)
if payment_record.get("amount") != expected_amount:
return CheckResult(False, "退款金额不匹配")
if payment_record.get("currency") != "CNY":
return CheckResult(False, "退款币种不匹配")
if not payment_record.get("provider_ref"):
return CheckResult(False, "缺少支付方流水号")
return CheckResult(True, "退款结果验证通过")
这里的 payment_record 应来自支付系统或可信事件存储,而不是来自 Agent 自己刚刚生成的文字。
4. 语义 Critic
语义 Critic 适合处理难以完全形式化的问题:
- 方案是否遗漏重要风险;
- 解释是否与证据矛盾;
- 代码修改是否可能破坏原有行为;
- 搜索结果是否真的支持结论;
- 文档是否满足隐含的受众和范围要求。
但语义 Critic 的输出应被当作证据之一,而不是无条件的真值来源。
例如,代码 Agent 修改了数据库迁移文件,可以并行运行:
- SQL 语法检查;
- 单元测试;
- 数据库迁移演练;
- LLM Critic 代码审查;
- 权限和破坏性操作规则检查。
Anthropic 将并行化描述为把独立子任务同时执行并聚合结果,也将 evaluator-optimizer 描述为“生成—评估—反馈—再生成”的循环;这些模式适用于把不同检查维度分开,而不是让同一个模型在一次调用中既生成又自我放行。(anthropic.com)
四、反思与验证应该检查什么
Agent 轨迹不能只记录最终文本。每个步骤都应形成可检查的事件。
{
"run_id": "run_123",
"step": 4,
"state_before": {
"goal": "查询订单并执行退款",
"known_order_id": "O1001",
"refund_status": "UNKNOWN"
},
"action": {
"kind": "tool_call",
"tool": "get_refund_status",
"arguments": {
"order_id": "O1001",
"idempotency_key": "refund-O1001"
}
},
"observation": {
"status": "SUCCEEDED",
"amount": 12000,
"currency": "CNY",
"provider_ref": "RF20260901001"
},
"checks": [
{
"name": "schema",
"result": "pass"
},
{
"name": "refund_postcondition",
"result": "pass"
}
],
"state_after": {
"refund_status": "SUCCEEDED",
"refund_amount": 12000
}
}
每一步至少需要区分以下对象:
1. 目标状态
目标状态是任务完成时必须满足的条件,而不是一句自然语言描述。
例如:
任务:把文件 report.csv 上传到对象存储,并返回可访问地址。
完成条件:
1. 文件存在;
2. 文件内容校验和与本地文件一致;
3. 对象存储返回成功;
4. 对象键为 reports/report.csv;
5. 访问权限符合任务要求;
6. 返回的 URL 能通过 HEAD 请求获得 200。
如果只把目标写成“上传文件”,Agent 可能在本地生成了上传请求就停止,即使远端对象不存在。
2. 前置条件
前置条件决定动作是否允许开始。
上传动作前:
- 文件路径已确认;
- 文件大小小于限制;
- 用户具有目标桶的写权限;
- 对象键通过路径安全检查;
- 未检测到同名对象,或已获得覆盖授权。
3. 后置条件
后置条件决定动作是否成功。
上传动作后:
- 服务端返回成功;
- 服务端对象大小等于本地大小;
- 服务端校验和一致;
- 读取或 HEAD 验证成功。
4. 不变量
不变量是执行过程中必须始终保持的条件。
例如支付流程:
- 不得在未确认用户身份时使用账户余额;
- 不得发送金额为负数或零的支付请求;
- 同一业务幂等键不得对应两个不同金额;
- 失败重试不得产生重复扣款。
不变量通常应放在规则检查层,而不是依赖 Critic 在事后提醒。
五、一个完整的 Agent 验证循环
下面给出一个框架无关的状态机。它适合解释 Responses API 自行控制循环的方式,也适合作为 Agents SDK 等运行时的内部抽象。OpenAI 文档明确区分了两种模式:Responses API 由应用接收函数调用、执行工具并再次调用模型;Agents SDK 则由 Runner 管理工具循环、切换 Agent,并在完成或等待审批时停止。(developers.openai.com)
flowchart TD
A[接收任务] --> B[解析目标与验收条件]
B --> C[初始化状态与预算]
C --> D[计划下一步]
D --> E{动作前规则检查}
E -->|拒绝| X[安全拒绝或请求人工]
E -->|允许| F[执行工具或生成候选结果]
F --> G[记录观察结果]
G --> H[结构检查与事实验证]
H -->|通过| I{完成条件满足?}
H -->|失败| J[Critic 诊断]
H -->|未知| K[补充证据或人工介入]
J --> L[更新状态]
L --> M{是否仍有预算且可修正?}
M -->|是| D
M -->|否| N[失败终止]
I -->|是| O[输出结果]
I -->|否| D
K --> M
关键路径如下:
- 接收任务:不要立即调用工具,先确定任务边界;
- 解析验收条件:把自然语言目标转成可检查的条件;
- 动作前检查:阻止危险、越权或参数错误的动作;
- 执行动作:工具调用可能成功、失败、超时或返回部分结果;
- 记录观察:工具结果必须进入轨迹;
- 执行验证:根据后置条件判断
pass/fail/unknown; - 反思修正:失败时定位原因,而不是只要求模型重新回答;
- 判断停止:成功、失败、未知、预算耗尽或需人工时都可以终止。
六、反思的三个粒度
1. 步骤级反思
步骤级反思只检查最近一次动作。
输入:
动作:调用 get_order(order_id="O1001")
观察:返回订单 O1002
诊断:
工具返回的订单 ID 与请求参数不一致,不能将其用于当前任务。
步骤级反思响应快,适合每一步执行后运行。它的缺点是看不到全局目标,可能无法发现“每一步都看似正确,但整体漏掉了关键子目标”。
2. 阶段级反思
阶段级反思检查一个子任务是否完成。
例如:
阶段:准备退款
要求:
- 已确认订单属于当前用户;
- 已确认订单状态可退款;
- 已确认可退金额;
- 已取得用户对退款金额的授权。
即使单步查询都成功,如果没有用户授权,阶段级 Verifier 仍应返回失败或等待。
3. 任务级反思
任务级反思检查整个轨迹是否达成目标,并判断是否应该继续。
例如,研究 Agent 的任务是:
“比较三种数据库方案,并给出有证据支持的建议。”
任务级检查不只是看最后文字是否流畅,而要检查:
- 三种方案是否都覆盖;
- 每个关键结论是否有来源;
- 来源是否真的支持结论;
- 是否混淆了事实和推测;
- 是否说明了适用边界;
- 是否给出基于用户约束的选择理由。
任务级 Critic 适合发现遗漏,任务级 Verifier 则应依据覆盖率、证据绑定和格式约束进行验收。
七、Critic 的反馈必须能改变状态
低质量的 Critic 通常只输出:
“回答还可以更严谨。”
这种反馈不能驱动 Agent。好的 Critic 必须把问题映射到状态中的可修正项。
{
"issue_id": "evidence-003",
"category": "unsupported_claim",
"severity": "high",
"claim": "方案 A 在高并发场景下成本最低",
"missing": "没有成本模型、请求规模和计费依据",
"repair": {
"type": "collect_evidence",
"queries": [
"方案 A 计费规则",
"方案 B 计费规则",
"目标请求规模下的成本估算"
]
},
"blocks_completion": true
}
其中:
category决定使用什么修复策略;severity决定是否阻塞完成;repair.type决定下一步动作;blocks_completion决定停止门是否允许通过。
可以把修复策略写成有限集合:
missing_information -> 查询或询问用户
bad_parameter -> 重建参数并重新校验
tool_failure -> 重试、切换工具或降级
contradiction -> 回溯并重新确认事实
policy_violation -> 拒绝或人工审批
insufficient_budget -> 提前终止并报告未完成原因
如果 Critic 只提供自然语言建议而没有结构化动作类型,运行时就无法稳定决定下一步,最终会退化成“模型自己读自己的批评”。
八、Verifier 必须有独立证据
验证的强度取决于证据与被验证对象之间的独立性。
可以把证据分成三类:
1. 环境事实
由外部系统产生:
- 数据库查询结果;
- API 返回状态;
- 文件哈希;
- 测试执行结果;
- 编译器输出;
- 浏览器实际页面状态;
- 支付服务回调。
这类证据通常最有价值。
2. 派生事实
由程序根据环境事实计算得到:
本地文件 SHA-256 == 远端对象 SHA-256
派生事实的可信度取决于计算逻辑是否正确、输入是否完整。
3. 模型陈述
由模型生成:
“根据这些结果,退款应该已经完成。”
模型陈述可以帮助解释证据,但不能在高风险场景中替代环境事实。
反例:同源证据
以下流程看似有两层校验:
- Generator 生成答案;
- Critic 读取答案并判断“是否正确”。
但如果 Critic 没有原始文档、工具结果或数据库状态,它看到的仍然只是 Generator 的输出。两者共享同一个错误来源,不能形成真正独立的验证。
更可靠的流程是:
原始数据/工具结果
├── Generator 生成答案
└── Verifier 根据原始数据检查答案
而不是:
Generator 生成答案
└── Critic 只审查答案文本
在实际系统中,Critic 仍然有价值,但它更适合发现解释层和推理层的问题;Verifier 负责把答案重新绑定到原始证据。
九、停止边界:什么时候必须结束
**停止边界(stopping boundary)**是 Agent 不能继续自主行动,必须结束、暂停、失败或转人工的条件集合。
停止不是只有一种状态。至少应区分:
SUCCEEDED 已满足完成条件
FAILED 已确认无法完成或违反约束
UNKNOWN 证据不足,无法确认成功或失败
NEEDS_APPROVAL 需要人工授权
BUDGET_EXHAUSTED预算耗尽
CANCELLED 被用户或系统取消
1. 成功停止
成功停止必须满足后置条件:
并且没有未处理的高严重性问题:
例如,“发送邮件”不能仅以邮件 API 返回 202 Accepted 为成功,除非业务定义就是“请求已被邮件服务接受”。如果任务要求“邮件已送达”,则还需要邮件服务的投递状态或其他可接受证据。
2. 失败停止
失败停止适用于:
- 明确违反业务规则;
- 权限不足;
- 目标不存在;
- 工具明确返回不可恢复错误;
- 修复次数已达到上限;
- 继续执行会增加风险。
失败停止不代表系统没有任何输出。应报告:
- 已完成的部分;
- 未完成的目标;
- 失败原因;
- 已尝试的动作;
- 是否可能安全重试;
- 需要用户提供什么信息。
3. 未知停止
UNKNOWN 是最容易被错误处理的状态。
例如:
动作:创建退款
结果:客户端超时
支付系统:无法查询
此时不能选择:
超时 => 失败 => 立即重试创建
因为第一次请求可能已经成功,立即重试可能造成重复退款。
更安全的流程是:
超时
-> 按幂等键查询原操作
-> 查询成功且状态 SUCCEEDED:成功停止
-> 查询成功且状态 FAILED:可按策略重试
-> 仍无法查询:UNKNOWN,暂停或人工介入
4. 预算停止
Agent 至少应有以下预算:
max_steps 最大动作次数
max_retries 单个动作最大重试次数
max_wall_time 最大运行时间
max_tokens 最大模型调用预算
max_tool_cost 工具调用成本上限
max_same_state 同一状态重复次数
这些预算不是为了让 Agent “尽量跑满”,而是为了给不可预测的循环设置硬边界。Anthropic 明确建议在 Agent 中使用最大迭代次数等停止条件,并指出自主性会带来更高成本和错误累积风险。(anthropic.com)
十、如何检测偏航、循环和无效进展
仅限制总步骤数还不够。Agent 可能在预算内重复做无效动作。
1. 状态哈希
把与任务相关的状态规范化后计算哈希:
import hashlib
import json
def state_hash(state: dict) -> str:
payload = json.dumps(
state,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
如果连续多次出现相同状态:
s_4 == s_5 == s_6
但 Agent 仍然执行相同动作,可以判定为循环风险。
2. 动作重复检测
def action_signature(action: dict) -> str:
return json.dumps(
action,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
def is_repeating(history: list[dict], action: dict, window: int = 3) -> bool:
signature = action_signature(action)
recent = history[-window:]
return sum(action_signature(item) == signature for item in recent) >= 2
但“重复动作”不一定错误。例如轮询异步任务可能需要多次调用。因此必须结合:
- 状态是否发生变化;
- 轮询间隔是否符合要求;
- 服务端是否返回
RUNNING; - 是否已达到轮询上限。
3. 进展函数
定义一个单调进展指标:
如果:
持续多次成立,并且没有获得新证据,那么继续执行通常没有价值。
例如一个包含五个子目标的任务:
初始 P = 0
查询用户身份并验证 P = 1
查询订单并验证 P = 2
检查退款资格并验证 P = 3
退款请求超时,P = 3
重新生成相同退款请求,P = 3
再次重新生成相同退款请求,P = 3
后两次没有增加信息,也没有改变状态。系统应进入未知或人工审批,而不是继续循环。
4. 偏航检测
偏航不一定是工具失败,也可能是目标逐渐改变。
原目标:
比较方案 A、B、C 的部署成本
轨迹却变成:
搜索方案 A 的历史背景
阅读方案 A 的社区讨论
比较方案 A 和 D 的功能
每一步都有局部合理性,但与验收条件无关。可以让 Critic 定期输出:
{
"goal_alignment": 0.31,
"completed_requirements": ["A 的成本来源"],
"missing_requirements": ["B 的成本", "C 的成本", "统一规模假设"],
"off_track_actions": ["历史背景", "社区讨论"],
"recommendation": "停止扩展背景,转向统一成本口径"
}
偏航检测的核心不是判断文本“像不像相关内容”,而是把当前动作与未完成验收条件关联起来。
十一、重试不是反思,反思也不是重试
工具失败后,Agent 常见的错误是无条件重试。
应先区分错误类型:
| 错误 | 示例 | 处理 |
|---|---|---|
| 参数错误 | 缺少 order_id |
修正参数后重试 |
| 鉴权错误 | 401、权限不足 | 不重复重试,刷新凭证或人工处理 |
| 限流 | 429 | 按策略等待并限制次数 |
| 暂时性网络错误 | 连接断开 | 指数退避重试 |
| 超时且操作有副作用 | 创建订单超时 | 先查询幂等状态 |
| 明确业务拒绝 | 余额不足 | 不重试同一请求 |
| 响应格式错误 | 无法解析 JSON | 检查工具契约或切换解析路径 |
一个安全的重试函数应把“是否可以重试”作为显式决策:
RETRYABLE = {
"TIMEOUT_READ_ONLY",
"NETWORK_TRANSIENT",
"RATE_LIMITED",
}
def retry_decision(error_code: str, *, has_side_effect: bool) -> str:
if error_code == "TIMEOUT" and has_side_effect:
return "QUERY_IDEMPOTENT_RESULT_FIRST"
if error_code in RETRYABLE:
return "RETRY_WITH_BACKOFF"
return "STOP"
反思用于解释失败原因,重试策略用于执行恢复动作。将二者混在一个提示词里,容易出现模型把不可重试错误描述成“再试一次可能成功”。
十二、一个可运行的最小验证循环
下面的代码不依赖特定 Agent SDK,使用一个虚构的订单服务模拟“查询—执行—验证—停止”。它展示的是运行时边界,而不是某个厂商 API 的固定接口。
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class RunStatus(str, Enum):
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
UNKNOWN = "UNKNOWN"
NEEDS_APPROVAL = "NEEDS_APPROVAL"
@dataclass
class AgentState:
order_id: str
expected_refund: int
refund_status: str = "UNKNOWN"
provider_ref: str | None = None
steps: int = 0
retries: int = 0
findings: list[str] = field(default_factory=list)
class MockPaymentService:
def __init__(self):
self.refunds: dict[str, dict[str, Any]] = {}
def create_refund(
self,
order_id: str,
amount: int,
idempotency_key: str,
) -> dict[str, Any]:
if idempotency_key in self.refunds:
return self.refunds[idempotency_key]
result = {
"order_id": order_id,
"status": "SUCCEEDED",
"amount": amount,
"currency": "CNY",
"provider_ref": f"RF-{order_id}",
}
self.refunds[idempotency_key] = result
return result
def get_refund_status(self, idempotency_key: str) -> dict[str, Any] | None:
return self.refunds.get(idempotency_key)
def verify_postcondition(
state: AgentState,
record: dict[str, Any] | None,
) -> tuple[bool, str]:
if record is None:
return False, "没有找到退款记录"
if record.get("order_id") != state.order_id:
return False, "订单号不匹配"
if record.get("status") != "SUCCEEDED":
return False, f"退款未成功: {record.get('status')}"
if record.get("amount") != state.expected_refund:
return False, "退款金额不匹配"
if record.get("currency") != "CNY":
return False, "币种不匹配"
if not record.get("provider_ref"):
return False, "缺少支付方流水号"
return True, "后置条件全部满足"
def run_refund_agent(
service: MockPaymentService,
*,
order_id: str,
amount: int,
max_steps: int = 5,
) -> tuple[RunStatus, AgentState]:
state = AgentState(
order_id=order_id,
expected_refund=amount,
)
if amount <= 0:
state.findings.append("退款金额必须为正整数")
return RunStatus.FAILED, state
idempotency_key = f"refund:{order_id}"
while state.steps < max_steps:
state.steps += 1
# 动作前规则检查
if state.expected_refund > 100_000:
state.findings.append("超过单次自动退款上限,需要人工审批")
return RunStatus.NEEDS_APPROVAL, state
# 先查询已有结果,避免超时后重复产生副作用
existing = service.get_refund_status(idempotency_key)
if existing is not None:
ok, reason = verify_postcondition(state, existing)
if ok:
state.refund_status = "SUCCEEDED"
state.provider_ref = existing["provider_ref"]
return RunStatus.SUCCEEDED, state
state.findings.append(reason)
return RunStatus.FAILED, state
# 执行动作
result = service.create_refund(
order_id=state.order_id,
amount=state.expected_refund,
idempotency_key=idempotency_key,
)
# 观察后的独立验证
ok, reason = verify_postcondition(state, result)
if ok:
state.refund_status = "SUCCEEDED"
state.provider_ref = result["provider_ref"]
return RunStatus.SUCCEEDED, state
state.findings.append(reason)
# 没有可修复证据时停止,而不是盲目循环
if state.steps >= 2:
return RunStatus.UNKNOWN, state
state.findings.append("达到最大步骤数")
return RunStatus.UNKNOWN, state
if __name__ == "__main__":
service = MockPaymentService()
status, state = run_refund_agent(
service,
order_id="O1001",
amount=12000,
)
print(status.value)
print(state)
预期输出类似:
SUCCEEDED
AgentState(
order_id='O1001',
expected_refund=12000,
refund_status='SUCCEEDED',
provider_ref='RF-O1001',
steps=1,
retries=0,
findings=[]
)
这个示例的关键不是退款逻辑,而是执行顺序:
- 先检查参数;
- 再检查高风险规则;
- 先查询幂等结果;
- 没有结果时才执行有副作用的操作;
- 执行后依据返回记录验证后置条件;
- 通过才返回成功;
- 无法确认时返回
UNKNOWN,而不是伪造失败或成功。
真实系统还需要处理网络超时、服务端异步状态、幂等键持久化和人工审批恢复。OpenAI 的 Agents SDK 文档将 guardrails、human review、结果状态和可恢复运行状态列为 Agent 运行时能力,说明验证不仅是提示词问题,也属于运行生命周期和状态管理的一部分。(developers.openai.com)
十三、并发验证:什么时候可以并行,什么时候不能
验证任务可以并行,但前提是检查之间没有冲突,并且它们读取的是同一个稳定快照。
可以并行的情况
例如代码变更评审:
同一版本代码
├── 编译检查
├── 单元测试
├── 安全扫描
├── 依赖许可证检查
└── LLM Critic
这些任务主要是读操作,结果可以最后聚合:
{
"compile": "pass",
"unit_test": "pass",
"security_scan": "fail",
"license_check": "pass",
"critic": "warning"
}
聚合器应按规则决定最终状态:
安全扫描失败 => FAILED
单元测试失败 => FAILED
Critic 警告 => 可修复但不一定阻塞
不应直接并行的情况
如果多个动作会修改同一资源,就必须考虑竞态:
Agent A:退款 100 元
Agent B:退款 100 元
即使两个 Agent 都通过了各自的动作前检查,也可能同时看到“可退余额为 100 元”,最终产生重复操作。
解决方式包括:
- 数据库事务;
- 分布式锁;
- 幂等键;
- 版本号或乐观锁;
- 服务端原子状态迁移;
- 让验证与提交在同一可信边界内完成。
形式上,若资源版本为 ,动作提交时要求:
提交成功后版本递增:
如果版本不一致,说明观察已过期,Agent 必须重新读取,而不是继续使用旧判断。
并发 Critic 的边界
多个 Critic 投票不等于多个独立事实源。如果所有 Critic 都只读取同一份错误答案,投票只能增加“共识感”,不能增加真实性。
并发适合增加视角,不适合替代环境验证。
十四、常见误解及其失败表现
误解一:让模型“再检查一遍”就是 Verifier
失败表现:
第一次:退款成功。
第二次:我检查后确认退款成功。
第二次没有访问支付状态,也没有检查流水号,属于自我复述。
修正方式:Verifier 必须绑定可追溯证据和明确后置条件。
误解二:Critic 说有问题,就一定要重新规划
失败表现:
- Critic 提醒“可以进一步优化”;
- Agent 不断重写已经通过的结果;
- 步骤和成本持续增加;
- 结果没有可测量改善。
修正方式:为问题设置严重级别和阻塞属性:
info 不阻塞
warning 可选修复
error 必须修复
critical 必须停止或人工处理
误解三:验证通过就可以继续执行所有动作
验证通常只对某个对象、某个阶段或某个版本成立。
例如:
已验证订单 O1001 可退款
并不代表:
可以直接退款任意金额
验证范围必须包含:
- 资源标识;
- 版本或时间;
- 金额和币种;
- 用户和权限;
- 证据来源;
- 有效期。
误解四:最大步数越大,任务成功率越高
更大的步数预算有时可以提高完成率,但也会增加:
- 错误累积;
- 重复调用;
- 成本;
- 延迟;
- 对外部系统的副作用;
- 偏航后无法回收的风险。
Agent 的动作数量不是质量指标。更重要的是每一步是否产生新证据、是否推进已验证目标。
误解五:所有失败都应该自动修复
某些失败是不可修复的:
用户没有权限
付款超过风控上限
资源已永久删除
策略明确禁止自动执行
此时继续反思只会制造更多解释。正确的结果是明确失败或请求人工,而不是让模型寻找绕过规则的路径。
十五、生产诊断:从终态反查轨迹
当用户反馈“Agent 说完成了,但实际上没有完成”,应按以下顺序诊断:
1. 检查成功条件是否存在
如果系统没有明确写出“什么叫完成”,就无法判断是 Agent 错了,还是产品定义含糊。
2. 检查成功是否由模型文本决定
危险代码通常类似:
if "成功" in model_output:
return SUCCESS
成功状态必须来自结构化验证,而不是关键词。
3. 检查工具结果是否被保留
轨迹中应能看到:
tool_call
tool_result
verification
state_transition
final_output
如果只有最终回答,没有工具原始结果,就无法判断 Agent 是否错误理解了观察。
4. 检查验证器使用的输入
常见错误包括:
- Verifier 读取了模型总结,而不是原始工具结果;
- 查询的是缓存旧数据;
- 查询资源 ID 与动作资源 ID 不一致;
- 忽略了币种、版本、租户或权限;
- 把
PENDING当作SUCCEEDED; - 把空结果当作“没有问题”。
5. 检查 unknown 是否被错误转换
建议在日志中保留三值结果:
PASS
FAIL
UNKNOWN
不要在适配层直接把 UNKNOWN 映射成 FAIL 或 PASS。不同业务对未知状态的处理不同:
- 只读搜索:可以返回部分结果;
- 发送邮件:可能允许报告“已提交”;
- 扣款退款:通常需要暂停或人工;
- 文件删除:通常不能在状态未知时重复执行。
6. 检查是否发生了状态竞态
若验证在另一个服务或线程中执行,需要记录:
observed_version
verified_version
committed_version
否则可能出现:
读取可退余额 = 100
另一个请求已退款 80
Agent 仍按旧结果退款 100
十六、如何选择反思和验证的放置位置
并非每一步都需要完整的 LLM Critic。验证应尽量靠近被验证的动作。
工具调用前
适合放:
- 参数 Schema;
- 权限检查;
- 业务规则;
- 风险等级;
- 人工审批门。
工具调用后
适合放:
- 返回结构检查;
- 状态码处理;
- 后置条件;
- 幂等状态查询;
- 数据一致性检查。
阶段结束时
适合放:
- 子目标覆盖检查;
- 缺失信息诊断;
- 证据完整性检查;
- 是否进入下一阶段。
最终输出前
适合放:
- 输出格式;
- 敏感信息检查;
- 证据绑定;
- 用户问题是否全部回答;
- 是否仍包含未经确认的推断。
Anthropic 建议优先使用能够清晰拆分的工作流,并在中间步骤加入程序化 gate;只有当任务步骤难以预先确定、需要模型动态决定行动时,才更适合使用自主 Agent。(anthropic.com) 这意味着验证位置也应遵循同一原则:能写成确定规则的检查,不要交给自由生成的 Critic。
十七、一个实用的完成判定公式
可以把一次运行的完成条件写成:
其中:
- :目标条件全部满足;
- :没有违反硬规则;
- :关键结论有足够证据;
- :没有超出预算、权限和风险边界。
展开为:
表示 个目标条件必须全部成立。
表示 个禁止规则中不能有任何一条成立。
表示每个关键声明都必须绑定有效证据。
如果某个条件无法判断:
则整体不应直接判定为成功。系统需要根据业务策略选择:
补充查询
请求用户确认
暂停等待异步结果
转人工
报告未完成
这套形式化表达的价值在于,它迫使工程师回答:
- 哪些条件是目标;
- 哪些条件是禁止规则;
- 哪些条件需要外部证据;
- 哪些预算会阻止继续;
- 哪些未知状态可以接受。
如果这些问题没有答案,Agent 的“完成”就只是模型生成的一种语气。
十八、反思与验证的工程边界
反思适合解决:
- 任务是否偏航;
- 计划哪里不完整;
- 哪个假设需要重新检查;
- 下一步应收集什么信息;
- 失败是否可能恢复。
验证适合解决:
- 结果是否满足明确条件;
- 工具结果是否与请求一致;
- 系统状态是否真正改变;
- 代码、数据和输出是否通过测试;
- 是否允许进入下一阶段。
规则检查适合解决:
- 是否越权;
- 是否超过金额、范围或次数限制;
- 是否违反安全策略;
- 是否允许调用某个工具;
- 是否需要人工审批。
停止边界适合解决:
- 已成功;
- 已失败;
- 状态未知;
- 预算耗尽;
- 发生循环;
- 风险需要人工决策。
把这四类机制混成一个提示词,通常会得到一个“会解释、但不一定会停;会批评、但不一定会验证;会重试、但不一定知道副作用”的 Agent。
一个可靠的 Agent 应当让状态转换由证据驱动:
动作产生观察
观察更新状态
状态触发规则与验证
验证决定是否完成
Critic 只负责诊断可修复问题
预算与风险决定最终边界
最终,Agent 的成熟度不体现在它能否生成更长的反思文本,而体现在它是否能够明确地区分:
我认为完成了
系统证明完成了
系统无法确认是否完成
系统禁止继续完成
这四句话对应四种完全不同的运行结果。只有后面三种能够被状态、证据、规则和停止边界可靠地管理,Agent 才真正具备工程上的可控性。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 任务分解:目标、子任务、前置条件、产物和验收
- 下一篇:Agent 不确定性与拒答:证据不足、置信边界、升级和回退
- 延伸:Agent ReAct 规划:思考与行动循环、观察、偏航和终止
- 延伸:Agent 轨迹与工具评测:步骤正确性、参数、效率、恢复和终态
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论