Spring AI遇到429或超时后为什么重复执行工具?重试边界与幂等完整排查
文章摘要
AI接口出现429、超时或连接中断后,开发者通常会增加自动重试。但在Tool Calling场景中,如果重试包裹了整个Agent流程,退款、发送邮件、创建工单、写数据库等工具可能被重复执行。更隐蔽的情况是模型请求超时,但工具其实已经完成;客户端重试后,模型再次发起相同工具调用。本文从模型层、Agent层、工具层和HTTP层四个重试边界出发,给出幂等键、状态机、结果查询和可重试错误分类的完整方案。
一、典型事故
用户说:
给客户创建一个售后工单执行链路:
模型选择create_ticket → 工具创建工单成功 → 返回模型时连接超时 → Agent整体自动重试 → 再次调用create_ticket → 创建第二个工单从用户视角只发了一次请求,系统却产生两个业务对象。
如果工具是:
- 退款;
- 支付;
- 发券;
- 发邮件;
- 删除数据;
- 创建订单;
后果会更严重。
二、为什么“重试一次”会跨越多个层级
一个AI请求可能同时存在:
网关重试 HTTP客户端重试 Spring AI Provider重试 Resilience4j重试 Agent步骤重试 工具SDK重试 消息队列重投如果每层都重试3次,最坏情况不是3次,而可能是乘法放大。
例如:
网关2次 × 应用3次 × 工具SDK3次 = 18次潜在调用必须明确每层的职责。
三、四种重试边界
1. 模型调用重试
适合:
- 429;
- 暂时性5xx;
- 连接建立失败;
- 无副作用的模型请求。
风险:
如果模型调用发生在工具执行后,重试可能重新生成工具调用。
2. Agent步骤重试
适合:
- 结构化输出解析失败;
- 计划校验失败;
- 可恢复的推理错误。
风险:
整个步骤可能包含多个工具副作用。
3. 工具调用重试
适合:
- 只读查询;
- 明确幂等写入;
- 服务端支持幂等键。
4. 业务流程重试
适合:
- 有持久化状态机;
- 可以查询当前执行状态;
- 能从检查点继续。
不能简单重新运行整个流程。
四、哪些错误可以自动重试
通常可重试
429 rate_limit_exceeded 502 503 504 连接被拒绝 短暂DNS失败 读超时且确认无副作用通常不可直接重试
400参数错误 401认证失败 403权限不足 404资源不存在 insufficient_quota 内容安全拒绝 业务校验失败状态未知
最危险的是:
请求超时超时只说明客户端没有按时收到结果,并不说明服务端没有执行。
写操作超时后应该:
先查询执行状态 → 再决定是否重试五、幂等键必须在模型之外生成
不要让模型自己生成随机幂等键。
模型可能每次重试都生成不同值。
正确做法:
业务请求进入 → 应用生成operationId → 同一个业务动作的所有重试复用例如:
StringidempotencyKey=String.join(":",tenantId,conversationId,requestId,"create_ticket");如果一次请求中允许创建多个工单,还要加入业务对象标识或步骤编号。
六、工具服务端如何实现幂等
表结构:
CREATETABLEtool_idempotency(idempotency_keyVARCHAR(200)PRIMARYKEY,tool_nameVARCHAR(100)NOTNULL,request_hashVARCHAR(128)NOTNULL,statusVARCHAR(30)NOTNULL,result_jsonTEXT,created_atTIMESTAMPNOTNULL,updated_atTIMESTAMPNOTNULL);状态:
PROCESSING SUCCEEDED FAILED_RETRYABLE FAILED_FINAL执行流程:
收到请求 → 插入PROCESSING → 已存在则读取状态 → SUCCEEDED直接返回历史结果 → PROCESSING返回处理中 → 可重试失败按规则执行伪代码:
@TransactionalpublicToolResultexecute(Stringkey,ToolRequestrequest){Optional<IdempotencyRecord>existing=repository.findById(key);if(existing.isPresent()){returnrestore(existing.get(),request);}repository.insertProcessing(key,hash(request));try{ToolResultresult=doExecute(request);repository.markSucceeded(key,result);returnresult;}catch(RuntimeExceptionex){repository.markFailed(key,ex);throwex;}}七、相同幂等键但参数不同怎么办
攻击或代码错误可能发送:
相同key +不同参数例如第一次退款100元,第二次使用同一个key退款200元。
服务端必须比较request_hash。
如果不同:
返回409 Conflict不能把第二次请求当成第一次的成功结果。
八、模型返回的tool_call_id能不能当幂等键
不建议单独使用。
tool_call_id通常只在一次模型响应中唯一。
Agent整体重试后,模型可能生成新的ID。
更稳定的是:
业务operationId +工具名 +步骤ID可以把tool_call_id作为追踪字段,而不是唯一业务幂等依据。
九、重试应该包裹哪一层
错误:
@Retry(name="ai")publicStringrunAgent(Stringmessage){returnagent.run(message);}如果agent.run()内部执行写工具,整个流程会重跑。
更安全:
模型只读推理调用 → 可重试 工具写操作 → 幂等执行 最终回答生成 → 可重试,但复用工具结果将流程持久化:
PLANNED TOOL_EXECUTED ANSWER_GENERATING COMPLETED最终回答失败后,从TOOL_EXECUTED继续,不再重复执行工具。
十、检查点设计
publicrecordAgentCheckpoint(StringexecutionId,Stringstate,StringtoolName,StringtoolResultLocation,intmodelAttempt,inttoolAttempt){}执行:
模型选工具 → 保存计划 → 工具执行 → 保存结果 → 模型生成回答任何一步失败都从最近检查点恢复。
十一、只读工具是否可以随便重试
只读工具通常风险较低,但仍可能有:
- 外部API计费;
- 强限流;
- 数据查询压力;
- 非稳定快照;
- 重复下载大文件。
建议设置:
最大重试次数 指数退避 随机抖动 总超时 并发限制十二、指数退避与Jitter
固定间隔:
1秒、1秒、1秒大量实例会同时重试,造成惊群。
推荐:
1秒 2秒 4秒并加入随机抖动。
Resilience4j示例:
resilience4j:retry:instances:aiModel:max-attempts:3wait-duration:1senable-exponential-backoff:trueexponential-backoff-multiplier:2retry-exceptions:-java.io.IOException-java.util.concurrent.TimeoutException异常列表需要按实际Provider SDK调整。
十三、429的Retry-After要不要遵守
如果响应提供:
Retry-After: 10应优先遵守。
但还要区分:
rate_limit_exceeded → 等待后重试 insufficient_quota → 不重试两者都可能是HTTP 429。
十四、熔断器应该包在哪里
建议在模型Provider适配层设置熔断:
业务Service → ModelGateway → Circuit Breaker → Provider不要用一个熔断器同时覆盖:
- 模型;
- 向量库;
- 所有工具;
否则其中一个工具失败会关闭整个AI系统。
按依赖隔离:
openai-chat qdrant-search order-tool mail-tool十五、降级策略
模型不可用:
Sol → Terra → Luna → 规则模板RAG不可用:
生成回答 → 降级为关键词搜索结果写工具不可用:
自动执行 → 创建待办 → 转人工降级不能绕过审批和权限。
十六、需要记录哪些指标
model_retry_count tool_retry_count agent_restart_count idempotency_hit_count idempotency_conflict_count unknown_execution_status_count circuit_breaker_open_count fallback_model_count duplicate_business_object_count重点告警:
同一operationId出现多个业务对象十七、完整排查清单
□ 是否同时存在多层重试 □ 重试是否包裹整个Agent □ 写工具是否支持幂等键 □ 相同业务动作是否复用同一个key □ 是否保存request_hash □ 超时后是否先查询状态 □ 最终回答失败是否重复执行工具 □ tool_call_id是否被误作唯一幂等键 □ 429是否区分限流与额度不足 □ 熔断器是否按依赖隔离 □ 是否有检查点和状态机总结
Tool Calling场景中,最大的错误不是“没有重试”,而是:
在错误的边界重试生产级方案应该做到:
模型调用可重试 +工具写操作幂等 +业务流程有检查点 +超时先查状态 +最终回答复用工具结果只有把模型推理和业务副作用分开,自动重试才不会变成重复执行。
