用户点击了两次、工具超时后自动重试、模型又发了一次同样请求,都可能让一笔售后动作被执行多次。校验金额正确,并不能解决重复执行。
一次请求只能有一个业务含义
幂等键应绑定可信主体、对象和关键参数。相同键携带不同金额时不能返回一个看似成功的旧结果,更不能重新执行。查询旧请求与插入新请求需要事务或等价的原子机制。
模型生成的请求编号不应是唯一依据。会话与业务服务可以分配稳定编号,重试继续使用它。若下游支付调用结果未知,先查询交易状态,不能直接再打款。
10月5日Google Research讨论了动作的上下文。这里的上下文还包括这项动作是否已经发生:同一句合法指令重复执行,结果也可能不再合适。
两张同一请求卡只产生一个结果;冲突参数必须拒绝,不进入“再试一次”的路径。
用事务演示幂等占位
以下是可离线运行的最小示例,使用Python标准库。输入均为教学构造,不连接业务系统。
import sqlite3 db = sqlite3.connect(":memory:", isolation_level=None) db.execute("CREATE TABLE requests (id TEXT PRIMARY KEY, amount INTEGER NOT NULL)") def reserve(key, amount): if type(amount) is not int or amount <= 0: raise ValueError("bad amount") db.execute("BEGIN IMMEDIATE") try: old = db.execute("SELECT amount FROM requests WHERE id=?", (key,)).fetchone() if old and old[0] != amount: raise ValueError("idempotency conflict") if not old: db.execute("INSERT INTO requests VALUES (?,?)", (key, amount)) db.execute("COMMIT") return "REPLAY" if old else "RESERVED" except Exception: db.execute("ROLLBACK") raise assert reserve("r-1", 100) == "RESERVED" assert reserve("r-1", 100) == "REPLAY" try: reserve("r-1", 200) except ValueError: pass else: raise AssertionError("conflict accepted") assert db.execute("SELECT COUNT(*) FROM requests").fetchone()[0] == 1 db.close() print("reservation/replay/conflict cases passed; no payment made")两张同一请求卡只产生一个结果;冲突参数必须拒绝,不进入“再试一次”的路径。
本例仅在内存SQLite中做占位,没有扣款、退款或跨服务事务。真实交易需要处理请求已提交但响应丢失的未知状态。
把未知结果列为单独状态
不要把超时混进失败:失败可以被确认,超时只说明没有按时收到答复。记录下游交易号并查询,必要时进入人工处理,才能避免重复资金动作。
并发多连接、真实支付回执、数据库故障和灾难恢复均未测试。这个小例子验证的是唯一键和参数一致性,不是生产资金安全。
参考资料
- Google contextual security
- MCP authorization