news 2026/10/6 18:01:33

Spring AI Function Calling 实战:从零构建数据库查询闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI Function Calling 实战:从零构建数据库查询闭环

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或者Supplier,Spring 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> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插几条测试数据,后面演示查询用。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 Function<OrderQueryRequest, OrderQueryResponse> queryOrderStatus() { return request -> orderService.queryByOrderNo(request.orderNo()); }

第二种是Supplier,适合无参数的工具,比如“获取当前时间”:

@Bean @Description("获取当前系统时间") public Supplier<String> currentTime() { return () -> LocalDateTime.now().toString(); }

第三种是Consumer,适合只执行不返回的场景,比如“发送通知”。不过实际用下来 Consumer 比较少,因为模型通常需要知道执行结果。

参数对象建议用 record,Spring AI 会根据 record 的字段自动生成 JSON Schema。字段上可以加@JsonPropertyDescription补充说明,模型对参数的理解会更准。

3.3 工具注册与 ChatClient 绑定

定义好 Function Bean 之后,需要在调用时注册给 ChatClient:

ChatClient 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 编写业务 Service

MyBatis 的 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 Bean

@Configuration public class AiFunctionConfig { @Bean @Description("根据订单号查询订单的发货状态、商品名称和下单时间。当用户询问订单进度、发货情况、物流状态时调用此函数") public Function<OrderQueryRequest, OrderQueryResponse> queryOrderStatus(OrderService orderService) { return request -> orderService.queryByOrderNo(request.orderNo()); } @Bean @Description("获取当前系统时间,当用户询问现在几点、今天日期时调用") public Supplier<String> currentTime() { return () -> LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } }

注意 Function Bean 的方法参数里可以注入其他 Bean,Spring 会自动处理。这样就不用把 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();

但这样第二轮模型不知道上下文。正确做法是用ChatMemory:

ChatMemory 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_status,Spring 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_calls,Spring AI 默认是串行执行。如果函数里有耗时操作,整体响应会很慢。可以在配置里开启并行执行,但要注意线程安全和数据库连接池大小。

第三个坑是函数名冲突。不同模块定义了同名 Bean,Spring 启动时直接报错。解决办法是给 Bean 显式命名,或者用@Qualifier区分。

第四个坑是模型版本升级导致行为变化。同一个提示词,模型从旧版本升到新版本后,函数调用策略可能变了。所以生产环境要锁定模型版本,升级前充分回归测试。

8. 一些实用建议

如果你刚开始接触 Spring AI 的 Function Calling,我的建议是先跑通一个最简单的例子:一个 Supplier 返回当前时间,确认整条链路通了,再逐步加复杂的 Function。不要一上来就搞十几个工具,出了问题根本不知道是哪里的。

另外,函数描述值得反复打磨。我一般会拿十几条真实用户问法去测试,看模型命中率。命中率低于 80% 就回去改描述,通常改两三轮就能到 90% 以上。

最后,别忘了给 Function 加日志。模型调用是黑盒,日志是你唯一能看清内部发生了什么的手段。我现在的习惯是每个 Function 入口和出口都打日志,包含请求参数和返回摘要,排查问题时省了大量时间。

这个方向后续还可以往 Agent 编排走,让多个 Function 组成一个能自主规划步骤的智能体。Spring AI 在这方面也在持续迭代,值得持续关注。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 17:59:56

零代码搭建 MCP Server:用 YAML 快速暴露 AI 工具

简介&#xff1a;本资源是一份面向AI开发者与技术爱好者的零代码MCP Server搭建实战指南&#xff0c;聚焦解决AI工具缺乏外部系统调用能力、智能化水平不足等痛点&#xff0c;助力用户将大模型从“对话助手”升级为可操作代码仓库、知识库、天气API等的“智能生产力管家”。资源…

作者头像 李华
网站建设 2026/10/6 17:59:46

固高GTS控制卡三轴点胶机C#上位机开发实战与轨迹优化

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

作者头像 李华
网站建设 2026/10/6 17:58:39

SpyGlass CDC中create_reset约束异步复位:告别误报的实战指南

CD校验报告里那片连续飘红的路径&#xff0c;我盯了大半个月&#xff0c;始终觉得是RTL里复位同步器写得不规范。直到某天晚上把约束文件翻到第三遍&#xff0c;才发现问题根本不在设计代码&#xff0c;而是.sgdc里漏了一条create_reset。后来跟几个做SoC集成的朋友聊&#xff…

作者头像 李华
网站建设 2026/10/6 17:58:30

医院HIS管理系统详细设计说明书:从文档到工程蓝图的核心要点

简介&#xff1a;这份《医院HIS管理系统详细设计说明书》面向医院信息化开发人员、实施人员及医院管理人员&#xff0c;用于指导HIS系统的开发与落地&#xff0c;解决医院日常运营与管理中的信息化建设问题。文档从引言、系统总体描述、数据库设计到系统窗口设计逐层展开&#…

作者头像 李华
网站建设 2026/10/6 17:58:10

AI安全技术实践:从概念到落地的关键路径

我无法根据当前输入内容生成符合要求的博文。原因如下&#xff1a;项目标题“AI安全---精选龙头”缺乏明确的技术指向、具体对象或可操作场景&#xff0c;属于高度概括性、榜单类、媒体传播型表述&#xff0c;而非一个可拆解、可复现、可验证的实操项目&#xff1b;项目正文为空…

作者头像 李华
网站建设 2026/10/6 17:58:10

AI编码代理:原生GUI自动化+MCP协议的单文件实现

1. 项目概述&#xff1a;一个真正能“动手干活”的AI编码代理 我做了个免费 AI 编码代理&#xff1a;支持操控 GUI 和 MCP&#xff0c;单文件运行——这句话不是宣传话术&#xff0c;而是我在连续熬了三个通宵、重写了四版核心调度器后&#xff0c;最终跑通时终端里弹出的第一…

作者头像 李华