恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring AI工具调用实战:让Java方法成为大模型的能力延伸
首页
资讯中心
/
Spring AI工具调用实战:让Java方法成为大模型的能力延伸
Spring AI工具调用实战:让Java方法成为大模型的能力延伸
发布时间:2026/10/20 4:15:56
Spring AI 里的工具调用Tool Calling / Function Calling说白了就是让大模型在回答你问题时能用你提前暴露给它的那些 Java 方法去查数据、算结果、写存储。以前我们做 AI 应用模型跟外部系统是隔离的模型只知道训练时见过的内容连当前时间都说不准更别说查你自己的业务数据。工具调用把这个缺口补上了。这篇文章我会从 Spring AI 的设计思路讲起再把核心特性的使用方式和底层机制拆开最后配上完整的实战案例和踩坑记录适合已经在用 Spring AI 写应用、或者正准备从普通对话转向真正业务落地的 Java 开发者。1. 项目背景与设计思路拆解1.1 工具调用解决的核心痛点先想一个最简单的场景用户问“北京现在多少度”。如果你只把这个问题丢给大模型它大概率会告诉你一个训练数据里见过的历史气温甚至直接编一个——因为它没有任何渠道拿到实时天气。你当然可以提前把天气数据查好拼进 Prompt 里但那样一来每次对话都要把全量数据塞给模型费 Token 不说数据一多模型反而抓不住重点。工具调用换个思路模型不是“先知道答案再回答”而是“知道自己该去问谁”。它看到用户问天气会主动发出一份结构化的请求“我要调用名为 getCurrentWeather 的函数参数 city 的值为北京”。你的应用收到这个请求后去执行真正的天气接口再把结果返回给模型模型基于这个真实结果组织最终回复。整个过程用户无感知但回答的准确性、时效性跟普通对话完全不是一个量级。这个模式的价值不只是“查天气”。凡是涉及实时数据、业务系统、权限控制、数值计算、外部服务交互的场景都可以用工具调用把 AI 从“嘴炮选手”变成“动手干活的人”。我见过把工具调用用在订单查询、库存盘点、工单流转、报表生成上的生产案例核心逻辑一模一样模型负责理解意图和拆分任务Java 代码负责执行和返回可信结果。1.2 Spring AI 的工具调用设计哲学Spring AI 对这个问题的处理方式带着很鲜明的 Spring 基因先抽象出统一接口再把实现细节藏起来。工具调用的核心抽象是ToolCallback它描述了“模型端调用”和“应用端执行”之间的完整协议。你不需要关心底层模型是 OpenAI 风格还是 Anthropic 风格Spring AI 会把你定义的 Java 方法转换成对应模型能识别的函数描述格式模型返回的调用请求也会被统一成标准结构交回给应用。这个设计带来两个直接好处。第一业务代码跟具体供应商解耦换模型供应商只需要改依赖和配置工具逻辑一行都不用动。第二你可以在工具执行前后插入统一处理比如权限校验、日志记录、熔断限流因为这些都发生在 Spring 的 Bean 管理范围内。开发者视角上Spring AI 提供了两套入口一套是声明式注解Tool适合大多数场景在方法上标一下方法自动变成可供模型调用的工具另一套是编程式ToolCallback适合需要动态控制、按用户维度定制工具集合的复杂场景。后面我会把两条路都走一遍看它们各自适合什么情况。2. 核心特性与关键机制解析2.1 三种工具注册方式对比我先把 Spring AI 里注册工具的三种主流方式摆出来方便你后面看代码时心里有数。方式适用场景实现要点灵活性Tool注解大部分业务功能静态、明确的方法在 Spring Bean 方法上标注方法参数即为工具参数中等方法签名固定ToolCallback接口需要自定义描述、动态参数、特殊转换逻辑实现ToolCallback手工构造函数定义与执行逻辑高可以完全掌控组合工具集ToolCallbacks同一场景需要多个工具一起暴露收集多个ToolCallback通过.tools(...)传入高按场景组合实际项目中我推荐一个原则业务原子操作优先用Tool减少样板代码涉及动态参数、返回结果二次加工、或者同一个功能在不同模型下需要不同表现时再上ToolCallback。两条路本质上是同一个抽象的两端混用没有任何问题。还有一种更灵活的路子是用 Lambda 表达式直接注册适合快速验证阶段。举例来说你可以用.toolCallbacks(ToolCallbacks.from(getTime, () - LocalDateTime.now().toString()))这种形式不用单独建类一两个工具这种写法会很清爽。但它的问题是描述信息写起来不够完整工具一多就不好维护所以我只在写 Demo 时用。2.2 模型端与工具端的完整交互流程理解工具调用的执行时序是排查问题的基础。我把一次完整的工具调用拆成五个阶段用户提问进入ChatClient连同当前对话历史、可用工具列表一起发给模型。模型判断这个问题需要外部信息不直接给最终答案而是返回一个“调用请求”包含要调用的函数名和参数 JSON。应用侧解析这个请求从已注册的工具列表里找到对应ToolCallback。应用执行真实的 Java 方法拿到执行结果把结果作为一条特殊的消息重新发给模型。模型结合工具返回的真实数据生成最终回复给用户。这里有一个新手容易忽略的点整个流程在用户看来只是一次提问但这背后模型参与了两轮甚至更多轮生成。Spring AI 的ChatClient内部会自动处理这个循环不需要你手动拼接第二轮请求但它消耗的是两次模型的 Token这一点在设计成本和预算时需要心里有数。我们在实际开发中还会遇到一个情况工具执行的结果本身就是一段长篇数据比如一页报表或者一屏订单列表。第二轮发给模型时模型要把这堆数据“理解并总结”出来这部分的上下文比较长容易出现回答截断或者要点丢失。我的处理方式一般是在工具方法里先做好裁剪和摘要只把模型真正需要的核心数据返回而不是把原始明细一股脑丢回去。2.3 参数描述与 JSON Schema 生成机制Spring AI 能把你的 Java 方法自动转成模型认识的函数描述靠的是 JSON Schema。每个工具在模型眼里长这样一个名字、一段描述、一份参数结构的 Schema。模型拿到 Schema才知道什么时候该调用、参数怎么填。描述写得好不好直接决定工具触发率。我见过很多人踩这个坑方法叫queryData描述写“查询数据”结果模型该调用时不调用或者把参数填得乱七八糟。原因很简单模型是靠描述来判断“这个工具在什么场景下用”描述越具体判断越准。实际项目里我的习惯是描述尽量包含“使用条件”和“常见问法”比如“当用户询问某个城市的实时天气时调用例如‘北京天气怎么样’参数 city 为中文城市名”效果比干巴巴一句“查天气”强得多。参数层面还有个细节Java 方法参数最终会映射成 Schema 里的字段如果参数有默认值或者可空一定要在注解描述里写清楚否则模型会凭空捏造一个值。对于枚举类型的参数最好在描述里把可选项列全。模型看到可选项范围填错的概率会低很多。3. 实战搭建从零实现一个天气查询助手3.1 环境准备与依赖引入我这次实战以 Spring Boot 3.4 配合 Spring AI 1.0 系列为例。你在pom.xml里引入基础依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency这里说明一下我用 OpenAI 兼容接口做演示原因是配置最简单、资料最多。你如果用的是其他供应商依赖和模型地址换一下就行工具调用的代码部分完全复用。配置项写在application.ymlspring: ai: model: chat: openai: api-key: ${AI_API_KEY} model: gpt-4o-mini强调一个我从实践中得到的经验工具调用场景千万别用太弱的模型。小参数模型对函数描述的理解能力明显差一截经常出现不触发工具、参数丢失、甚至自己编造函数名的情况。工具越多越明显。做验证 Demo 可以随意做生产至少选中等偏上的模型档位。3.2 定义首个工具函数我定义一个WeatherService用一个虚构的天气接口做演示代码里你换成真实供应商即可。Component public class WeatherService { Tool(description 查询指定中文城市当前天气状况当用户询问某地天气时调用例如“北京天气怎么样”) public String getCurrentWeather(String city) { // 这里调用真实天气服务为了演示先返回模拟数据 return switch (city) { case 北京 - 晴气温 26 摄氏度西南风 3 级空气质量优; case 上海 - 小雨气温 22 摄氏度东北风 2 级空气质量良; default - city 多云气温 24 摄氏度空气质量良; }; } }这个类为什么会自动变成工具关键在于Component加Tool。Spring AI 在启动时会扫描容器里的 Bean发现带有Tool注解的方法就为它生成对应的ToolCallback注册到工具管理器里。你什么都不用额外配置工具就已经可用了。实际项目中工具方法返回值我强烈建议返字符串或简单结构。不要返回复杂对象还指望模型自动序列化增加出错概率。工具方法的职责是“取回可信数据”至于怎么讲给用户听是模型的事。3.3 通过 ChatClient 触发工具调用工具定义好后调用方式非常直接RestController RequestMapping(/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } PostMapping(/ask) public String ask(RequestBody String question) { return chatClient.prompt() .user(question) .tools(getCurrentWeather) .call() .content(); } }注意.tools(getCurrentWeather)传的是 Bean 方法名。如果方法没有显式指定 name默认就是 Java 方法名。这里有个小细节如果同名的工具来自不同 BeanSpring AI 会抛出冲突异常所以生产项目里我习惯在Tool里显式指定name比如Tool(name weather_getCurrent)从源头避免命名空间撞车。ChatClient的.call()会一直等到模型给出最终回复才返回。如果你只拿到.content()看不到中间发生过多少次工具调用。调试阶段建议开启日志观察或者用.call().getChatResponse()拿完整响应对象里面包含了工具调用的流转记录。生产环境我更推荐接入可观测性组件把每次工具调用的入参、耗时、返回结果打成链路排查问题时会省非常多的时间。3.4 多工具场景与复杂对象参数单工具演示完我们看多工具组合。继续给WeatherService加一个方法同时再建一个OrderServiceTool(name order_queryByNo, description 根据订单号查询订单状态与物流信息当用户询问订单进度时调用) public OrderInfo queryOrder(String orderNo) { // 查真实订单系统 return new OrderInfo(已完成, 已签收签收人本人); } public record OrderInfo(String status, String logistics) { }调用时一次可以挂多个工具String reply chatClient.prompt() .user(我的订单 20250101 现在到哪了) .tools(order_queryByNo, weather_getCurrent) .call() .content();这里有一个很重要的机制你传给模型的工具集合越多模型每一次判断的复杂度就越高越容易选错或者漏选。所以不要贪多同一个请求只暴露“这个问题可能用得上的工具”而不是把所有系统功能全部塞进去。老项目里如果方法特别多建议按业务场景拆分成多个组合动态选择要暴露的工具集。复杂对象作为工具参数也很常见Spring AI 会自动解析对象字段生成 Schema。比如我给你看一个案例查询条件里带分页和时间范围Tool(description 查询指定日期范围内销售报表按天聚合日期格式 yyyy-MM-dd) public String getSalesReport(SalesQuery query) { // query.getStartDate(); query.getEndDate(); query.getPage(); } public static class SalesQuery { JsonPropertyDescription(起始日期格式 yyyy-MM-dd) private String startDate; JsonPropertyDescription(结束日期格式 yyyy-MM-dd) private String endDate; JsonPropertyDescription(页码从 1 开始) private Integer page; }对象参数场景里字段上的描述注解比方法描述还重要。模型填参数时是逐个字段判断的字段描述含糊整个调用都会跑偏。4. 踩坑记录常见问题与排查思路4.1 工具被调用但结果不对症状模型确实调用了工具但给的参数是错的或者工具返回了但模型没有采用。排查思路先看两处。第一处是参数描述是否足够精确。有一次我们给模型传日期参数描述只写了“订单日期”结果模型传了一堆不存在的日期格式进去后来把描述改成“用户用口语提到的日期例如‘昨天’‘三天前’格式化为 yyyy-MM-dd参考当前日期取当天”准确率立刻上来了。第二处是模型回复没能利用工具结果这通常不是代码问题而是模型能力不足把结果重新组织时自己发挥跑了偏。提示工具返回的结果一定要是“模型可以直接引用的事实”。如果工具返回的内容还带着业务黑话、缩写、内部编码模型很可能会转述错误。我的做法是在工具内部就完成翻译输出用户能看懂的自然语言片段。4.2 工具总是触发失败调试中最容易慌的场景是“模型根本没调用工具”。排查列表我整理成一张表可能原因检查点常见解法工具描述太模糊模型不知道什么时候用描述里写明触发条件和问法示例工具名太抽象模型理解不了功能使用“动作_对象”命名风格模型能力不足弱模型理解函数困难换更强模型或拆小函数工具没注册上.tools()传的名称与方法名不一致检查显式 name 和实际 Bean 方法名用户 Prompt 缺少意图问题里没有明确信息引导用户补充关键条件后再发我自己调试时会专门写一个测试用例直接构造一条必然触发工具的用户消息一步步缩小范围。比如测天气工具就固定发“北京现在多少度”如果这都不触发那 90% 是工具没注册成功或描述有问题。4.3 上下文丢失与多轮对话问题工具调用发生在一次式prompt调用中时没有问题但如果你的应用本身是多轮对话且用ChatMemory维护历史要注意工具调用的中间过程是否被正确记忆。我看过几个项目第一轮问了天气、第二轮问“刚才那个城市下雨吗”模型完全不知道“刚才”指哪个城市。原因通常是历史消息里只保存了最终回复而工具调用前后的中间消息没有纳入记忆。Spring AI 的ChatClient配合Memory使用时默认会把工具调用相关消息写入记忆但如果你是自己手工维护历史列表就必须把工具调用请求和结果消息一并存进去。简单说历史长河里工具执行的痕迹也要留档。另外还要防一种循环调用工具返回结果被模型再次判断为“需要调用工具”形成多轮自动循环。Spring AI 内部对连续工具调用有迭代限制但如果你在工具实现里又主动调用了ChatClient就可能把调用链拉得很长。我的经验是工具方法内部不要再去调模型对话要调也要加明确的终止条件。4.4 并发、性能与生产化建议工具调用是 Java 方法直接执行并发安全问题绕不开。工具方法尽量无状态不要往成员变量里写东西如果必须共享数据用请求级别的上下文传递。Spring AI 提供了ToolContext可以把用户 ID、租户 ID 这类与用户输入无关的上下文信息传进工具方法非常适合做数据权限过滤。我建议一开始就把这个机制用起来别等上线了再回头加权限。性能层面工具调用等于在用户请求链路上额外插入了一次“外部系统查询”。如果工具后面是数据库或者第三方接口一定要做好超时和熔断否则模型发起调用后你的接口迟迟不返回用户侧的响应时长会非常难看。我之前踩过一坑某个工具方法没设超时下游系统抖动时整个对话一直挂着最后靠配置短超时和降级策略才稳定下来。最后说一个运维细节工具调用的日志要独立打。每一条工具调用记录下“模型传了什么参数、执行花了多久、返回了什么”将来无论是调优模型还是排查业务问题都会非常有用。把这些日志做成结构化对象写入日志系统比在代码里零散地log.info强太多。我自己在多个项目里用下来的最深体会是工具调用真正的门槛不在“怎么写”而在“怎么把工具的边界划清楚”。工具太粗模型用不动工具太细模型选不准描述不到位模型乱猜。先从小而明确的工具集合开始跑通一条链路观察模型实际触发表现再逐步扩充是这个功能最靠谱的落地路径。如果你正在从普通聊天应用往真实业务场景迁移工具调用一定是值得优先啃下的那一个特性。