写 Agent 的时候,重试几乎一定会发生:超时重试、限流退避、工具调用失败重投、用户连点两次。如果下游接口没做幂等,一次重试可能就变成两笔扣款、两条工单、两份重复写入的日志。这篇不讲协议怎么配,只讲工程上怎么判断哪些环节必须幂等、幂等键怎么设计、写在哪一层。
写 Agent 后端的人迟早会撞上这个场景:一次工具调用超时了,模型看到的是「失败」,于是它换个说法再调一次;前端用户手快点了两下;网关返回 502 被客户端库自动重投。对模型来说,这些都是「这次没成功」;对下游数据库来说,这可能就是两条工单、两笔扣款、两份写进日志的记录。
这篇只讲工程上怎么判断、怎么落地。协议层面怎么配、要不要用某些标准头部,站内其它文章已经讲过,这里不重复。核心就三件事:哪些环节必须幂等、幂等键怎么设计、写在哪一层。
先认清:Agent 链路里重试不是异常,是常态
普通后端也会重试,但 Agent 的特殊性在于决策方是模型。模型拿到的只是一句错误信息,它没法判断这次失败是「请求没到达下游」还是「下游执行完了、响应在回程丢了」。这两种情况在它眼里长得一模一样,所以它的自然反应就是再试一次。重试的概率因此比人手写的客户端高得多。
- 读取超时:下游处理慢于客户端等待时间,客户端放弃等待但下游可能已经执行
- 网关 5xx:通常是瞬时故障,各级组件默认会退避重投
- 限流退避:被限流后按策略延迟重发,同一意图被送进去多次
- 工具调用超时后模型重试:模型自己决定换个措辞再调一次同一个工具
- 前端用户重复提交:连点、刷新、断线重连都可能制造第二份请求
哪些调用必须幂等,哪些可以不做
不是所有接口都值得加幂等。加幂等要付出存储、延迟和代码复杂度的代价,判断依据只有一条:这次调用会不会改变外部状态,以及改变之后能不能无痛回滚。下面这张表可以直接拿去对项目里的工具清单勾一遍。
| 调用类型 | 是否需要幂等 | 理由 |
|---|---|---|
| 纯查询、检索、读缓存 | 不需要 | 不改变外部状态,重复执行只是多一次开销 |
| 写库、建单、改状态 | 必须 | 重复执行会留下两条真实记录 |
| 带计费或扣额度的调用 | 必须 | 重复执行等于重复收费,事后对账很麻烦 |
| 发通知、发消息、发邮件 | 建议做去重 | 重复发送不致命但会打扰用户,去重成本低 |
| 外部第三方接口 | 看对方是否支持去重标识 | 对方没有去重能力时,本地先落待确认状态,靠对账兜底 |
一个容易被忽略的边界:流式输出中途断开后客户端重连,不要试图用幂等键把两次请求合并。这是两次独立的请求,语义上是「重新问一遍」,合并只会让返回内容错乱。幂等管的是同一个业务意图被重复投递,不是同一条连接被重连。
什么情况下不适用:当下游是外部第三方、完全不接受任何去重标识,你也没法拿到它的事务 ID 时,硬重试只会放大损失。退路是本地先落一条「待确认」记录,把这次调用标记成结果未知,靠事后的对账流程或人工确认收尾,而不是让模型继续重试。
幂等键怎么设计:作用域、生成时机、过期清理
幂等键的作用是回答「这两个请求是不是同一件事」。所以它必须跟业务意图绑定,而不是跟某一次网络请求绑定。用时间戳、随机 UUID、自增请求号都做不到这一点,因为重试时它们会变。
- 作用域
- 常见组合是会话 ID 加工具名加这一步的序号;粒度太粗会把两次真实操作误判成一次,太细又等于没有去重
- 生成时机
- 第一次调用前就生成好,随请求一起带下去,重试时复用同一个键;重试时才生成等于每次都是新请求
- 服务端约定
- 同一键但参数不一致时直接拒绝,而不是覆盖旧记录,否则会把上游的逻辑 bug 掩盖成静默成功
- 过期清理
- 幂等记录要设保留窗口,太短起不到作用,太长则存储成本高;窗口要跟重试策略的时长对齐
- 先定意图边界列出这个 Agent 里所有会改外部状态的工具,逐个写明「什么算一次操作」,比如一次下单、一次建档。
- 拼出键的组成用会话 ID 加工具名加步骤序号拼键,确保同一步重试时拼出来完全一样。
- 落库并加约束在幂等记录表上对键加唯一约束,让并发重复投递在数据库层就被挡掉,而不是只靠应用层判断。
- 写进日志和追踪每次调用都记下请求 ID、幂等键、是否命中已有记录,出问题时能靠键把两次请求对上。
幂等放在哪一层:客户端、网关还是业务服务
三种放法各有取舍。客户端自带键最灵活,但依赖每个调用方都自觉;网关统一注入能兜底,但网关看不懂业务语义,只能按请求特征做粗粒度去重;业务层判断最准,但每个接口都要改代码。实践中通常是分层:网关挡住连点和明显的短时间重复提交,业务层做真正的语义幂等,两边职责别混。
- 重试次数和幂等记录的保留窗口要对齐,超过窗口的重试基本等于一次新请求
- 重试策略里的超时时间要短于幂等窗口,否则会出现「键已过期但重试还在发」的空档
- 网关层去重只是兜底,不能替代业务层判断,因为它分不清两次相同请求是重试还是用户真的想再来一次
- 内部只读的批处理任务不要加幂等,只会白白增加存储和延迟
什么情况下不适用:纯内部的离线跑批、只读的检索任务、幂等键根本传不到下游的第三方接口,这些场景加幂等的收益远低于成本,不如把精力放在超时和对账上。
怎么验证幂等真的生效了
幂等写完了不代表生效,必须能观察到。最直接的办法是人为重放:把同一个请求连发两次,然后去下游看记录数。只看接口返回是不够的,因为返回「成功」也可能是命中了旧记录。
# 人为重放同一个幂等键,观察下游记录数与日志字段
curl -X POST http://localhost:8000/agent/tool \
-H 'Content-Type: application/json' \
-H 'X-Idempotency-Key: sess-42-tool-3-step-1' \
-d '{"args": {"action": "create_ticket"}}'
# 连发两次,第二次应命中已有记录而不是新建
curl -X POST http://localhost:8000/agent/tool \
-H 'Content-Type: application/json' \
-H 'X-Idempotency-Key: sess-42-tool-3-step-1' \
-d '{"args": {"action": "create_ticket"}}'| 日志字段 | 用途 |
|---|---|
| 请求 ID | 定位单次网络请求,和上游追踪串起来 |
| 幂等键 | 判断两次请求是不是同一件事 |
| 是否命中已有记录 | 区分「真执行了」和「直接返回缓存结果」 |
| 首次执行还是复用 | 排查重复写入时的第一现场 |
小规模演练也值得做:找一台机器故意断网或注入延迟,让 Agent 走一遍完整链路,看有没有重复写入。用通用场景就行,比如一个 5 人小团队内部的工单 Agent,写库和发通知这两步最容易出问题。长期看,可以把「重复请求命中率」当成常规指标盯,这个值突然涨上去,说明上游重试太频繁,该回头调超时和并发策略了。
下一步把你项目里的工具清单列出来,按副作用分类勾一遍,再看哪些环节缺幂等键;顺手去站内核对一下各家接口的错误码约定,重试策略要跟它对齐才不出岔子。→常见问题
只加重试不加重试次数上限,是不是也能避免重复?
不行。次数上限只降低重复概率,不解决重复本身。网络抖动、网关重投、用户连点这些都不受你的重试逻辑控制,只要下游没去重,重复照样落地。上限是成本控制手段,幂等才是正确性手段,两者要一起做。
幂等键用请求 ID 行不行?
要看请求 ID 在哪生成。如果每次重试都重新生成,那它只是个追踪 ID,起不到去重作用。真正能用的键必须在第一次调用前生成、重试时原样带着走。多数框架里的 trace id 不满足这个条件,别直接拿来用。
第三方接口不支持幂等键怎么办?
退一步在本地做状态机:调用前先写一条「处理中」记录,返回后再改状态。重试前先查本地状态,不确定就转人工或对账。别在无法判定结果的情况下无限重试,那只会把重复操作放大。