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 才能放心进入真实写入流程。
延伸阅读:


