
1. 为什么 Function Calling 值得单独拿出来讲Function Calling 这个词这两年在 AI 应用开发圈子里出现的频率越来越高但很多人第一次听到会有点懵——大模型不是负责聊天、写文案、生成代码的吗怎么还跟“调用函数”扯上关系了其实这正是大模型从“玩具”走向“生产力工具”的关键一步。简单说Function Calling 就是让大模型在对话过程中能够主动判断“这个问题我需要查一下数据库”或者“这个请求我得调用一个外部接口”然后输出一个结构化的调用意图由我们的后端代码去真正执行再把结果喂回给模型让它继续组织语言回答用户。我拿一个真实场景举例。假设你做了一个智能客服用户问“我上个月的订单发货了没”。如果没有 Function Calling模型只能瞎猜或者告诉你“我无法查询订单信息”。但有了 Function Calling模型会识别出这里需要调用一个叫queryOrderStatus的函数参数是orderId和month然后你的 Spring Boot 服务收到这个意图去 MySQL 里查真实数据把结果返回给模型模型再用自然语言告诉用户“您上个月的订单已于 3 月 15 日发货预计 3 月 18 日送达”。整个过程用户感知不到背后发生了什么但体验直接从“人工智障”变成了“真能办事”。Spring AI 是 Spring 生态里专门做 AI 应用开发的框架它把 Function Calling 这套机制封装得相当顺手。你不需要自己去解析模型返回的 JSON也不需要手动拼装工具描述只要用Bean定义一个Function或者SupplierSpring AI 会自动帮你注册成模型可调用的工具。这对于 Java 开发者来说简直是福音——我们不用去学 Python 那一套 LangChain 的写法直接用熟悉的 Spring 注解和依赖注入就能搞定。这篇文章我打算从零开始把 Spring AI 的 Function Calling 完整讲一遍。包括环境怎么搭、工具怎么定义、参数怎么传、多轮对话怎么处理、MySQL 怎么接、踩过哪些坑、怎么调试。适合已经有 Spring Boot 基础、想快速把 AI 能力集成到现有 Java 项目里的后端开发。如果你还在纠结“Java 能不能做 AI 应用”看完这篇应该就有答案了。2. 环境准备与项目骨架搭建2.1 版本选型和依赖引入Spring AI 的版本迭代挺快的我写这篇内容时稳定可用的是 1.0.0 系列对应的 Spring Boot 是 3.4.x。这里有个坑要提前说Spring AI 对 Spring Boot 版本有硬性要求如果你还在用 Spring Boot 2.x那基本没法直接上得先升级。我试过在 2.7 的项目里强行引入结果一堆自动配置类加载失败最后老老实实升到了 3.4。Maven 依赖主要加这几个dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.4/version /dependency如果你用的是 Spring AI Alibaba 那一套依赖坐标会不一样但核心 API 基本兼容。我建议新手先用 OpenAI 兼容的 starter因为大部分国内模型服务都提供了 OpenAI 兼容接口改个base-url就能切换。配置文件里至少要写这几项spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.example.com chat: options: model: qwen-plus temperature: 0.7注意api-key 千万别硬编码在 yaml 里提交到 Git用环境变量或者配置中心。我见过有人直接把 key 推到公开仓库结果第二天账单跑了几百块。2.2 项目分层结构设计我习惯把 AI 相关的代码单独放一个包不要和业务代码混在一起。结构大概是这样com.example.ai ├── config // ChatClient、Function 注册配置 ├── function // 所有可被模型调用的工具类 ├── service // 业务服务被 function 调用 ├── controller // 对外 HTTP 接口 └── model // DTO、请求响应对象这样分的好处是当模型调用链出问题时你能快速定位是工具定义的问题、还是业务逻辑的问题、还是模型本身理解错了。我踩过一次坑把 Function 定义直接写在 Controller 里结果调试时完全分不清是 HTTP 参数绑定错了还是模型传参错了后来拆开就清晰多了。2.3 数据库准备Function Calling 最有价值的场景之一就是查数据库。我建了一张简单的订单表来演示CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id bigint NOT NULL, product_name varchar(128) DEFAULT NULL, status tinyint DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插几条测试数据后面演示查询用。MySQL 安装这块就不展开了Windows 下用 installer 一路下一步就行记得字符集选 utf8mb4不然中文商品名会乱码。3. Function Calling 核心机制拆解3.1 模型是怎么“知道”该调用哪个函数的很多人以为模型真的在执行函数其实不是。模型做的事情是你提前把可用的函数列表包括函数名、描述、参数 schema一起发给它它在生成回复时如果判断需要调用某个函数就会输出一段结构化的 JSON类似{ name: queryOrderStatus, arguments: { orderNo: ORD20240315001 } }真正执行的是你的 Java 代码。Spring AI 在中间做了两层转换第一层是把你的FunctionBean 转成模型能理解的工具描述第二层是拿到模型返回的调用意图后反射调用对应的 Bean把结果再包装成消息发回给模型。这里的关键在于函数描述的质量。模型判断要不要调用、调用哪个完全依赖你写的 description。我试过把描述写成“查询订单”结果用户问“帮我看看我买的东西到哪了”模型有时候不调用。后来改成“根据订单号查询订单的发货状态和物流信息当用户询问订单进度、发货情况时使用”命中率明显提升。3.2 Spring AI 中 Function 的三种定义方式Spring AI 支持几种定义方式我逐个说下适用场景。第一种是java.util.function.Function适合有输入有输出的场景Bean Description(根据订单号查询订单状态) public FunctionOrderQueryRequest, OrderQueryResponse queryOrderStatus() { return request - orderService.queryByOrderNo(request.orderNo()); }第二种是Supplier适合无参数的工具比如“获取当前时间”Bean Description(获取当前系统时间) public SupplierString currentTime() { return () - LocalDateTime.now().toString(); }第三种是Consumer适合只执行不返回的场景比如“发送通知”。不过实际用下来 Consumer 比较少因为模型通常需要知道执行结果。参数对象建议用 recordSpring AI 会根据 record 的字段自动生成 JSON Schema。字段上可以加JsonPropertyDescription补充说明模型对参数的理解会更准。3.3 工具注册与 ChatClient 绑定定义好 Function Bean 之后需要在调用时注册给 ChatClientChatClient chatClient ChatClient.builder(chatModel) .defaultFunctions(queryOrderStatus, currentTime) .build();也可以按次注册String answer chatClient.prompt() .user(帮我查下订单 ORD20240315001 的状态) .functions(queryOrderStatus) .call() .content();我一般用 defaultFunctions 把常用工具都挂上特殊场景再按次覆盖。注意函数名要和 Bean 名称一致Spring AI 默认用 Bean 名作为工具名。如果你用Bean(myFunc)改了名字注册时也要用这个名字。4. 完整实战从接口到数据库的闭环4.1 定义请求响应对象先定义工具用的 DTO用 record 最简洁public record OrderQueryRequest( JsonPropertyDescription(订单编号格式如 ORD20240315001) String orderNo ) {} public record OrderQueryResponse( String orderNo, String productName, String statusText, String createTime ) {}JsonPropertyDescription这个注解很关键它会进入发给模型的 schema 里帮助模型正确填充参数。我试过不加描述模型有时候会把订单号理解成用户 ID。4.2 编写业务 ServiceMyBatis 的 Mapper 就不贴了标准写法。Service 层做个状态转换Service public class OrderService { Autowired private OrderMapper orderMapper; public OrderQueryResponse queryByOrderNo(String orderNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null) { return new OrderQueryResponse(orderNo, null, 订单不存在, null); } String statusText switch (order.getStatus()) { case 0 - 待付款; case 1 - 已付款; case 2 - 已发货; case 3 - 已完成; default - 未知状态; }; return new OrderQueryResponse( order.getOrderNo(), order.getProductName(), statusText, order.getCreateTime().toString() ); } }这里有个细节返回给模型的结果要尽量用自然语言友好的格式不要返回一堆数字状态码。模型看到“2”不一定知道是已发货看到“已发货”就能直接组织语言。4.3 注册 Function BeanConfiguration public class AiFunctionConfig { Bean Description(根据订单号查询订单的发货状态、商品名称和下单时间。当用户询问订单进度、发货情况、物流状态时调用此函数) public FunctionOrderQueryRequest, OrderQueryResponse queryOrderStatus(OrderService orderService) { return request - orderService.queryByOrderNo(request.orderNo()); } Bean Description(获取当前系统时间当用户询问现在几点、今天日期时调用) public SupplierString currentTime() { return () - LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } }注意 Function Bean 的方法参数里可以注入其他 BeanSpring 会自动处理。这样就不用把 OrderService 写成静态或者手动 new 了。4.4 Controller 对外接口RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatModel chatModel) { this.chatClient ChatClient.builder(chatModel) .defaultFunctions(queryOrderStatus, currentTime) .build(); } PostMapping public String chat(RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .call() .content(); } }启动项目用 Postman 发一条“帮我查下订单 ORD20240315001 发货了没”如果配置正确你会看到模型先触发函数调用然后返回类似“您的订单 ORD20240315001商品无线耳机已于 2024-03-15 发货当前状态为已发货”的回复。4.5 多轮对话中的上下文保持单轮调用简单但真实场景往往是多轮。比如用户先说“查下我的订单”模型问“请提供订单号”用户再给订单号。这时候需要把历史消息带上String answer chatClient.prompt() .user(查下我的订单) .call() .content(); // 第二轮 String answer2 chatClient.prompt() .user(ORD20240315001) .call() .content();但这样第二轮模型不知道上下文。正确做法是用ChatMemoryChatMemory memory MessageWindowChatMemory.builder() .maxMessages(20) .build(); ChatClient chatClient ChatClient.builder(chatModel) .defaultFunctions(queryOrderStatus) .defaultAdvisors(new MessageChatMemoryAdvisor(memory)) .build();这样每次调用会自动带上历史消息模型能理解“ORD20240315001”是在回答上一轮的追问。maxMessages控制窗口大小太大费 token太小会丢上下文我一般设 20 条左右。5. 常见问题与排查技巧实录5.1 模型不调用函数怎么办这是最高频的问题。排查顺序我总结成一张表现象可能原因排查方法完全不调用函数没注册检查 defaultFunctions 名称是否和 Bean 名一致偶尔不调用描述不够清晰在 Description 里补充触发场景关键词调用错函数多个函数描述重叠让每个函数的描述边界清晰避免语义交叉参数传错schema 描述缺失给字段加 JsonPropertyDescription报序列化错误参数类型不匹配检查 record 字段类型和模型传的值是否兼容我遇到过一次模型死活不调用最后发现是 Bean 名用了驼峰queryOrderStatus但注册时写成了query_order_statusSpring AI 找不到就静默忽略了。这种问题不会报错只能靠日志。5.2 函数执行异常怎么处理如果函数内部抛异常Spring AI 默认会把异常信息传给模型模型可能会回复“查询失败”。但更好的做法是在函数内部捕获返回一个友好的错误对象return request - { try { return orderService.queryByOrderNo(request.orderNo()); } catch (Exception e) { log.error(查询订单失败, e); return new OrderQueryResponse(request.orderNo(), null, 查询失败请稍后重试, null); } };这样模型拿到的是结构化结果能组织出更自然的回复而不是把堆栈信息暴露给用户。5.3 调试 Function Calling 的实用技巧打开 Spring AI 的 debug 日志能看到完整的请求和响应logging: level: org.springframework.ai: DEBUG日志里会打印发给模型的 tools 定义、模型返回的 tool_calls、以及函数执行结果。我调试时基本靠这个比猜快多了。另一个技巧是先用curl直接调模型接口确认模型本身支持 Function Calling。有些小模型或者老版本模型不支持这个能力你代码写得再对也没用。5.4 性能与 token 消耗优化每次调用都把函数定义发给模型会消耗额外 token。函数越多消耗越大。我的经验是单个请求注册的函数不超过 5 个多了模型选择困难token 也浪费函数描述控制在 50 字以内说清楚“什么时候用”比“怎么实现”更重要参数 schema 尽量简单避免嵌套过深的对象如果确实有很多工具可以按业务域分组根据用户意图先做一次路由再注册对应组的函数。6. 进阶玩法与扩展方向6.1 结合 Spring AI Alibaba 接入国内模型Spring AI Alibaba 对国内模型的支持更原生配置方式略有不同但 Function Calling 的 API 基本一致。切换时主要改依赖坐标和配置文件业务代码几乎不用动。我实测下来国内模型在中文场景下的函数调用准确率反而更高尤其是涉及中文商品名、地址这类参数时。6.2 把工作流引擎的流程转成 Function现在很多团队用工作流引擎编排 AI 流程比如 Dify。一个常见的需求是把工作流里的某个节点转成 Spring AI 的 Function。思路是把工作流的输入参数定义成 record把工作流的 HTTP 调用封装成 Function Bean描述里写清楚这个工作流节点负责什么。这样模型就能在对话中触发工作流实现“对话即编排”。6.3 多商户跨境商城场景的落地思路热词里提到多商户跨境商城这个场景其实特别适合 Function Calling。比如用户问“我买的那个日本化妆品到哪了”模型需要先根据用户 ID 查订单再根据订单查物流物流可能还涉及跨境段和国内段。可以把这三个查询拆成三个 Function让模型自己决定调用顺序。跨境场景还要注意时区和货币转换这些都可以封装成独立 Function模型按需调用。6.4 监控与可观测性生产环境一定要加监控。Spring Boot Admin 可以监控服务健康度但 Function Calling 的调用成功率、平均耗时、失败原因这些需要自己埋点。我一般会在 Function 包装层加一个切面记录每次调用的函数名、参数、耗时、结果状态上报到监控系统。这样出问题时能快速定位是模型没调用、还是调用了但业务失败。7. 我踩过的几个真实坑第一个坑是函数返回值太大。有一次我直接把订单列表整个返回结果 token 爆了模型回复被截断。后来改成只返回摘要信息或者分页返回问题解决。第二个坑是并发调用。模型有时候会一次性返回多个 tool_callsSpring AI 默认是串行执行。如果函数里有耗时操作整体响应会很慢。可以在配置里开启并行执行但要注意线程安全和数据库连接池大小。第三个坑是函数名冲突。不同模块定义了同名 BeanSpring 启动时直接报错。解决办法是给 Bean 显式命名或者用Qualifier区分。第四个坑是模型版本升级导致行为变化。同一个提示词模型从旧版本升到新版本后函数调用策略可能变了。所以生产环境要锁定模型版本升级前充分回归测试。8. 一些实用建议如果你刚开始接触 Spring AI 的 Function Calling我的建议是先跑通一个最简单的例子一个 Supplier 返回当前时间确认整条链路通了再逐步加复杂的 Function。不要一上来就搞十几个工具出了问题根本不知道是哪里的。另外函数描述值得反复打磨。我一般会拿十几条真实用户问法去测试看模型命中率。命中率低于 80% 就回去改描述通常改两三轮就能到 90% 以上。最后别忘了给 Function 加日志。模型调用是黑盒日志是你唯一能看清内部发生了什么的手段。我现在的习惯是每个 Function 入口和出口都打日志包含请求参数和返回摘要排查问题时省了大量时间。这个方向后续还可以往 Agent 编排走让多个 Function 组成一个能自主规划步骤的智能体。Spring AI 在这方面也在持续迭代值得持续关注。