资讯详情

资讯详情

Spring AI实战:ReactAgent让大模型从对话到动手调用工具

从第1掌打到第8掌手里的Spring AI应用大概已经能从“配置模型拼提示词”走到“能对话、能流式输出”的地步了。但说实话这种状态下的AI应用还停留在“带记忆的搜索引擎”水平——用户说一句模型回一段信息永远停留在“生成”层面碰不到真实业务。第9掌的“或跃在渊”其实说的就是这一步让模型从只负责动嘴变成能动手干活。这一掌要啃的是Spring AI里最核心的一个概念ReactAgent也就是基于ReAct模式构建的工具调用型智能体。如果你还没接触过这个概念我换个说法你立刻就能懂让大模型在回答问题时可以主动去调用你准备好的Java方法——查数据库、调外部API、做计算——然后把结果消化成最终答案。这件事在Spring AI里叫Tool Calling在阿里云百炼那边叫“函数调用”而把它串成一个完整的推理-行动-观察循环就是你今天要练的ReactAgent。这篇文章我不会只丢一个Demo给你我会把我从框架选型、接入百炼、手写工具、编排多工具到上线踩坑的完整链路拆开讲每一个“为什么这么做”背后都是我实测过的结论。1. 或跃在渊是哪一步从只会聊天到敢伸手干活1.1 我理解的ReactAgent和ReAct的本来面目“或跃在渊”是《易经》乾卦九四的爻辞讲的是龙在深渊里蓄势准备往上跃。放在这个系列里恰好就是Agent这一步的处境模型本身还很“虚”但一旦给它工具它就有了跃出水面的锚点。ReAct这个词是Reason和Act的拼写——推理加行动。它描述的循环非常简单模型收到用户问题先在心里想要回答这个问题我缺什么信息模型发现缺信息于是输出一个特殊的“调用工具请求”里面带上工具名和参数。Spring AI收到这个请求去调用你注册的Java方法拿到结果。结果被回填进模型上下文模型“看到”结果继续推理。如果还缺信息就重复第2步如果够了就输出最终答案。这个过程不是我的发明它是ReAct论文里提出的Agent工作模式Spring AI把它做成了框架级能力阿里云百炼的通义千问系列模型对工具调用也做了专门优化。你可以在百炼控制台的功能配置里看到“工具调用”的开关本质上就是给模型开放了一组可执行的函数描述。我经常用一个类比来解释这件事ChatClient默认情况下像一个只会侃侃而谈的顾问你问他一件事他能基于训练数据给你编一个大概齐的答案但编的可能是错的而ReactAgent是你给他配了一个秘书团——能查天气的、能查考勤的、能算费用的——他遇到自己拿不准的事就不再硬编了而是转手问秘书团要数据。1.2 为什么ChatClient的默认行为堵在了一道坎上直接把提示词写上“你可以调用工具”是不会生效的。模型不是海龟汤里那个万能答题者它需要的是严格的函数签名和描述并且在模型决定调用后由你的代码来真正执行。我见过不少人第一次写Agent时犯这个错他们在System Prompt里写“如果你需要天气信息就调用getWeather方法”。但Spring AI里只写提示词是接不上的因为模型根本不知道getWeather长什么样、参数是什么、返回值是什么。模型需要的是OpenAI/百炼协议里的functions JSON Schema。Spring AI对这件事的封装方式很优雅你只需要在类的方法上打一个Tool注解写上description框架就能在运行时自动扫描方法签名生成模型需要的JSON Schema。模型调用工具时框架自动反序列化参数反射调用方法把返回值写回对话上下文。这一步的体验转折非常明显——我第8掌写的应用用户问“今天杭州和上海最高气温差多少度”模型只能抱歉地说“我无法获取实时天气”。加了Tool之后同样的问题模型会先调两次工具再输出一句“杭州今天最高31度上海28度差3度”。这就是ReactAgent帮你翻过的第一道坎让模型敢承认自己不知道也让它有能力去知道。2. 为什么百炼Spring AI没有让我纠结太久2.1 三条路的对比手写SDK调用、LangChain风格框架、Spring AI在动手写代码之前我先花了两天时间做选型对比。选择阿里云百炼作为模型服务这没什么可犹豫的——通义千问的qwen-plus、qwen-max系列模型对工具调用的支持非常稳定而且百炼平台可以直接买资源包对国内开发者来说无论是网络延迟、合规还是计费都比接海外模型端点省心得多。麻烦的是Java这边的框架。我比较过三条路第一条直接拿百炼的SDK手写工具调用循环。DashScope的SDK本身支持function calling但你要自己维护循环——把模型返回的tool_call解析出来执行方法拼装结果再发起下一次请求。代码不难但多轮工具调用时很啰嗦而且流式输出、上下文管理、失败重试都要自己写。我搭了个原型光一个“查完天气再查日历”的双工具流程就写了200多行胶水代码。第二条用LangChain风格的Java移植版。这类框架对Agent编排的抽象确实更多但问题也很现实社区活跃度一般很多生态组件年久失修遇到问题只能自己读源码。对我这种“要用在生产环境”的人来说风险偏高。第三条就是Spring AI。它最大的优势不是功能多而是它是Spring官方生态的一部分——自动配置、ChatClient链式API、Tool注解扫描、工具调用回调全部是框架一等公民。更重要的是Spring AI官方仓库里直接提供了DashScope的starter同时阿里云也在com.alibaba.cloud.ai命名空间下维护了Spring AI Alibaba适配包。对已经在用Spring Boot的团队来说接入成本最低排查问题也有现成的社区和文档。我的判断是如果你的核心诉求是快速稳定地把Agent做进Spring Boot项目里不想养一套自研Agent框架Spring AI就是当前最合理的答案。它不花哨但把Agent最关键的“模型输出→Java方法→上下文回填”这条链路焊得足够结实。2.2 DashScope接入两种姿势专用starter与OpenAI兼容端点百炼平台提供两种接入方式我都实际跑通过这里给你做个对比。第一种是DashScope专用starter。依赖坐标长这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-dashscope-spring-boot-starter/artifactId version1.1.0/version /dependency如果用的是Spring AI Alibaba的适配包则是dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter-dashscope/artifactId version1.0.0/version /dependency专用starter的好处是配置项精简模型名、API-KEY、base-url都已经帮你映射好了参数命名也更贴合DashScope的习惯。我用它的时候application.yml里几乎只需要写三行。第二种是OpenAI兼容端点。百炼对外提供了一个OpenAI兼容的BaseURL也就是https://dashscope.aliyuncs.com/compatible-mode/v1。这意味着你可以直接用Spring AI的OpenAI starter把base-url指过去模型名选qwen-plus或qwen-max即可。两种方式的取舍我总结如下对比项DashScope专用starterOpenAI兼容端点依赖复杂度依赖少但按DashScope定制依赖通用OpenAI starter天然熟悉配置项精简fewer surprises有些参数语义不完全一致版本适配跟随Spring AI Alibaba迭代跟随Spring AI主线基本都兼容流式/tool调用支持均支持均支持我生产环境用的是专用starter因为它的重试策略和限流回退是针对百炼的响应头做过适配的。你如果只是想快速验证OpenAI兼容端点可以直接复用你已有的SDK经验几分钟就能跑通。3. 接入配置里真正影响成败的细节3.1 API-KEY放哪、模型怎么选qwen-plus还是qwen-max先说API-KEY的管理。百炼平台创建的API-KEY有Access Key和主账号Key之分建议用子账号的Key权限只开模型推理相关。代码里一定不要硬编码Key用环境变量或配置中心spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 chat: options: model: qwen-plus temperature: 0.7模型选型上我做了好几轮实测。qwen-turbo便宜但tool calling的稳定性差一些表现在偶尔会把工具参数变成简写、JSON格式不规范qwen-plus价格适中工具签名理解准确率已经很高适合生产环境的主力qwen-max效果最好但成本和响应延迟都上去了我只在复杂推理场景才切到它。实践建议是先把模型名配成qwen-plus跑通Agent全链路然后压测时故意换成qwen-turbo看报错你就能直观体会到“模型能力下限”对Agent的影响。这对你评估线上模型水位很有帮助。3.2 超时、重试与上下文长度配置文件的隐藏参数Agent场景比普通对话更容易触发超时——因为一次最终回答背后可能隐藏着多轮工具调用每一轮都是一次完整的大模型请求。默认的HTTP超时在工具调用慢时往往不够用。我这里给出一份生产环境参考配置spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.5 max-tokens: 4096 client: connect-timeout: 30s read-timeout: 120sread-timeout我故意调到120秒因为包含工具调用的Agent请求最长可能要等待模型把多轮工具结果都消化完。如果你用的是OpenAI兼容端点注意在options里确认parallelToolCalls的行为有些版本支持并行工具调用有些模型一次只调用一个工具这个会在第5章细说。还有一个容易被忽略的点工具调用结果会反复写回上下文。如果工具返回的数据量很大比如查出来一张几十行的表多轮之后Token消耗会膨胀得很快。我的做法是在第四、五章详细讲的工具返回前做裁剪和摘要而不是把原始结果一股脑塞给模型。4. 写一个能查天气、能算日期的ReactAgent4.1 用Tool把本地方法变成模型的手现在到了最激动人心的部分。我先给你看一个最小可用的Agent代码它同时具备“查天气”和“计算日期”两个工具。Component public class WeatherCalendarTools { Tool(description 根据城市名称查询今天的最高气温和最低气温返回单位为摄氏度) public String getTodayTemp(String city) { // 这里按道理要接真实天气API我先用模拟数据演示结构 MapString, String mock Map.of( 杭州, 最高31度最低24度多云, 上海, 最高29度最低22度小雨, 北京, 最高26度最低18度晴 ); return mock.getOrDefault(city, 暂无该城市数据); } Tool(description 计算两个日期之间相差多少天日期格式必须为yyyy-MM-dd返回数字) public int diffDays(String startDate, String endDate) { DateTimeFormatter fmt DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate start LocalDate.parse(startDate, fmt); LocalDate end LocalDate.parse(endDate, fmt); return (int) ChronoUnit.DAYS.between(start, end); } }注意几个关键点Tool注解里的description极其重要。模型不会“看代码”它只靠这段描述来决定什么时候调用、传什么参数。我实测过description写得含含糊糊比如只写“天气查询”模型在判断“杭州今天适不适合穿短袖”这个问题时就容易犹豫写清楚“返回单位为摄氏度”模型就更敢直接拿返回值来推理“热不热”。方法名本身也尽量语义完整。虽然Spring AI默认会序列化方法名给模型但一个大写开头的getTodayTemp显然比temp1让模型更容易理解。工具方法的返回值类型我推荐用String或简单对象不要直接返回一个复杂的实体对象。模型对长文本JSON的解析能力虽然强但简短的、结论式的返回值能显著降低后续推理的跑偏率。4.2 tools()提交之后发生了什么一次完整调用链有了工具类接下来把它绑定到ChatClient并调用Service public class AgentService { private final ChatClient chatClient; public AgentService(ChatClient.Builder builder, WeatherCalendarTools tools) { this.chatClient builder .defaultSystem(你是一个生活助手工具信息可以能辅助你回答时就用工具不要自己编造数据。) .defaultTools(tools) .build(); } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }这段代码跑起来后当你输入“杭州和上海今天温差大吗”框架内部发生的调用链大致是这样的框架把用户问题和System提示拼好后附带两个工具的JSON Schema一起发给qwen模型。模型返回一个tool_call内容是“调用getTodayTemp参数city杭州”。Spring AI尝试验证这个调用如果参数合法就反射执行你的Java方法。得到“最高31度最低24度”之后框架把它作为tool result追加进消息列表再次发给模型。模型又发出第二个tool_callgetTodayTemp参数city上海。执行完毕再次回填模型这次拿到两组天气数据开始做比较推理。模型输出最终答案“杭州31度上海29度相差不大。”看到没有模型不是一次性把两个城市都查完的而是“查一个→看结果→再查一个→再比较”这种迭代方式。这正是ReAct循环的直观体现Thought该查杭州→Action调用工具→Observation31度→Thought还得查上海→Action→Observation→Final Answer对比温差。Spring AI把整个循环封装在call()里同步阻塞直到拿到最终文本。这个设计对业务接入很友好——你感知不到背后转了几次只要拿到最终结果。但我必须提醒你这同时也意味着在工具调用频次高的时候你的HTTP接口耗时会是“模型多次请求耗时之和”所以生产环境一定要配合第6章的异步化方案。5. 多工具编排让模型自己排工序5.1 先查天气再算日子的实战案例第4章的工具还只涉及“查一下就回答”的简单场景。真正让ReactAgent展现价值的是“需要多个工具按顺序配合才能回答”的场景。我举一个练手时觉得特别适合理解的例子用户问“我想在5月20日之后一周内挑一天去杭州办户外活动哪天比较合适帮我算一下这几天分别是周几”。这个问题里模型至少要做这些事先算出5月20日之后一周内是哪几天需要日期工具查出这几天的天气需要天气工具根据“户外活动”和“天气”做推理选出最合适的一天。想想看如果模型没有工具它只能基于训练数据瞎猜“5月22日应该天气不错吧”——这种答案你敢信吗。但有了工具模型会自己排一个工序链。由于我们上一章的工具类里getTodayTemp只能查“今天”不具备“未来某天”查询能力这里我把工具增强了一下Tool(description 查询指定城市指定日期的天气预报日期格式yyyy-MM-dd返回天气概况) public String getWeatherForecast(String city, String date) { MapString, String mock Map.of( 杭州-05-21, 多云24-31度适合户外, 杭州-05-22, 中雨22-27度不适合户外, 杭州-05-23, 晴25-33度适合户外 ); return mock.getOrDefault(city - date.substring(8), 暂无数据); } Tool(description 计算某个日期是星期几日期格式yyyy-MM-dd返回星期几) public String dayOfWeek(String date) { DateTimeFormatter fmt DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate d LocalDate.parse(date, fmt); return d.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.CHINESE); }注意工具描述里我写明了参数格式必须是yyyy-MM-dd这个不起眼的细节决定了模型能不能“算对关键的一天”。实测中模型第一次可能给你传2025/05/21但如果你在description里明确“日期格式必须为yyyy-MM-dd”qwen-plus基本能正确格式化日期后再调用。完整案例跑下来模型的输出大概是这样第一次调用dayOfWeek(2025-05-21)得到“星期三”然后枚举性地计算21到27号分别是周几再对21、22、23号分别查天气因为周末才适合活动最终把“适合户外、非雨天、气温适中”的日子推荐出来。你不需要在代码里写任何“先调用A再调用B”的逻辑模型自己在推理过程中搞定了工序编排。这就是ReactAgent的名字里“React”的含义——先想后做边做边想。5.2 并行工具调用与可控执行的取舍这里要提一个进阶差异并行工具调用。qwen-plus这类模型在一次响应里有时会同时发出多个tool_call比如一口气让你查“杭州天气”和“上海天气”。Spring AI对这种并行请求的处理方式是依次执行回调再一起回填结果。好处是省掉一轮“逐个反馈再追问”的时间坏处是当工具之间存在依赖关系比如第二个工具要用第一个工具的结果时并行调用会出错。我的经验是在工具设计阶段尽量让每个工具“自给自足”不要设计那种“必须先查A才能调B”的强依赖工具。如果确实需要强依赖宁可把它做成一个组合工具方法比如calculateBestDate(city, startDate, days)在Java方法内部串起天气和日期两段逻辑而不是让模型在多个工具之间维持全局状态——模型对依赖关系的理解还是不如代码里写死来得可靠。另外如果你发现模型频繁地同时调用太多工具导致上下文混乱可以显式关闭并行spring: ai: dashscope: chat: options: parallel-tool-calls: false关闭后模型一次只调用一个工具观察它一步步“自言自语”地完成推理调试时更清晰逻辑也更稳。上线后如果对耗时敏感再把它打开也不迟。6. 从Demo到生产那些不踩不算数的坑6.1 模型不按契约返回时的三层兜底工具调用最气人的事情就是模型不按你给的JSON Schema返回。我在用qwen-turbo测试时遇到过好几次模型把描述里的示例当成了真实数据返回或者干脆把工具名称编错。比如我明明只提供了getWeatherForecast它却给我返回一个get_weather。这种场景的三层兜底是我把线上事故逼出来以后总结的第一层参数校验。在Tool方法入口做严格校验不合法直接抛业务异常。Spring AI会把方法抛出的异常作为工具执行结果回填给模型模型看到“参数格式错误”通常会自动纠正自己的参数。这一层要保证的是异常信息对模型足够友好减小“诡异报错串”的复现概率。第二层模型兜底。如果你的核心链路对工具调用准确率要求高就不要把模型选到最便宜的那档。qwen-plus和qwen-max的tool calling稳定度差距在复杂参数场景下非常明显。省下的那几分钱不够赔你排查一次错误调用的时间。第三层人工评审与超时熔断。给ChatClient调用包一层超时控制超过N秒直接返回降级文案而不是让请求无限排队。工具是帮你干活的不是拖垮你接口的。另外还有个安全细节值得提工具方法里如果拼接了用户输入比如拿city参数去请求外部API要考虑参数注入风险。虽然大模型大概率不会恶意构造但第二个工具把你带到本地文件读取或内网探测的场景你还是要防一手的——尤其是生产环境里Agent权限比较大的时候。6.2 上下文膨胀、限流和工具副作用上线前必查清单最后分享一份上线前必查的清单每一条我都实际踩过工具结果别原样塞回上下文。工具返回的字符串会被直接追加进对话历史。如果你的工具返回一个几KB的JSON几轮对话下来上下文可能膨胀到上万Token。我在工具方法里加了一个裁剪工具方法超过300字符就截断必要时让模型自己请求完整数据——但这招要谨慎截断了就查不到了。限流处理必须做。百炼的API有QPS限制Agent内部一次请求可能要打模型三次以上。我上线第一天就被429教育了后来加的指数退避重试才稳住。Spring AI自带的重试策略一般够基础场景但你需要关注一下提供商的限流头。日志里要能看到完整的工具调用轨迹。我强烈建议开启ChatClient的观察日志或者自己在工具方法里打点。线上排查“模型为什么得出这个结论”时工具返回值和调用次序是最有价值的线索。异步化是必然选择。Web接口同步等待一个可能触发5次工具调用的Agent请求用户体验几乎是不可接受的。我最终是把Agent调用丢到线程池或异步队列里前端通过轮询获取结果见到的耗时立刻从“卡死感”变成“可接受”。以上这些坑不是我看文档看出来的是亲手把Agent推到生产环境边界之后一点点磨出来的。如果你也能把这套流程完整跑下来你自己的“或跃在渊”这掌就算练扎实了。我个人现在的体会是ReactAgent这个模式真正考验人的不是框架API而是你能不能把“工具的边界”设计清楚。模型能调的工具越多它表面上越聪明但工具描述不清、参数不严、副作用不管它也会非常稳定地把事情搞砸。这也是我把大量篇幅放在工具设计和踩坑上的原因——框架只是最后那一层纱真正撑起Agent效果的是那些能被模型调用的方法本身是否值得信赖。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →