AI agent 幂等与去重实践:避免重复创建、重复发送和重复扣费

HTMLPAGE 团队
18 分钟阅读

AI agent 的重试和多步调用很容易造成重复写入。本文给出幂等键设计、数据库约束、去重窗口、外发保护和重试场景处理。

#AI agent #幂等 #去重 #工具调用

AI agent 经常需要重试:模型输出格式错了要重来,工具超时要再调一次,用户刷新页面可能再次提交任务。如果写入工具没有幂等和去重设计,就会出现重复创建任务、重复发送消息、重复扣费等问题。

幂等的核心是:同一个意图执行多次,系统结果仍然只发生一次。它不是“不要重试”,而是允许系统放心重试,因为重复请求不会重复产生副作用。

先给结论:所有写入动作都需要幂等键

场景幂等键建议
创建草稿userId + sourceTaskId + draftType
发送通知taskId + recipient + template
扣减额度billingAccount + actionId
更新状态entityId + expectedVersion
上传文件contentHash + targetFolder

没有幂等键,重试就有副作用。

先区分三种重复

AI agent 里的重复不止一种,处理方式也不同:

重复类型来源解决方式
用户重复提交刷新页面、连点按钮前端禁用 + 服务端幂等键
系统重试工具超时、队列重跑幂等键 + 结果缓存
模型重复决策agent 多次决定调用同一工具run 内调用记录 + 状态检查

只做前端防抖不够,因为队列重试、服务端超时和模型重复决策都发生在后端。

一、幂等键要来自业务意图

不要用随机 UUID 当作唯一幂等键。如果每次重试都生成新 UUID,系统仍然会认为是新请求。

幂等键应该来自同一个业务意图:同一用户、同一任务、同一动作、同一目标对象。

比如“发送审核通知”的幂等键可以这样生成:

function buildNotifyIdempotencyKey(input) {
  return [
    'notify-review',
    input.taskId,
    input.recipientId,
    input.templateId,
    input.entityVersion
  ].join(':')
}

如果同一个任务、同一个收件人、同一个模板、同一个实体版本重复发送,就应该返回第一次发送结果,而不是再发一封。

二、去重窗口要按业务设置

有些动作只需要几分钟内去重,比如按钮重复点击;有些动作需要永久去重,比如一次扣费;有些动作需要按版本去重,比如状态更新。

动作去重窗口
表单提交5-10 分钟
草稿创建当前任务周期
外发通知同一任务永久
成本扣减actionId 永久

去重窗口过短挡不住重复,过长又可能影响合法操作。

可以把去重窗口写进工具声明,避免 agent 或调用方误用:

{
  "tool": "sendReviewNotice",
  "idempotency": {
    "required": true,
    "keyFields": ["taskId", "recipientId", "templateId", "entityVersion"],
    "ttl": "permanent"
  }
}

这样工具注册表、测试脚本和文档可以共用同一份规则。

三、状态检查比盲目写入更稳

写入前先读当前状态:草稿是否已存在,通知是否已发送,额度是否已扣减,目标版本是否还是预期版本。

这种“读状态 -> 判断 -> 写入”的流程能减少重复和冲突。关键动作要配合事务或数据库唯一约束。

数据库层最好也要兜底:

CREATE TABLE agent_tool_runs (
  id BIGSERIAL PRIMARY KEY,
  idempotency_key TEXT NOT NULL,
  tool_name TEXT NOT NULL,
  status TEXT NOT NULL,
  result JSONB,
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  UNIQUE (idempotency_key, tool_name)
);

流程上先插入 agent_tool_runs,如果唯一约束冲突,就读取已有结果。不要只靠内存缓存,因为队列 worker 重启后缓存会丢。

四、外发动作要更保守

邮件、短信、客户通知这类外发动作无法轻易撤回。即使内部状态能回滚,用户也已经收到信息。

外发前建议:预览 -> 人工确认 -> 幂等发送 -> 记录 messageId。重试时如果已有 messageId,就返回已发送结果,不再发送第二次。

外发工具返回结果时,要明确区分“新发送”和“命中幂等”:

{
  "success": true,
  "data": {
    "messageId": "msg_789",
    "deliveryState": "already_sent",
    "sentAt": "2026-05-06T10:30:00Z"
  },
  "nextAction": "continue"
}

这样 agent 不会因为“没有新发送”误以为失败。

五、成本扣减必须和业务动作绑定

如果 agent 调用一次模型、生成一次文件、发送一次通知都要扣额度,扣减动作也必须幂等。否则工具超时后重试可能导致重复扣减。

推荐做法是把扣减和业务动作绑定到同一个 actionId:

actionId = taskId + toolName + businessTarget + entityVersion

扣减表对 actionId 做唯一约束。业务动作失败时,不要提前永久扣减;可以先冻结额度,成功后确认扣减,失败后释放。

六、失败案例:超时后重复发送三封邮件

一个 agent 发送邮件后接口超时,模型判断“可能没发成功”,于是重试两次。实际邮件已经发出,客户收到三封相同内容。

修复后,发送工具使用 taskId + recipient + template + entityVersion 作为幂等键。即使接口超时,再次调用也会先查询发送记录,避免重复外发。同时工具返回 deliveryState=already_sent,让 agent 知道这是成功结果,不需要继续补救。

七、幂等 Checklist

  • 所有写入工具是否有幂等键
  • 幂等键是否来自业务意图
  • 去重窗口是否按动作设置
  • 写入前是否检查当前状态
  • 外发动作是否有预览和确认
  • 成本扣减是否永久去重
  • 重试是否不会产生额外副作用
  • 数据库是否有唯一约束兜底
  • 外发工具是否区分新发送和已发送
  • 成本扣减是否绑定 actionId

结语

AI agent 的可靠性不只看回答是否正确,也看失败和重试时是否安全。幂等键、去重窗口、状态检查、数据库唯一约束、外发保护和成本扣减保护做好后,agent 才能放心进入真实写入流程。

延伸阅读: