登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

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 仍不足以阻止旧进程继续写远端。本例不实现接管,也不自动清除未知状态。

幂等记录状态与结果引用的静态关系说明图
说明图:记录状态决定可用响应类别,结果引用关联既有业务结果;UNKNOWN 不是可直接重试的失败结论。

把最小决策代码放在工具执行前

下面的 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。本文状态命名和代码为独立设计示例,不是这些平台的状态字段规范。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>