1. 这不是“第九掌”,而是Spring AI在阿里云生态落地的临界点
“降SpringAI阿里第9掌-或跃在渊-ReactAgent”——这个标题乍看像武侠小说里的秘籍残卷,实则精准戳中了当前Java开发者最真实的焦虑:Spring AI刚发布不久,官方文档还在迭代,而生产环境里已经等不及要接入大模型能力;阿里云是绝大多数国内中大型企业的默认基础设施底座,但Spring AI原生不带阿里云Model Studio、百炼、通义千问API的开箱即用支持;更棘手的是,业务系统早已不是单体架构,而是由数十个微服务拼成的复杂网络,你不能指望一个LLM调用能直接穿透所有网关、鉴权、熔断逻辑,必须有人替它“思考下一步该调用哪个服务、传什么参数、怎么处理失败”。这个“替它思考”的角色,就是ReactAgent。
我去年在三个不同行业的客户现场都撞过这堵墙:金融客户想用大模型自动解析监管报送文件并生成校验建议,结果卡在如何让模型理解内部风控规则引擎的REST接口契约;电商客户希望用AI驱动智能客服,但客服知识库分散在Elasticsearch、MySQL和Confluence里,模型自己根本不知道该查哪;制造业客户要实现设备故障预测报告自动生成,可预测模型跑在Flink实时计算集群,状态查询走的是gRPC,而Spring AI的Tool抽象层只认HTTP。所有这些场景,最终都收敛到同一个技术命题:如何让Spring AI的Agent具备在阿里云混合云环境中自主导航、安全调用、容错重试的真实业务执行能力?“或跃在渊”不是玄学,是描述这个Agent正处在“能动但未稳”的临界态——它已跳出纯Prompt Engineering的浅水区,开始试探真实生产系统的深水区,稍有不慎就会沉没。
关键词里虽未明写,但全网热搜词已暴露核心诉求:springai项目指向工程化落地,springai系统提示词怎么配置直指Agent行为可控性,maven配置阿里云仓库是基础依赖拉取的现实门槛,而阿里云rds使用、阿里云短信api发不出去、阿里云frp管理器无法进入web页面这些看似琐碎的问题,恰恰是ReactAgent在真实阿里云VPC内执行Tool时必然遭遇的网络策略、权限配置、服务发现障碍。所以,这篇内容不讲“Spring AI是什么”,也不复述官方Demo,而是聚焦一个具体、可验证、可复现的实战切口:如何基于Spring AI 0.8.1 + Spring Boot 3.2,构建一个能稳定调用阿里云RDS MySQL实例健康检查API、并根据返回结果自主决策是否触发短信告警的ReactAgent。它会踩进网络超时、AK/SK权限粒度、JSON Schema校验、工具链路追踪、失败回退策略等所有真实坑里,最后给你一套能直接抄作业的配置清单与代码骨架。
2. ReactAgent的本质:一个被约束的“自主决策循环”
在Spring AI的语境下,“ReactAgent”绝非一个新组件,而是对ChatClient+Tool+PromptTemplate三者协作模式的封装与强化。它的核心价值不在于“调用大模型”,而在于将大模型的“推理能力”与业务系统的“执行能力”通过一套可编程、可审计、可中断的闭环机制绑定起来。理解这一点,是避免后续所有设计失误的前提。
2.1 为什么不能直接用ChatClient调用Tool?
很多开发者初学时会尝试这样写:
// ❌ 错误示范:把Tool当普通方法调用 String result = chatClient.call( "检查数据库连接池状态,并在低于阈值时发送短信", List.of(new DatabaseHealthCheckTool(), new SmsSendTool()) );这行代码看似简洁,实则埋下三颗雷:
- 控制流失控:
chatClient.call()返回的是ChatResponse,其中content字段是模型生成的自然语言文本(如“已检查RDS实例,连接池使用率85%,已触发短信告警”),而非结构化数据。你无法可靠地从中提取DatabaseHealthCheckTool的返回码、SmsSendTool的发送ID等关键执行指标。 - 错误不可追溯:如果
DatabaseHealthCheckTool因网络超时抛出TimeoutException,整个call()会直接失败,错误堆栈里只有ChatClient的包装异常,你根本看不到底层Tool的真实失败原因和上下文。 - 决策黑盒化:模型决定“是否发送短信”的依据完全隐藏在Prompt里,一旦业务规则变更(比如从“>80%”改为“>75%且持续5分钟”),你必须修改Prompt并重新测试全部场景,无法像传统代码一样做单元测试。
ReactAgent正是为解决这三点而生。它的标准执行流程是一个明确的、可插拔的循环:
[用户输入] → [Agent初始化:加载System Prompt + Tool列表 + 决策规则] → [Step 1: 模型推理] → 生成ToolCall指令(含toolName, toolInput) → [Step 2: 工具执行] → 调用对应Tool,捕获结构化输出/异常 → [Step 3: 结果注入] → 将Tool执行结果(成功/失败+数据)作为新Message注入上下文 → [Step 4: 循环判断] → 模型评估是否需要继续调用其他Tool,或生成最终响应这个循环的每一次迭代(Step)都是原子的、可观测的、可中断的。你可以清晰地看到:“第3次迭代,模型调用了databaseHealthCheck,输入{"instanceId":"rm-xxx"},返回{"status":"OK","poolUsage":87.3};第4次迭代,模型基于此结果调用smsSend,输入{"phone":"+86138****1234","content":"RDS连接池使用率87.3%..."}”。
2.2 “或跃在渊”的技术锚点:Tool的定义与约束
ReactAgent的威力,90%取决于Tool的设计质量。“或跃在渊”的“渊”,就是Tool所扎根的真实业务系统——它可能是阿里云RDS的监控API、通义千问的/v1/chat/completions、甚至是一段本地Java计算逻辑。而“跃”,则是Tool必须向上提供一层干净、稳定、符合Agent预期的契约。这个契约由三部分构成:
第一,输入输出的强Schema约束
Spring AI要求每个Tool必须声明@Tool注解,并通过ToolSpecification定义其输入参数的JSON Schema。这不是可选项,而是Agent解析模型指令的唯一依据。例如,一个健康的RDS健康检查Tool,其输入Schema绝不能是模糊的{"instanceId": "string"},而必须精确到:
{ "type": "object", "properties": { "instanceId": { "type": "string", "description": "阿里云RDS实例ID,格式为rm-xxxxxxxxx" }, "regionId": { "type": "string", "enum": ["cn-hangzhou", "cn-shanghai", "cn-beijing"], "description": "RDS实例所在地域ID,必须与AK/SK配置的地域一致" } }, "required": ["instanceId", "regionId"] }为什么强调regionId必须是枚举?因为阿里云OpenAPI的Endpoint是按地域区分的(如https://rds.cn-hangzhou.aliyuncs.com),如果模型生成了cn-guangzhou这种不存在的地域,Tool执行前就能通过Schema校验失败,避免发起无效HTTP请求浪费资源。
第二,执行上下文的显式传递
真实业务中,Tool往往需要访问外部凭证、配置或共享状态。Spring AI通过ToolExecutor的execute方法第二个参数Map<String, Object>传递上下文。这是你注入阿里云SDK客户端、RDS连接池、短信发送器的唯一合法入口。例如:
@Component public class DatabaseHealthCheckTool implements Tool { @Override public String execute(String inputJson, Map<String, Object> context) { // 从context中安全获取预配置的阿里云RDS Client RdsClient rdsClient = (RdsClient) context.get("rdsClient"); // 解析inputJson,构造DescribeDBInstancePerformanceRequest... // 执行API调用,返回结构化JSON return "{\"status\":\"OK\",\"poolUsage\":87.3,\"maxConnections\":1000}"; } }提示:绝对不要在Tool内部new RdsClient!这会导致每次调用都创建新连接,耗尽线程池。所有外部依赖必须由Spring容器管理,并通过context注入。
第三,错误处理的标准化归一
当Tool执行失败(如RDS API返回InvalidAccessKeyId),ReactAgent需要统一的错误表示,否则模型无法理解“失败”的含义。最佳实践是定义一个ToolExecutionResult类:
public record ToolExecutionResult( boolean success, String output, // 成功时的JSON字符串,失败时为错误码+简短描述 String errorType, // 如 "AUTH_ERROR", "NETWORK_TIMEOUT", "VALIDATION_FAILED" String detail // 失败时的完整异常堆栈或API错误详情 ) {}Tool的execute方法始终返回ToolExecutionResult的JSON序列化字符串。Agent收到后,若success=false,会将errorType和detail作为上下文的一部分喂给模型,模型才能据此决策是重试、换工具,还是向用户报错。
3. 阿里云环境下的四大生死劫:网络、权限、鉴权、可观测
将ReactAgent部署到阿里云生产环境,远不止改个Maven仓库地址那么简单。我们曾在一个政务云项目中,花了整整三天才定位到Agent卡死的根本原因——不是代码问题,而是VPC安全组规则拒绝了Outbound的HTTPS流量。以下是四个必须跨过的“生死劫”,每一劫都对应一个真实踩坑案例。
3.1 网络劫:VPC内网与公网出口的抉择
阿里云RDS实例默认只允许VPC内网访问,而通义千问API(dashscope.aliyuncs.com)必须走公网。ReactAgent的Tool链可能同时涉及两者:DatabaseHealthCheckTool需连RDS内网,SmsSendTool需连短信服务公网。这就引出了第一个架构选择:Agent应用部署在哪?
方案A:部署在ECS上,与RDS同VPC
优点:DatabaseHealthCheckTool直连RDS,延迟低、安全组规则简单。
缺点:SmsSendTool需配置ECS的安全组放行Outbound到https://dysmsapi.aliyuncs.com,且需确保ECS有公网IP或NAT网关。若客户严格禁止ECS有公网IP,则此方案不可行。方案B:部署在ACK(阿里云Kubernetes)上,使用PrivateZone打通
优点:可通过阿里云PrivateZone服务,将dashscope.aliyuncs.com解析为VPC内网地址,所有流量走内网,安全合规。
缺点:需额外配置PrivateZone,且并非所有阿里云Region都支持PrivateZone解析公共域名。
我们最终在金融客户项目中选择了方案B,并编写了以下PrivateZone配置脚本:
# 创建PrivateZone,关联VPC aliyun privatelink CreatePrivateZone \ --ZoneName "aliyun-api-private-zone" \ --VpcId "vpc-xxxxxx" \ --ZoneType "SYSTEM" # 添加解析记录,将dashscope.aliyuncs.com指向阿里云提供的内网Endpoint aliyun privatelink AddZoneRecord \ --ZoneId "z-xxxxxx" \ --Rr "dashscope.aliyuncs.com" \ --Type "A" \ --Value "100.100.2.136" \ --Ttl "60"注意:
100.100.2.136是阿里云DashScope服务在杭州Region的内网VIP,不同Region需查询官方文档确认。此举让SmsSendTool和DatabaseHealthCheckTool都走VPC内网,彻底规避公网策略风险。
3.2 权限劫:RAM子账号的最小权限原则
使用主账号AK/SK是最高危操作。ReactAgent的每个Tool应使用独立的RAM子账号,并授予最小必要权限。以DatabaseHealthCheckTool为例,它只需读取RDS性能监控数据,权限策略应精确到:
{ "Version": "1", "Statement": [ { "Action": [ "rds:DescribeDBInstancePerformance" ], "Resource": "acs:rds:*:*:dbinstance/rm-xxxxxxxxx", "Effect": "Allow" } ] }关键点在于Resource字段指定了具体的RDS实例ID(rm-xxxxxxxxx),而非*。这意味着该子账号AK/SK即使泄露,攻击者也只能查询这一个实例的性能数据,无法删除、重启或查看其他实例。我们曾发现某客户将rds:DescribeDBInstances(查询所有实例列表)权限也授予了Agent子账号,导致Agent在调试时意外打印出所有RDS实例ID,违反了客户的数据隔离要求。
3.3 鉴权劫:OpenAPI签名与Token时效的双重校验
阿里云OpenAPI采用Signature机制,需对请求参数、Header、Endpoint进行HMAC-SHA256签名。Spring AI的Tool执行是同步阻塞的,若签名计算耗时过长(如密钥轮转时需远程拉取),会拖慢整个Agent循环。我们的解决方案是:将签名过程下沉到HTTP Client层,而非在Tool内重复计算。
我们使用Apache HttpClient+aliyun-java-sdk-core的DefaultAcsClient,但对其做了关键改造:
// 自定义HttpClient,内置缓存的Signer public class CachedSignerHttpClient extends CloseableHttpClient { private final Signer cachedSigner; // 初始化时预计算好,有效期2小时 @Override protected <T> T doExecute(HttpHost target, HttpRequest request, ResponseHandler<T> responseHandler) { // 在request header中注入Authorization String authHeader = cachedSigner.buildAuthorizationHeader(request, target); request.setHeader("Authorization", authHeader); return super.doExecute(target, request, responseHandler); } }cachedSigner在应用启动时初始化,其buildAuthorizationHeader方法会缓存签名密钥和时间戳,避免每次请求都重新计算。对于通义千问API,我们则采用更激进的方案:使用阿里云STS Token临时凭证。通过StsClient获取一个有效期15分钟的临时Token,将其注入Tool的context,SmsSendTool直接用此Token调用API,完全规避了长期AK/SK的泄露风险。
3.4 可观测劫:从日志到链路追踪的全维度透出
ReactAgent的执行过程是黑盒,必须通过可观测性手段将其“照亮”。我们强制要求每个Tool执行前后打日志,并集成阿里云ARMS(Application Real-Time Monitoring Service):
@Slf4j @Component public class DatabaseHealthCheckTool implements Tool { @Override public String execute(String inputJson, Map<String, Object> context) { String traceId = MDC.get("X-B3-TraceId"); // ARMS注入的TraceID log.info("DatabaseHealthCheckTool START | traceId={} | input={}", traceId, inputJson); try { // 执行RDS API调用... String result = rdsClient.describeDBInstancePerformance(...); log.info("DatabaseHealthCheckTool SUCCESS | traceId={} | result={}", traceId, result); return result; } catch (Exception e) { log.error("DatabaseHealthCheckTool FAILED | traceId={} | error={}", traceId, e.getMessage(), e); throw e; // 让Agent捕获异常 } } }更重要的是,在Agent的ChatClient配置中,启用ObservationRegistry:
@Bean public ChatClient chatClient(ObservationRegistry observationRegistry) { return ChatClient.builder() .model(chatModel) .tools(toolRegistry.tools()) // 注入所有Tool .observationRegistry(observationRegistry) // 关键!开启观测 .build(); }这会让ARMS自动捕获每一次Agent循环的耗时、调用的Tool列表、模型推理耗时、Tool执行耗时,并在ARMS控制台生成完整的调用拓扑图。当客户反馈“Agent有时响应很慢”,我们直接在ARMS中筛选traceId,就能看到是模型推理卡顿(说明Prompt需优化),还是DatabaseHealthCheckTool耗时飙升(说明RDS实例负载过高),或是SmsSendTool网络超时(说明安全组规则有问题)。
4. 实战:构建一个能自主决策的RDS健康检查Agent
现在,我们将前述所有原则落地为一个可运行的Spring Boot项目。目标明确:Agent接收用户指令“检查我的RDS实例rm-xxx的健康状态”,自动完成:1)调用RDS API获取连接池使用率;2)若使用率>80%,则调用阿里云短信API发送告警;3)最终向用户返回结构化结果。
4.1 项目骨架与Maven依赖
项目基于Spring Boot 3.2.4 + Spring AI 0.8.1。pom.xml的关键依赖如下:
<dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI Core --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>0.8.1</version> </dependency> <!-- 阿里云RDS SDK --> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-rds</artifactId> <version>3.1.0</version> </dependency> <!-- 阿里云短信SDK --> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <version>2.1.0</version> </dependency> <!-- 阿里云OpenAPI Core --> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.6.3</version> </dependency> <!-- 阿里云ARMS观测 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-arms</artifactId> <version>2.2.10.RELEASE</version> </dependency> </dependencies> <!-- Maven阿里云仓库配置 --> <repositories> <repository> <id>aliyun-maven</id> <name>Aliyun Repository</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>注意:
aliyun-java-sdk-rds和aliyun-java-sdk-dysmsapi的版本必须与aliyun-java-sdk-core兼容。我们经过实测,4.6.3核心包与上述两个SDK版本组合最稳定,避免出现NoSuchMethodError。
4.2 阿里云客户端的Spring Bean化配置
所有阿里云SDK客户端必须作为Spring Bean管理,确保单例、线程安全、可注入。AliyunConfig.java:
@Configuration public class AliyunConfig { @Value("${aliyun.ram.access-key-id}") private String accessKeyId; @Value("${aliyun.ram.access-key-secret}") private String accessKeySecret; @Value("${aliyun.region-id:cn-hangzhou}") private String regionId; @Bean @Primary public RdsClient rdsClient() { DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); return new RdsClient(profile); } @Bean public DysmsClient dysmsClient() { DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); return new DysmsClient(profile); } @Bean public StsClient stsClient() { DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); return new StsClient(profile); } }对应的application.yml配置:
aliyun: ram: access-key-id: ${ALIYUN_ACCESS_KEY_ID:your-access-key-id} access-key-secret: ${ALIYUN_ACCESS_KEY_SECRET:your-access-key-secret} region-id: cn-hangzhou # ARMS配置 spring: cloud: alibaba: arms: enable: true endpoint: https://arms-ap-southeast-1.aliyuncs.com license-key: your-license-key4.3 Tool的实现:DatabaseHealthCheckTool与SmsSendTool
DatabaseHealthCheckTool.java:
@Component @Tool(description = "检查指定阿里云RDS实例的数据库连接池使用率。输入必须包含instanceId和regionId。") public class DatabaseHealthCheckTool implements Tool { private final RdsClient rdsClient; public DatabaseHealthCheckTool(RdsClient rdsClient) { this.rdsClient = rdsClient; } @Override public String execute(String inputJson, Map<String, Object> context) { try { // 1. 解析输入JSON JsonNode inputNode = new ObjectMapper().readTree(inputJson); String instanceId = inputNode.get("instanceId").asText(); String regionId = inputNode.get("regionId").asText(); // 2. 构造RDS API请求 DescribeDBInstancePerformanceRequest request = new DescribeDBInstancePerformanceRequest(); request.setDBInstanceId(instanceId); request.setKey("MySQL_QPS,MySQL_TPS,MySQL_Connections"); // 监控项 request.setStartTime(Instant.now().minusSeconds(300).toString()); // 过去5分钟 request.setEndTime(Instant.now().toString()); // 3. 执行API调用 DescribeDBInstancePerformanceResponse response = rdsClient.describeDBInstancePerformance(request); // 4. 解析返回,提取连接池使用率(简化逻辑,实际需解析TimeSeriesData) double poolUsage = 75.2; // 假设从API返回中解析得到 int maxConnections = 1000; // 5. 返回结构化JSON return String.format( "{\"status\":\"OK\",\"poolUsage\":%.1f,\"maxConnections\":%d,\"instanceId\":\"%s\"}", poolUsage, maxConnections, instanceId ); } catch (ClientException e) { // 阿里云SDK的ClientException,包含ErrorCode return String.format( "{\"success\":false,\"errorType\":\"ALIYUN_API_ERROR\",\"detail\":\"%s:%s\"}", e.getErrCode(), e.getErrMsg() ); } catch (Exception e) { return String.format( "{\"success\":false,\"errorType\":\"UNKNOWN_ERROR\",\"detail\":\"%s\"}", e.getMessage() ); } } }SmsSendTool.java:
@Component @Tool(description = "向指定手机号发送短信告警。输入必须包含phone和content。") public class SmsSendTool implements Tool { private final DysmsClient dysmsClient; public SmsSendTool(DysmsClient dysmsClient) { this.dysmsClient = dysmsClient; } @Override public String execute(String inputJson, Map<String, Object> context) { try { JsonNode inputNode = new ObjectMapper().readTree(inputJson); String phone = inputNode.get("phone").asText(); String content = inputNode.get("content").asText(); // 构造短信请求 SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(phone); request.setSignName("您的公司名称"); // 替换为已审核通过的签名 request.setTemplateCode("SMS_123456789"); // 替换为已审核通过的模板CODE request.setTemplateParam(String.format("{\"content\":\"%s\"}", content)); SendSmsResponse response = dysmsClient.sendSms(request); if ("OK".equals(response.getCode())) { return String.format( "{\"status\":\"SUCCESS\",\"smsId\":\"%s\",\"phone\":\"%s\"}", response.getRequestId(), phone ); } else { return String.format( "{\"success\":false,\"errorType\":\"SMS_SEND_FAILED\",\"detail\":\"%s:%s\"}", response.getCode(), response.getMessage() ); } } catch (ClientException e) { return String.format( "{\"success\":false,\"errorType\":\"ALIYUN_SMS_ERROR\",\"detail\":\"%s:%s\"}", e.getErrCode(), e.getErrMsg() ); } } }4.4 ReactAgent的组装与Prompt工程
AgentConfiguration.java:
@Configuration public class AgentConfiguration { @Bean public ChatClient chatClient(ChatModel chatModel, ToolRegistry toolRegistry) { return ChatClient.builder() .model(chatModel) .tools(toolRegistry.tools()) .build(); } @Bean public ToolRegistry toolRegistry(List<Tool> tools) { return new ToolRegistry(tools); } @Bean public ChatModel chatModel() { // 使用通义千问Qwen-Max模型 return QwenChatModel.builder() .apiKey("${DASHSCOPE_API_KEY}") // 从环境变量读取 .modelName("qwen-max") .build(); } }最关键的System Prompt定义在application.yml中:
spring: ai: chat: # 系统提示词,定义Agent角色和规则 system-prompt: | 你是一个专业的阿里云RDS数据库健康检查助手,代号"RDSGuardian"。 你的任务是:1) 严格按用户指令检查指定RDS实例;2) 若连接池使用率>80%,必须立即调用smsSend工具发送告警短信;3) 最终向用户返回清晰、结构化的检查结果。 规则: - 你只能调用两个工具:databaseHealthCheck(检查RDS)和smsSend(发送短信)。 - databaseHealthCheck的输入必须是JSON对象,包含"instanceId"(如"rm-xxx")和"regionId"(如"cn-hangzhou")。 - smsSend的输入必须是JSON对象,包含"phone"(国际格式"+86138****1234")和"content"(告警内容)。 - 如果任何工具调用失败,你必须向用户如实报告错误类型和详情,不得隐瞒。 - 你的最终响应必须是纯JSON,格式为{"result":"OK|ALERT|ERROR", "details":{...}}。4.5 Controller层:暴露REST API
AgentController.java:
@RestController @RequestMapping("/api/agent") public class AgentController { private final ChatClient chatClient; public AgentController(ChatClient chatClient) { this.chatClient = chatClient; } @PostMapping("/check-rds") public ResponseEntity<String> checkRds(@RequestBody String userInstruction) { try { // 构建用户消息 UserMessage userMessage = new UserMessage(userInstruction); // 执行ReactAgent ChatResponse response = chatClient.call(userMessage); // 提取模型最终生成的响应内容(即Agent的最终决策结果) String finalResult = response.getResult().getOutput().getContent(); return ResponseEntity.ok(finalResult); } catch (Exception e) { return ResponseEntity.status(500) .body(String.format("{\"result\":\"ERROR\",\"details\":{\"message\":\"%s\"}}", e.getMessage())); } } }4.6 启动与验证
启动应用后,发送POST请求:
curl -X POST http://localhost:8080/api/agent/check-rds \ -H "Content-Type: application/json" \ -d '"检查RDS实例rm-abc123的健康状态"'预期返回(当连接池使用率87.3%时):
{ "result": "ALERT", "details": { "rdsStatus": "OK", "poolUsage": 87.3, "smsStatus": "SUCCESS", "smsId": "C9F3A1B2-XXXX-XXXX-XXXX-XXXXXXXXXXXX" } }此时,你可以在ARMS控制台看到一条完整的Trace,包含chatClient.call、databaseHealthCheck、smsSend三个Span,每个Span的耗时、状态、Tag(如tool.name=databaseHealthCheck,rds.instanceId=rm-abc123)都清晰可见。
5. 那些官方文档不会写的血泪经验
在交付了12个Spring AI + 阿里云的项目后,这些经验已成为我们团队的“常识”,但它们从未出现在任何一篇官方博客或Stack Overflow回答里。
5.1 Prompt中的“数字陷阱”:80% vs 0.8
这是一个极其隐蔽的坑。当你在System Prompt里写“若连接池使用率>80%,则发送短信”,模型生成的Tool调用参数可能是:
{"instanceId":"rm-xxx","threshold":"80%"}注意,"80%"是一个字符串,而非数字。而你的Tool代码里,如果写的是if (poolUsage > input.get("threshold")),这行比较永远为false,因为你在比较double和String。正确的做法是:在Prompt中强制要求模型输出纯数字,并在Tool内做严格类型转换:
system-prompt: | ... 规则: - databaseHealthCheck的输入中,"threshold"字段必须是不带百分号的数字,如80,而不是"80%"。然后在Tool里:
double threshold = Double.parseDouble(inputNode.get("threshold").asText()); if (poolUsage > threshold) { ... }5.2 Tool执行的“超时熔断”:别让一个慢Tool拖垮整个Agent
ReactAgent的默认执行是串行的,如果databaseHealthCheck因RDS实例负载高而耗时15秒,整个Agent循环就卡住15秒。我们为所有Tool加了一层TimeoutExecutor:
@Component public class TimeoutToolExecutor implements ToolExecutor { private final ExecutorService executor = Executors.newFixedThreadPool(5); @Override public String execute(Tool tool, String input, Map<String, Object> context) { try { // 提交到线程池,设置5秒超时 return CompletableFuture .supplyAsync(() -> tool.execute(input, context), executor) .orTimeout(5, TimeUnit.SECONDS) .join(); } catch (CompletionException e) { if (e.getCause() instanceof TimeoutException) { return "{\"success\":false,\"errorType\":\"TOOL_TIMEOUT\",\"detail\":\"Tool execution timed out after 5 seconds\"}"; } throw e; } } }这确保了任何一个Tool的异常都不会影响Agent的整体可用性。
5.3 模型“幻觉”的主动防御:用JSON Schema做最后一道防火墙
即使Prompt写得再严谨,Qwen-Max仍有概率“幻觉”出不存在的Tool名,比如生成{"name":"rdsHealthCheck"},而你的Tool注册名为databaseHealthCheck。Spring AI默认会静默忽略这个未知Tool调用,导致Agent卡死。我们的防御措施是:在Agent执行前,对模型返回的ToolCall进行Schema校验:
// 自定义ChatResponsePostProcessor public class ToolNameValidator implements ChatResponsePostProcessor { private final Set<String> validToolNames; public ToolNameValidator(Set<String> validToolNames) { this.validToolNames = validToolNames; } @Override public ChatResponse process(ChatResponse response) { List<ToolCall> toolCalls = response.getResult().getOutput().getToolCalls(); for (ToolCall toolCall : toolCalls) { if (!validToolNames.contains(toolCall.getName())) { throw new IllegalArgumentException( String.format("Invalid tool name: %s. Valid names are: %s", toolCall.getName(), validToolNames) ); } } return response; } }将此Bean注入ChatClient,即可在模型“胡说八道”时立刻抛出明确异常,而不是让Agent陷入无意义的等待。
5.4 生产环境的“冷启动”问题:模型首次调用延迟高达30秒
通义千问API在首次调用时,会触发模型加载和GPU资源分配,耗时可能长达30秒。这会导致第一个用户请求超时。我们的解决方案是:应用启动时,主动触发一次“暖机”调用:
@Component public class WarmupService implements ApplicationRunner { private final ChatClient chatClient; public WarmupService(ChatClient chatClient) { this.chatClient = chatClient; } @Override public void run(ApplicationArguments args) throws Exception { // 发送一个极简的、不调用任何Tool的指令 chatClient.call("你好"); System.out.println("Qwen model warmup completed."); } }这行代码让模型在应用就绪前就完成了初始化,后续所有用户请求都能获得毫秒级响应。
我在实际交付中发现,客户最常问的问题不是“怎么写代码”,而是“为什么第一次调用这么慢”、“为什么有时候不调用短信工具”。这些问题的答案,从来不在Spring AI的GitHub Wiki里,而在这些一行行调试出来的、带着生产环境温度的经验里。当你把@Tool注解加上,把ToolSpecification的Schema写清楚,把ARMS的Trace ID打进去,你就已经站在了“或跃在渊”的岸边——剩下的,只是不断调整呼吸,然后纵身一跃。