大家好,我是直奔標杆!专注Java开发者AI转型干货分享,和大家一起从零基础吃透Spring AI,稳步向AI领域进阶~ 今天带来《Spring AI 零基础到实战》系列的第十六课,也是AI智能体(Agent)入门的核心一课,手把手带大家解锁Tool Calling技术,让大模型真正拥有“动手能力”!
在前面的课程中,我们已经帮大模型解锁了两大技能:持久化记忆能力,以及通过RAG技术从私有知识库中精准检索文档的能力。但很多小伙伴在将大模型接入真实业务系统时,都会遇到一个共性痛点——大模型“眼高手低”,只会说不会做。
比如你问它“查一下今天北京的天气”,或者让它“设一个明早8点的闹钟”,它要么一本正经地胡说八道,要么无奈回复“无法获取实时数据,无法操作设备”。相信很多同学都踩过这个坑,其实问题的核心很简单:大模型就像一个被关在云端“小黑屋”里的天才大脑,上知天文下知地理,却没有一双能触碰真实世界的“双手”,没法执行查MySQL、调第三方API、发邮件等实际操作。
今天这节课,我们就一起打破这层枷锁,吃透AI智能体的核心技术——Tool Calling(工具调用)。借助Spring AI的底层封装,我们只需一个简单的@Tool注解,就能让大模型轻松调用本地Java方法,实现真正的“知行合一”,这也是Java开发者转型AI必须掌握的核心技能之一,建议大家跟着实操,有疑问随时在评论区交流~
本节学习目标(建议收藏,对照自查)
认知重塑:打破“大模型能自己执行代码”的误区,搞懂Tool Calling“跨界协作”的底层逻辑,避免踩坑;
状态机透视:看懂Spring Boot如何充当“全能助理”,将复杂的多轮网络请求,封装成全自动闭环状态机;
极简实战:告别繁琐硬编码,用@Tool与@ToolParam注解,优雅定义并一键挂载外部工具,新手也能快速上手;
源码解密:扒开Spring AI底层源码,搞懂反射机制与ChatModel内部拦截器的作用,知其然更知其所以然。
Tool Calling交互逻辑:不是“越权控制”,是“默契协作”
很多同学听到“AI自动调用本地代码”,都会觉得很神秘,甚至以为是“黑客操作”。其实剥开神秘面纱就会发现,它的本质就是大模型与Spring Boot应用的一次默契跨界合作,分享一个通俗的类比,帮大家快速理解:
把大模型比作坐在全封闭办公室里的天才CEO——懂的多、思路活,但没法亲自下楼办事;我们的Spring Boot应用,就是那个随叫随到的全能助理,负责把CEO的指令落到实处。
两者协作的核心,就是给大模型一份“工具说明书”(符合特定规范的JSON Schema),清晰告诉它:本地有哪些工具、能做什么事、需要传递哪些参数。如果手动拼接这份JSON说明书,不仅繁琐还容易出错,但Spring AI用Java声明式注解,直接帮我们抹平了这份复杂,这也是Spring AI的强大之处!
极简实战:从0到1实现AI调用本地Java方法(附完整代码)
实战是掌握技术的最好方式,我们从最简单的场景入手:让AI获取服务器当前时间,对比“未挂载工具”和“挂载工具”的差异,大家跟着敲代码,就能快速get核心用法。
场景1:未挂载工具——大模型“束手无策”
先做一个对照组测试,不挂载任何工具,让大模型回答时间相关问题,代码如下(可直接复制测试):
/** * 作者:直奔標杆(CSDN) * 说明:未挂载工具的测试案例,演示大模型的能力局限 */ @Test void testWithoutTool() { ChatClient chatClient = chatClientBuilder.build(); String content = chatClient.prompt("今天是几号?") .call() .content(); System.out.println(content); }运行结果(和我们预期一致,大模型无法获取实时时间):
抱歉,我无法提供当前的日期。如果你需要知道今天的具体日期,可以查看你的设备或日历应用。
场景2:@Tool注解声明工具类——给AI“造一只手”
新建一个普通Java类,写一个获取当前时间的方法,唯一的操作就是给方法加上@Tool注解,相当于给大模型提供“工具说明书”,代码如下:
/** * 作者:直奔標杆(CSDN) * 说明:以注解方式定义工具类,Spring AI自动识别 */ public class DateTimeToolsWithAnnotation { /** * 获取用户时区的当前日期和时间 * 重点说明(新手必看): * 1. @Tool的description参数,就是给大模型看的“工具简介”, * 大模型会根据这个描述,判断用户提问是否需要调用该工具; * 2. 方法名getCurrentDateTime,会自动作为该工具的唯一标识ID,无需额外配置。 */ @Tool(description = "获取用户时区的当前日期和时间") public String getCurrentDateTime() { // 业务逻辑:调用系统API获取真实时间(可直接复用) System.out.println("Spring AI 触发了本地方法:获取用户时区的当前日期和时间"); return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); } }场景3:一行代码挂载工具——实现全自动闭环
工具类定义完成后,只需在ChatClient的链式调用中,加一行.tools()方法,就能将工具挂载到大模型,代码如下(重点看注释标注的核心行):
@Test public void testWithTool() { String content2 = ollamaClient.prompt("今天星期几?") // 核心代码:将工具实例挂载到大模型,一行搞定 .tools(new DateTimeTools()) .call() .content(); System.out.println(content2); }运行结果(惊喜!大模型成功调用本地方法,返回真实时间):
Spring AI 触发了本地方法:获取用户时区的当前日期和时间 今天是2026年3月31日。
这里重点强调一下:仅仅加了一行.tools(new DateTimeTools()),Spring AI就在底层自动完成了“发送请求→判断需调用工具→反射调用本地Java方法→获取结果→二次请求大模型→生成最终回复”的全闭环,完全不用我们手动干预,这就是Spring AI封装的魅力,也是我们Java开发者的优势——不用从零造轮子,专注业务逻辑即可。
进阶实战:复杂参数工具调用(模拟真实业务场景)
刚才的案例没有入参,体现不出大模型“提取参数”的推理能力。在真实业务中,比如查询天气、查询订单,大模型需要从用户的口语化提问中,精准提取关键参数(比如城市、订单号),这时候就需要@ToolParam注解登场了,我们以“查询天气”为例,实战演示复杂工具调用。
步骤1:定义带参工具类(含入参和出参)
public class WeatherTools { // 1. 定义工具出参(用Java Record简化代码,新手可直接复用) public record WeatherResponse(double temp, String unit, String condition) {} /** * 2. 带入参的工具方法(模拟查询实时天气) */ @Tool(description = "获取指定地点的实时天气情况,用于回答用户的天气查询需求") public WeatherResponse getWeather( // 3. @ToolParam核心作用:告诉大模型参数的意义、格式和必填性 // 这是大模型能从口语中提取“北京”这个参数的关键! @ToolParam(description = "城市的名称,例如:江苏、南京、北京,必填项,不可省略", required = true) String location ) { System.out.println("Spring AI 触发了本地方法!目标城市: " + location); // 模拟业务逻辑:根据城市返回模拟天气(真实场景可替换为调用天气API) double temp = location.contains("北京") ? 30.0 : 25.0; String cond = "多云转晴"; // 将结果返回给大模型,由大模型润色成自然语言回复 return new WeatherResponse(temp, "C", cond); } }步骤2:挂载工具并测试
@Test public void testWeatherTool() { String content = ollamaClient.prompt("帮我看看北京最近的天气咋样啊?需要带伞吗?") .tools(new WeatherTools()) .call() .content(); System.out.println("最终回复: " + content); }运行结果与分析
运行结果:
Spring AI 触发了本地方法!目标城市: 北京 最终回复: 北京最近的天气是多云转晴,温度约为30摄氏度。目前的天气状况不需要带伞。
这里大家可以重点观察大模型的推理能力:它不仅判断出需要调用getWeather工具,还从“帮我看看北京最近的天气咋样啊?”这句口语化提问中,精准提取出location="北京"这个参数,传递给本地Java方法,最后结合返回的天气数据,贴心给出“不用带伞”的建议——这就是Tool Calling的核心价值,让大模型从“只会说”变成“会做+会说”。
补充一个小技巧(实战避坑):@ToolParam的description一定要写详细,明确参数格式和必填性,这样能有效避免大模型猜测参数、传递错误参数的问题,这也是我实战中总结的经验,分享给大家~
源码揭秘:一行代码背后,Spring AI到底做了什么?
很多同学可能会好奇:我们只加了@Tool注解和一行.tools()代码,Spring AI底层到底完成了哪些操作?今天就扒开核心源码,带大家看透本质,避免只会用不会懂的情况,也方便大家后续排查问题。
核心逻辑分为两步:工具注册(将@Tool注解的方法转化为大模型可识别的格式)和工具调用(大模型触发后,反射执行本地方法并完成闭环),我们逐一看源码(精简核心代码,去掉冗余逻辑,新手也能看懂)。
第一步:工具注册——将Java方法转化为ToolCallback
当我们调用.tools(new DateTimeTools())时,底层会调用ToolCallbacks.from(toolObjects)方法,将我们传入的工具对象,转化为大模型可识别的ToolCallback数组,核心源码如下:
// 核心调用链路:tools()方法 → ToolCallbacks.from() → MethodToolCallbackProvider.getToolCallbacks() // 1、ToolCallbacks#from 方法(入口) public static ToolCallback[] from(Object... sources) { return MethodToolCallbackProvider.builder().toolObjects(sources).build().getToolCallbacks(); } // 2、MethodToolCallbackProvider#getToolCallbacks(核心逻辑:提取@Tool注解方法) /** * 作者:直奔標杆(CSDN) * 核心功能:扫描所有@Tool注解的方法,转化为ToolCallback,供大模型调用 * @return ToolCallback[] 工具回调数组 */ public ToolCallback[] getToolCallbacks() { // 1. 遍历所有工具对象,处理每个对象中的@Tool方法 var toolCallbacks = this.toolObjects.stream() .map(toolObject -> Stream // 2. 获取工具对象的所有方法(处理AOP代理对象,避免获取不到原始方法) .of(ReflectionUtils.getDeclaredMethods( AopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass())) // 3. 过滤条件:只保留@Tool注解、非函数式、用户声明的方法 .filter(this::isToolAnnotatedMethod) .filter(toolMethod -> !isFunctionalType(toolMethod)) .filter(ReflectionUtils.USER_DECLARED_METHODS::matches) // 4. 将符合条件的方法,转化为ToolCallback(大模型可识别的格式) .map(toolMethod -> MethodToolCallback.builder() .toolDefinition(ToolDefinitions.from(toolMethod)) // 提取工具定义(说明书) .toolMetadata(ToolMetadata.from(toolMethod)) // 提取工具元数据 .toolMethod(toolMethod) // 绑定要调用的方法 .toolObject(toolObject) // 绑定工具对象实例 .toolCallResultConverter(ToolUtils.getToolCallResultConverter(toolMethod)) // 结果转换器 .build()) .toArray(ToolCallback[]::new)) .flatMap(Stream::of) .toArray(ToolCallback[]::new); // 验证工具的有效性(比如方法名唯一、参数完整) validateToolCallbacks(toolCallbacks); return toolCallbacks; }简单总结:这一步的核心是“扫描+转化”——Spring AI通过反射机制,扫描工具对象中所有带@Tool注解的方法,提取方法信息(描述、参数、返回值),转化为大模型能看懂的ToolCallback数组,相当于自动帮我们生成了“工具说明书”,不用手动拼接JSON。
第二步:工具调用——闭环执行(反射调用+二次请求)
当我们调用.call()方法时,会将上面生成的ToolCallback数组,封装成请求发送给大模型;如果大模型返回“需要调用工具”的指令,Spring AI会反射调用本地Java方法,获取结果后,再次请求大模型生成最终回复,核心源码(OpenAiChatModel#internalCall)如下:
// OpenAiChatModel#internalCall 源码精简剖析(核心逻辑) public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) { // 1. 发送第一趟网络请求,获取大模型的初始响应 // ...(省略冗余的请求构建、发送逻辑) // 2. 判断:大模型是否需要调用工具?(由拦截器判断) if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) { // 3. 核心操作:反射调用本地Java方法,获取执行结果 ToolExecutionResult toolResult = this.toolCallingManager.executeToolCalls(prompt, response); // 4. 闭环判断:是否直接返回结果?还是二次请求大模型? // returnDirect=false(默认):将结果塞入上下文,二次请求大模型,生成自然语言回复 // returnDirect=true:直接返回工具执行结果,不经过大模型润色 return toolResult.returnDirect() ? ChatResponse.builder()...build() // 直接返回结果 : this.internalCall(new Prompt(toolResult.conversationHistory()), response); // 二次请求,完成闭环 } // ...(省略其他逻辑) }这就是Spring AI的强大之处:所有复杂的操作(反射调用、多轮网络请求、结果转换),都被底层封装好了,我们只需要关注“定义工具”和“挂载工具”,不用关心底层细节——这也是Java开发者转型AI的优势,利用现有框架,快速落地AI能力。
本节总结(必看,梳理核心要点)
通过本节课的学习和实战,相信大家已经掌握了Spring AI Tool Calling的核心用法,这里直奔標杆给大家梳理3个核心要点,帮助大家巩固记忆,避免踩坑:
认知层面:大模型本身没有“执行能力”,它只是“决策大脑”,负责判断是否需要调用工具、提取参数;真正的执行主体,是我们的Spring Boot应用,Tool Calling本质是“大脑(大模型)+ 双手(Spring Boot工具)”的协作。
实战层面:用@Tool注解定义工具(给大模型写说明书),用@ToolParam注解定义参数(帮助大模型提取参数),一行.tools()代码挂载工具,就能实现全自动闭环,新手可直接复用本节课的代码模板。
源码层面:核心是“反射机制”和“状态机闭环”——Spring AI通过反射扫描@Tool方法,转化为大模型可识别的格式;调用时通过拦截器判断,反射执行本地方法,再通过递归调用完成二次请求,实现全自动交互。
其实Tool Calling并不复杂,关键是多实操、多思考,本节课的代码大家可以多敲几遍,感受Spring AI封装的便捷性。只要你会写Java方法,无论是查库存、发邮件,还是操控设备,都能通过Tool Calling让大模型帮你实现,真正实现“AI赋能业务”。
下节预告(干货预警)
本节课我们学的是“注解式工具注册”,属于“自动挡”玩法,简单高效,但实战中会遇到更复杂的场景:比如需要调用第三方Jar包中的方法(无法修改源码,不能加@Tool注解),或者想把业务层已有的Function接口直接作为工具,该怎么办?
下一节课,我们将脱下“优雅的伪装”,直捣黄龙——《Spring AI Tool Calling 底层核心三剑客与编程式注册源码大揭秘》,拆解ToolCallback、ToolDefinition等底层接口,教大家纯手工挂载工具,彻底吃透Tool Calling的底层逻辑,解决复杂场景下的工具调用问题。
精彩继续,咱们下节课见!大家在实操中遇到任何问题,都可以在评论区留言,直奔標杆会一一回复,和大家一起交流进步~
往期内容(衔接学习,循序渐进)
Java开发者AI转型第十三课!知识库终局方案:Spring AI Vector Store架构演进与ETL全链路入库实战
Java开发者AI转型第十四课!Spring AI向量数据库实操:检索召回与相似度检索实战详解
Java开发者AI转型第十五课!Spring AI神技:模块化RAG引擎一键闭环实战
我是直奔標杆,专注Java开发者AI转型干货分享,每一节课都贴合实战,拒绝空谈理论。关注我,一起从零基础吃透Spring AI,稳步成为具备AI能力的Java工程师!