今天 arXiv cs.AI 新列表里有一篇标题非常工程化的论文:“Verified Tool Calls Improve LLM Agent Reliability Under Non-Atomic Failures”。它指出一个生产 Agent 系统经常忽略的问题:很多框架默认工具调用是原子的,要么成功,要么失败;但真实世界的 API 经常处于中间态。请求可能已经被服务端接收,客户端却超时;写入可能部分成功;状态可能延迟可见;同一个动作重复执行可能带来真实损失。
这不是边角问题,而是 Agent 可靠性的核心问题。普通程序员写支付、发信、库存、部署系统时,会自然引入幂等键、状态机和确认查询。但 LLM Agent 框架常常把工具包装成一个函数:模型传参数,函数返回 JSON,模型继续思考。这个抽象对演示很友好,对生产副作用很危险。
原子幻觉
所谓原子幻觉,是系统把复杂外部动作看成单一结果:
sendInvoice(customerId, amount) -> success
sendInvoice(customerId, amount) -> failure
真实情况可能是:
request timed out, invoice was sent
request timed out, invoice was not sent
request accepted, email delayed
request accepted, duplicate blocked
request partially completed, audit record missing
人类工程师看到超时会先查状态,Agent 却可能立刻重试。尤其在多步任务里,模型会把“没有拿到成功返回”解释成“动作没有发生”,然后计划下一次调用。结果是重复邮件、重复订单、重复退款、重复创建云资源。
论文给出的方向
论文摘要提到的三件事很重要:postcondition verification、verify-before-retry logic 和 idempotency keys。翻译成工程语言,就是:
| 机制 | 作用 |
|---|---|
| 幂等键 | 同一个业务动作多次请求只产生一次副作用 |
| 执行后验证 | 工具返回后查询真实状态是否满足后置条件 |
| 重试前确认 | 超时或失败后先查状态,再决定是否重试 |
这个方案的价值在于它不要求换模型。它把可靠性放在工具包装层,让 Agent 面对的是更真实、更可审计的执行语义。
一个反例:重复退款
假设客服 Agent 接到用户请求:“这笔订单重复扣款,请退一笔。”工具是:
refundPayment({ paymentId, amount })
如果请求超时,Agent 看到 timeout,可能再次调用。没有幂等键时,两次退款都可能成功。即使第二次失败,Agent 也未必知道第一次是否已经生效。正确包装应该是:
refundPayment({
paymentId,
amount,
idempotencyKey: "refund_order_8821_duplicate_charge",
})
但幂等键还不够。工具返回超时后,系统应该执行:
1. queryRefundStatus(idempotencyKey)
2. 如果已成功,返回 verified_success
3. 如果明确不存在,允许重试
4. 如果状态未知,升级人工或延迟重查
最危险的不是失败,而是未知状态。Agent 系统必须把 unknown 当成一等状态,而不是把它粗暴映射成 failure。
工具包装层设计
一个可靠工具不应该只暴露 run,还应该暴露后置条件和验证函数:
type ToolResult =
| { status: "verified_success"; data: Record<string, unknown> }
| { status: "verified_failure"; error: string }
| { status: "unknown"; retryAfterMs: number; reason: string };
type ReliableTool = {
name: string;
sideEffect: "none" | "low" | "high" | "irreversible";
run: (args: Record<string, unknown>, key: string) => Promise<unknown>;
verify: (args: Record<string, unknown>, key: string) => Promise<ToolResult>;
};
Agent 调用的不是原始 API,而是可靠包装器:
async function callWithVerification(
tool: ReliableTool,
args: Record<string, unknown>,
idempotencyKey: string,
): Promise<ToolResult> {
try {
await tool.run(args, idempotencyKey);
} catch (error) {
const checked = await tool.verify(args, idempotencyKey);
if (checked.status !== "unknown") return checked;
return { status: "unknown", retryAfterMs: 5000, reason: "run failed and state is not visible yet" };
}
return tool.verify(args, idempotencyKey);
}
注意这里的关键不是捕获异常,而是捕获异常后不立刻重试。先验证,再决定。很多生产事故都发生在“失败后自动重试”这一步。
副作用分级
并不是所有工具都需要同样严格的控制。可以按副作用分级:
| 等级 | 示例 | 策略 |
|---|---|---|
| none | 搜索、读取、计算 | 可自动重试 |
| low | 创建草稿、写临时文件 | 幂等键加日志 |
| high | 发邮件、改价格、创建工单 | 验证后重试,必要时人工确认 |
| irreversible | 付款、退款、删除、部署生产 | 默认人工审批或双确认 |
这个分级应该写进工具注册表,而不是藏在提示词里。模型可以理解提示词,但生产控制必须由代码执行。
状态机比布尔值可靠
工具返回值不要用单一 ok: true 或 ok: false。更可靠的是状态机:
planned
dispatched
pending_visibility
verified_success
verified_failure
unknown
escalated
有了状态机,Agent 就能知道当前不是“失败”,而是“等待可见性”。这对云资源、邮件队列、支付网关、CRM 同步特别重要,因为这些系统天然异步。
对 Agent 框架的要求
未来成熟 Agent 框架不应该只比拼模型和工具数量,而应该提供工具语义层:
- 每个工具声明副作用等级。
- 每次调用自动生成业务幂等键。
- 工具包装器内置后置条件验证。
- 重试策略由代码决定,不由模型自由决定。
- 所有未知状态进入审计队列。
这会让 Agent 少一点“自主”,但多很多可靠性。真正的生产自主不是模型想做什么就做什么,而是在清晰约束下完成任务。
长期影响
这篇论文的深层意义在于把 Agent 可靠性从“提示工程问题”拉回“分布式系统问题”。API 超时、状态延迟、幂等、重试、补偿事务,这些都不是新问题。新的只是 LLM Agent 把这些老问题放大了,因为它会持续规划并自动行动。
如果你的 Agent 只读知识库,原子幻觉影响有限;如果它能写数据库、调用支付、改权限、操作生产环境,就必须从第一天实现 verified tool calls。否则系统迟早会在一次超时后做出第二次不可逆动作。
参考来源:arXiv cs.AI 新论文 Verified Tool Calls Improve LLM Agent Reliability Under Non-Atomic Failures,Hacker News 与 Reddit 关于 Agent 工具数量、执行权限和自动重试风险的近期讨论。