ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI 2.0.1 集成 Qwen 实现 Function Calling 实战指南

Spring AI 2.0.1 集成 Qwen 实现 Function Calling 实战指南 如果你跟我一样是做后端出身的最早接触大模型时大概率是抵触的模型能帮我算数学题、写招聘 JD但一碰到业务系统它就哑火了——因为它拿不到数据库里的订单状态调不了物流接口也无法替用户提交工单。直到我真正用上 Function Calling工具调用才意识到过去那套意图识别 switch 分发的后端思路在 AI 时代完全换了一套玩法。这篇文章我把整个过程从 0 到 1 拆开讲基于 Spring AI 2.0.1接入阿里云百炼的 Qwen 系列模型完整落地一套带工具调用的项目。内容覆盖原理、编码、调试、Dify 工作流迁移参考以及我实测踩过的坑。不管你是刚接触 Spring AI 的新手还是已经在生产环境里写过 RAG 的老手都能从里面找到能直接抄作业的部分。1. Function Calling 到底解决了什么问题从聊天机器人到会动手的助手1.1 一个后端程序员的第一直觉为什么要让模型碰我的函数我刚开始做 AI 集成时脑子里默认的架构是这样的先用一个意图分类模型判断用户想干嘛然后写一堆 if/else 去路由背后的业务逻辑。这个方案不是不能用但维护成本极高尤其当业务动作超过十个时意图标签会互相打架业务部门还天天要求加新功能。Function Calling 的解题思路完全不同你不需要提前给模型规定哪种问题调用哪个函数只需要把函数的名字、参数说明、功能描述作为元数据同步给模型。模型根据用户提问自己去决定要不要调用、调用哪个、参数填什么。它更像是在下单而不是在执行。1.2 模型的工作流你以为是代码在调用函数其实是模型在下单我自己在没拿到抓包数据之前一直以为函数调用是代码本地直接执行。实际链路是这样的应用把工具定义JSON Schema随用户消息一起发给模型。模型读完用户问题发现自己知识有限于是返回一个特殊的tool_calls字段里面写着我想调用 getWeather参数是杭州。应用收到这个指令后在本地执行真正的天气查询逻辑。应用把执行结果作为一条 tool 消息回传给模型。模型结合工具结果合成一段正常的自然语言回答。注意步骤 3 的执行方是应用代码不是模型。模型只是负责决策怎么用工具真正做事的还是你的 Java 方法。理解这一点特别关键很多人以为 Spring AI 会自动执行函数其实框架只是把模型下的单和你的函数做了绑定和调度。1.3 适用场景与不适合场景Function Calling 最典型的落地场景是需要和内部系统打交道的助手、需要实时数据支撑的问答、需要多步骤操作的 Agent 流程。比如查天气、查库存、创建工单、发邮件这些动作背后都有确定性逻辑模型自己答不准但调用工具后能给你一个可靠结果。不适合的场景也有纯粹娱乐闲聊、需要超高并发且对延迟极度敏感的场景。工具调用本质上是多轮交互每轮都要走一次模型推理延迟和 token 消耗天然比单次问答高。如果你的业务只是讲个笑话或者翻译一句话硬套工具调用纯属浪费。2. 环境与工程准备Spring AI 2.0.1、百炼账号与最小可运行工程2.1 版本选择Spring AI 2.0.1 与 Spring AI Alibaba 的前世今生Spring AI 的版本变化非常快。早期 0.8.x 的时代工具注解叫FunctionCalling后来 1.0 正式版改成了ToolAPI 风格也有不小调整。我在本地锁定的是spring-ai 2.0.1这是 2.x 系列里相对稳定且语法统一的版本。这里有个绕不开的话题Spring AI Alibaba 到底怎么了网上关于它停更的讨论很多。我观察到的实际情况是Spring AI 主线项目已经将阿里云百炼DashScope作为官方模型提供商支持spring-ai-starter-model-dashscope可以直接使用Spring AI Alibaba 仓库的发布节奏确实没有 2024 年那么密集但官方集成通道并没有关闭。说白了不是死了是功能并入了主线——今天接入百炼 Qwen直接依赖主线 starter 反而更省心。2.2 搭建工程Maven 依赖与配置文件我用的是 Spring Boot 3.x Spring AI 2.0.1 的组合。最小工程只需要两个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId version2.0.1/version /dependency如果你所在团队还在用 Spring AI Alibaba 的自定义 starter可以保留com.alibaba.cloud.ai:spring-ai-alibaba-starter但注意它的注解与主线可能不一致。我推荐直接用主线 starter后面讲的Tool、ChatClient都是主线 API网上资料也多遇到问题更好搜。配置文件长这样spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen3.7 temperature: 0.7qwen3.7这个模型名是我在百炼控制台里选的具体规格名以你的账号实际可见为准。关键是api-key一定要用环境变量注入不要写死在仓库里。2.3 用 Spring AI 三行代码先跑通一个普通对话在折腾函数之前我建议你先验证一下基础链路通不通。Spring AI 的ChatClient是可以直接这么用的var client ChatClient.builder(chatModel).build(); String answer client.prompt() .user(你好请简单介绍一下你自己) .call() .content(); System.out.println(answer);这里唯一要确认的是ChatModel这个 Bean 已经存在。用了spring-ai-starter-model-dashscope之后Spring Boot 会自动装配好。如果你连这一步都卡住先去看控制台有没有解析到dashscope.api-key比调函数快得多。3. 第一个 Tool让模型学会查天气3.1 创建一个带 Tool 的函数基础链路通了之后就可以开始第一个工具了。我做的第一个 Demo 是天气查询理由是这个场景人尽皆知而且不依赖复杂业务系统。Spring AI 2.x 的做法是用Tool注解标记方法import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; Component public class WeatherTools { Tool(name getWeather, description 查询指定城市的实时天气情况) public String getWeather(ToolParam(description 城市名称比如杭州、上海) String city) { // 这里通常是调用外部天气 API我把逻辑简化了 return city 晴26 摄氏度东南风 2 级空气质量优; } }有几个细节要特别说。description不是写给人看的是写给模型看的。模型靠这段话判断什么时候该调这个函数所以描述要尽量写清楚边界。比如你可以写仅当用户明确询问天气时调用不要只写一个天气函数。ToolParam的 description 也很重要。模型拿到参数说明后才知道往里面填什么值。我见过一个坑参数描述写得太泛模型把全国所有城市名都传了一遍最后还是靠我在方法里做白名单校验才拦住。3.2 把工具注册进 ChatClient有了工具类之后注册极其简单var response client.prompt() .system(你是一个生活助手回答天气问题时必须使用天气工具) .user(杭州今天热不热) .tools(new WeatherTools()) .call() .content();tools(...)方法会自动扫描传入对象里的Tool注解方法生成 JSON Schema 传给模型。如果你有多个工具类直接.tools(new WeatherTools(), new OrderTools())就行。我建议显式调用.tools(...)而不是放到全局配置里因为不同业务场景对工具集合的要求完全不同。全局注册会让模型在每一个对话里都背着所有工具token 消耗会明显上涨。3.3 运行时到底发生了什么请求体里的 tools 与 tool_calls跑起来之后我强烈建议你打开抓包工具或者加一条 DEBUG 日志看看真实请求体。模型中那边收到的工具定义大概长这样{ type: function, function: { name: getWeather, description: 查询指定城市的实时天气情况, parameters: { type: object, properties: { city: { type: string, description: 城市名称比如杭州、上海 } }, required: [city] } } }而模型返回的内容里不再只是content还会带一个tool_calls数组{ tool_calls: [ { id: call_123, type: function, function: { name: getWeather, arguments: {\city\:\杭州\} } } ] }看到这个结构你就理解我前面说的模型在下单是什么意思了。Spring AI 拿到了这段响应后会找到匹配的 Java 方法执行然后把结果拼成 tool 消息回传。3.4 一个反直觉的发现函数的执行不是模型做的这一点我必须单独拿出来强调因为团队里第一次接入的人几乎都会理解偏差模型只负责生成tool_calls不会真实执行你的方法。执行是在你的 JVM 里完成的模型只不过是基于 JSON 描述远程控制了你的代码。这个设计其实是合理的。如果真把执行权交给模型你的数据库密码、第三方接口密钥全部都要暴露给模型服务商安全性直接崩盘。而现在的机制下工具描述只是元数据模型无法看到实现细节敏感信息依然留在本地。4. 进阶多工具编排、参数校验与错误兜底4.1 同时挂三个工具让模型自己挑真实项目里不会只有一个工具。我在客服系统里同时挂了查订单、查物流、查售后政策三个工具Component public class CustomerServiceTools { Tool(name queryOrder, description 根据订单号查询订单基本信息) public String queryOrder(ToolParam(description 用户提供的订单号) String orderId) { // 查询数据库 return orderService.findBrief(orderId); } Tool(name queryLogistics, description 根据订单号查询物流轨迹) public String queryLogistics(ToolParam(description 订单号) String orderId) { // 调用物流接口 return logisticsService.trace(orderId); } Tool(name queryAfterSalePolicy, description 查询售后政策包括退换货规则和保修期限) public String queryAfterSalePolicy(ToolParam(description 商品品类) String category, ToolParam(description 购买天数用于判断是否在保修期) Integer days) { return policyService.findPolicy(category, days); } }用户说我的订单 102938 到哪了模型会自行选择queryOrderqueryLogistics甚至先查订单再查物流多个工具按顺序执行。你不需要写任何路由逻辑这是 Function Calling 和传统意图识别最大的区别。4.2 参数校验与 tool 消息回传参数校验是另一个容易翻车的点。模型可能传超出预期的值比如days -1或者category 全部。我在工具方法内部加了防御逻辑if (days null || days 0 || days 365) { return 购买天数不合法请用户确认购买时间; }这里有个小技巧当参数不合法时不要直接抛异常而是返回一段描述性文字。因为这段文字会被当作 tool 消息回传给模型模型会基于它重新组织回答告诉用户系统检测到你输入的购买天数可能不对。如果你抛异常部分模型会直接道歉并终止体验很差。4.3 错误处理工具抛异常时模型在干嘛工具内部真正调用第三方接口时网络异常很难避免。我做过对比实验工具直接抛RuntimeException时百炼的 Qwen 系列模型通常会在下一轮说抱歉我无法完成该操作虽然不报错但很生硬。更优雅的做法是捕获异常并转换为可读文本try { return thirdPartyService.queryWeather(city); } catch (Exception e) { return 天气服务暂时不可用请提醒用户稍后再试; }这样模型就能在回复中加入天气服务暂时不可用建议稍后再查这类话术。记住一点工具调用里的所有返回值最终都会变成模型的上下文你写进返回值的每一个字都可能成为模型向你用户输出的内容。工具返回值要当作用户可见的文案来写。5. 从 Dify 工作流到 Java 代码迁移思路与实战映射5.1 Dify 的节点模型 vs Spring AI 的组件最近很多人问我把 Dify 工作流转成 Spring AI Java 代码的问题。我在公司里也做过两套系统的迁移。先说结论Dify 的很多节点可以在 Spring AI 里找到对应物但并不能一一精确复制因为 Dify 是可视化编排Spring AI 是代码编排。我整理了一个映射表方便你对照Dify 节点Spring AI 对应方案开始节点用户输入ChatClient.prompt().user(...)LLM 节点ChatClient ChatModel工具节点Tool 方法 ToolCallingManager知识库检索节点向量库相似度检索 内容注入 System Prompt条件分支节点Java 代码中直接写 if/switch代码节点普通 Java 方法天然替代变量聚合节点通过 ChatMemory 或上下文对象保存结束节点call().content() 返回核心不同在于Dify 里工具节点侧重流程固定而 Spring AI 里工具调用更多是模型自主决策。如果原工作流非常线性迁移到 Spring AI 后甚至可以用普通方法链直接完成只有在依赖模型判断走哪条分支的时候才需要引入 Function Calling。5.2 一个订单状态查询工作流的完整迁移我在 Dify 里曾搭过一个订单查询工作流流程是用户输入订单号 - 判断是否输入了订单号 - 调用订单查询工具 - 拼接结果返回。迁到 Spring AI 后代码反而比画图更清爽String answer client.prompt() .system(你是电商客服。查询订单时调用 queryOrder 工具如果用户没给订单号先询问订单号。) .user(帮我看看我的快递到哪了) .tools(new CustomerServiceTools()) .call() .content();就这么简单。原来在 Dify 里要拖拽十几个节点的流程Java 代码反而浓缩成了几行。有些时候不是工作流复杂而是可视化工具把复杂度显性化了。5.3 迁移过程中的 3 个注意点第一Dify 里的变量注入往往依赖预设字段Spring AI 则完全靠 prompt 和工具参数传值你需要把每个变量来源想清楚这比 Dify 的连线更考验设计。第二Dify 的工具节点通常是一次性调用而 Spring AI 可以支持多轮工具调用循环模型会边看结果边决策下一步这一点迁移后效果通常更好。第三Dify 的调试界面非常适合业务人员看流程迁移到 Java 后这类可视化能力会丢失。建议在业务团队还没适应代码之前先把日志平台配好否则业务人员会非常没有安全感。6. 性能、超时与调试我在百炼上实测踩过的坑6.1 工具描述越短越省 token工具描述和参数描述都会拼进每次请求的 token 里。如果你的系统同时注册了 10 个工具每个工具描述 50 个字每次对话光工具定义就吃掉上千 token成本和延迟都会明显上升。我的优化方法是把 tool description 控制在 20 字以内参数 description 控制在 10 字以内把详细的判断规则放到工具方法内部去处理。模型只需要知道什么时候用、传什么值不需要看完整业务规则。经过实测同样的业务场景精简描述后首 token 延迟从 1500ms 降到 900ms 左右效果非常明显。6.2 工具调用循环的迭代上限Spring AI 内部会在一次对话里自动执行多轮工具调用直到模型认为任务完成。大多数情况下这是好事但也可能失控比如模型反复用一个工具调同一份数据或者工具之间互相报错导致重试。我在生产环境里给它加了一道保险spring: ai: chat: client: max-tool-executions: 5这个配置把单次用户请求里的工具执行次数限制在 5 次内。如果超出框架会主动终止并让模型基于已有信息收尾。我的经验是正常客服问答 2-3 轮工具调用就够了超过 5 轮基本就是模型在兜圈子。6.3 日志与可观测如何看清每一次 tool_calls调试阶段最烦的是看不到模型到底选择了哪个工具。建议至少在测试环境把 Spring AI 包日志打开logging: level: org.springframework.ai: DEBUG开启后你会在控制台看到类似这样的核心日志tool call: getWeather, arguments: {city:杭州} tool response: 杭州晴26 摄氏度...这一步能帮你快速区分问题出在模型选错了工具还是工具执行报错还是回传结果没被模型正确理解。我团队里新人第一次调通工具后我都会让他们先看着日志口述一遍完整流程确认真的理解了调用链。6.4 流式场景下的工具调用如果你用.stream()做流式输出要注意工具调用的处理逻辑和普通问答不同。流式场景下模型可能先输出一段空内容再携带tool_calls接着执行工具最后再流式输出最终回答。Spring AI 的ToolCallingManager对这块做了封装但你需要在业务侧做好结果收集不能假设流式返回的第一段内容就是最终答案。我实际做下来觉得前期用同步调用调试逻辑确认没问题后再切流式是最稳妥的路径。一上来就搞流式碰到工具调用时你会被混乱的输出片段整崩溃。7. 关于 Spring AI Alibaba 的现状与选型建议7.1 停更传闻是怎么来的网上关于spring ai alibaba 停更的讨论不少。我专门去翻过仓库和发布记录结论是Spring AI Alibaba 并没有彻底停摆但它与 Spring AI 主线的边界确实发生了变化导致很多开发者产生了误解。早期百炼接入确实主要靠 Spring AI Alibaba 的 starter社区也很活跃。后来 Spring AI 主线官方直接支持了 DashScope 模型主线的spring-ai-starter-model-dashscope逐渐成为更被推荐的接入路径。两套 starter 功能重叠Spring AI Alibaba 的更新频率自然就降下来了。你可以把它理解为任务完成了重心转移了而不是项目死了。7.2 今天接百炼 Qwen 的最佳姿势以我最近的实战经验来说接百炼 Qwen 模型最推荐的方式就是主线 starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId version2.0.1/version /dependency原因有三个。第一API 与 Spring AI 官方文档完全一致出问题好搜。第二Tool、ChatClient、Advisor这些核心组件都是主线的不牵扯额外适配层。第三主线版本迭代快bug 修复及时长期维护成本低。如果你一定要用 Spring AI Alibaba 的自定义功能那先把两套 API 的差异列清楚再动手特别是注解和鉴权方式避免写到一半发现混用了。7.3 我的选型建议如果你是全新项目直接主线 Spring AI DashScope starter其他什么都别加。如果你是老项目已经在用 Spring AI Alibaba并且稳定运行不要因为停更传闻急着重写功能不坏就能继续用。真正要做的是在新功能开发时逐步向主线 API 收敛给团队留一条平滑迁移的路。对于模型选择我个人测试下来Qwen 系列的函数稳定性在中文场景下表现很好工具描述的语义理解比较准很少出现明明有工具却不用的情况。但你数据里如果有大量英文、混语言场景建议自己在测试集上多跑几轮对比不要只看benchmark。8. 实战中的性能、超时与其它细节8.1 HttpClient 超时设置工具调用链路很容易碰到模型返回 tool_calls 很快但第三方业务接口很慢的情况。如果你在外层给请求设置的超时时间过短工具还没执行完HTTP 就被掐断了最终用户收到一句莫名其妙的报错。我目前的经验值普通工具调用场景connectTimeout 设 3sreadTimeout 设 30s。如果工具涉及上传、复杂计算readTimeout 再放宽到 60s。这些可以在自定义RestClient或 DashScope 的 HTTP 客户端配置里调。8.2 并发与线程安全Tool注解的方法默认是单例对象的公共方法所以必须保证方法本身是无状态的。不要在工具方法里用成员变量保存用户上下文否则高并发下数据会串。如果多个用户同时调用同一个工具参数只会从tool_calls的 JSON 里解析出来不会混。但如果你在方法内部用了 ThreadLocal 或者可变的全局变量灾难就来了。我的团队规定工具方法必须是纯函数所有依赖都通过参数传入。8.3 与 ChatMemory 的组合运用工具调用的中间结果默认会留在对话上下文里如果你显式配置了ChatMemory那每一轮的工具执行结果都会被保存。这在多轮对话里很有用用户先问订单再问那我这个能退款吗模型能记住上一轮查到的订单状态直接推导。但要注意记忆膨胀问题。工具返回的原始 json 可能很长全部塞进记忆会让上下文爆炸。我建议把工具返回值先做摘要再放回上下文比如查询订单只保留已发货预计周三送达而不是把完整物流列表全部返回。这个优化对长对话项目基本是必须的。我见过一个线上事故用户连续问了 20 轮每轮工具返回 2 千字物流轨迹最后一次请求直接把上下文撑爆模型开始胡说八道。从那以后我所有工具方法的返回值都刻意精简只输出结论性信息。工具调用的调试最后再分享一个小技巧当你怀疑模型为什么不用某个工具时把它单独放进一个隔离环境只注册这一个工具再传一句非常明确的问题。如果单独场景下能用说明是工具集过大或描述歧义如果单场景都不能用那基本是工具定义或模型版本的问题。用二分法定位比盯着日志猜快得多。
返回列表