新闻详情

新闻详情

首页 / 资讯中心 / 详情

Function Calling实战:用Spring AI让Java接入Qwen等大模型

发布时间:2026/10/2 3:58:30来源:尧图网络
Function Calling实战:用Spring AI让Java接入Qwen等大模型
前阵子接手一个传统ERP系统的“AI订单助手”需求时我最深的一个感触是LLM 像个知识面很广但手脚不通的实习生——聊常识没问题一碰到实时库存、运费规则、订单状态这类业务信息就直接哑火。要让模型真正“办事”绕不开的正是 Function Calling。再配合 Spring AI 这套 Java 生态下的 AI 框架就能把“模型决定调什么函数、Java 执行函数、结果再喂回模型”这一整条链路跑得明明白白。这篇文章是我从零搭到能上线用的完整记录原理、选型、代码、排坑一条龙适合刚接触 Spring AI 的团队也适合正想把 Agent 项目往工程化方向收口的人。1. 先讲清楚 Function Calling 到底解决了什么问题1.1 模型“不会算”也“看不见”业务的真正原因经常有人问我大模型都这么强了为什么还搞不定一个“查订单”的小需求因为大模型本质上是一个语言模型它靠的是训练时见过的文本模式来做预测。你问它“李白是哪朝人”它能答因为语料里到处是这个你问它“订单 SO-2024-0912 现在到哪一步了”它就毫无办法因为这份数据只存在于你的 ERP 数据库里模型既没看过也不可能“凭空推理”出来。同样的道理也适用于计算。LLM 能算 11能算两位数乘法但让它算连续三个月各销售区域的运费加总、税金、折扣之后再告诉你毛利率它大概率会在某一步“自信地”出错。这不是模型笨而是它的架构就不是为精确计算设计的。你让实习生口算一百行账单他也会算错但如果你递给他一个计算器和一个接口他就能给你一份靠谱的报表——Function Calling 就是这个“计算器加业务接口”的协议层。更准确地说Function Calling 让模型不再是被动答题的“聊天框”而是具备了“提出调用请求”的能力当用户提问需要实时数据或精确计算时模型会按约定好的协议返回一个结构化的函数调用意图。真正去查数据库、调用外部 API、执行规则引擎的仍然是你的 Java 代码。模型只是“指挥官”业务系统才是“手脚”。1.2 一次函数调用背后的完整协作闭环模型和小工具有多轮协作整个链路在底层是这么走的用户提问进入模型同时把系统提示词和已注册函数的描述信息一起送过去。模型判断当前提问需要外部数据于是不再直接生成答案而是输出一个结构化的“Tool Call”比如帮我调用queryOrder参数是orderId SO-2024-0912。Spring AI 收到这个调用意图后在 Java 侧找到名为queryOrder的函数反射调用并拿到结果。这个结果会被组装成一条新的消息再送回模型。模型根据工具返回的真实数据生成面向用户的最终回复。这五步看着简单却是 Function Calling 的全部精髓。你可能也注意到了步骤 2 里模型“决定调用”但真正“执行”的是 Java 侧代码。这样设计有个很大好处函数里可以加权限校验、日志、限流、审计所有业务边界和安全都在你自己的代码里兜着模型永远不会直接碰数据库。1.3 为什么偏偏选 Spring AI 而不是其他方案目前市面上的方案很多直接调各家模型的 HTTP 接口自己拼协议、用 LangChain4j、或者干脆把业务逻辑全部搬到 Dify 这类低代码平台。我最终选择 Spring AI核心原因是它把“AI 应用”这件原本很“脚本化”的事纳入到了 Spring 的开发范式里。Spring AI 做的事情可以理解为“AI 领域的 JDBC”它定义了一套统一的客户端接口让你用几乎一致的方式对接 OpenAI、通义百炼、DeepSeek、Ollama 等不同模型。业务代码不绑定某家模型换模型只需要改基础配置这对企业项目来说太关键了。再加上它对 Function Calling、结构化输出、多轮对话记忆、流式响应都有完整封装配合 Spring Boot 的自动装配和依赖注入Java 工程师可以零成本上手。我踩过不少坑之后最大的感受是Spring AI 的价值不是追新而是“稳”。它让 AI 能力变成了你熟悉的三层结构——Controller 调 ServiceService 里注入 ChatClient业务方法注册成工具一切都是有类型、可测试、可维护的 Java 代码。2. 核心细节拆解Java 方法是如何被“翻译”给模型的2.1 从 ChatClient 到 ChatModelSpring AI 的统一抽象动手写代码之前得先分清两个容易混的概念ChatClient和ChatModel。ChatModel是底层模型接口每个模型提供商都用自己的实现比如阿里百炼对应DashScopeChatModelOpenAI 对应OpenAiChatModelChatClient则是更上层的门面负责帮你组装 Prompt、处理工具调用、简化流式输出。在 Spring AI 里你通常不会直接碰ChatModel而是注入ChatClientService public class OrderAssistantService { private final ChatClient chatClient; public OrderAssistantService(ChatClient.Builder chatClientBuilder) { this.chatClient chatClientBuilder .defaultSystem(你是一个订单助手回答用户问题时必须使用工具获取真实数据。) .build(); } }ChatClient.Builder是自动注入的构建时可以设定全局默认参数。这个 pattern 跟平时写 RestTemplate、JdbcTemplate 很像Spring 生态的老读者基本能无缝切换。2.2 函数注册的两种主要形态Spring AI 里给模型提供可调用函数常见的有两种写法。第一种是BeanFunctionDescriptionBean Description(根据订单号查询订单状态、物流信息数据来自内部ERP系统) public FunctionOrderQueryRequest, OrderQueryResponse queryOrder(OrderRepository orderRepository) { return request - { OrderInfo order orderRepository.findByOrderId(request.orderId()); return new OrderQueryResponse(order.status(), order.logisticsDetail()); }; }第二种是Tool注解打在 Service 方法上Tool(description 根据订单号查询订单状态) public OrderQueryResult queryOrder(ToolParam(description 订单号) String orderId) { return orderRepository.queryResult(orderId); }两种方式 Spring AI 都能识别并把方法签名转换成模型可理解的 JSON Schema。区别在于Bean方式更适合作为独立可复用的工具注册Tool方式更适合把现有 Service 方法直接暴露给模型代码侵入更小。我个人的习惯是如果是新写一个原子能力用Bean如果是复用已有 Manager/Service 方法用Tool。2.3 JSON Schema 的自动生成让模型知道怎么“按格式”调用模型能准确执行函数靠的不是“看懂你的 Java 代码”而是看你交给它的函数描述。Spring AI 会把函数名、参数定义和方法上的Description一起转换成一份 JSON Schema 传给模型{ name: queryOrder, description: 根据订单号查询订单状态、物流信息数据来自内部ERP系统, parameters: { type: object, properties: { orderId: { type: string, description: 订单号 } }, required: [orderId] } }模型拿到这份 Schema 后就知道“噢我如果想知道订单状态应该给这个函数传一个字符串orderId”。所以为什么很多同学反映“模型老乱传参数”大概率是Description写得太敷衍。比如只写“查询订单”没说订单号格式模型就可能给你传整数或者把两个字段合并成一个。描述文案就是你写给模型看的接口文档越具体模型越听话。这里还有一个容易忽略的细节函数的入参和返回值建议使用 POJO 或 record尽量不要用Map。因为 Spring AI 需要依据类型信息生成 JSON SchemaMap会让它无法推断字段最后常常生成一个空 Schema模型拿到也是懵的。3. 实操Spring AI 2.0 连接百炼 Qwen3.7 从零写起3.1 建项目与配置依赖我用的环境是 JDK 17、Spring Boot 3.5.x模型接入走的是阿里云百炼 DashScope底层模型用 Qwen3.7 系列。接入方式比较省事的是阿里官方维护的 Spring AI Alibaba 起步依赖。以 Maven 为例pom.xml 里加parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.3/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0.2/version /dependency /dependencies这里特别提醒Spring AI 各组件之间的版本联动很敏感建议直接用 Spring Boot 的 BOM 或对应 AI 的 BOM 统一管理不要自己手动填一堆 starter 版本不然很容易出现某个模块加载时找不到类或方法签名对不上的问题。3.2 配置文件与重点参数在application.yml里配spring: application: name: function-calling-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY:替换成你的key} chat: options: model: qwen3.7 temperature: 0.3如果你手上的模型列表里没有qwen3.7换成qwen-plus或qwen-max也一样配置只差一个模型名。这里我把temperature调低到0.3原因是工具调用场景更看重“稳定输出”温度太高会让模型在挑选函数和参数时变得飘忽。还有一个容易被忽略的参数是max-tool-callbacks或者模型上下文长度控制。在和百炼对接时模型默认单轮对话允许调用的工具次数是有限的如果你的工作流里需要连续查三次数据才能回答务必确认配置允许多次 Tool Call否则模型第一次返回调用意图后连结果还没拿回来就断掉了。3.3 完整示例订单查询 运费计算双工具下面这个例子完整展示了两个工具协同工作的场景。假设用户问“订单 SO-2024-0912 现在发货了吗如果走顺丰到杭州运费大概多少”我们注册两个函数一个负责查订单状态一个负责按规则计算运费。public record OrderQueryRequest(String orderId) {} public record OrderQueryResponse(String status, String carrier, String destination) {} Bean Description(根据订单号查询订单的当前状态、承运商和目的地) public FunctionOrderQueryRequest, OrderQueryResponse queryOrder(OrderRepository orderRepository) { return request - orderRepository.findByOrderId(request.orderId()); } public record FreightRequest(String orderId, String expressCompany, String city) {} public record FreightResponse(String amount, String estimatedDays) {} Bean Description(根据订单号、快递公司和目的城市计算运费与预计送达天数) public FunctionFreightRequest, FreightResponse calculateFreight(FreightRuleService freightRuleService) { return request - freightRuleService.calculate(request); }然后在 Service 里发给模型Service public class OrderAssistant { private final ChatClient chatClient; public String ask(String userMessage) { return chatClient.prompt() .user(userMessage) .tools(queryOrder, calculateFreight) .call() .content(); } }不要小看这短短一段代码。整个运行过程里模型可能会多次调用工具先调queryOrder拿到状态和目的地发现用户在问运费又调calculateFreight算费用最后整合两部分数据生成自然语言回复。如果用户运气好模型可能连续调用几次都不需要你写任何额外的“判断逻辑”——这就是 Function Calling 和硬编码规则的本质区别模型自主编排工具调用顺序你只负责提供工具和收口结果。3.4 流式输出和异步场景下的工具调用注意点生产环境里用户往往等不了整段回复生成完所以流式输出几乎成了标配。Spring AI 支持public FluxString askStream(String userMessage) { return chatClient.prompt() .user(userMessage) .tools(queryOrder, calculateFreight) .stream() .content(); }但请注意流式模式下工具调用的结果不会作为一个完整的“函数返回消息”直接出现在content()流里。你真正要做的是先把函数调用的中间过程处理好再让最终内容以流式输出。以我的经验建议第一版先老老实实用.call()同步跑通再改流式。千万别一上来就流式调试工具调用问题时会被“输出一半突然调用函数”的体验折磨到怀疑人生。4. 进阶Dify 工作流迁移成 Spring AI Java 代码的取舍4.1 Dify 工作流与 Spring AI 的本质差异最近在社区里常看到“把 Dify 工作流转成 Spring AI Java 代码”的讨论GitHub 上也出了不少这类迁移项目。这背后折射的是一个真实需求很多团队用 Dify 快速验证了 AI 功能可一旦要嵌进核心业务系统低代码平台的劣势就出现了——不好做精细的权限控制、不好做代码审查、不好跟现有微服务链路打通。把人工作流换到 Java 代码就成了一件很自然的事。但要先想清楚Dify 工作流是“设计时编排”你在画布上把节点串好运行时就按这个图一步步走Spring AI 的 Agent/工具调用是“运行时编排”模型根据用户问题动态决定下一步调什么。两者差别很大。把 Dify 工作流搬过来最简单粗放的思路是把每个节点翻译成一个函数然后硬编码一个顺序执行器。这么做能跑但基本等于用 Java 重新实现了一遍 Dify 画布失去了 Agent 该有的灵活性。我的建议是分两类来处理如果原 Dify 工作流的节点顺序基本固定那么直接在 Service 里按顺序调用对应方法即可完全不依赖模型编排。如果原工作流里有大量“条件分支”那才适合把每个条件分支收敛成独立的函数交给模型在运行时判断。4.2 我从 Dify 迁移到 Spring AI 的调整思路从落地角度看迁移工作大概分四步梳理工作流里的所有“原子动作”把它收敛成 Java Service 方法比如“查订单”“查库存”“算运费”每个动作一个方法。给每个方法补充Description描述里把触发条件和参数说明写清楚这是模型正确调用的基础。把工作流里的“记忆变量”换成 Spring 的 ConvexMemory 或自定义缓存比如上下文里保存上一轮的查询结果。把原工作流里的固定分支写成一个普通的 Java 路由逻辑无法预判的分支才交给模型调用工具。另外聊一下社区里出现的“Dify workflow YAML 转 Spring AI Java 代码”工具目前这类工具生成的大多是脚手架能快速生成节点对应的类和方法骨架但真正的业务逻辑、数据库查询、权限控制仍需自己去填。我建议把它当“草稿生成器”用不过度迷信。4.3 关于“Spring AI Alibaba 停更了吗”的查证不少人在论坛问“spring ai alibaba 是不是停更了”。我在实际使用中看到的结论是没有停更只是版本迭代和坐标迁移导致了一些误读。Spring AI Alibaba 早期版本用com.alibaba.cloud.ai下面的spring-ai-alibaba-starter后来随着 Spring AI 主项目升级到 2.0阿里侧也同步发布了对应新版本的适配。社区里有人看到旧的 0.x 版本不再更新或某些模块改名就误以为整个项目停了。实际上用 2025 年下半年到现在的版本依然能正常拉取依赖、正常连接百炼。这里我给你一个建议第一次接触别追新直接参考官方仓库 release 页当前标注的最新稳定版本配合 Spring Boot 的版本一起锁定。如果是公司内已有项目升级前务必看 release notes因为 Spring AI 2.0 相比 1.0 在配置项、包名、API 名称上都有调整闷头升级一定会炸。5. 避坑清单与常见问题速查5.1 环境与依赖类问题我整理了一份实操中最高频的问题表格这些都是我真实踩过、也给同事排查过的现象根因解决方式启动时报找不到ChatClient.Builder引入的 starter 不对或版本太旧确认使用的是 spring-ai-alibaba-starter 合适的版本并锁定 Spring Boot 版本运行时提示找不到某个工具函数tools(name)里写的名字和Bean方法名不一致统一采用英文短横线或驼峰保持一致并在配置中排查工具自动扫描路径JSON Schema 生成失败模型乱传参入参用了Object或Map无法推断字段改成固定的 POJO/record字段名尽量英文且含义单一模型说了一堆话却不调用工具系统提示词压制了工具能力检查defaultSystem中是否限制了“不借助外部数据直接回答”必要时明确要求“必须调用工具”同一条链路多次调用工具却中断单轮工具调用次数上限或超时调大模型侧的工具调用次数上限并设置合理的超时重试策略5.2 业务与性能细节工具函数本身也是要调数据库、调外部接口的。一旦模型真的开始调用了你服务的线上 QPS 就会混合着“模型请求自身的延迟”和“业务接口的延迟”。这里我在实战中基本固定了三个原则工具函数里必须做超时控制。用 Spring 的Async或者 HttpClient 的超时配置都行千万别让模型等一个三分钟才能返回的数据。工具函数的异常必须转成“可读的自然语言”。模型调用工具时如果抛出异常Spring AI 通常会把异常信息以某种方式回传给模型。但如果你直接抛NullPointerException模型也会困惑。建议在函数内部 catch 好返回一段“查询失败请重试或联系管理员”类的结果模型的回复质量会明显提升。对敏感操作要增加二次确认。不是所有工具都适合让模型直接调用比如“删除订单”“改价格”。这类高风险工具我建议在函数入口检查一个标志位或者干脆不注册给模型改由人工在前端确认后再触发。5.3 论调试技巧没有日志就没有排障Function Calling 最反直觉的一点是你以为出错在代码其实出错在“提示词”或“Schema 描述”。所以排查问题千万别盯着 Java 栈先把请求和响应的完整链路日志打出来。推荐的日志关键点有三个发往模型的消息列表含工具定义、模型返回的工具调用请求、工具执行结果。Spring AI 的ChatClient可以用事件监听机制把每次 Tool Call 的参数和结果都记录下来开发阶段我会写一个简单的日志切面把这些中间过程按 JSON 格式打到日志文件里。排查模型“乱回答”时这些日志比任何代码断点都有用。一个屡试不爽的小技巧复制日志里发给模型的那段 messages JSON到百炼的在线对话调试里手工重现一次不断改函数描述直到模型每次都能正确选函数为止。这样能快速隔离出“是模型问题还是代码问题”。5.4 长期维护的架构建议项目上线只是开始后面维护 AI 功能要面对的最大问题是“函数会越来越多”。我手上这个订单助手第一版只有 4 个函数三个月后涨到了 20 多个。函数一多模型反而容易“选择困难”每次调用前都要把函数列表全扒一遍既慢还可能选错。这是很现实的工程问题。我后来用的办法是给函数做分组按业务域拆成多个工具集比如订单域、商品域、售后域。模型只加载当前场景相关的工具集而不是全量注册。Spring AI 的ToolCallback支持按需加载我一般通过路由或对话意图简单判断一下用户问题属于哪个域再决定tools(...)传哪些函数名。对模型响应速度和准确率都有显著改善。另一个维护建议函数描述写成“给模型看的用户说明书”而不是“给开发者看的代码注释”。不要写“根据 orderId 查询 order_info 表”而要写“当用户询问订单状态、物流信息时用这个工具查询参数 orderId 是订单的唯一编号形如 SO-10位数字”。后面这句比前面那句对模型友好十倍。最后再分享一个我养成的习惯每次新工具上线先写一条“必测 prompt 清单”。比如订单工具上线必测“我的订单到哪了”“SO-2024-0912 什么状态”“把最近订单金额加起来”这三类提问覆盖直接调用、参数抽取、多步计算。AI 功能不像传统接口有明确的输入输出用例只有把测试 prompt 固化下来后面改模型、改版本时才不会慌。这套方法陪我扛过了好几个大版本升级也帮你少走一大堆弯路。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

不同特征值的特征向量线性无关——证明、误区与对角化应用 2026/10/2 7:32:32

不同特征值的特征向量线性无关——证明、误区与对角化应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RK3566 USB OTG识别失败的硬件根源与协同调试 2026/10/2 7:32:32

RK3566 USB OTG识别失败的硬件根源与协同调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
艾思控RS485驱动器:工业现场物理层可靠性核心 2026/10/2 7:32:26

艾思控RS485驱动器:工业现场物理层可靠性核心

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
WSL2+Windows 11 GPU加速配置指南:驱动、CUDA与内核四维校准 2026/10/2 7:32:25

WSL2+Windows 11 GPU加速配置指南:驱动、CUDA与内核四维校准

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Cartographer建图漂移排查与Lua参数调优实战 2026/10/2 7:32:12

Cartographer建图漂移排查与Lua参数调优实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
SAP FICO凭证过账接口:财务控制权的数字化移交 2026/10/2 7:32:12

SAP FICO凭证过账接口:财务控制权的数字化移交

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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