幂等性设计
核对日期:2026-05-09。
1. 定义与边界
幂等性(Idempotency)指同一个操作在相同业务语义下执行一次或多次,最终可观察结果一致。Agent 系统中的幂等性重点覆盖工具副作用、队列重试、API 重试、回放调试和人工审批后的继续执行。
幂等不是“不重试”,也不是“所有操作都天然安全”。它需要业务键、去重存储、状态机和副作用边界共同保证。
2. 为什么重要
Agent 经常会因为模型超时、工具超时、队列重复投递、客户端重试而重复执行某一步。如果这一步是查询,影响较小;如果是创建订单、退款、发邮件、删除数据,就可能造成严重事故。
3. 核心机制
幂等键应由业务语义决定:
idempotency_key = tenant_id + operation + business_object_id + semantic_step_id
示例:
{
"operation": "ticket.create",
"idempotency_key": "tenant_a:ticket.create:run_01:step_4",
"request_hash": "sha256:...",
"status": "succeeded",
"result_ref": "ticket_123",
"created_at": "2026-05-09T10:00:00Z"
}
处理流程:
4. 架构模式
| 模式 | 适用场景 | 注意点 |
|---|---|---|
| 客户端幂等键 | 对外 API,客户端会重试 | 服务端仍要校验 request hash。 |
| 服务端业务键 | Agent 内部步骤 | 需要稳定 step id。 |
| 数据库唯一约束 | 创建类操作 | 错误处理要返回既有结果。 |
| Outbox/Inbox | 跨服务消息 | 适合事件驱动和补偿。 |
5. 工程实现
工具网关幂等包装:
def call_tool_idempotently(tool_name, payload, key):
existed = idem_store.get(key)
if existed and existed.request_hash == hash_payload(payload):
return existed.result
if existed:
raise IdempotencyConflict("same key with different payload")
idem_store.reserve(key, request_hash=hash_payload(payload))
try:
result = tool_client.call(tool_name, payload)
idem_store.mark_success(key, result)
return result
except Exception as exc:
idem_store.mark_failed(key, retryable=is_retryable(exc))
raise
6. 生产实践
- 幂等记录的 TTL 要覆盖客户端和队列最大重试窗口。
- 对“处理中”的幂等键设置租约,避免 worker 崩溃后永久卡住。
- 对同一幂等键但不同请求体返回冲突,而不是悄悄复用结果。
- 幂等键和 trace id 关联,便于排查重复执行。
- 对外部不支持幂等的 SaaS,在本地工具网关做预占和补偿。
7. 常见反模式
- 用随机 UUID 作为每次重试的幂等键,等于没有幂等。
- 只在 API 层做幂等,worker 和工具层仍然会重复副作用。
- 幂等键不包含租户,造成跨租户冲突或信息泄漏。
- 不保存结果,只保存“执行过”,导致重试方无法拿到一致响应。
- 把所有失败都缓存,导致临时错误无法恢复。
8. 评测方法
- 重复请求测试:同一请求发送 2 到 10 次,最终只产生一次副作用。
- 并发测试:多个 worker 同时处理同一 key,只有一个成功执行。
- 冲突测试:同一 key 不同 payload 应返回冲突。
- 崩溃测试:副作用前后分别 kill 进程,验证恢复策略。
9. 安全与治理
- 幂等记录可能包含业务对象引用,要按租户隔离。
- 不把完整敏感 payload 存入幂等表,使用 hash 和受控引用。
- 高风险工具调用强制要求幂等键;缺失则拒绝执行。
- 回放模式默认禁用真实副作用,只验证幂等路径和 mock 结果。
10. 权威资料
- Stripe Idempotent requests: https://docs.stripe.com/api/idempotent_requests
- RFC 9110 HTTP Semantics: https://datatracker.ietf.org/doc/html/rfc9110
- Amazon SQS FIFO exactly-once processing: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-exactly-once-processing.html
- Celery task idempotency guidance: https://docs.celeryq.dev/en/stable/userguide/tasks.html
- Temporal documentation: https://docs.temporal.io/
11. 二次精修:幂等键分层
Agent 的幂等性要按“请求、任务、工具、副作用”分层设计。一个全局 key 不够,因为同一 Agent run 内可能有多个可重试步骤。
| 层级 | 幂等键来源 | 存储位置 | 去重结果 |
|---|---|---|---|
| API request | 客户端 key 或业务请求 hash | API 幂等表 | 返回首次响应 |
| Agent task | run_id + step_index | 任务表 | 不重复执行同一步 |
| Tool call | tool_name + normalized_args + business_id | tool call ledger | 返回已确认结果 |
| External side effect | 业务单号、支付单号、工单号 | 下游系统 | 下游保证唯一 |
| Notification | event_id + channel + recipient | 通知表 | 不重复通知用户 |
CREATE TABLE idempotency_ledger (
scope TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
status TEXT NOT NULL,
request_hash TEXT NOT NULL,
response_ref TEXT,
expires_at TIMESTAMP NOT NULL,
PRIMARY KEY (scope, idempotency_key)
);
12. 幂等执行流程
13. 与重试、回放、审计的关系
- 重试必须依赖幂等键,否则“提高成功率”会变成重复下单、重复退款、重复发信。
- 回放环境应使用只读工具替身,或强制所有副作用工具命中 ledger 的历史结果。
- 审计日志要记录 request hash,发现同一 key 携带不同参数时应拒绝而不是覆盖。
- 评测要构造重复投递、worker 崩溃后重试、下游超时但实际成功等样本。
- 安全治理要限制幂等键中出现 PII,建议使用业务 ID 加 HMAC,而不是明文用户信息。
14. 验收指标
| 指标 | 目标 |
|---|---|
| Duplicate Side Effect Rate | 0 |
| Idempotency Conflict Rate | 可观测,异常升高告警 |
| Retry Success Without Duplicate | 高于 99% |
| Ledger Lookup Latency P95 | 不成为主链路瓶颈 |
| Expired Key Replay Blocked | 高风险业务必须拦截 |
15. 补充权威资料
- Stripe Idempotent requests: https://docs.stripe.com/api/idempotent_requests (核对日期:2026-05-09)
- AWS Builders Library: Making retries safe with idempotent APIs: https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ (核对日期:2026-05-09)