AI 工具调用怎样设计幂等键避免重复执行
来源:17golang原创
时间:2026-10-23 05:45:12 319浏览 收藏
AI 工具调用避免重复执行,关键是给“一次已授权的业务操作”分配稳定幂等键,而不是每次模型输出后生成新 UUID。重试复用原键,服务端原子占位、比较参数摘要,成功后复用已保存结果;如果远程请求超时且结果未知,应先查询或对账,不能直接换键重做。
最小规则可以记成:同键同参数复用结果,同键不同参数报冲突,进行中的请求等待,结果未知的请求暂停自动执行。本文以“给文档添加标签”说明工程设计,不调用真实模型或业务接口,也不声称示例已在生产验证。
从回答问题到修改数据,重试的代价变了
Gemini 官方函数调用文档把外部动作列为用途之一。模型能提出函数名和参数,并不意味着一次业务动作只会被执行一次。应用重启、队列重投、工具响应丢失,都可能让同一个用户要求再次进入执行器。这是接口可靠性问题,不是靠一句“请勿重复调用”就能约束的模型行为。
Gemini 官方文档:https://ai.google.dev/gemini-api/docs/function-calling
AWS 的幂等 API 设计资料讨论了调用方请求标识、参数不一致处理与原子性;Stripe API v1 则展示了保存响应供同键重试复用的具体契约。这些资料说明幂等并非 AI 独有能力。把它用于工具执行层,是本文的工程建议,不代表各模型平台已替业务系统自动实现。
幂等键认业务操作,摘要认参数内容
设定用户已确认“给文档 doc-27 添加待归档标签”,后端建立 operation_id,并保存所属租户、操作种类、工具契约版本与首次规范化参数。这个 operation_id 应随任务恢复和消息重投保留。若用户明确发起另一次独立操作,则建立新的操作记录。
| 标识 | 本方案中的用途 | 不能替代什么 |
|---|---|---|
| operation_id | 业务层批准的一次动作 | 不能每次重试重建 |
| call_id | 模型协议的一次工具调用关联 | 不能直接代表跨轮业务意图 |
| 幂等键 | 租户、工具版本、业务操作的稳定身份 | 不是访问凭证 |
| 参数摘要 | 识别同键请求是否改了内容 | 不是授权或数据加密 |
不要只对函数名和参数做哈希作为幂等键:两次独立、合法且参数完全相同的操作,可能被错误合并。也不要把参数哈希拼进键后就不再存摘要,否则原操作被改参时可能得到新键,绕过冲突判断。本方案把身份和内容分开保存。

原子占位让执行器和业务服务各司其职
执行器要做工具白名单、字段检查和授权绑定;业务服务维护幂等记录及副作用。建议的记录包含 key、digest、state、result_ref、owner_token 和时间字段,并以 key 建唯一约束。新增占位和竞争处理必须是存储层原子操作;“先查不存在,再插入”不能作为多进程防重保证。
只有原子占位返回 created=True 的请求取得执行资格。已有 SUCCEEDED 记录时,先确认调用者仍有读取权限,再返回原结果引用;RUNNING 返回处理中,不再次执行。同键摘要不一致返回冲突,不能覆盖原参数。这样执行器无需猜测前一位工作进程是否完成。
这对异步任务消费者、工具网关和业务接口维护者都有价值,但收益不同:消费者减少重投造成的额外动作,网关能解释重复请求,业务服务保有最终状态。代价则是持久化记录、保留策略、恢复机制和额外查询,不能只加一个内存字典就声称完成改造。
远程超时不能直接当成失败重做
最危险的情况是远端已添加标签,但本地尚未写成功记录就崩溃。单独保存 RUNNING 或使用分布式锁,不能消除这个窗口。若副作用与幂等记录在同一事务数据库内,可设计成同事务提交;跨远程系统时,要让下游也识别稳定操作键,或提供按业务标识查询结果的能力。没有这两种条件时,保留 UNKNOWN,并进入人工或自动对账。
RUNNING 超过租约不等于原执行者已经停止。需要接管时,应有执行所有权版本或 fencing 机制,且副作用端能够拒绝过期执行者;仅替换本地 owner_token 仍不足以阻止旧进程继续写远端。本例不实现接管,也不自动清除未知状态。

