:Tool Calling,让大模型调用你的 Java 方法)
系列Spring AI 入门实战——从第一次对话到数仓查询助手本篇目标让模型调用一个返回模拟订单统计的 Java 方法并根据返回值回答。技术基线Java 17、Spring Boot 4.1.0、Spring AI 2.0.1。工程承接继续使用第 2 篇shop-ai-client保留MonthRange新增两个类。1. 助手会填表了但还没有计算器小林的助手已经能把“查询 2026 年 8 月每日支付金额”变成一份规范的查询意图。小周又问“那你能不能直接给我每天的金额”助手沉默了。它知道该查哪段时间却没有数据入口。这就像你请了一位能说会道的新同事来开店。他可以解释优惠券的规则但如果你不让他看收银系统他当然不知道昨天卖了多少杯奶茶。今天我们给助手一个工具。不过先不连数据库先提供一份写在 Java 里的模拟结果。这样一来出问题时能集中观察工具调用过程不会被数据库连接、SQL 方言和初始化脚本分散注意力。2. Tool Calling模型发申请程序来执行Tool Calling 常被翻译为“工具调用”。名字听起来很大实际流程很容易理解应用把工具的名字、用途和参数说明发给模型。模型判断当前问题需要这个工具返回工具名和参数。Spring AI 找到对应的 Java 方法并执行。方法结果回到模型模型再组织自然语言回答。模型并没有钻进 JVM也不会在你电脑上自动执行一段 Java。它提出调用请求真正握着执行权的是应用。官方 Tool Calling 说明工具描述相当于菜单参数相当于订单Java 方法才是后厨。菜单上写得再漂亮也不能代替后厨检查食材。在 Spring AI 2.0 中正常使用自动配置的ChatClient.Builder工具循环由自动注册的ToolCallingAdvisor协调。下面的示例不需要自己编写“调用模型—执行工具—再调用模型”的循环。3. 做一个能看得见的工具在src/main/java/com/example/shopai/新增MockOrderTools.javapackagecom.example.shopai;importjava.math.BigDecimal;importjava.util.List;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.ai.tool.annotation.ToolParam;importorg.springframework.stereotype.Component;/** 第三篇的假数据工具用来观察模型调用 Java 方法的过程。 */ComponentpublicclassMockOrderTools{privatestaticfinalLoggerlogLoggerFactory.getLogger(MockOrderTools.class);publicrecordDailyAmount(Stringdate,BigDecimalamount){}publicrecordMockResult(Stringsource,Stringunit,ListDailyAmountrows){}Tool(namemock_daily_amount,description 查询模拟的每日支付金额。必须提供完整自然月开始日包含、结束日不包含。 固定统计 PAID、FINISHED 状态金额单位元只返回有付款的日期。 只有 2026 年 8 月有演示数据其他月份返回空列表不能声称是真实业务数据。 )publicMockResultdailyAmount(ToolParam(description月份第一天如 2026-08-01)StringstartDate,ToolParam(description下个月第一天如 2026-09-01)StringendExclusive){MonthRangerangeMonthRange.parse(startDate,endExclusive);log.info(mock_daily_amount start{}, endExclusive{},range.start(),range.endExclusive());if(!2026-08-01.equals(startDate)){returnnewMockResult(MOCK,CNY,List.of());}returnnewMockResult(MOCK,CNY,List.of(newDailyAmount(2026-08-01,newBigDecimal(150.00)),newDailyAmount(2026-08-02,newBigDecimal(280.00)),newDailyAmount(2026-08-03,newBigDecimal(39.90)),newDailyAmount(2026-08-31,newBigDecimal(300.10))));}}这份代码里最值得注意的不是那四个金额而是工具契约。Tool的description告诉模型这个方法能解决什么问题。ToolParam则说明每个参数如何填写。开始时间包含、结束时间不包含也明确写在工具说明中。方法内部仍然调用MonthRange.parse()。假如模型传入了一个季度的范围Java 会拒绝执行。不能因为“系统提示词已经要求按月”就把这道校验省掉。我们特意返回了source: MOCK。这张标签能提醒模型也能提醒正在调试的自己数据来自代码中的常量尚未发生数据库查询。金额使用BigDecimal而且从字符串创建。不要用new BigDecimal(0.1)给入门教程额外引入一个浮点数故事。4. 有工具还得把工具交给助手再新增MockController.javapackagecom.example.shopai;importjakarta.validation.Valid;importjava.util.Map;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RestController;/** 为这一个问答接口注册本地工具。 */RestControllerpublicclassMockController{privatefinalChatClientchatClient;publicMockController(ChatClient.Builderbuilder,MockOrderToolstools){this.chatClientbuilder.defaultSystem( 你是订单助手使用简体中文。 用户缺少年份或月份时先追问不能自己猜测。 查询每日支付金额时调用工具不能凭记忆生成金额。 只使用工具返回的日期和金额明确标注模拟数据。 空列表表示该演示月份无数据工具报错时说明失败不能编造结果。 ).defaultTools(tools).build();}PostMapping(/ai/mock)publicMapString,Stringask(ValidRequestBodyAskRequestrequest){StringanswerchatClient.prompt().user(request.question()).call().content();returnMap.of(answer,answernull?模型没有返回文本。:answer);}}关键是构造方法中的.defaultTools(tools)它把工具对象注册给这个ChatClient。只有Component并不代表模型自动拥有容器里所有工具。你仍然需要明确告诉当前客户端本次工作允许使用哪些工具。第 1 篇的/ai/chat依然只是普通问答第 2 篇的/ai/intent依然只解析意图本篇的/ai/mock才注册本地工具。三个入口分开便于比较它们的差别。本篇直接让模型填写工具参数是为了把 Tool Calling 本身看清楚。第 6 篇会重新接上第 2 篇的IntentParser组成“先解析和检查意图再查数”的完整流程。5. 跑一次把“魔法”拆开看模型环境变量沿用第 1 篇重新启动客户端mvn spring-boot:run发送请求curl-sShttp://127.0.0.1:8080/ai/mock\-HContent-Type: application/json\-d{question:查询 2026 年 8 月每天的支付金额请列出每天的数值。}不要急着看最终回答先看控制台。进入工具方法后应出现下面这条业务日志时间戳等日志前缀会因环境不同而不同mock_daily_amount start2026-08-01, endExclusive2026-09-01这个日志来自我们自己的 Java 方法。它比一句“我已经帮你查询了”更有说服力。工具返回的业务数据是确定的日期模拟支付金额元2026-08-01150.002026-08-02280.002026-08-0339.902026-08-31300.10模型应围绕这四行数据回答并说明“模拟数据”。例如根据演示工具返回的模拟数据8 月 1 日支付金额为 150.00 元8 月 2 日为 280.00 元8 月 3 日为 39.90 元8 月 31 日为 300.10 元。工具只返回有数据的日期未列出的日期没有在本次结果中展开。上面是表达形式示例不是某次真实模型调用的录屏转写。验收要检查工具是否执行、日期和金额是否忠于工具结果不能只检查回答读起来是否流畅。你还可以在方法第一行打断点。看到执行停在那里Tool Calling 的神秘感基本就消失了最终执行的还是你熟悉的 Java 方法。6. 工具说明怎么写模型才容易用对不太好的说明是“查询数据。”因为模型会继续猜什么数据允许哪段时间金额是什么口径空列表是什么意思更好的说明要覆盖三个问题何时调用、需要什么、会返回什么。本篇工具明确了“每日支付金额”“完整自然月”“PAID、FINISHED”“模拟数据”。这些词不是文案装饰它们构成了工具的使用说明书。但也不必把一整篇文章塞进 description。长说明既增加请求内容也容易淹没真正的限制。把固定规则交给代码执行把模型需要理解的业务含义放进描述即可。另一个容易忽略的点是工具结果也会影响最后的回答。如果返回的只有四个裸数字模型很难知道日期、单位和来源。带上适当的业务字段通常比让模型“自己理解一下”可靠。7. 三个常见问题“加了Tool为什么没看到方法执行”先确认通过.defaultTools(tools)注册到了当前客户端再检查所选模型是否支持工具调用。最后看用户问题如果只问“什么是支付金额”模型可能不需要查询工具。不要把“没调用”一概认定为框架故障。“日志执行了一次为什么模型请求不止一次”因为模型先提出工具调用再读取工具返回值生成回答。一次用户问答可能包含多次模型交互延迟和计费也应按实际请求理解。“工具报错了提示词要求不要编造就够了吗”还不够。代码需要清楚地区分成功结果与错误调用方也需要保留错误信息。本篇先把异常暴露出来第 6 篇会补上面向用户的失败处理。对于金额等重要结果验收还应核对实际工具返回值。8. 小周有了一个新主意小周看到金额表挺满意“那我能不能把这个查数工具也给另一个助手用”小林看了看MockOrderTools。现在它住在这个 Spring Boot 进程里其他应用没有统一的接入方式。下一篇我们把工具搬到独立服务里为它装上一扇标准化的门。这扇门的名字就是 MCP。上一篇《Spring AI 入门二提示词与结构化输出让回答变成 Java 对象》下一篇《Spring AI 入门四认识 MCP编写第一个工具服务》