
生产级 Tool 异常治理当外部 RPC 接口返回 500 时如何优雅引导大模型重试把大模型接入业务系统做 Agent智能体时最迷人也最凶险的能力莫过于 Function Calling工具调用。我们给模型提供了一堆Tool方法模型根据用户的意图自主决定何时调用、传什么参数。在本地写几个 Mock 数据测试时整个流程丝滑流畅仿佛真有了通用人工智能的雏形。但只要把这套代码推到生产环境现实的残酷很快就会上课后端的 Dubbo 服务偶尔抖动超时、下游的微服务正在发布返回 500、数据库连接池满了抛出异常……在传统微服务体系下我们有熔断、降级、重试切面但在 Agent 的执行闭环里如果底层 RPC 接口直接炸了个 500会发生什么很多团队的做法是直接把异常原封不动丢给大模型结果大模型要么当场“疯掉”——拿着同样的入参死循环疯狂调用十几遍把模型 Token 和接口耗尽要么开始“满嘴跑火车”——看到返回了 500居然凭空给用户编造了一个假订单号和虚假发货状态。今天就结合我们在订单助手上的治理实践聊聊当 Tool 背后的 RPC 接口不可用时如何建立异常分类与智能自愈引导机制。大模型面对 RPC 异常的三种典型失控在排查了数百个线上异常 Case 后我们总结出未经治理的 Tool 异常会导致模型产生以下三种灾难行为幻觉补偿Hallucination下游返回500 Internal Server Error大模型没有理解这是系统故障反而误认为“没有查到数据但业务需要闭环”于是自己臆造数据回答用户“您的订单已于今日上午发出单号为 SF10001889……”。入参狂躁重试Retry Loop当接口报错原因是必填参数缺失或格式不合规时如果返回的错误信息是晦涩的堆栈模型无法理解为什么失败会执着地用原参数反复发起 Tool 调用直到打满 Agent 的max-iterations最大迭代轮数白白耗费几十万 Token。推卸责任与恐慌暴露接口抛出的 Java 异常堆栈、SQL 报错甚至包含数据库 IP 的信息被大模型一股脑吐给了终端用户既不安全又显得极不专业。核心思想把系统错误翻译为“模型认知指令”治理的核心原则非常明确大模型不是程序员不要把原始异常堆栈喂给它大模型是推理引擎必须把工程异常翻译成模型能理解的业务指令。我们将 Tool 执行结果统一划分为三类并制定不同的反馈策略异常类别典型场景对模型的系统提示策略期望模型的行为临时故障可重试网络超时、503、网关抖动明确告知是瞬时抖动建议间隔 1 秒重试 1 次上限 2 次控制节奏重试若超限则降级安抚用户入参错误可修复日期格式不符、手机号少一位指出哪个字段不符合规范要求模型修正参数后重调修正入参并再次调用 Tool业务终态不可重试用户不存在、权限不足、余额不足明确告知为业务规则拦截严禁再次调用该工具终止 Tool 调用直接向用户解释原因生产级 Tool 异常包装器设计在 Spring AI 体系中每个工具调用都会走ToolCallback逻辑。我们可以实现一个统一的 AOP 切面或装饰器将任何底层的 RPC / HTTP 异常进行兜底拦截转化为自然语言形式的“智能引导提示词”。以下是我们生产环境采用的工具调用防御拦截器package com.yali.ai.tool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * 生产级工具调用异常自愈与引导包装器 */ Component public class ResilientToolAdvisor { private static final Logger log LoggerFactory.getLogger(ResilientToolAdvisor.class); // 记录单次会话内每个 Tool 的连续失败次数防止无限循环 private final ConcurrentHashMapString, AtomicInteger retryCounter new ConcurrentHashMap(); private static final int MAX_TOOL_RETRY 2; public String executeWithGuidance(String conversationId, String toolName, String inputJson, ToolExecutor executor) { String counterKey conversationId : toolName; AtomicInteger counter retryCounter.computeIfAbsent(counterKey, k - new AtomicInteger(0)); try { // 执行真实的 RPC 业务调用 String result executor.execute(inputJson); // 成功后重置计数器 counter.set(0); return result; } catch (RpcTimeoutException | Gateway503Exception e) { int currentFailures counter.incrementAndGet(); log.warn(工具 [{}] 发生网络抖动, 当前第 {} 次失败, convId: {}, toolName, currentFailures, conversationId); if (currentFailures MAX_TOOL_RETRY) { // 熔断强行掐断重试 counter.set(0); return String.format( [SYSTEM ERROR]: 外部服务 [%s] 发生严重网络故障且连续重试失败。 【给模型的强制指令】严禁再次调用该接口严禁编造任何虚假数据 请直接委婉告知用户“当前系统网络繁忙暂时无法查询请稍后 5 分钟再试”。 , toolName); } // 优雅引导重试 return String.format( [SYSTEM WARNING]: 调用外部服务 [%s] 时出现瞬时超时错误。 【给模型的指令】这只是临时的网络抖动底层数据依然有效。 请检查你的参数是否正确如果确认无误你可以重新尝试调用一次该工具如果不需要请向用户说明稍后重试。 , toolName); } catch (IllegalArgumentException e) { log.warn(工具 [{}] 入参校验未通过: {}, toolName, e.getMessage()); return String.format( [PARAM ERROR]: 工具入参错误%s。 【给模型的指令】请检查你传递给工具的 JSON 参数纠正对应的字段格式后重新调用。 , e.getMessage()); } catch (Exception e) { log.error(工具 [{}] 发生未知致命异常, toolName, e); counter.set(0); return String.format( [FATAL ERROR]: 后台服务发生未预期的系统故障错误摘要%s。 【给模型的强制指令】操作无法继续完成。请终止任务并向用户表达歉意。 , e.getMessage()); } } FunctionalInterface public interface ToolExecutor { String execute(String params) throws Exception; } }配合系统 Prompt 筑牢防御网光在 Tool 返回值里加提示还不够大模型的遵循能力受制于整体上下文。我们在系统 System Prompt 中加入了一条全局防御公约工具交互基本守则当工具返回结果包含[SYSTEM ERROR]或[FATAL ERROR]时代表外部世界已无法响应你必须立刻停止调用工具严禁自行编造结果。当遇到网络不可用时如实向用户反馈系统繁忙承认系统故障不是你的劣势编造虚假信息是对用户最大的伤害。对同一个工具的调用在单轮对话中不得超过 3 次超过即视为任务失败。在双重机制的保障下线上由于下游微服务抖动导致的 Agent 幻觉率直接下降了 95% 以上接口死循环调用问题彻底绝迹。鸭梨的避坑忠告在做架构设计时千万不要把大模型当成绝对理性的代码逻辑单元。大模型本质上是一个具备发散思维和强联想能力的概率预测机。把一个抛出 NPE 或 500 的“工程废料”直接扔给它它就会在概率的迷雾里展开天马行空的想象。工程团队的职责就是给这台概率机器铺上平整坚固的导轨用严谨的引导提示词、熔断计数器和状态机把不确定的故障收敛到确定的业务兜底逻辑中。能把异常收敛好你的 Agent 系统才算真正跨过了玩具阶段。