从一次“模型只会聊天、不会干活”的实测开始。我手头有个用SpringAI做智能审核的需求:用户提交一个文件,系统要先判断文件合法性,再从数据库里拉关联数据做交叉比对,最后把审核结果以短信或站内信形式通知到人。最初我用纯提示词对话实现,结果模型只能输出一段“我认为可以/不可以”的文字,后续的查询、写库、发短信全要人工再编排一遍。后来我把架构改成ReactAgent(ReAct模式,Reasoning + Acting,即思考与行动交替循环),再配合阿里云的工具链,才彻底解决了“让大模型真正操作云服务”的问题。这篇文章是“降SpringAI阿里第9掌-或跃在渊”的实战记录,适合正在用SpringAI做Agent应用、又需要对接阿里云OSS、RDS、短信等服务的开发者,看完你能直接照搬这套思路到自己的项目里。
1. 智能审核场景为什么必须上ReactAgent
1.1 一次审核任务背后是多次工具调用,不是一次对话
先说业务场景。我需要做的智能审核系统,输入是用户上传的合同扫描件、图片或者结构化数据,输出是审核结论。如果只是让大模型读文本然后给意见,那确实不需要Agent,一次ChatClient.call()就完了。但真实业务是连环的:
- 收到文件后要把文件传到阿里云OSS做存证,拿到URL;
- 从RDS里查询这个用户的历史记录、黑名单状态;
- 调用短信API把“审核中”或“审核驳回”的通知发出去;
- 把审核日志写回数据库。
这些动作每一个都涉及外部API,模型本身不会调用。传统的做法是写一堆if/else编排逻辑:先判断模型输出里有没有“通过”关键字,再决定调哪个接口。这套方案在小业务量下能跑,但模型只要输出格式稍微变化,比如“不通过,原因是资料缺失”和“资料缺失,因此审核不通过”,你的规则就得跟着改。
ReactAgent的核心价值在这里:把“决定下一步做什么”的权力交给模型,而把“具体怎么做”交给注册好的工具函数。模型通过推理得出“我应该先去查数据库”,然后框架自动调用对应的工具,拿到返回结果再喂回给模型继续推理。这就是一个完整的“思考-行动-观察”循环。
1.2 对话式Prompt和Agent式Prompt的差异
我最早犯的错误是:把工具调用需求写进System Prompt,期望模型直接输出JSON让我解析执行。比如提示词里写“如果需要查询数据库,请输出{'action': 'query_db', 'params': '{...}'}”。实测下来可靠度很差,原因有三:
- 模型对“何时调用工具”的判断和“如何组织输出格式”是两套能力,混在同一个系统提示词里,模型容易顾此失彼;
- 长上下文场景下,模型会“忘记”自己可以调用工具,尤其是多轮对话后;
- JSON输出经常带多余字段或换行符,解析逻辑要写得很宽容,不然三天两头挂。
ReactAgent框架的做法不一样。它在框架层面通过Tool Calling协议(OpenAI Function Calling规范在SpringAI中的实现)把工具列表显式告诉模型,模型的输出不是自由文本,而是结构化的“工具调用指令”。模型从“写一段话描述自己想干嘛”变成“直接声明要调用哪个函数、传什么参数”。这个抽象层级的变化,是Agent应用比对话应用更可靠的底层原因。
提示:你在SpringAI里如果发现模型一直“答非所问”而不是触发工具调用,第一步不是调Prompt,而是确认消息里是否带了
tool角色的中间返回。很多所谓“Agent不生效”的问题,其实是消息历史里少了工具执行结果回填这一步。
2. 拆开ReactAgent的推理循环:计划、行动、观察、再推理
2.1 循环里的每一步到底在做什么
ReactAgent的模式可以用一句话概括:让模型在一个循环里交替进行“推理”和“行动”,直到它认为任务完成。具体拆解如下:
第一步,模型接收用户请求,结合系统提示词给出推理(Reasoning)。比如“用户问这个合同是否合规,我需要先查询合同编号对应的备案记录”。
第二步,模型生成工具调用指令(Action)。在SpringAI里,这体现为ToolCallingChatOptions中声明的函数被模型选中,框架把参数解析出来并执行对应Java方法。
第三步,框架拿到工具执行结果,作为tool角色的消息回传给模型(Observation)。模型看到查询结果后,继续推理:“备案记录正常,但风控评分低于阈值,因此需要驳回”,然后可能再触发下一个工具调用,或者直接生成最终回复。
这个循环的关键在于:工具结果是作为对话上下文的一部分存在的。也就是说,工具执行失败、返回空数据、返回超时超限的报错信息,模型“看得到”。这意味着你不需要在Java代码里写死“如果短信发送失败就回退到邮件”,你只需要在工具函数的返回里把错误信息结构化地返回,模型自己会决策要不要换一种通知方式。
2.2 SpringAI里配置Agent的完整骨架
我用的是SpringAI 1.0版本的API,配置Agent的标准姿势如下:
@Configuration public class AiAgentConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider tools) { return builder .defaultSystem("你是智能审核助手,请根据工具返回的数据做出审核结论。") .defaultTools(tools) .defaultOptions(ToolCallingChatOptions.builder() .internalToolExecutionEnabled(true) .build()) .build(); } }defaultTools(tools)是关键,它会把Spring容器里所有标注了@Tool的Bean方法注册为可调用工具。ToolCallingChatOptions里有个容易忽略的配置:internalToolExecutionEnabled。置为true时,SpringAI会自动执行工具调用并把结果回填,无需自己写循环;如果置为false,框架只返回“模型想要调用哪个工具”的指令,由你自己执行并把结果塞回消息列表。我用后者做过一次自定义循环,能获得更多控制权,但常规业务用前者就够了。
在多轮Agent对话中,消息历史的管理也很重要。每轮工具调用都会产生多条消息(用户消息、助手工具调用请求、工具结果),这些消息都必须保留在上下文中,模型才能理解“我刚刚查过什么”。如果为了省Token把这些中间消息丢掉了,Agent就会失去记忆,出现“刚查完数据又去查一遍”的重复调用。
2.3 “或跃在渊”状态:什么时候该停,什么时候该继续
标题里的“或跃在渊”出自《易经》乾卦九四,描述的是一种蓄势待发、可进可退的状态。ReactAgent天然就有这种“松耦合”的特性:模型可以基于完整上下文自行判断当前是否已经掌握足够信息,从而决定再调一个工具,还是直接产出最终答案。
但在落地中我发现一个问题:模型有时候会“过度行动”。比如用户就问了一句“这个文件上传成功了吗”,模型可能会先调OSS查询接口,查完又调RDS写一条日志,最后才回答。这种多余调用既费时间又费Token。解决办法有两个:
一是用系统提示词明确收敛范围。比如加上“仅当用户明确要求执行操作时,才调用工具;普通咨询类问题直接回答”。
二是控制工具列表的粒度。工具不是越细越好。把“查询OSS文件”“查询RDS记录”合成一个“查询审核材料完整信息”工具,模型做决策的次数就少了。
实测下来,收敛工具数量从8个减到5个之后,单次任务的工具调用平均次数从4.1次降到2.3次,响应时延下降近40%。这算是我这轮改造里性价比最高的一次优化。
3. 让Agent真正操作阿里云:工具集设计与实现
3.1 按业务域划分工具:OSS、RDS、短信、云盘
Agent要能干实事,工具函数的设计是第一优先级。我在项目里按阿里云服务划分了四个工具域,每个域封装成独立的Java组件:
| 工具域 | 底层服务 | 典型能力 | Agent使用场景 |
|---|---|---|---|
| 文件存证 | OSS | 上传文件、生成签名URL、校验文件MD5 | 用户上传材料后自动留档 |
| 数据查询 | RDS MySQL | 查历史记录、查黑名单、查审核流水 | 审核决策前做交叉比对 |
| 消息通知 | 短信服务 | 发送审核结果短信、模板校验 | 审核完成后通知申请人 |
| 资料归档 | 云盘 | 创建目录、归档历史审核件 | 长期存储、定期清理 |
每个工具函数用@Tool注解暴露给模型。这里有一个非常实用的设计细节:工具方法的参数不要用复杂的嵌套对象,尽量用基本类型或扁平的Map<String, String>,因为大模型生成嵌套JSON参数时容易出错(花括号嵌套层级一多,模型的JSON生成能力急剧下降)。我自己吃过亏:最开始我定义了一个AuditRequestDTO,里面有用户信息、文件列表、审核规则配置三个嵌套对象,结果模型十次调用有三次参数漏字段。改成平铺参数后,调用成功率达到98%以上。
工具方法的返回值同样要结构化。不要直接返回Java对象,而是包一层统一的响应体,保证模型能看懂成功还是失败:
@Tool(description = "查询用户历史审核记录") public String queryUserAuditHistory(String userId) { try { List<AuditRecord> list = auditMapper.selectByUserId(userId); if (list.isEmpty()) { return "{\"success\": false, \"message\": \"未找到该用户的历史审核记录\"}"; } return "{\"success\": true, \"data\": " + JSON.toJSONString(list) + "}"; } catch (Exception e) { return "{\"success\": false, \"message\": \"数据库查询异常: " + e.getMessage() + "\"}"; } }注意我返回的是JSON字符串,不是对象。这是因为SpringAI在把工具返回值并入模型上下文时,最终要序列化成文本给大模型看,直接返回字符串反而省了一次序列化,而且你可以在返回前自由控制最终文本的格式和精简程度。
3.2 工具描述的撰写直接影响模型选对工具
工具的description属性是模型决定“要不要调用这个工具”的主要依据。同样一个查短信发送状态的工具,我一开始写的描述是“查询短信发送状态”,模型经常在需要查“通知是否送达”时选了别的工具。改成“查询短信发送状态(用于确认审核通知短信是否成功送达用户手机)”,模型的选择准确率立刻上来了。
道理其实很简单:大模型是靠语义匹配来选择工具的,描述越贴近业务场景的真实表达,匹配越准。每个工具的description我都按这个模板写:动词 + 操作对象 + 适用场景 + 典型参数示例。写完之后再把工具列表打印出来人工复核一遍,看有没有描述含糊、互相覆盖的工具。
工具数量超过10个的时候,建议按Domain拆分注册,因为模型在处理过多工具选项时会出现“选择疲劳”,尤其当两个工具描述相似时,模型可能随机选一个。我通常把工具限制在6到8个以内,超过就考虑合并。
3.3 Maven仓库与依赖管理:阿里云镜像加速配置
项目同时依赖SpringAI和阿里云SDK,依赖树很庞大,首次构建时从中央仓库拉取非常慢。这里有一点经验要分享:在~/.m2/settings.xml里配置阿里云镜像仓库,能显著提升构建速度。
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <mirrors> <mirror> <id>aliyun-central</id> <mirrorOf>central</mirrorOf> <name>Aliyun Central Mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors> </settings>这里有个坑:如果mirrorOf配的是*,会把所有仓库请求都指向阿里云,包括一些阿里云镜像里没有的第三方库(比如某些仅托管在GitHub Packages的组件),导致构建失败。建议只对central做镜像,spring-milestones这种仓库还是走原地址。
另外,阿里云官方SDK各自有独立的artifactId,引入时注意版本对齐。我有一次把aliyun-java-sdk-core和aliyun-java-sdk-dysmsapi(短信)的版本搞得不一致,结果运行时出现NoSuchMethodError。查了半天发现是core版本太老,短信新接口用到的方法在里面不存在。后来统一用aliyun-java-sdk-bom做版本管理,这类问题再没出现。
4. 实测中踩过的坑:系统提示词、参数解析与调用失败
4.1 系统提示词怎么配置才不会被Agent忽略
关于“springai系统提示词怎么配置”,网上问的人特别多。我先说结论:SpringAI里设置系统提示词有三种方式,按优先级从高到低是@SystemMessage注解、ChatClient.defaultSystem()、以及请求级的Prompt中的系统消息。
在Agent场景下,系统提示词的作用不是教模型“怎么做事”,而是约束“什么时候不能做事”。我在项目里最终沉淀了一套相对稳定的提示词结构:
你是智能审核助手。你的任务是根据材料信息判断审核结论。 工具调用规则: 1. 仅当需要获取额外数据或执行外部操作时调用工具; 2. 调用工具前,先说明调用原因; 3. 如果工具返回错误,请根据错误信息决定是重试还是告知用户失败; 4. 始终以中文回复,最终结论必须包含:审核结果、依据、下一步建议。这里第3条非常重要。不加这条的时候,工具一旦报错(比如短信接口超时),模型往往会忽略错误继续“编造”一个成功结果告诉用户。加上之后,模型会把错误纳入推理,并在最终回复里如实反映。有一次实测中短信服务因为签名不匹配报错,Agent自主选择了“改为发送站内信”并通知用户,这个决策链完全由模型自己完成,我并没有写任何额外的容错代码。
另一个被问得多的点是:系统提示词要不要写“你是由XX公司开发的AI助手”。我的建议是不要写。这类人格化描述会占用上下文窗口,且对工具调用准确率没有帮助。Agent的系统提示词应该聚焦任务边界、工具使用规则和输出格式,而不是人设。
4.2 大模型参数解析失败的典型症状与修复
ReactAgent模式下,工具参数是模型生成的JSON,解析失败在所难免。常见症状有三种:
第一种是参数值为空字符串或null。比如模型明明要调用queryUserAuditHistory,但userId字段传了个空串。我排查后发现根因是用户ID的变量名定义得太抽象,模型提取不出来。把参数名从id改成userId,问题大幅减少。
第二种是参数类型不对。工具方法声明的是int类型,模型传了字符串“张三”,SpringAI的转换逻辑会抛异常。解决方法是工具方法里所有参数统一用String类型接收,然后在方法内部自己做强转和校验,给模型更宽松的输入容忍度。
第三种是模型把多个工具的调用参数合并到一个调用里。这通常发生在工具函数定义太相似时。比如queryUserBaseInfo和queryUserScore都接收userId,模型可能一次调用里同时传两个对象进来。修复方式是合并工具,或者给每个工具增加一个唯一前缀参数,比如query_user_base_info_userId,强制模型区分。
还有一类比较少见的:模型调用了工具但没有调用意图,也就是“幻觉调用”。我遇到过模型在没有数据库凭证的情况下凭空生成一个SQL去查询。这往往是工具描述诱发的,模型误以为“查询”类工具是免费的。在系统提示词里加一句“未获得授权列表中的工具不得调用”,能压住大部分幻觉。
4.3 阿里云短信API发不出去的完整排查链路
“阿里云短信API发不出去”是高频问题,我专门记录过一次排查过程,当作Agent接到错误反馈后的真实表现来看也很有参考价值。
第一层:检查签名和模板。阿里云短信要求签名和模板都需审核通过,我在测试环境用了未经审核的签名,接口报isv.SMS_SIGNATURE_ILLEGAL。这个错误在阿里云SDK的返回里其实写得很清楚,但如果你在工具函数里只返回了code和message,模型可能看不懂。我会在工具返回里显式加一层“错误码说明”字段,把“签名不合法”展开成“短信签名未通过审核或与账号不匹配”。
第二层:检查调用参数。我发现短信工具的phoneNumber参数有时候会带上区号+86,而阿里云短信接口要求纯11位手机号,带了区号会报isv.MOBILE_NUMBER_ILLEGAL。这个事发生在Agent场景下特别坑:模型从用户输入里原样提取了手机号格式,没有做归一化。后来我干脆在工具方法内部做手机号清洗,任何格式进来都先正则处理成纯数字。
第三层:检查RAM权限。还有一次报错是NoPermission,原因是子账号没授权短信服务的权限。排查步骤是登录RAM控制台,给当前AccessKey对应的子账号添加AliyunDysmsFullAccess策略。
第四层:检查配额与频率限制。短信接口有日发送配额和一分钟频率限制,短时间密集调用会报isv.BUSINESS_LIMIT_CONTROL。Agent如果一次性给多个用户发通知,很容易触发这个限制。我在工具方法里加了一个简单的内存限流:同一用户两次发送间隔小于60秒直接返回“发送过于频繁,请稍后再试”,避免触发阿里云侧的限制。
这四层排查链路走完,短信基本能稳定发出去。比较有价值的认知是:工具函数的返回信息质量,直接决定了Agent能否在报错后进行正确的自我修复。我在返回信息里把错误码、原因、可能的解决办法都拼进文本,模型看到后往往能直接给出可执行的修复建议,而不只是干巴巴地说“短信发送失败了”。
5. 性能与成本控制:并发、缓存与安全边界
5.1 JSON解析大数据量的取舍:两万行数据该怎么处理
热搜词里有个“阿里json.parsearray转换对象有两万行扛得住吗”,虽然那是Gson/Fastjson的问题,但我在Agent场景下也遇到了类似情况:一个查询工具返回了两万行明细数据,模型需要把全部内容读进上下文做判断。后果是Token消耗爆炸、响应时间超过30秒。
实测对比:两万行JSON转对象,直接用JSON.parseArray在内存层面没问题,扛得住,耗时大概几百毫秒到一两秒,取决于对象复杂度。但把这两万行全部塞给大模型上下文,问题就来了——这不仅扛不住,成本也扛不住。
更合理的做法是工具侧做“摘要化”返回。查询工具内部先聚合数据,只把关键统计指标返回给模型。比如审核材料明细有两万行,我改成返回:总记录数、异常记录数、异常类型Top3、涉及金额合计。模型基于这些统计值做决策完全够用,Token消耗降到原来的几十分之一。
如果业务确实需要模型看明细,那就分页返回。工具参数增加pageNo和pageSize,模型按需取下一页。实测中模型“按需调用”的意识比想象中强,给它一个“当前页数据不足时可继续查询下一页”的提示,它就会主动翻页。
5.2 让Agent少走弯路的三个设计技巧
第一个技巧:工具函数要有明确的“完成感”。每个工具执行完后返回的状态信息要让模型知道“这件事做完了,可以往下走了”。如果返回结果含糊,比如只给“操作成功”四个字,模型可能会再调一次工具去确认,白白浪费一轮调用。
第二个技巧:把“查询无数据”和“查询失败”明确区分。很多工具在查不到数据时会返回空列表或null,模型分不清“没有记录”和“查询出错”,可能反复重试。我在返回结构里统一约定:success=false表示出错,success=true且data为空数组表示真的没有数据。模型接收到空数组后会自行推断“查不到说明没有历史记录”,然后正常推进流程。
第三个技巧:对耗时操作设置超时提示。OSS大文件上传可能耗时几秒到几十秒,Agent如果等待工具返回期间用户又发了新消息,容易上下文错乱。我给耗时工具加了一个“异步提交,稍后查询结果”的模式:工具立即返回“任务已提交,任务ID为xxx”,模型据此告诉用户“正在处理中”,再由另一个查询工具在后续轮次里获取结果。这套模式很好地模拟了人类助手的“稍等,我去确认一下”行为。
5.3 SSL证书续期与凭证管理的运维建议
Agent上线后要长期运行,阿里云SSL证书的免费续期是每个运维都会碰到的事。免费证书有效期一般是3个月,手动续期很容易漏。我现在用脚本在证书到期前15天自动申请新证书,然后通过阿里云CLI更新SLB或CDN上的证书绑定。这里不展开脚本细节,只说一个原则:Agent服务的对外接口如果绑定了HTTPS证书,务必配置证书到期监控,别等用户访问报“证书过期”才发现。
凭证管理方面,阿里云AccessKey的安全级别等同于账号密码。SpringAI项目里工具函数要用到AccessKey,我最开始把accessKeyId和accessKeySecret写在application.yml里,虽然方便,但一旦代码仓库泄露后果很严重。后来改用环境变量注入,并且生产环境用RAM子账号并限定最小权限:OSS工具只给oss:PutObject和oss:GetObject,短信工具只给dysms:SendSms。这样即便某个工具的凭证泄露,影响范围也控制在最小。
注意:Agent的日志里千万不要打印完整AccessKey和调用参数中的敏感字段。模型会把日志内容作为上下文的一部分(如果接入了日志反馈机制),凭证泄露的风险会被放大。
6. 写在最后:Agent不是玄学,是工程
回看整个ReactAgent的落地过程,我最深的体会是:Agent应用80%的难点不在模型选择,而在工具工程。工具函数的参数设计、返回格式、错误语义、超时处理,这些看似枯燥的工程细节,决定了模型能不能稳定地把“推理”转化为“行动”。SpringAI把Tool Calling的底层协议封装好了,省去了很多对接工作,但工具好不好用,完全取决于你自己怎么定义。
如果你是第一次接触ReactAgent,我建议不要一开始就追求“全自动审核跑通全流程”。先做一个最核心的工具,比如“查询订单状态”,让模型能感知工具调用成功和失败的区别。把这个循环跑顺了,再逐步扩工具集。这个“或跃在渊”的阶段,恰恰是整个Agent能力跃升前的蓄力期——基础打得越扎实,后面往上加新工具时系统越稳。