ARTICLE DETAIL

资讯详情

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

Spring AI Function Calling 实战:Java 后端从环境搭建到生产级落地

Spring AI Function Calling 实战:Java 后端从环境搭建到生产级落地 Function Calling 这个词在过去一年里被聊得很多但真正落到 Java 后端项目里跑通的人其实没想象中多。大部分资料要么停留在 Python 示例要么只讲概念不讲工程落地。我最近用 Spring AI 完整走了一遍从环境搭建到生产级 Function Calling 的链路踩了不少坑也积累了一些文档里不会写的经验。这篇内容就是把这套流程完整拆开从依赖选型、接口设计、参数映射、异常处理到和 MySQL 的结合一步步讲清楚怎么在 Spring Boot 项目里把 Function Calling 真正用起来。适合有 Java 基础、想在自己的后端服务里接入大模型能力的开发者也适合正在做 AI Agent 相关项目、需要让模型调用本地业务方法的朋友。读完你应该能独立搭出一套可运行的 Function Calling 服务并且知道哪些地方容易翻车。1. 先把 Function Calling 这件事的本质说透1.1 它到底解决了什么问题很多人第一次接触 Function Calling会以为这是模型在执行代码。其实不是。模型本身不会去连你的数据库也不会去调你的 Service 方法。它做的事情只有一件根据用户的自然语言输入判断现在需要调用哪个函数、传什么参数然后输出一段结构化的 JSON。真正执行函数的永远是你自己的 Java 代码。这个认知非常关键。因为一旦你误以为模型会帮你执行就会在架构设计上走偏比如把数据库连接信息塞进 prompt或者指望模型自己处理事务。正确的分工是模型负责意图识别 参数抽取你的后端负责参数校验 业务执行 结果回传。举个具体场景。用户说帮我查一下订单号 20240512001 的物流状态。传统做法你要写正则去解析订单号要写意图分类模型去判断这是查询物流。有了 Function Calling你只需要定义一个queryLogistics(String orderNo)函数把函数签名和描述告诉模型模型就会返回{orderNo: 20240512001}这样的结构化参数。剩下的查询逻辑还是你自己写。1.2 Spring AI 在其中的角色Spring AI 本质上是把上面这套告诉模型有哪些函数、解析模型返回的函数调用、执行后把结果再喂回模型的流程做了封装。它提供了一套统一的抽象让你不用直接去拼各家大模型的 HTTP 请求体。你定义好Bean形式的 FunctionSpring AI 会自动把它转换成模型能理解的工具描述tool schema并在模型返回 tool_calls 时帮你路由到对应的 Java 方法。这里有个容易忽略的点Spring AI 的 Function 注册机制是基于 Spring 容器的。也就是说你的函数得是一个 Spring Bean或者至少能被容器管理。这跟直接写 Python 脚本里随手定义一个函数完全不是一个思路。理解这一点后面配置的时候就不会迷糊。1.3 和 Agent、工作流的边界现在热词里经常出现 spring ai agent、dify 工作流转 spring ai java 代码 这类词。我的理解是Function Calling 是 Agent 的底层能力之一但 Agent 还包含规划、记忆、多轮决策等更上层的东西。如果你只是想让模型调用一两个业务方法Function Calling 就够了不用上 Agent 框架。反过来如果你要做多步骤任务编排那 Function Calling 只是其中一环还需要考虑状态管理和循环控制。我个人的建议是先把单轮 Function Calling 跑稳再考虑往上叠 Agent。很多项目一上来就搞复杂编排结果连最基本的参数映射都没搞对调试起来非常痛苦。2. 环境搭建依赖选型和版本坑2.1 Spring Boot 版本与 Spring AI 的匹配关系Spring AI 对 Spring Boot 版本是有要求的。目前主流的 Spring AI 1.0.x 系列建议搭配 Spring Boot 3.2 及以上。如果你还在用 Spring Boot 2.3.x 或 2.6.x那基本没法直接用得先升级。这一点在热词里也有人问 spring boot 2.3.x 2.6.x我的建议是别硬扛升级到 3.x 是更省事的路。JDK 方面Spring Boot 3.x 要求 JDK 17 起步。如果你本地还是 JDK 8IntelliJ IDEA 社区版里配置项目 SDK 的时候记得切到 17。我见过有人编译报错半天最后发现是 IDEA 里项目结构还挂着 1.8。依赖引入大概是这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency如果你用的是国内模型平台比如百炼上的 qwen 系列那 starter 要换成对应的适配包。热词里提到 spring ai 2.0 连接百炼 qwen3.7说明这块需求确实存在。不同平台的 starter 名字不一样引入之前先确认清楚别拿 OpenAI 的包去连别的平台那样请求格式对不上。2.2 配置文件里最容易写错的三处第一处是base-url。很多平台的接口地址不是默认的https://api.openai.com你得改成平台给的地址。写错的话报错通常是 401 或者 404但错误信息不一定直白。第二处是模型名称。不同平台模型名差异很大有的叫qwen-plus有的叫gpt-4o-mini。名字写错会直接返回模型不存在的错误。第三处是temperature和max-tokens这类参数。Function Calling 场景下temperature 建议调低一点比如 0.1 到 0.3。因为你要的是稳定的参数抽取不是创意写作。temperature 太高模型可能给你返回格式飘忽的 JSON解析起来很头疼。spring: ai: openai: base-url: https://your-platform-endpoint api-key: ${API_KEY} chat: options: model: your-model-name temperature: 0.2提示api-key 千万别硬编码在配置文件里提交到仓库。用环境变量或者配置中心注入这是基本的安全习惯。2.3 本地要不要装 MySQLFunction Calling 本身不依赖数据库。但如果你要做的场景是让模型查订单让模型查用户信息那数据库就是必须的。热词里大量出现 mysql 安装、mysql 8.0 安装教程、mysql 在 windows10 上怎么安装说明很多人的卡点其实在环境。我的经验是本地开发用 Docker 起一个 MySQL 最省事一条命令搞定不用折腾安装向导。docker run -d --name mysql-dev \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEdemo \ -p 3306:3306 \ mysql:8.0如果 Docker 拉镜像失败检查一下镜像源配置。这个问题在热词里也有人提到 docker安装mysql失败多半是网络或者镜像源的问题换个源基本能解决。3. 定义你的第一个 Function从接口设计开始3.1 函数签名怎么设计才合理这是整个 Function Calling 里最考验功力的地方。模型能不能正确抽取参数很大程度上取决于你的函数签名和描述写得清不清楚。先说命名。函数名要语义明确用动词开头比如queryOrderStatus、calculateShippingFee、getUserProfile。别用doSomething、handleData这种含糊的名字模型看了也不知道什么时候该调。再说参数。参数类型尽量用基础类型或者简单的 POJO别搞嵌套很深的泛型结构。模型对复杂结构的理解能力有限参数一复杂抽取准确率就掉。如果确实需要多个参数宁可拆成多个函数也别硬塞进一个。public record OrderQueryRequest(String orderNo, String phoneLast4) {} Bean public FunctionOrderQueryRequest, String queryOrderStatus() { return request - { // 实际查询逻辑 return orderService.queryStatus(request.orderNo(), request.phoneLast4()); }; }这里用 record 是个好选择因为它是不可变的而且字段语义清晰。Spring AI 会通过反射读取字段名和类型生成对应的 JSON Schema。3.2 函数描述模型判断要不要调的唯一依据很多人只写函数名不写描述结果模型要么不调要么乱调。函数描述description是模型判断当前用户输入是否需要调用这个函数的核心依据。描述要写清楚三件事这个函数做什么、什么情况下该调、参数分别代表什么。我一般会这样写查询订单的物流状态。当用户询问某个订单的配送进度、快递位置、预计到达时间时调用此函数。orderNo 是订单编号phoneLast4 是下单手机号后四位用于身份校验。这段描述里当用户询问……时调用就是给模型的触发条件。写得越具体模型判断越准。如果你有多个功能相近的函数描述之间的区分度就更重要否则模型容易调错。3.3 参数校验不能省模型返回的参数不能直接信。它可能返回空字符串可能返回格式不对的订单号甚至可能幻觉出一个不存在的参数。所以函数内部第一件事就是校验。return request - { if (request.orderNo() null || !request.orderNo().matches(\\d{11,20})) { return 订单号格式不正确请提供有效的订单编号; } // 继续业务逻辑 };注意这里返回的是给模型看的提示文本不是抛异常。因为抛异常会中断整个调用链而返回一段说明文字模型可以据此告诉用户订单号格式不对请重新提供。这个区别很关键是 Function Calling 里处理错误的基本姿势。4. 把 Function 注册进 ChatClient 并跑通第一轮4.1 注册方式与常见报错Spring AI 里注册 Function 有几种方式。一种是通过Bean定义 Function 类型的 Bean然后在构建 ChatClient 时用.functions(beanName)指定。另一种是用Description注解配合方法引用。我倾向于用 Bean 方式因为依赖注入更自然测试也方便。Configuration public class FunctionConfig { Bean Description(查询订单物流状态用户询问配送进度时调用) public FunctionOrderQueryRequest, String queryOrderStatus(OrderService orderService) { return request - orderService.queryStatus(request.orderNo(), request.phoneLast4()); } }构建 ChatClientChatClient chatClient ChatClient.builder(chatModel) .defaultFunctions(queryOrderStatus) .build();常见报错是 No function found with name xxx。这通常是 Bean 名字和注册时写的名字对不上。Bean 默认名字是方法名如果你用了Bean(customName)改了名字注册时就得用改后的名字。4.2 一次完整的调用链路拆解用户输入帮我查下订单 20240512001 到哪了整个链路是这样的第一步Spring AI 把你的问题和已注册函数的 schema 一起发给模型。schema 里包含函数名、描述、参数结构。第二步模型判断需要调用queryOrderStatus返回一个 tool_call参数是{orderNo: 20240512001}。第三步Spring AI 拦截这个 tool_call找到对应的 Bean把参数反序列化成OrderQueryRequest执行你的 lambda。第四步你的函数返回结果字符串Spring AI 把这个结果作为 tool 消息追加到对话历史再次发给模型。第五步模型根据函数返回结果生成自然语言回复给用户。理解这五步调试的时候就知道问题出在哪一环。比如模型没调函数那是第一步 schema 或描述的问题参数不对那是第二步抽取的问题执行报错那是第三步你代码的问题。4.3 多函数场景下的选择逻辑当你有多个函数时模型需要自己选。这时候描述之间的边界就很重要。我做过一个测试同时注册queryOrderStatus和queryRefundStatus两个函数如果描述都写得很笼统模型经常把退款查询走到订单查询上。后来我把描述改成查询订单的物流配送状态和查询订单的退款处理进度区分度上来了准确率明显提升。注意函数数量不是越多越好。我实测下来单次注册超过 10 个函数后模型选择准确率会下降。如果业务函数很多考虑按场景分组或者用路由层先做一轮筛选。5. 和 MySQL 结合让模型真正查到数据5.1 数据访问层的设计Function 内部要查数据库走的就是常规的 Spring Boot MyBatis 或者 JPA 那套。热词里 spring boot mybatis 的 java 开源多商户跨境商城源码 说明 MyBatis 在业务系统里用得很多。我这边用 MyBatis 举例。Mapper public interface OrderMapper { Select(SELECT status, logistics_info FROM orders WHERE order_no #{orderNo} AND phone_last4 #{phoneLast4}) OrderInfo selectByOrderNo(Param(orderNo) String orderNo, Param(phoneLast4) String phoneLast4); }注意 SQL 里带了phone_last4条件这是身份校验的一部分。Function Calling 场景下模型可能被诱导去查别人的订单所以权限校验必须在数据层做不能只靠模型自觉。5.2 返回结果怎么组织给模型看函数返回给模型的内容直接影响模型最终回复的质量。如果你返回一大坨 JSON模型可能抓不住重点。我的做法是返回一段结构清晰的文本把关键字段列出来。public String queryStatus(String orderNo, String phoneLast4) { OrderInfo info orderMapper.selectByOrderNo(orderNo, phoneLast4); if (info null) { return 未找到该订单请确认订单号和手机号后四位是否正确; } return String.format(订单状态%s物流信息%s更新时间%s, info.getStatus(), info.getLogisticsInfo(), info.getUpdateTime()); }这样模型拿到的是人话它转述给用户的时候也更自然。如果你返回原始 JSON模型有时候会直接把 JSON 念出来体验很差。5.3 事务和超时控制Function 执行是在模型调用链路里的如果函数里做了耗时操作整个响应会很慢。所以数据库查询要加超时复杂操作要考虑异步。另外Function 内部如果涉及写操作事务边界要自己控制好别指望模型帮你管事务。我一般会给 Function 内部的数据库操作设一个 3 秒超时超过就返回查询超时请稍后重试。这样即使用户体验有损也不会把整个请求拖死。6. 踩坑实录那些文档里不会写的问题6.1 参数反序列化失败最常见的一个坑是模型返回的参数类型和你的 POJO 对不上。比如你定义的是Integer count模型返回了count: 3字符串。Spring AI 底层用的 Jackson 有时候能自动转换有时候不能取决于配置。我的做法是参数尽量用 String在函数内部自己转这样最稳。还有一个坑是模型返回了额外的字段。比如你只定义了orderNo模型返回了{orderNo: 123, reason: 用户查询}。如果 Jackson 配置了FAIL_ON_UNKNOWN_PROPERTIES就会直接报错。建议在 ObjectMapper 里关掉这个选项。6.2 模型不调用函数有时候用户明明问的是订单模型却直接编了个答案没调函数。原因通常有三个一是函数描述没写触发条件二是 temperature 太高三是系统提示词里没强调涉及订单查询必须调用工具。我的解决办法是在 system prompt 里明确写当用户询问订单、物流、退款相关问题时必须调用相应工具获取真实数据不得凭空回答。 这句话加上之后不调用的情况明显减少。6.3 多轮对话里的上下文丢失Function Calling 在多轮对话里有个细节函数调用的结果需要作为消息历史的一部分保留下来。如果你每轮都新建 ChatClient 或者清空历史模型就记不住上一轮查了什么。Spring AI 的 ChatMemory 可以帮忙管理但要注意配置合适的窗口大小太小会丢上下文太大又费 token。6.4 中文参数的处理中文场景下模型抽取的中文参数有时候会带多余空格或者标点。比如用户说查一下订单 12345 的物流模型可能返回orderNo: 12345 带个尾空格。函数内部记得 trim 一下不然数据库查不到。7. 从能跑到好用几个提升稳定性的实践7.1 给函数加日志和埋点Function 执行是黑盒出问题不好查。我习惯在每个 Function 入口打一条日志记录入参和出参。这样线上出问题的时候能快速判断是模型抽取错了还是业务逻辑错了。return request - { log.info(Function queryOrderStatus called with: {}, request); String result orderService.queryStatus(request.orderNo(), request.phoneLast4()); log.info(Function queryOrderStatus returned: {}, result); return result; };7.2 降级和兜底模型服务本身可能不稳定超时或者限流都可能发生。Function Calling 链路里如果模型这一步挂了整个功能就不可用。我的做法是加一层降级模型调用失败时返回一个引导用户走传统表单查询的提示而不是直接报错。7.3 参数白名单校验对于枚举类参数比如订单状态查询模型可能返回一个不存在的状态值。函数内部要做白名单校验只接受预定义的几个值其他一律拒绝。这既是安全考虑也是数据质量考虑。7.4 控制函数粒度我见过有人把一个函数写成万能查询参数里带个 type 字段根据 type 走不同分支。这种设计模型很难用对因为描述里说不清楚每种 type 的适用场景。正确做法是按业务语义拆成独立函数每个函数职责单一。8. 关于 Spring AI 版本演进的一点个人观察Spring AI 这个项目迭代挺快的从早期版本到 1.0 再到现在的 2.0.xAPI 有不小变化。热词里有人问 spring ai alibaba 停更了吗也有人关注 spring ai 2.0.1。我的建议是生产项目锁定一个稳定版本别追最新。因为 Function Calling 这块的 API 在不同版本间改过好几次升级一次可能要改不少代码。如果你现在开始一个新项目用 1.0.x 的稳定版就够了功能完全够用。等 2.0 生态成熟了再考虑迁移。迁移的时候重点看 Function 注册方式和 ChatClient 构建方式这两块这两处变动最大。另外不同模型平台对 Function Calling 的支持程度不一样。有的平台支持并行调用多个函数有的只支持单个。选平台的时候要确认这一点不然设计多函数协作的时候会受限。9. 一个完整的可运行示例结构把上面的东西串起来一个典型的项目结构大概是这样config/FunctionConfig.java定义所有 Function Beanservice/OrderService.java业务逻辑mapper/OrderMapper.java数据访问controller/ChatController.java对外接口config/ChatClientConfig.java构建 ChatClientController 层大概长这样RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/chat) public String chat(RequestBody String message) { return chatClient.prompt() .user(message) .call() .content(); } }这个结构跑通之后你就可以往里加更多函数、加记忆、加流式输出。但基础链路一定是先跑稳。我在实际项目里最大的体会是Function Calling 的难点不在模型而在工程。模型能力已经够用了真正花时间的是参数设计、错误处理、权限校验、日志埋点这些脏活累活。把这些做扎实功能才敢上生产。另外一个小技巧是调试阶段把模型的原始返回打出来看很多时候问题一眼就能定位比猜快得多。
返回列表