新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring AI Tools实战:从@Tool注解到function-call源码解析

发布时间:2026/9/26 18:08:52来源:尧图网络
Spring AI Tools实战:从@Tool注解到function-call源码解析
做Agent开发的朋友一定遇到过这种场景用户问“帮我查一下订单到哪了”模型一本正经地回你“我暂时无法查询实时物流信息”。这不是模型笨是因为它本质上是个文本生成器你给它再多的提示词它也只能输出文字翻不了数据库、调不了接口、写不了文件。要让大模型真正“动手干活”业界标准答案就是function-calling也就是让模型在回答问题时声明“我想调用某个函数”由外部系统执行完再把结果喂回去。Spring AI把这一整套机制封装成了Java开发者几乎零门槛的Tool注解底层自动完成工具描述生成、参数JsonSchema、tool_calls解析和方法反射调用。这篇文章我从入门到实战再到源码完整拆一遍Spring AI里的Toolsfunction-call实现。不管你是刚接触Spring AI的新手还是已经写了几个Agent想深入掌握工具机制的老手都可以参考这份笔记。我会用智谱GLM的兼容接入做演示原因是国内网络环境下它的Spring AI starter很成熟省去很多配置折腾。1. 为什么说function-call是Agent落地的第一块基石1.1 大模型的能力边界它天生不会“做事”大模型的训练目标是从海量文本里学习概率分布所以它擅长的是“生成看起来合理的文本”而不是“执行真实世界的动作”。你让模型“查一下订单”它没有订单系统的接口地址你让模型“把数据写入数据库”它也没有MySQL的连接密码。本质上模型活在文字世界里和现实世界之间隔着一堵墙。而且这里还有一个更麻烦的限制模型的知识有截止日期你库里的业务数据它完全不知道。前端开发同学可能觉得“这不就是个API调用问题吗”实际上在LLM应用的架构里这不是API调不调的问题而是模型需要具备“表达调用意图”的能力。function-call解决的就是这件事——模型不负责执行它只负责用结构化的方式说清楚“我想调用哪个函数、参数是什么”由你的Java代码来真正执行。1.2 三种扩展方式的取舍RAG、微调与工具调用市面上让模型“变能干”的思路无非三类。第一类是继续训练或者说微调把业务知识融进模型权重里成本高、周期长而且业务一变就得重训不现实。第二类是RAG通过检索把相关资料塞进提示词上下文里适合“知识问答”但你要是让它“提交一个订单”RAG只能提供文档依然没人执行操作。第三类就是function-call。它的核心思路是模型不改、知识可以没有、执行权放在外部系统模型只负责“做决策”。比如用户问“周五的会议帮我订个会议室”模型通过工具描述知道系统里有个bookMeetingRoom(roomId, startTime, endTime)函数于是生成一个结构化的调用请求Java代码收到后去调会议室服务再把结果转成文本还给模型模型最后用自然语言告诉用户“已经订好了”。三类方案其实可以混用但在“让模型做事”这个维度上function-call无可替代。捷径是模型把自然语言转成参数调用你负责干活这是目前Agent架构里最成熟、成本最低的一条路。扩展方式是否修改模型能否执行外部操作成本典型场景微调是否高垂直领域风格迁移、固定格式输出RAG否否中知识问答、私有文档检索function-call否是低任务执行、系统集成、Agent编排1.3 Spring AI把function-call抽象成了什么在原生OpenAI协议里函数调用需要你手工构造functions数组塞进请求体模型返回的tool_calls又要你手工解析一套流程下来代码又脏又容易出错。Spring AI的做法是把这套协议包装成Java接口和注解开发者只写普通方法加一个Tool注解框架自动做协议转换。Spring AI在这一块的核心抽象是ToolCallback接口所有能被模型调用的函数最终都变成一个ToolCallback实现。接口里有三个方法返回工具名、返回工具描述、返回参数JsonSchema还有一个真正执行工具的方法。你日常写的Tool注解方法最后会被包成MethodToolCallback所以理解Spring AI的Tools机制核心就是理解ToolCallback这条线。2. 跑通第一个Tool一个查询工具从0到12.1 工程依赖与模型接入以智谱GLM为例先用Spring Boot 3.4.x建一个空工程然后加上Spring AI的BOM和智谱模型starter。版本这里说一下Spring AI 1.0.x是当前主流稳定主线如果你在GitHub上看到0.9.x的老教程ChatClient这类API可能对不上建议以1.0为准。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-zhipuai/artifactId /dependency配置里填API Key和模型名智谱的接入地址是兼容OpenAI格式的Spring AI的starter会直接处理好spring: ai: model: zhipuai: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-plus2.2 用Tool注解定义一个Java工具方法工具方法就是一个普通Java方法区别在于加了Tool注解。注解里的description字段非常关键模型就是靠这段描述来决定“什么情况下调用这个函数、参数怎么填”的所以你写的描述要像写接口文档一样清晰。Service public class OrderService { Tool(description 根据订单号查询订单当前物流状态返回发货状态和预计送达时间) public String queryLogistics(String orderId) { // 这里实际会查数据库或调用物流API return 订单 orderId 已揽收正在发往广州中转站预计3天后送达; } }这个OrderService必须交给Spring管理因为Spring AI在解析时会从Bean里去扫描方法。方法修饰符必须是public私有方法不会被识别框架需要通过反射来调用它。2.3 ChatClient中的tools配置与一次完整调用ChatClient是Spring AI里统一封装的大模型客户端类似于一个面向LLM的RestTemplate。组装请求时用.tools(...)把工具对象传进去框架会自动扫描该Bean里的Tool方法并注册Service public class AgentService { private final ChatClient chatClient; private final OrderService orderService; public AgentService(ChatModel chatModel, OrderService orderService) { this.chatClient ChatClient.builder(chatModel).build(); this.orderService orderService; } public String queryOrder(String userMessage) { return chatClient.prompt() .user(userMessage) .tools(orderService) .call() .content(); } }跑起来之后你问一句“帮我查一下订单A-1001的物流”实际发生的事是模型看着工具描述知道有个queryLogistics函数于是在回复中生成一个tool_calls结构Spring AI收到后帮你反射调用queryLogistics(A-1001)拿到返回字符串再发给模型模型最终给出“您的订单A-1001已揽收预计3天后送达”这样的自然语言回答。用户根本感知不到中间有工具跳转。3. 源码视角一次工具调用在Spring AI内部经历了什么3.1 从注解到ToolCallback方法如何变成模型看得懂的“工具说明书”模型看到的不是Java源代码而是关于这个工具的“元数据”工具名叫什么、什么时候用、参数是什么结构。Spring AI在注册工具时会扫描Bean里的方法读取Tool注解上的名字和描述然后用JsonSchema生成器把方法参数转换成JSON Schema结构。我用简化的逻辑来描述这个过程实际源码就是这样分层设计的// 简化表示方法 - ToolCallback 的构建过程 ToolCallback callback MethodToolCallback .builder(orderService, orderService.getClass().getMethod(queryLogistics, String.class)) .name(queryLogistics) .description(根据订单号查询订单当前物流状态返回发货状态和预计送达时间) .inputType(String.class) .build();转换完成后的工具描述在请求模型时会被塞进消息里模型看到的大致是这样一份“说明书”{ type: function, function: { name: queryLogistics, description: 根据订单号查询订单当前物流状态返回发货状态和预计送达时间, parameters: { type: object, properties: { orderId: { type: string } }, required: [orderId] } } }这一步是整个function-call机制的底层基础。没有JsonSchema模型就不知道参数该怎么填所以很多时候工具“调不起来”并不是模型不行而是你的参数Schema描述得太含糊。3.2 模型返回tool_calls之后ToolCallingManager的解析循环当模型判断需要调用工具时它不会直接输出普通文本而是返回一个结构化的tool_calls里面包含工具名和JSON字符串格式的参数。Spring AI把这块逻辑收敛在ToolCallingManager的实现类里我读1.0.x源码时核心处理思路是这样的// 简化伪代码工具调用解析链路 for (AssistantMessage assistantMessage : assistantMessages) { if (assistantMessage.hasToolCalls()) { for (ToolCallBlock toolCall : assistantMessage.getToolCalls()) { ToolCallback callback toolCallbackResolver.resolve(toolCall.name()); String result callback.call(toolCall.arguments()); toolResponses.add(new ToolResponseBlock(toolCall.id(), toolCall.name(), result)); } } } return new ToolExecutionResult(List.of(new ToolResponseMessage(toolResponses)));ToolCallingManager负责把模型请求里的tool_calls解析出来按名字把参数JSON字符串传给对应的ToolCallback收集执行结果构成ToolResponseMessage最后把这条新消息追加回上下文。框架会继续保持对话循环直到模型不再请求调用工具为止——也就是说一次用户提问里模型完全可以连续调用好几个工具。3.3 工具执行结果如何回填给模型完成闭环拿到执行结果之后Spring AI不是直接把这个结果返回给用户而是作为一条角色消息发给模型。模型看到“工具执行结果”之后会把结果和原始问题整合生成最终的自然语言回答。这个设计看起来很绕但它是必要的工具返回的往往是结构化字符串比如“SHIPPED, 2025-06-01”用户看不懂模型也不能直接把这个丢给用户而是要组织成“您的订单已发货预计6月1日送达”。如果工具执行失败结果本身也会作为文本回传模型可以据此给出错误说明或引导用户换一种问法。理解了这个闭环你就明白了为什么工具方法要返回文本而不是直接返回对象。Spring AI会把这个返回值序列化成字符串再塞给我模型所以你在工具方法里直接返回String最省事返回复杂对象也行但容易被序列化干扰。4. 实战改造把订单助手从“能聊天”变成“能干活的”4.1 业务拆解给助手配齐三件套工具前面那个查询物流的工具太单薄现在做一个有点业务含量的订单助手。假设后台有三个真实需求查订单状态、查用户可用优惠券、给订单应用优惠券。每个都做成独立工具方法Service public class OrderService { Tool(description 根据订单号查询订单当前状态返回订单状态与预计送达时间) public String queryOrderStatus(String orderId) { return 订单 orderId 已支付正在仓库打包预计明天发货; } } Service public class PromotionService { Tool(description 查询用户当前可使用的全部优惠券返回每张券的面额、有效期和适用条件) public String listAvailableCoupons(String userId) { return 5元无门槛券一张满100减10元券一张有效期至2025年6月30日; } Tool(description 为指定订单应用优惠券返回应用优惠后的订单金额) public String applyCoupon(String orderId, String couponId) { return 订单 orderId 已应用优惠券优惠后金额为95元; } }调用端把所有工具一次性配进去剩下的事交给模型决策String answer chatClient.prompt() .user(帮我查一下订单A-1001的状态如果我有5元券就给我的这个订单用上) .tools(orderService, promotionService) .call() .content();4.2 多工具编排模型自己决定调用顺序这段代码跑起来后模型的行为很有意思。它会在第一轮先同时申请调用queryOrderStatus(A-1001)和listAvailableCoupons(user-001)拿到两个结果后判断“确有5元券”再发起第二次applyCoupon(A-1001, 5元券ID)调用。最终才输出“您的订单已应用5元优惠券实付95元”的回复。这里有个特性值得注意OpenAI兼容协议里模型可以一次返回多个tool_callsSpring AI拿到后会逐个解析执行。如果你的工具之间没有依赖关系并行执行能省一轮模型请求如果工具之间有先后依赖模型会在多轮中自己决定下一步调什么。对Java开发者来说你不太需要手动编排这些工具的执行顺序重点是把每个工具描述得足够完整让模型“会选”。4.3 事务、异常与幂等工具方法不是普通业务方法把业务方法暴露成工具之后有一个容易被忽视的差异模型可能在一次对话里连续调用同一个工具也可能在参数不完整时编造参数。所以工具方法要具备幂等性设计比如applyCoupon不能每次调用都真实扣减一次优惠券建议先校验该订单是否已应用过该券queryOrderStatus这类纯查询方法则天然安全。异常处理同样要小心。工具方法如果直接抛出异常整个对话流程可能直接断裂用户看到的就是“系统错误”。更稳妥的做法是在工具方法内部捕获业务异常把错误信息作为字符串返回比如“优惠券已过期无法使用”这样模型就能基于错误信息给出友好提示而不是对话崩溃。工具方法里涉及数据库操作的场景Transactional同样可以用但要清楚事务边界只在方法内部不会跨多次工具调用。5. 痛点排查Tools不生效、参数错乱、上下文丢失5.1 工具没有注册进对话的最常见原因我见过最多的“工具不生效”案例排名第一的是忘记在prompt().tools(...)里传入工具对象。注解写了、方法也写了调用时没配tools()模型根本不知道有这个函数存在。第二个常见原因是方法不是publicSpring AI反射拿不到私有方法。第三个原因是工具类没有交给Spring管理导致按Bean名字解析时找不到对象。我整理了一个自测清单出问题时按顺序过一遍调用链路上有没有写.tools(orderService)传的是Bean对象还是Bean名字工具方法是不是public是不是定义在Spring管理的Bean里Tool注解有没有写描述描述是否足够让模型判断什么时候调用同一个Bean里有没有两个方法最终生成了同名工具后注册的会覆盖先注册的模型是不是不支持function-call部分轻量模型对工具的支持很弱5.2 参数Schema与模型幻觉导致的调用失败另一个高频坑在参数层面。模型不是人它只能靠你的描述和参数Schema来猜测该怎么填参数。如果你的方法叫queryByOrderId(String orderId)但description里没写清楚orderId格式模型可能把用户说的“我的最后一个订单”编造成一串不存在的单号传进来。解决方案是在描述里尽可能写清楚参数约束例如“订单号格式为字母A加四位数字例如A-1001”。复杂的嵌套对象在Spring AI 0.9到1.0之间变化很大新版本能自动从方法签名里生成Schema但为了减少出错概率工具参数还是优先用基础类型、String、List和简单DTO。如果模型传参类型和你Java方法类型不一致框架在反序列化时会抛异常这类问题日志里一般能看到JsonProcessingException定位起来并不难。5.3 多轮对话中上下文与工具结果的互斥工具调用是附着在对话上下文里的每执行一次工具系统就要往消息列表里塞入“工具请求”和“工具结果”两条消息。如果在一个长会话里频繁调用工具token消耗会成倍增长模型也可能被过期的工具结果带偏。我的建议是把“工具调用消息”和“需要长期保留的对话历史”分开管理。Spring AI的ChatMemory可以配置只保留最近的若干条用户/助手消息但工具过程中的中间消息尽量别纳入长期记忆工具方法返回的结果也要精简只返回必要的业务状态别把整个数据库查询结果原封不动丢回去。你往回塞的每个字符都是成本。6. 进阶绕开Tool注解自定义ToolCallback的玩法6.1 什么场景必须自定义ToolCallbackTool注解方法适合固定写死的工具集但有些场景注解做不到。比如工具列表来自数据库或配置文件不同的租户能看到不同的工具集再比如你希望所有工具在执行前统一做权限校验、接口鉴权、调用链日志记录用注解方式就得在每个方法里重复写。这时候就需要直接实现ToolCallback接口按接口契约自己控制工具的名称、描述、Schema和执行逻辑。自定义ToolCallback是比Tool更底层的玩法也是理解Spring AI Tools机制的必经一步。你不需要再依赖Bean扫描完全可以动态构建工具列表传给ChatClient。6.2 实现一个动态读取配置的工具集实现接口的核心是四个方法工具名、工具描述、输入Schema、执行逻辑。下面是一个简化示例API细节会随版本微调思路是一样的Component public class DynamicOrderTool implements ToolCallback { Override public String getToolName() { return queryExternalOrder; } Override public String getToolDescription() { return 查询外部订单状态参数orderId为字符串订单号; } Override public JsonSchema getToolInputSchema() { return JsonSchema.builder() .name(queryExternalOrderInput) .type(JsonType.OBJECT) .addProperty(orderId, JsonSchema.builder().type(JsonType.STRING).build()) .build(); } Override public String call(String toolInput) { // 解析模型传来的JSON参数 JsonNode node new ObjectMapper().readTree(toolInput); String orderId node.get(orderId).asText(); // 这里可以加权限校验、灰度逻辑、审计日志 return 外部订单 orderId 已签收; } }定义好这个Bean之后和普通工具一样通过.tools(dynamicOrderTool)注册。实际项目中我推荐在工具数量超过十个之后统一抽一个工具注册中心把所有的ToolCallback收集起来传进ChatClient并且给每个工具配上独立的鉴权和限流策略这样线上排查问题时能直接看日志知道“模型在哪个环节调用了哪个工具”。6.3 关于版本选型与兼容性的思考写这篇的时候Spring AI已经走过了0.9到1.0的大版本切换2.0也在路上了。我自己的体会是如果你的项目是全新的直接用当前稳定主线版本不要追0.x老教程如果已经在老版本上有了业务升级时重点看ToolCallback、ChatClient、ToolCallingManager这几个类的API变动工具机制本身很稳定变的主要是包结构和配置项。经常有人拿Spring AI和LangChain4j比较。简单说如果整个后端都是Spring技术栈Spring AI的自动配置、Bean容器集成、starter生态会让你少写很多胶水代码工具调用直接复用容器里的Bean即可LangChain4j的图编排能力更强适合复杂的多Agent状态流。但单论function-call这一个点Spring AI的注解方案对Java团队来说学习曲线是最低的。最后再分享一个我实际做项目时总结的习惯工具方法返回的字符串永远带上状态前缀比如“SUCCESS:已发货”“FAIL:优惠券不存在”这样模型在组织最终回答时能更准确地判断业务状态而不是对着错误的文字描述反复猜测。每次有新工具上线我会先用固定参数手动跑一遍调用链确认Schema生成无误、参数解析正常再丢给模型去自由发挥。等你把这一套工具机制吃透再往多Agent编排、复杂状态机方向走就会轻松很多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WorkBuddy定时任务实战:每天十点半自动推送AI日报到微信 2026/9/26 18:48:00

WorkBuddy定时任务实战:每天十点半自动推送AI日报到微信

1. 为什么我要给 WorkBuddy 设一个“十点半闹钟”每天早上到工位,第一件事不是泡茶,而是打开各种信息源翻一遍:项目群里有没有新需求、昨天提交的代码有没有异常、行业里又出了什么新工具。这套动作重复了几个月之后,我意识到它本…

阅读更多 →
AI Agent从设计稿自动搭建FairyGUI UI结构:方案与踩坑记录 2026/9/26 18:48:00

AI Agent从设计稿自动搭建FairyGUI UI结构:方案与踩坑记录

先讲个真实经历。最近两个月,我几乎把所有能挤出来的时间都用在了同一件事上:让 AI Agent 替我把 FairyGUI 的 UI 结构从设计稿里“搭”出来。起因是新项目的界面量实在太大,光一个主界面就三百多个节点,手拼一遍得小一整天&#…

阅读更多 →
DeskcommCRM:以沟通为核心驱动的桌面端客户管理工具实战解析 2026/9/26 18:47:54

DeskcommCRM:以沟通为核心驱动的桌面端客户管理工具实战解析

DeskcommCRM 这个名字,我第一次看到的时候就觉得有意思。在 CRM 这条已经不算新鲜的产品赛道上,敢把 "Communication" 直接缩写进产品名的并不多。桌面端(Desk)加通讯(Comm)再加客户管理&#xf…

阅读更多 →
屏幕亮度调节的三大物理层级与护眼校准法 2026/9/26 18:47:54

屏幕亮度调节的三大物理层级与护眼校准法

1. 为什么“调亮度”这件事,90%的人从没调对过?你有没有过这种体验:下午三点盯着屏幕写方案,眼睛干涩发烫,眨眼时像有砂纸在磨;晚上关灯刷手机,屏幕白光刺得瞳孔一缩,半小时后头痛隐…

阅读更多 →
WorkBuddy定时任务+deepseek-v4-flash:搭建AI日报自动推送系统 2026/9/26 18:47:54

WorkBuddy定时任务+deepseek-v4-flash:搭建AI日报自动推送系统

1. 为什么我要折腾一个自动推送的 AI 日报每天早上到工位,第一件事是打开浏览器翻十几个页面,看行业动态、看竞品更新、看技术社区的新帖子,一圈下来半小时没了,真正记下来的没几条。这个习惯我坚持了大半年,直到某天早…

阅读更多 →
MySQL到Elasticsearch同步工具:全量与增量同步设计与踩坑 2026/9/26 18:47:47

MySQL到Elasticsearch同步工具:全量与增量同步设计与踩坑

做个人项目的时候,最烦的不是写CRUD,而是要把业务数据放到ES里去做搜索和分析。最初我都是手工写脚本,一条SQL查出来然后循环写入ES,后来发现脚本越来越多,代码重复、配置乱、跑起来还要担心中途失败。某个周末我干脆花…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