Agent 为什么会重复执行?幂等、重试与断点恢复
内容摘要:Agent 会重试、会崩溃、会并发。若写操作没有幂等保护,重试就会重复扣款、重复建单。本章讲幂等、超时、重试与退避、熔断、限流、补偿与断点续跑,并给出可运行、可测试的实现。 难度:高级 |前置知识:06 工具调用、09 状态
常见误区
工具调用超时就代表没有执行成功,可以直接重试;任务恢复时从失败节点重新跑一遍最稳妥。
先给结论
超时只表示调用方没有及时得到结果,不代表副作用未发生。写操作必须先设计幂等键与结果查询,再决定重试、补偿或人工处理;Checkpoint 恢复也要避免重放已完成副作用。
带着这三个问题阅读:
- 循环、并发、超时与进程恢复如何造成重复执行?
- 幂等、重试、熔断、限流和补偿分别解决什么问题?
- 哪些错误可重试,哪些错误重试只会放大故障?
一、为什么 Agent 特别需要可靠性工程
Agent 的循环、恢复、并发都会导致同一写操作被执行多次:
解药是幂等(idempotency):用幂等键保证"同一操作执行多次,效果等于一次"。
二、幂等键:写操作的护身符
"""idempotency.py —— 幂等执行器(可离线测试)。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class IdempotencyStore:
"""记录已执行的幂等键与结果。生产用 DB 唯一约束/Redis SETNX。"""
_done: dict[str, dict] = field(default_factory=dict)
def run_once(self, key: str, op: Callable[[], dict]) -> dict:
if key in self._done:
return {**self._done[key], "_deduped": True} # 命中:直接返回旧结果,不重复执行
result = op()
self._done[key] = result
return {**result, "_deduped": False}
# 示例:创建工单,重复调用不会建两张单
store = IdempotencyStore()
def create_ticket(): return {"ticket_id": "T-1001", "status": "created"}
print(store.run_once("key-abc", create_ticket)) # 首次执行
print(store.run_once("key-abc", create_ticket)) # 重复:_deduped=True幂等键怎么生成:由业务语义决定(如 hash(用户+动作+目标+时间窗)),而非随机——随机键无法识别"同一操作"。跨请求要持久化。
三、超时、重试与退避
只对可恢复错误重试,且必须有上限:
| 错误类型 | 可重试? | 处理 |
|---|---|---|
| 网络抖动、5xx、超时 | 是(有限次) | 指数退避 + 抖动 |
| 限流 429 | 是 | 退避,尊重 Retry-After |
| 认证 401/权限 403 | 否 | 直接失败,别刷 |
| 参数错误 4xx | 否 | 修参数,别重试 |
import random, time
def retry_with_backoff(op, *, max_attempts=3, base=0.2, is_retryable=lambda e: True):
"""指数退避 + 抖动;只重试可恢复错误;有上限。"""
for attempt in range(1, max_attempts + 1):
try:
return op()
except Exception as e:
if attempt == max_attempts or not is_retryable(e):
raise
sleep = base * (2 ** (attempt - 1)) + random.uniform(0, base)
time.sleep(sleep)关键:写操作重试前必须先有幂等键,否则"重试成功"可能是"重复执行"。
四、熔断与限流
- 熔断(Circuit Breaker):某依赖连续失败到阈值,暂时"断开",快速失败一段时间再半开试探,避免雪崩。
- 限流(Rate Limiting):限制对模型/工具的调用速率,防止刷爆额度或触发下游限流。
- Agent 循环内也要有"连续失败上限"(08 的熔断停止)。
五、补偿与 Saga
不可用分布式事务时,用补偿:每个写步骤配一个"撤销"动作,失败时反向补偿。适合"建单→通知→扣配额"这类多步写。补偿动作本身也要幂等。
六、长任务与断点续跑
结合状态持久化:把任务切成可 checkpoint 的步骤,崩溃后从最近 checkpoint 恢复,已完成步骤靠幂等键跳过。异步长任务用队列 + 定时器驱动,人工审批等待用带外回调而非阻塞。
七、失败路径
- 无幂等的重试 → 重复副作用。
- 无限重试 → 放大故障、烧额度。
- 对不可重试错误重试 → 浪费且掩盖问题。
- 恢复不跳过已完成步骤 → 重复执行。
八、测试与验证
"""test_idempotency.py"""
from idempotency import IdempotencyStore
def test_run_once_dedup():
store = IdempotencyStore(); calls = []
def op(): calls.append(1); return {"v": 1}
r1 = store.run_once("k", op); r2 = store.run_once("k", op)
assert r1["_deduped"] is False and r2["_deduped"] is True
assert len(calls) == 1 # 实际只执行一次- 重试:注入前 N 次失败的假依赖,断言最终成功且不超上限。
- 熔断:连续失败后断言进入 open 状态、快速失败。
九、安全与权限
十、性能与成本
- 重试与退避影响尾延迟;限流保护成本上限(18)。
十一、框架差异
- LangGraph 的持久化/重放需配合幂等;多数框架不自动保证工具幂等,这是你的责任。
十二、关联章节
面试复述
Agent 可靠性的核心是把不确定调用变成可判定状态。写操作先幂等,再设置超时和分类重试;长任务通过 Checkpoint 恢复,但不能重复已确认的副作用。
复述时先给结论,再补机制、适用边界和一个可验证指标;不要只罗列框架名称或产品功能。
迁移练习
为创建工单接口定义幂等键生成规则和结果查询接口,模拟“服务端成功但客户端超时”,验证重试不会生成第二张工单。
检索关键词
幂等键 idempotency 指数退避 重试上限 熔断 限流 补偿 Saga 断点续跑 长任务队列
官方来源
- 云厂商幂等/重试最佳实践;Temporal 等工作流引擎文档作为工程对照(
verified_at: 2026-09-07)。
