更新于

LLM API 写操作幂等性设计


LLM 应用上线后包含大量有副作用的写操作:创建生成任务、扣减额度、保存对话、发送邮件,或让 Agent 调用外部工具。客户端超时重试、消息队列重复投递和连续点击,都可能让同一意图执行两次。幂等性的目标不是阻止重试,而是让等价请求只产生一次业务结果。

一、先界定什么才是“同一次写操作”

设计幂等之前,先把接口的业务结果说清楚。一次聊天请求可能同时写入会话消息、用量账单和审计日志;一次 Agent 运行还可能创建工单并发送通知。只给主表加唯一键,却允许下游副作用重复发生,仍然不算幂等。

建议为每个写接口列出三类结果:必须只发生一次的核心写入、允许重复的观测事件,以及可重建的派生数据。扣费与外部通知通常属于第一类,指标上报可属于第二类。这个分类决定事务边界和补偿策略。

还要区分“相同参数”和“相同意图”。用户在一分钟内两次输入同一提示词,可能确实想生成两个答案,不能直接对请求体做哈希去重。幂等身份应由调用方显式声明,并限定在租户、用户或业务对象的作用域内。

二、幂等键要由业务意图产生

常见做法是在写请求中携带 Idempotency-Key。键应在发起业务动作时生成,并在网络重试、网关转发和后台补偿时保持不变。若每次重试都重新生成 UUID,服务端看到的仍是不同请求。

一个可落地的键空间可以写成:

scope = tenant_id + endpoint + idempotency_key
fingerprint = SHA-256(normalized_business_payload)

scope 防止不同租户或操作互相碰撞;fingerprint 用于检测“同一个键却换了参数”。规范化负载应排除时间戳、追踪 ID 等非业务字段,并稳定处理 JSON 字段顺序。同键异参应返回冲突错误,不能静默复用旧结果。

方案适用场景主要风险
调用方随机 UUID用户点击、SDK 自动重试调用方必须持久化并复用
业务单号订单、批处理任务单号作用域要明确
请求体哈希严格相同的机器任务会误伤用户主动重复操作
时间窗口去重日志、低价值事件边界时间存在漏判

结论很直接:优先使用调用方生成、服务端校验负载指纹的显式幂等键,不要把模糊的内容相似度当成交易级防重机制。

三、用状态机保存结果,而不只是加一把锁

幂等记录至少需要 PROCESSINGSUCCEEDEDFAILED_RETRYABLE 三种状态,也可增加 FAILED_FINAL。首次请求通过数据库唯一约束原子地创建记录;后续请求读取状态并采取不同动作。

  • SUCCEEDED:返回第一次保存的状态码和响应摘要,不再次调用模型或工具。
  • PROCESSING:返回“处理中”及查询地址,或在较短时间内等待原任务完成。
  • FAILED_RETRYABLE:允许持有同一幂等键的协调器重新取得执行权。
  • FAILED_FINAL:返回已保存的确定性失败,避免无意义重放。

不要只依赖 Redis 分布式锁。锁在进程崩溃或租约到期后会消失,无法回答“副作用到底完成没有”。Redis 可承担快速查询,但数据库幂等记录与业务唯一约束才是最终裁决者。

四、并发请求必须由数据库原子裁决

两个相同请求可能同时抵达。先查询、再插入存在竞态:两边都查不到,然后都开始执行。应给 (tenant_id, endpoint, idempotency_key) 建唯一索引,并用 INSERT ... ON CONFLICT 争夺所有权。

INSERT INTO idempotency_records
  (tenant_id, endpoint, idem_key, fingerprint, status, lease_until)
VALUES
  (:tenant, :endpoint, :key, :fingerprint, 'PROCESSING', :lease_until)
ON CONFLICT DO NOTHING;

只有插入成功者可以执行核心逻辑。失败者读取现有记录,先核对指纹,再根据状态返回。对于执行时间较长的 LLM 任务,可使用带版本号的租约续期;接管过期任务时通过条件更新状态与版本,防止旧工作进程在恢复后覆盖新结果。

业务写入和幂等状态尽量放在同一数据库事务中。如果需要调用无法参与事务的外部系统,可使用 transactional outbox:本地事务同时落业务结果与待发送事件,由投递器按事件 ID 重试。这样即使进程在提交后崩溃,也不会丢失后续动作。

五、模型调用与工具调用要分两层防重

模型生成本身通常是昂贵但不一定有业务副作用的操作;工具调用则可能真正修改外部世界。两者应采用不同粒度的幂等键。运行级键控制整次请求,工具级键可以由 run_id + step_id + tool_name 派生,并作为业务引用传给下游。

例如 Agent 在超时后恢复时,可以重新读取已保存的工具结果,而不是再次发送邮件。若下游接口支持幂等键,就原样传递稳定键;若不支持,应在本地建立工具执行账本,保存请求指纹、外部资源 ID 和最终结果。对“已提交但响应丢失”的不确定状态,优先通过外部查询接口对账,不能直接假定失败后重做。

流式响应也要单独考虑。客户端断线不代表服务端任务失败。建议把生成任务与 SSE 连接解耦:任务使用稳定 generation_id 继续运行,事件按递增序号持久化或短期缓存,重连后从最后确认序号继续消费。更完整的恢复模式可参考 Anthropic 流式消息中断恢复实战

六、失败缓存、过期时间与可观测性

并非所有失败都该缓存。参数错误、权限不足等确定性失败可以记录并原样返回;网络抖动、上游限流等暂时失败应进入可接管状态。对于未知结果,例如支付请求已发出但连接在响应前断开,应标记为 UNKNOWN 并启动对账,避免自动重放造成双写。

幂等记录不能无限增长。过期时间至少覆盖客户端最大重试窗口、队列最大延迟和人工补偿周期。删除记录前还要确认业务表仍有可防重的唯一引用;金融或审计敏感操作更适合长期保留轻量索引,而不是只依赖短 TTL 缓存。

上线后至少监控这些指标:幂等命中率、同键异参冲突数、PROCESSING 超时数、租约接管数、重复副作用拦截数,以及首次与复用响应的延迟差。日志应记录哈希化或截断后的幂等键、作用域、指纹和状态迁移,不要把完整提示词、密钥或用户隐私写入日志。

七、上线前检查清单

发布前可逐项验证:调用方是否在所有重试中复用同一键;唯一索引是否覆盖正确作用域;同键异参是否返回明确冲突;并发压测是否只产生一个业务结果;进程在模型完成前后崩溃能否恢复;外部副作用是否有查询或对账路径;记录过期后是否仍受业务唯一约束保护。

还要做故障注入:分别在取得执行权、外部调用返回、业务事务提交和响应发出前终止进程,再用同一请求重试。顺序调用测试覆盖不了最危险的窗口。重试节奏可结合 LLM API 错误重试策略 设计:重试提高成功率,幂等约束成功次数。

八、相关阅读

如果你需要用统一接口接入多种主流模型并集中管理调用链路,YoTradeApi 可提供兼容的 API 接入能力,便于在应用侧落实幂等键、重试和观测策略。