把最小决策代码放在工具执行前
下面的 Python 代码只构造标识并解释原子占位的返回值,没有实现数据库、网络请求或真实副作用。make_ticket 使用的租户及操作 ID 必须来自服务端可信记录;模型仅提供业务参数,不能通过改键绕过已有操作。字符串规范化规则应由领域定义,不能随意去空格、改大小写而改变用户意图。
import hashlib
import json
from dataclasses import dataclass
@dataclass(frozen=True)
class Ticket:
key: str
digest: str
def make_ticket(tenant_id, operation_id, args):
# tenant_id 和 operation_id 来自已授权的业务记录,不接受模型自由填写。
if not all(isinstance(x, str) and x for x in (tenant_id, operation_id)):
raise ValueError("缺少可信业务标识")
if not isinstance(args, dict) or set(args) != {"document_id", "label"}:
raise ValueError("参数字段不符")
if not all(isinstance(v, str) and v for v in args.values()):
raise ValueError("字段必须是非空字符串")
# 本例只处理字符串字段;业务规范化必须先完成,再保存首次参数。
identity = json.dumps(
[tenant_id, "attach_label", "v1", operation_id],
ensure_ascii=False, separators=(",", ":"),
)
payload = json.dumps(
args, sort_keys=True, ensure_ascii=False, separators=(",", ":"),
allow_nan=False,
)
return Ticket(
"tool:" + hashlib.sha256(identity.encode("utf-8")).hexdigest(),
hashlib.sha256(payload.encode("utf-8")).hexdigest(),
)
def decide(ticket, claim):
# claim 必须来自原子占位接口,不得用普通查询的“未找到”代替。
if claim["digest"] != ticket.digest:
return "CONFLICT"
if claim["created"]:
# 仅本次原子插入成功者获得执行资格,不表示已经执行成功。
return "EXECUTE"
if claim["state"] == "SUCCEEDED":
return "REPLAY"
if claim["state"] == "RUNNING":
return "WAIT"
if claim["state"] in {"UNKNOWN", "FAILED_FINAL"}:
return "RECONCILE_OR_RETURN_FAILURE"
raise ValueError("未知占位状态,拒绝自动执行")
存储接口应在一次原子操作中返回 created、digest、state;created 为真表示插入了一条绑定本次摘要的 RUNNING 记录。EXECUTE 仅是资格,完成后还需按 owner_token 条件写入结果及 SUCCEEDED。REPLAY 需要读取受权限保护的 result_ref;WAIT 不进入工具;CONFLICT 交还调用方重新确认;其他结果进入对账或返回已知失败。不要把这个决策函数误当成可以直接上线的完整幂等执行器。
采用时可以先给单个文档写入工具建立上述存储契约,再在测试环境人为模拟并发重投、成功响应丢失和远端超时。本文没有执行这些测试;它们是读者接入前应覆盖的情形,而不是本文提供的测试结果。
上线后看重复副作用,而不只看命中率
建议观察同键结果复用数、参数冲突数、RUNNING 停留时长、UNKNOWN 积压和已确认的重复副作用数。幂等命中率高不一定是好消息,也可能说明重投失控;命中率低也不能证明没有重复,因为错误换键会把同一操作伪装成新动作。记录业务操作 ID 与协议调用标识的关联,有助于区别这两类情况,但日志不要存敏感参数全文。
幂等键保留多久?按最长合法重试、队列重投和恢复窗口制定,并明确过期后的行为。不能把某个厂商的期限当成所有系统通用值;未决操作更不应只因 TTL 到期就丢掉。
失败都能用同键重试吗?取决于错误是否确认没有副作用,以及下游的契约。终态失败可保存并返回,结果未知则先核实。Stripe API v1 与 API v2 的失败重试规则也不同,接入时必须按具体接口判断。
有幂等键还需要授权和人工确认吗?需要。幂等只约束同一动作的重复处理,不决定动作是否合法、用户是否同意,也不保证任意跨系统操作“恰好一次”。
官方参考:https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/;https://docs.stripe.com/api/idempotent_requests。本文状态命名和代码为独立设计示例,不是这些平台的状态字段规范。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
491 收藏
-
114 收藏
-
456 收藏
-
395 收藏
-
260 收藏
-
321 收藏
-
150 收藏
-
263 收藏
-
378 收藏
-
290 收藏
-
科技周边 · 人工智能 | 1小时前 | JSON · API · python · 人工智能 · 结构化输出 · 结构化输出 JSON Schema Gemini API response_schema response_mime_type additionalProperties145 收藏
-
467 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习