Hotdry.

Article

AI Agent 写工具调用的幂等执行层:MCP ToolAnnotations 与去重表参数

结合 MCP 官方 schema 中的 readOnlyHint/idempotentHint 与 RFC 9110 的重试语义,说明如何在 Agent 运行时用工具调用指纹与去重表把 at-least-once 交付收敛为可审计的副作用控制。

2026-06-17ai-systems

LLM Agent 在超时、限流、进程崩溃或 Kubernetes Job 重提时,往往会对同一次工具调用再次发起 tools/call 或等价的 function call。模型侧通常只关心「是否拿到结果」;下游 API、数据库或工单系统关心的是「副作用是否被重复执行」。MCP 规范在工具元数据里提供了 readOnlyHintidempotentHint 等注解,但它们被明确定义为提示(hints),既不能替代服务端幂等实现,也不能作为自动重试的唯一依据。要在生产环境安全重试,需要在 Agent 运行时与工具适配层之间增加一层幂等执行层(idempotent execution layer)

问题背景:at-least-once 是默认现实

典型 Agent 循环在以下场景会重复派发工具:

  • 传输层重试:HTTP/Streamable MCP 连接在响应体未完整到达时断开,客户端按 JSON-RPC 语义重发同一 id 的请求(是否安全取决于工具语义,而非协议本身)。
  • 框架级重试:编排器对「工具超时」做指数退避,未区分只读查询与资金类写入。
  • 检查点恢复:工作流从 checkpoint 重放时,若未记录「该 tool_call_id 已成功落库」,会再次执行副作用步骤。
  • 多 Worker 竞态:批处理 Agent 分片时,若租约过期与任务重领叠加,两个 Pod 可能对同一业务键各执行一次写工具。

HTTP 语义对此有清晰表述:RFC 9110 将 PUT、DELETE 及 safe 方法定义为幂等,并规定客户端不应自动重试非幂等方法,除非能证明请求语义实际上是幂等的,或能检测原始请求从未被应用。Agent 工具调用在协议上等价于「模型发起的 POST」—— 默认可重试性为

MCP 2025-06-18 schema 中的 ToolAnnotations 则试图在发现阶段向客户端传递风险信号(字段定义见官方 schema.json):

字段 默认值 规范含义(摘要)
readOnlyHint false true 时表示工具不修改环境
destructiveHint true readOnlyHint == false 时才有意义;false 表示仅做追加型更新
idempotentHint false readOnlyHint == false 时才有意义;相同参数重复调用无额外环境效应
openWorldHint true 是否与开放外部实体交互(如全网搜索 vs 封闭记忆库)

这些注解不进入 JSON-RPC 校验路径;错误标注不会导致调用被拒绝。因此执行层必须独立实现幂等与审批策略。

可落地实现:三层结构

建议把「模型看到的工具」与「真实副作用」拆开:网关 / Tool Host 统一拦截,再委托给 MCP Server 或内部 HTTP 适配器。

1. 工具调用指纹(dedup key)

在副作用发生之前,用稳定键查询去重表。推荐指纹组成:

dedup_key = SHA-256(
  tenant_id + "|" +
  session_id + "|" +
  tool_call_id + "|" +
  tool_name + "|" +
  canonical_json(arguments)
)

参数说明:

  • tool_call_id:优先使用 LLM 返回的 call id(OpenAI tool_calls[].id、Anthropic tool_use.id)。检查点重放时必须复用同一 id,否则指纹无法对齐历史。
  • canonical_json:对 arguments 做键排序、UTF-8 规范化、浮点固定精度(若业务接受),避免 { "a":1,"b":2 }{ "b":2,"a":1 } 产生不同哈希。
  • 可选业务幂等键:若工具 schema 暴露 idempotency_keyclient_mutation_id,应纳入哈希或作为唯一索引列,便于与 Stripe/Payment 类 API 对齐。

去重表(PostgreSQL 示例)最小字段:

类型 说明
dedup_key byteachar(64) 主键
status enum in_flight / completed / failed
response_payload jsonb 成功时缓存工具结果(供重试直接返回)
created_at timestamptz 插入时间
expires_at timestamptz TTL 到期后可清理

推荐 SQL 流程(伪代码级):

-- 1) 抢占:插入 in_flight,冲突则进入 2)
INSERT INTO agent_tool_dedup (dedup_key, status, expires_at)
VALUES ($1, 'in_flight', now() + interval '24 hours')
ON CONFLICT (dedup_key) DO NOTHING;

-- 2) 若未插入成功,查询现有行
SELECT status, response_payload FROM agent_tool_dedup WHERE dedup_key = $1;

-- 3) 执行真实工具后
UPDATE agent_tool_dedup
SET status = 'completed', response_payload = $2
WHERE dedup_key = $1 AND status = 'in_flight';

in_flight 租约:长耗时工具(>30s)应为 in_flight 行设置 lease_expires_at;租约过期后允许新 Worker 接管,但业务层仍需工具自身幂等(例如下游支持 Idempotency-Key 头)。

2. 与 MCP 注解联动的重试矩阵

在运行时把注解映射为策略,而非直接信任默认值(destructiveHint 默认 trueidempotentHint 默认 false):

readOnlyHint idempotentHint 自动重试 去重表 人工审批
true 允许(仍建议缓存读结果降本) 可选
false true 允许,依赖去重表 + 下游幂等 必须 destructiveHint
false false 禁止自动重试 必须(防双写) destructiveHint=true 时建议强制

对通过 HTTP 暴露的工具,适配器应在写请求上附加 Idempotency-Key: <dedup_key 前 32 字节 hex>(许多支付与云 API 支持该头);只读 GET 可依赖 safe 语义重试,但仍应限制次数与总超时。

3. 可观测性与审计

为每次拦截记录结构化事件(不必采集 prompt 正文):

  • agent.session_idtool.nametool_call_id
  • mcp.read_only_hintmcp.idempotent_hint(来自 tools/list 缓存)
  • dedup.hitmiss / replay_completed / blocked_in_flight
  • 下游 HTTP 状态码或 MCP isError

OpenTelemetry 语义约定仓库已将 GenAI/MCP 相关属性迁出主 semantic-conventions 仓库独立维护;在约定稳定前,建议将上述字段挂载为 agent.* 自定义属性,并预留与 mcp.method.name 等注册属性的对齐空间。

4. 推荐默认参数

参数 建议值 说明
去重表 TTL 24–72 h 覆盖「隔夜 Job 重提 + 人工重跑」窗口
in_flight 租约 工具 P99 耗时 × 1.5,下限 60s 防止永久占锁
自动重试次数(写工具) 0(仅人工触发) 除非 idempotentHint=true 且去重表已启用
自动重试次数(只读) ≤ 3,指数退避 1s–8s 配合总 deadline
去重表清理 每日 batch DELETE WHERE expires_at < now() 避免表无限增长

风险与边界

注解不可当作合约。 MCP schema 写明 idempotentHint 仅在 readOnlyHint == false 时有意义,且默认 false。第三方 MCP Server 可能全部留空或误标;执行层应以服务端实现的幂等为准,注解只影响 UI 与重试策略。

指纹碰撞与语义幂等不是一回事。 「相同 JSON 参数」可能对下游仍非幂等(例如 create_issue 每次创建新工单)。需要业务级 idempotency_key 或改为 update_* / upsert 工具契约。

缓存响应与部分失败。 若工具在外部系统已成功、但在返回 Agent 前进程崩溃,去重表可能停在 in_flight。租约到期后的第二次执行可能触发双写 —— 下游 API 必须支持幂等键,或采用 outbox + 状态查询(先 GET 再决定是否 POST)。

读工具的数据新鲜度。readOnlyHint=true 的结果做长期缓存会导致 Agent 基于陈旧数据规划下一步;读缓存 TTL 宜短(秒级到分钟级),并与 openWorldHint 区分对待。

隐私与合规。 response_payload 可能含 PII;去重表应加密静态存储、按租户隔离,并遵守数据保留策略。调试时避免把完整参数写入日志,可只记录哈希前缀。

与模型多轮行为无关。 即使执行层完美幂等,模型仍可能在tool_call_id 下再次调用同一工具(用户未确认却重复下单)。产品层需要 Human-in-the-Loop 或业务规则引擎,本文不展开。

参考来源

ai-systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com