Agent 在本地开发环境跑得好好的上了生产突然就出问题了——死循环狂刷 Token 把月度预算在两小时内消耗殆尽某个外部 API 响应慢Agent 卡住 30 秒什么都返回不了模型明明没调用任何工具却在回答里写了一串假数字。这些都是我或者我认识的朋友们真实踩过的坑。所以上线前有几个工程问题是必须解决的没有例外。一、防死循环——最大迭代次数Agent 最大的风险之一就是陷入循环模型反复调用同一个工具拿不到想要的答案或者拿到了答案但就是不结束一直在那转圈。我见过一个真实案例一个 Agent 的工具返回了“查询失败请重试”模型看到这个提示就真的重试了然后又失败又重试……最后用了七八十次工具调用才因为 Token 耗尽强制停下来。那次的 API 费用让团队老板直接问“这玩意儿怎么这么贵”。必须设置最大工具调用次数这是没有任何例外的硬性要求。Spring AI 默认允许工具多次调用但没有内置硬性上限。我们可以用 Advisor 机制给它加上package com.jichi.agent.advisor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; Aspect Component public class MaxIterationsAdvisor { private static final int DEFAULT_MAX_ITERATIONS 10; private static final ThreadLocalAtomicInteger CALL_COUNT ThreadLocal.withInitial(AtomicInteger::new); public static void reset() { CALL_COUNT.remove(); } Around(annotation(org.springframework.ai.tool.annotation.Tool)) public Object limitToolCalls(ProceedingJoinPoint joinPoint) throws Throwable { int count CALL_COUNT.get().incrementAndGet(); if (count DEFAULT_MAX_ITERATIONS) { CALL_COUNT.remove(); throw new AgentMaxIterationsException( Agent 超过最大工具调用次数 DEFAULT_MAX_ITERATIONS 任务中止); } return joinPoint.proceed(); } }在 Agent 服务入口每次任务开始前重置计数器MaxIterationsAdvisor.reset(); String result chatClient.prompt().user(message).call().content();上限设多少合适我的经验是简单查询 3-5 次分析类任务 8-12 次超过 15 次的场景要认真想想是不是任务太复杂应该拆开来处理。超过这个数字往往说明 Agent 在原地转圈不是在真的推进任务。依赖提醒Aspect需要spring-boot-starter-aop检查 pom.xml 里有没有这一行没有的话加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency二、超时控制工具调用可能因为外部依赖卡住——数据库慢查询、第三方 API 超时、网络抖动。不加超时整个请求就在那挂着线程占着不放积少成多就把线程池耗尽了。我推荐双层超时工具级别控制单次调用任务级别控制整体执行。工具级别超时——在工具方法里直接加Component public class TimeoutAwareTools { private final ExecutorService executor Executors.newCachedThreadPool(); Tool(description 查询外部天气 API获取实时天气数据) public String getWeatherFromApi(ToolParam(description 城市名) String city) { FutureString future executor.submit(() - callExternalWeatherApi(city)); try { return future.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); return 天气服务响应超时5秒无法获取实时数据请告知用户稍后重试; } catch (Exception e) { return 天气服务异常 e.getMessage(); } } }重要细节工具超时后返回字符串而不是抛异常。返回字符串的话模型能看到这个提示并做出合理判断比如告诉用户服务暂时不可用抛出异常的话整个 Agent 任务会直接崩掉。任务级别总超时——在 Controller 层加PostMapping public ResponseEntityString run(RequestBody TaskRequest request) { try { String result CompletableFuture .supplyAsync(() - agent.run(request.task())) .get(60, TimeUnit.SECONDS); return ResponseEntity.ok(result); } catch (TimeoutException e) { return ResponseEntity.status(408) .body(任务执行超时60秒请拆分任务后重试); } }推荐配置工具级别 5-10 秒任务级别 30-60 秒。三、Token 成本控制Agent 的多轮工具调用会快速积累 Token——每一轮都要把完整的对话历史发给模型工具调用次数越多Token 消耗越快。我见过一个查询任务因为触发了太多工具调用单次请求消耗了 2 万多 Token成本相当于几十次普通问答。加 Token 监控和预算限制。public class TokenBudgetAspect { private static final int MAX_TOKENS 8000; // 根据你的模型上下文和预算设置 private int totalTokens 0; Around(annotation(org.springframework.ai.tool.annotation.Tool)) public Object checkBudget(ProceedingJoinPoint pjp) throws Throwable { // 在每次模型调用后累计 tokens超过预算则抛出异常 // 实际获取 tokens 需要从 ChatModel 的响应中提取 } }Token 预算怎么定建议先跑一批正常任务看 p95 消耗是多少然后把预算设在 p95 的 1.5-2 倍。Token 监控数据一定要落库或打到监控系统否则费用账单突然飙高的时候你完全不知道是哪个接口出了问题。四、幻觉检测与防护这个问题更隐蔽Agent 可能“假装”调用了工具然后在回答里直接编造工具结果。我第一次遇到这个问题是在测一个数据分析 Agent让它查某个商品上周的销量工具日志里明明没有查询记录但 Agent 的回答里写了一个非常具体的数字。问题是这个数字是假的还假得很像真的。如果这个回答流入了业务报表后果不堪设想。加入工具调用结果的一致性校验Component public class HallucinationGuard { private final ChatClient verifierClient; public HallucinationGuard(Qualifier(dashScopeChatModel) ChatModel chatModel) { this.verifierClient ChatClient.builder(chatModel) .defaultSystem( 你是一个数据一致性检查员。 检查 Agent 的回答是否和工具实际返回的数据一致。 如果回答里出现了工具数据里没有的具体数字或事实判定为不一致。 只输出 CONSISTENT 或 INCONSISTENT: [不一致的具体内容] ) .build(); } public boolean isConsistent(String toolResults, String finalAnswer) { String result verifierClient.prompt() .user(工具实际返回的数据\n toolResults \n\nAgent 的回答\n finalAnswer) .call() .content() .strip(); return result.startsWith(CONSISTENT); } }幻觉检测不需要对所有输出都做只在有精确数据要求的场景做比如财务报告、库存数字、价格信息。普通问答类的输出没必要做否则整体响应时间会拖得很长。五、错误分类与用户友好提示Agent 出错了用户应该看到什么不是技术报错而是一句他们能理解、知道下一步怎么办的话。把各类错误统一在 Controller 层处理PostMapping(/chat) public ResponseEntityAgentResponse chat(RequestBody ChatRequest request) { try { String result CompletableFuture .supplyAsync(() - agent.chat(request.message())) .get(60, TimeUnit.SECONDS); return ResponseEntity.ok(AgentResponse.success(result)); } catch (AgentMaxIterationsException e) { return ResponseEntity.ok(AgentResponse.error( 这个问题比较复杂建议拆分成几个小问题分别来问, MAX_ITERATIONS)); } catch (TimeoutException e) { return ResponseEntity.ok(AgentResponse.error( 处理时间较长已超出等待上限请稍后重试, TIMEOUT)); } catch (Exception e) { if (e.getMessage().contains(Token 预算)) { return ResponseEntity.ok(AgentResponse.error( 这个问题处理量超出单次限额请拆分成更小的问题, BUDGET_EXCEEDED)); } return ResponseEntity.status(500) .body(AgentResponse.error(服务暂时开了个小差请稍后重试, INTERNAL_ERROR)); } }用户收到的错误提示里有errorCode字段前端可以根据这个做对应处理——比如MAX_ITERATIONS可以弹出“任务拆分建议”TIMEOUT可以提示“稍后重试”。这比直接展示一个通用的“系统错误”体验好很多。六、生产 Checklist上线前过一遍这个清单每一条都是我血的教训□ 最大工具调用次数是否设置建议 5-15□ 工具方法是否有超时保护外部 API 调用建议 5-10 秒□ 整体任务是否有总超时建议 30-60 秒□ Token 消耗是否有监控和报警突然飙高说明某个 Agent 跑偏了□ 写操作工具是否有权限校验发邮件、改数据库之前验 userId 和权限□ 工具返回值是否处理了异常不要抛异常返回错误描述字符串□ 是否有请求日志记录 userId、任务内容、工具调用次数、耗时□ 是否有降级策略Agent 挂了有没有兜底回复□ 是否测试过工具连续失败的场景而不只是测试正常路径□ 系统提示是否明确了不能做什么防止模型越权操作这张清单每次新上 Agent 功能之前都要过一遍。我自己当初图省事跳过了“工具连续失败测试”这一条结果上线第二天某个 API 限流了Agent 循环调用了几十次那次账单让我印象深刻。七、小结Agent 不是玩具上生产之前必须做好工程化防护。死循环、超时、幻觉、成本失控……这些问题在本地开发时几乎遇不到但一旦上线它们会以最意想不到的方式跳出来。希望你看了这节之后能少踩几个坑。