
最近带团队做面向企业级采购的 AI 智能体Agent接入了二十多个内部业务 Tool函数调用。上线第一周风控部门就急匆匆找上门来测试环境里大模型在帮采购员下一笔办公耗材订单时因为网络延迟稍微抖动了一下大模型判定“工具执行失败”并在下一轮思考中重新触发了一次下单工具结果系统在两秒内给同一个供应商生成了两笔完全一模一样的采购单。如果是以前写 Controller 接口前端按钮做个防抖、表单带个全局 Token或者在网关层挂个分布式锁很多工程师觉得这题早就做烂了。但把控制权交给大模型LLM Agent之后你会发现很多传统防御机制直接失效了。大模型具有自回归推理的不确定性并且目前主流的 ReAct 框架在遇到超时或返回结果不符合预期时内部自带重试机制。更要命的是大模型第二次重试时生成的tool_call_id是全新生成的随机字符串。如果你单纯以框架层的 ID 作为幂等键资金扣减、库存占用和订单创建就会立刻陷入重复提交的灾难。今天聊聊我们是如何针对大模型 Tool Execution 落地一套全局分布式防重与幂等保障体系的。为什么大模型容易触发重复写操作在分析防御策略之前先看清楚 Agent 执行循环Reasoning Loop中重复调用写操作的典型场景LLM 自我怀疑与幻觉重试大模型在多轮推理时如果上一轮拿到的 Tool 结果结构复杂或者包含警告信息模型可能会“自作聪明”地修改某些无关参数再次调用同一个下单接口。底层 HTTP 读超时Read Timeout导致的幽灵执行下游扣库存接口耗时 4 秒而 Agent 框架对 Tool 的执行超时时间设置了 3 秒。客户端切断了连接并向大模型抛出TimeoutException但下游微服务的扣款事务其实已经在数据库里成功提交了。大模型捕获到错误后大概率会重新发起调用。用户追问引发的历史上下文重放用户在聊天框里问“我刚才的订单下成功了吗”某些实现不严谨的 Agent 会把上一轮的 Tool 执行计划重新带入 Prompt 并再度触发。因此涉及任何具有副作用Side Effect的写操作工具都必须具备独立的全局幂等屏障。业务语义幂等键生成规则防重的第一个核心问题是拿什么作为 Key前面提到绝对不能直接用大模型传过来的tool_call_id也不能只用单次会话的sessionId。最稳健的方案是基于“会话上下文 业务意图语义指纹”来合成。我们设计的幂等键计算规则如下sessionId/conversationId确定具体是哪一次用户对话userIntentId/workflowStep当前 Agent 执行计划中的具体步骤标识toolName被调用的工具名称businessDigest提取入参中具有唯一业务语义的核心字段例如skuId、quantity、supplierId、bizOrderNo按键名自然升序排序后进行 MD5 / SHA-256 哈希。IdempotencyKey prefix : conversationId : toolName : SHA256(SortedBusinessParams)这样哪怕大模型生成了新的调用 ID或者换了提问口吻只要对同一个业务目标的写参数完全一致算出来的指纹就会百分之百命中缓存。三态状态机与防重切面落地单纯拿 Redis 做一个setnx并在 Tool 执行完毕后删掉根本不能叫幂等那只是防并发点击。真正的幂等必须维护执行状态机并在第二次重复请求到来时直接返回上一次已成功的快照结果从而让大模型误以为“调用成功”继续顺利推进下游逻辑。我们定义了三种状态PROCESSING正在执行中挂有短租期锁防止并发重入SUCCESS业务已执行成功Value 中直接缓存当时返回给模型的 JSON 报文保留 24 小时FAILED业务执行明确失败如库存不足根据失败类型决定是否允许重试。1. 核心注解定义Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface IdempotentTool { String bizType(); int lockSeconds() default 10; int cacheHours() default 24; }2. AOP 切面实现Aspect Component public class AgentToolIdempotentAspect { private final RedissonClient redissonClient; private final StringRedisTemplate redisTemplate; public AgentToolIdempotentAspect(RedissonClient redissonClient, StringRedisTemplate redisTemplate) { this.redissonClient redissonClient; this.redisTemplate redisTemplate; } Around(annotation(idempotentTool)) public Object aroundToolExecution(ProceedingJoinPoint joinPoint, IdempotentTool idempotentTool) throws Throwable { // 1. 从当前 ThreadLocal 上下文中提取会话信息与业务参数 AgentContext context AgentContextHolder.getContext(); String paramDigest calculateDigest(joinPoint.getArgs()); String bizKey String.format(tool_idem:%s:%s:%s, idempotentTool.bizType(), context.getConversationId(), paramDigest); String recordKey bizKey :record; String lockKey bizKey :lock; // 2. 检查是否有历史执行记录 String cachedResult redisTemplate.opsForValue().get(recordKey); if (cachedResult ! null) { // 直接命中历史成功结果解包返回不穿透业务逻辑 return JacksonUtils.toObject(cachedResult, ((MethodSignature) joinPoint.getSignature()).getReturnType()); } // 3. 抢占执行排他锁防止并发重入 RLock lock redissonClient.getLock(lockKey); boolean locked lock.tryLock(0, idempotentTool.lockSeconds(), TimeUnit.SECONDS); if (!locked) { // 正在并发执行中直接通知大模型等待或提示正在处理 throw new ToolExecutionConflictException(当前操作正在后台处理中请勿重复发起); } try { // 4. 执行实际业务代码如扣减库存、创建采购单 Object result joinPoint.proceed(); // 5. 执行成功将结果缓存状态固化 redisTemplate.opsForValue().set( recordKey, JacksonUtils.toJson(result), idempotentTool.cacheHours(), TimeUnit.HOURS ); return result; } catch (Exception ex) { // 发生异常时视情况决定是否清理缓存以便人工或降级重试 throw ex; } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private String calculateDigest(Object[] args) { // 提取核心字段并做 SHA-256 签名避免大模型多余空字段影响指纹计算 return DigestUtils.sha256Hex(JacksonUtils.toSortedJson(args)); } }生产落地的硬核细节与陷阱1. 数据库层的最终兜底防击穿底线Redis 是基于内存的无论配置了怎么样的主从哨兵或集群在极端的网络分区或主从切换时都有极低概率丢失毫秒级的 Key。对于金融扣款或订单生成数据库的唯一索引Unique Constraint永远是不可逾越的最后一道护城河。在业务表的设计上必须存在一个biz_unique_code字段该字段的值由切面生成的指纹直接注入。即便 Redis 缓存因为极端原因被穿透数据库层面的DuplicateKeyException也能把写操作拦在事务之外。2. 超时未决与两阶段冲正TCC / SAGA如果业务调用涉及到跨第三方系统例如银行打款、物流发卡一次网络超时可能导致你根本无法确认对方到底成功了没有。这时候不能简单地把操作标记为FAILED允许大模型重试否则就是给重复打款开大门。在我们的规范中凡是金额超过预警线的 Tool必须遵循两阶段确认机制预占阶段Try / Prepare冻结额度生成处于PENDING状态的凭据单。确认阶段Confirm由独立的后端任务或下一阶段的 Tool 发起二次确认。如果大模型中途断联后台定时轮询任务会自动对账要么推进完成要么发起冲正释放冻结。给大模型接工具不仅是写几个Tool注解那么轻松。AI 的逻辑是不确定性的但后端的资产保障必须具备确定性。