
最近好几个朋友来问Spring AI里Function Call到底怎么用尤其是从 Spring AI 1.0 GA 版本开始API做了不少调整网上的教程又良莠不齐照着抄经常跑不通。这个系列前面已经聊了模型接入、Prompt 模板、RAG、结构化输出今天这篇把 Function Call 单独拉出来讲清楚。它不是个炫技功能恰恰是大模型从聊天玩具变成业务系统一部分的关键节点——模型负责理解意图、拆解任务代码负责执行具体操作各干各擅长的事。这篇文章主要面向两类人一是已经用 Spring AI 跑通了基础对话、RAG想把手上的功能接出去的 Java 开发二是被 Function Call 各种概念绕晕想知道底层原理、好避坑的同学。我会从机制讲起再对比 Spring AI 的几种 API 姿势最后带一个完整实操把我踩过的坑一并抖出来。1. 为什么需要 Function Call它到底解决了什么问题1.1 大模型的两个天花板知识时效性与确定性计算先说一个最基本的认知大模型本质上是一个高压缩的文本生成器它的知识来自训练集是有截止日期的。你问它今天上海天气如何它不知道它只知道训练数据里 2023 年某个日期的天气规律你问它3.14 乘以 2.6 等于多少它经常算错因为它在做 token 概率预测不是在执行算术运算再比如业务场景里这个订单处于什么状态库存还剩多少这类数据在数据库里模型压根看不到。这不是模型笨而是架构决定的。大模型擅长的是语义理解、意图判断、文本生成但遇到需要实时数据、精确计算、业务规则校验、操作外部系统这类任务时它的表现就是一本正经地胡说八道。RAG 能解决一部分知识时效性问题把相关资料塞进上下文让模型参考但它解决不了执行动作的问题——模型说我可以帮你发货实际上它动不了任何物流系统。这时候就需要一种机制让模型在对话过程中检测到这个问题我答不了但我可以去调用一个函数来解决然后把结果拿回来再组织回答。这个机制就是 Function Calling也叫 Tool Calling。1.2 Function Call 的运行机制模型不是执行者而是决策者很多第一次接触的人会有一个误解觉得 Function Call 是模型调用我写的代码。这话说对了一半更准确的说法是模型根据对话内容决定应该调用哪一个工具、传什么参数真正执行的是你的应用代码。完整流程分四步你提前把若干函数的说明书注册给模型说明书包括函数名称、功能描述、参数结构JSON Schema。这一步在 Spring AI 里通常就是把一个 Bean 函数绑定到 ChatClient 上。用户发来一句话比如帮我查一下订单 SO20240901001 到哪了。模型在生成回答前先对比这句话和函数说明书判断这里需要用 orderStatusFunction 这个函数参数 orderNo 等于 SO20240901001。模型不是把这句话直接答给用户而是返回一个特殊结构给应用我要调用函数 orderStatusFunction参数是 {orderNo: SO20240901001}。Spring AI 收到这个结构后在服务端执行对应的 Java 方法拿到真实结果。应用把函数执行结果回传给模型模型基于这个真实结果生成一句给用户的最终回答您的订单正在运输途中当前位于上海分拨中心。整个过程看起来像模型自己去查了数据实际上是你的代码替它跑了腿。用一个生活化的类比你用户向老板模型问公司财务状况老板不直接回答而是让财务函数去查报表拿到数字之后老板再给你一个谈吐得体的答复。老板是决策者财务是执行者数据永远是财务说了算。这个机制的意义非常大它不改变模型的知识边界而是给了模型一条按需外接的路径。查询订单、查天气、算价格、写数据库、调用第三方 API只要定义成函数模型就能学会用而且技能可以无限扩充。1.3 和 RAG、Agent 的区别别把工具和能力混为一谈聊到 Function Call很多人会和 RAG 混在一起。我简单总结两种思路的差异帮你建立判断力RAG 是把外部资料先检索出来塞进 Prompt 上下文让模型带着资料回答。它解决的是模型不知道但资料里有的问题本质是增强模型的背景知识。Function Call 是模型判断我需要外部系统给我一个结果然后通过代码去拿。它解决的是模型需要实时数据或执行动作的问题本质是给模型接上手脚。RAG 更像是给员工一本参考资料让他翻阅Function Call 是让员工直接打电话给业务系统问现状。你可以两个都用并不冲突先 RAG 找到相关文档再 Function Call 去查文档里提到的订单实时状态这种组合在真实项目里很常见。另外Function Call 是 Agent智能体的核心基础。Agent 的规划-执行-观察-再规划循环里执行这一步落地就是靠函数调用。你学会了 Function Call后面学 Agent 框架就会觉得很顺反过来如果连 Function Call 的机制都理解不透用 Agent 框架基本等于黑盒操作出了问题完全不知道从哪排查。这也是我建议 Spring AI 使用者先把 Function Call 单独吃透的原因。2. Spring AI 的 Function Call 能力拆解从 API 设计到实现原理2.1 三种使用姿势ChatModel 直接调用、ChatClient 链式调用、流式调用Spring AI 从 1.0 GA 开始主推的是ChatClient这个类名和 Spring 生态里的RestClient、WebClient风格一致链式调用写起来非常舒服。但如果你很早就在用 Spring AI可能会见过ChatModel直接调用的写法它依然保留只是更底层一些。实际项目中两种方式各有适用场景我分别说一下。第一种ChatModel直调。这是最底层的编程模型你需要自己构造ChatRequest或使用Prompt然后传入包含函数调用选项的ChatOptions。优点是灵活适合自己封装公共组件、写一些框架级代码缺点是碎每个参数都要你手动拼。RestController public class OrderChatController { private final ChatModel chatModel; public OrderChatController(ChatModel chatModel) { this.chatModel chatModel; } PostMapping(/chat) public String chat(RequestBody String userMessage) { // 构造函数调用选项注册函数Bean名称 var options OpenAiChatOptions.builder() .withFunction(orderStatusFunction) .build(); var prompt new Prompt(userMessage, options); var response chatModel.call(prompt); return response.getResult().getOutput().getContent(); } }第二种ChatClient链式调用。这是目前官方文档主推的用法也是我推荐大多数业务团队使用的。它以会话为中心把system、user、functions、params都做成链式方法代码可读性高维护成本低String answer chatClient.prompt() .system(你是一个订单助手请礼貌简洁地回答用户问题。) .user(帮我查一下订单 SO20240901001 到哪了) .functions(orderStatusFunction) .call() .content();第三种流式调用。实际聊天应用几乎都要流式输出因为大模型生成完整回答可能要好几秒如果让用户一直干等体验很差。Spring AI 提供了stream()方法配合FluxString让内容按 token 逐个推送。这里有一个关键点在需要 Function Call 的场景里流式调用必须开启工具调用会话支持否则可能出现函数执行结果还没回来流已经结束了的问题。后面实操部分我会专门演示。2.2 函数的定义方式Bean 注册的完整链路在 Spring AI 里一个 Function Call 的函数本质上是一个java.util.Function。你把某个 Spring Bean 的某个方法包装成一个 Function然后通过Description注解告诉模型这个函数是干什么的、什么时候该用。先看一个最标准的定义Configuration public class OrderFunctions { private final OrderService orderService; public OrderFunctions(OrderService orderService) { this.orderService orderService; } Bean Description(查询指定订单的物流状态参数为订单号订单号格式形如 SO20240901001) public FunctionOrderStatusRequest, OrderStatusResponse orderStatusFunction() { return request - orderService.queryStatus(request.orderNo()); } }这里有几个细节值得注意。Description里的描述是模型判断什么时候该调用这个函数的唯一依据。写得太笼统模型可能在该用的时候不用写得太啰嗦又会挤占上下文空间影响模型理解。我的经验是描述里必须包含触发场景和参数格式示例。比如当用户询问订单物流、配送进度、包裹位置时调用此函数查询参数订单号格式为 SO 开头加一串数字这样一个描述下来模型基本不会搞错。参数对象OrderStatusRequest和返回对象OrderStatusResponse直接决定 Spring AI 生成的 JSON Schema。Spring AI 会利用 Jackson 的 Bean 序列化能力把 Java 对象转换成 JSON Schema 描述发给模型。所以你的字段名要起得规范一些最好加上注释或用JsonPropertyDescription这种注解说明字段含义否则模型可能理解不了orderNo到底是订单号还是订单数量。注册函数的方式有几种可以用Bean配合.functions(orderStatusFunction)按名称注册也可以在ChatClient构建时用.defaultFunctions(orderStatusFunction)注册默认函数。默认函数的意思是每次对话都会自动带上这些工具的说明书模型随时可以用不需要每次调用时再显式指定。我用得最多的是defaultFunctions因为业务助手通常有固定的工具集默认注册省心。这里要注意一个容易踩的坑Bean方法名和functions()里传入的名称必须完全一致。Spring AI 是根据 Bean 名称去容器里查找对应 Function 的如果你写了Bean(orderStatus)却在注册时写orderStatusFunction运行时会直接跟你翻脸。2.3 JSON Schema 自动生成与手工干预模型之所以知道函数的参数怎么传靠的不是研究你的 Java 代码而是拿到的参数 JSON Schema。Spring AI 内置了JsonSchemaGenerator它会根据你的参数类自动生成 JSON Schema。大部分情况下你不需要管它但有两种情况需要手工干预。第一种情况参数类里有可选字段、枚举、嵌套对象自动生成的 Schema 可能不符合预期。比如某个字段可能为空你需要标注nullable否则模型会强制传一个值导致校验失败。这时候可以用 Jackson 的JsonProperty(required false)或Nullable控制。第二种情况模型对参数理解有偏差频繁传错格式。比如你在 Schema 里描述date字段是字符串但没给格式模型可能传 2024-09-01、也可能传 2024/09/01。解决办法是给字段加格式描述甚至直接在方法描述里给一个完整示例。public record OrderStatusRequest( JsonPropertyDescription(订单号格式为 SO 加 11 位数字例如 SO20240901001) JsonProperty(required true) String orderNo, JsonPropertyDescription(查询类型可选值LATEST、ALL_HISTORY默认 LATEST) String queryType ) {}把字段含义都交代清楚模型传参的准确率会明显提升。这也是我从实践里总结的函数调用失败一半以上不是代码问题而是说明书写得不清楚导致模型理解错了参数。2.4 多模型兼容OpenAI、Ollama、Qwen 的差异Spring AI 把 Function Call 做了抽象理论上你换模型不需要改业务代码但实际情况并没有那么省心。不同模型服务商对工具调用这个能力的底层实现并不一样这里我梳理一下我实际测试下来的兼容性情况。OpenAI 是对 tool calling 支持最成熟、最稳定的。gpt-4o、gpt-4o-mini系列在函数调用上表现都非常好描述准确率高、参数解析极少出错。如果你的项目对稳定性要求高预算又允许无脑选 OpenAI 就行。Ollama 这种本地化方案支持工具调用但有两个前提一是模型本身要支持 tool calling比如llama3.1、qwen2.5、mistral的某些版本支持但并不是所有跑在 Ollama 里的模型都支持二是 Ollama 版本不能太老早期版本的工具调用格式有 bug我踩过好几次模型答应调用却不返回正确结构的坑后来升级到 0.5 以上版本才稳定。Qwen通义千问的开源版本qwen2.5系列也支持 Function Call但如果用的是阿里云百炼平台模型内部走的 API 规范和 OpenAI 兼容Spring AI Alibaba 也做了适配。这里提醒一句不要凭印象魔改配置建议查一下官方文档的spring.ai.alibaba配置用户名对应关系。还有一点要注意虽然 Spring AI 抽象了不同模型但函数调用的行为细节仍然有差异。比如 OpenAI 在模型非要调用一个不存在函数时会报错而某些模型可能会编造一个函数调用来应付你。这就涉及到一个我在第 4 章会展开讲的兜底策略永远不要假设模型 100% 按你的预期行动。3. 实操从零实现一个带函数调用的订单助手3.1 项目搭建与依赖基于 Spring Boot 3 快速起步实操部分我用一个订单查询助手来做演示这也是业务系统集成 AI 最典型的场景。技术栈是 Spring Boot 3.x Spring AI 1.0 GA OpenAI 兼容接口。如果你用的是本地大模型后半部分我会单独说明怎么切换。新建一个 Spring Boot 工程pom.xml里核心依赖就两个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency配置application.ymlspring: application: name: order-assistant ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: gpt-4o-mini temperature: 0.3注意引入 Spring AI 的 BOM 会省很多事dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementtemperature这里我特意设成 0.3。函数调用场景下我们希望模型尽量听话、按规则办事而不是自由发挥所以温度不宜太高。如果是闲聊场景可以调高但接业务系统的场景低温度是合理的起点。3.2 实现第一个函数查询订单状态先造一个简单的订单服务平时它可能是从数据库或远程接口查数据这里为了演示直接用静态 Map 模拟。Service public class OrderService { private static final MapString, String ORDER_STATUS Map.of( SO20240901001, 已发货当前位于上海分拨中心预计明日送达, SO20240901002, 已签收签收人前台小王, SO20240901003, 待支付订单尚未进入物流环节 ); public String queryStatus(String orderNo) { // 假设这里有数据库查询或远程调用 return ORDER_STATUS.getOrDefault(orderNo, 订单号不存在请核对后重试); } }接着定义请求参数和函数 Bean// 请求参数 public record OrderStatusRequest( JsonPropertyDescription(订单号格式为 SO 加 11 位数字例如 SO20240901001) String orderNo ) {} Configuration public class OrderFunctions { private final OrderService orderService; public OrderFunctions(OrderService orderService) { this.orderService orderService; } Bean Description(查询订单的物流状态和配送进度。当用户询问订单到哪了、发货没、什么时候送达时使用) public FunctionOrderStatusRequest, String orderStatusFunction() { return request - orderService.queryStatus(request.orderNo()); } }这里我故意把返回类型定义成String简单一点实际项目中你可以返回结构化对象模型同样能处理。3.3 注册函数并完成对话闭环在 Controller 里构造 ChatClient注册函数RestController public class OrderChatController { private final ChatClient chatClient; public OrderChatController(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个订单助手帮助用户查询订单相关信息。回答要简洁、准确。) .defaultFunctions(orderStatusFunction) .build(); } PostMapping(/chat) public MapString, String chat(RequestBody MapString, String request) { String userMessage request.get(message); String answer chatClient.prompt() .user(userMessage) .call() .content(); return Map.of(answer, answer); } }用 curl 测试一下curl -X POST http://localhost:8080/chat \ -H Content-Type: application/json \ -d {message: 帮我查一下 SO20240901001 这个订单到哪了}正常情况下返回结果是这样的{ answer: 您的订单 SO20240901001 已发货目前位于上海分拨中心预计明天可以送达。 }整个调用链路Spring AI 帮我们做了很多事情它把函数描述打包成 OpenAI 的tools参数发送给模型模型返回了tool_calls后Spring AI 自动找到对应的 Bean用模型传的参数执行方法再把结果拼装成tool消息回传给模型最后模型生成用户能读的最终回答。这些中间环节你都不用管但它内部的自动是有前提的——你的函数描述、参数类能被正确序列化成 Schema。这也是前面我为什么反复强调说明书重要。3.4 多函数并行与函数组合实际业务里不可能只有一个函数。比如用户问这个订单多少钱现在打到几折了你可能需要同时查订单金额和活动折扣。Spring AI 支持在一个会话里注册多个函数模型也会根据情况决定是调用一个还是多个。Bean Description(查询订单的当前折扣比例参数为订单号) public FunctionOrderStatusRequest, Double orderDiscountFunction() { return request - 0.85; // 模拟查到的折扣 }注册时一起挂上.defaultFunctions(orderStatusFunction, orderDiscountFunction)这里有一个有意思的现象如果用户同时问两件事OpenAI 这类支持并行工具调用的模型会返回多个tool_callsSpring AI 会逐个执行所有函数然后把所有结果一次回传给模型。我用 gpt-4o-mini 实测过一次双函数调用场景下端到端耗时大概比单函数多 200-400 毫秒主要花在函数执行和数据组装上还在可接受范围内。不过要注意不要给模型注册太多函数尤其是函数描述既长又模糊的情况。OpenAI 的 tools 列表本身也要占 token你注册 20 个函数每个描述 100 token光函数说明书就占 2000 token不仅费钱还会稀释模型对每个函数的注意力。经验值是单次会话 5-10 个相关函数足够更复杂的场景应该做成按需动态注册。3.5 流式输出让聊天体验真正落地如果上面那样一整段回答等它生成完才返回用户体验是很差的。改成流式输出用户能实时看到内容打出来。Spring AI 的 ChatClient 提供了stream()方法PostMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestBody MapString, String request) { String userMessage request.get(message); return chatClient.prompt() .user(userMessage) .stream() .content(); }前端用 SSE 或者 fetch stream 接收即可。这里关键来了传统上很多人以为流式就是FluxString一路返回 text但带有 Function Call 的流式需要特殊处理。因为流程变成了流式返回模型第一轮结果 → Spring AI 发现需要调用函数 → 暂停流式输出、执行函数 → 把函数结果加入对话 → 继续流式返回模型的第二轮回答。Spring AI 对这个场景的处理是你不需要改动业务代码但底层要走工具调用会话Tool Calling Session。之前的旧 API 写法里有人会遇到函数结果等不到、流直接关闭的问题多半是没走对会话开关。Spring AI 1.0 的ChatClient已经默认开启了工具调用会话支持所以你按我上面的写法就是对的。还要注意超时控制。流式输出期间如果某个函数执行时间特别长比如远程API调用耗时 5 秒而模型端的超时设得比较短就会出现函数还在跑、连接已断开的尴尬局面。建议给函数内部调用设置独立的超时时间和模型连接的 socket 超时区分开。4. 常见问题与排查技巧实录4.1 函数一直被无视问题基本出在描述和 System Prompt 上我见过最多的现象是函数明明注册了模型却跟用户胡说八道完全没有调用函数的迹象。排查时先做两步确认第一确认defaultFunctions或者functions里的 Bean 名称和实际注册的 Bean 名称一致第二打开调试日志看请求实际发给模型的内容里到底有没有包含 tools 参数。如果确认工具的说明书确实发出去了模型还是不用那几乎可以断定是描述有问题。比如你把函数描述写成订单状态查询函数模型可能不太确定快递到哪了是不是应该走这个函数。改成触发场景导向的描述当用户询问订单物流、配送进度、包裹位置、发货状态时调用此函数查询。参数为订单号格式 SO 加 11 位数字。 效果会立竿见影。还有一个隐蔽问题是 System Prompt 里写了类似如果不知道就告诉用户无法查询的话模型会优先遵从 System 指令而不是调用函数。要让模型知道查订单是你的分内事遇到这类问题应该调用函数而不是直接道歉。4.2 参数格式冲突数字被当成字符串记录类字段类型和模型传参不匹配也是高频报错。比如字段你定义成Long amount但模型传了一个字符串100.5Jackson 反序列化直接抛异常。排查方法很简单开启 Spring AI 的日志查看模型返回的 tool_calls 原始 JSON然后逐个比对字段类型。解决办法有两个方向一是把参数类字段定义得宽松一些全部用字符串接收再在函数里做类型转换适合参数简单、不涉及嵌套对象的情况二是在JsonPropertyDescription里写清楚类型和格式比如数值类型单位元不要加引号。实测下来第二个方向对 OpenAI 这类模型效果更好因为模型能理解数值这个概念。4.3 本地模型Ollama对 Function Call 支持不稳定的处理本地化部署是大趋势但说实话 Ollama 的 Function Call 体验和 OpenAI 还有差距。我用 Ollama 跑qwen2.5:7b和llama3.1:8b都试过前者相对稳定后者偶尔会把函数调用格式搞错返回的不是有效的 JSON 结构。应对策略有三个升级 Ollama 到最新版本老版本的工具调用格式兼容性问题很严重。选择对工具调用支持更好的模型。就我实测qwen2.5系列在开源模型里对中文场景的函数调用支持比较靠谱而一些较老的模型比如部分 7B llama 衍生版确实容易出问题。如果实在不行退回到模型只做意图识别、代码自己拼装参数的简化方案。也就是说不让模型自己生成参数 JSON而是让模型输出一个意图标签你的代码再去解析规则、查数据。这个方案虽然笨但在特定场景下比强行跑 Function Call 更稳定。4.4 超时、并发与函数结果缓存的实践函数调用不是银弹它把模型不可控和代码执行两个环节串在一起一旦函数执行阻塞整个对话流程都会被拖住。我建议做三层防护第一层是给函数执行加超时。Spring AI 没有直接提供函数超时配置但你可以在函数内部用CompletableFuture或者你的 HTTP 客户端超时配置来控制。比如用 RestClient 查外部接口时连接超时设 2 秒、读取超时设 5 秒超时快速返回兜底信息。第二层是并发控制。一个 AI 接口背后可能被多个用户同时调用如果每个请求都去查一次数据库数据库压力会很大。尤其是同一个订单被反复查询的场景加个本地 Caffeine 缓存key 为函数名加参数 hash过期时间 30 秒能挡住大量重复请求。第三层是结果兜底。函数执行失败时不要直接抛异常让整个对话崩掉优雅返回一个查询失败请稍后重试的结构化结果模型会把这个结果组织成一句自然的回答传给用户。用户看起来是系统在正常对话只是暂时没有数据。我还做过一个实践把函数的执行耗时记录到日志里按函数名聚合统计。这样哪个函数是性能瓶颈、哪个函数被调用的频率最高一目了然后续优化就有了数据支撑而不是靠猜。4.5 常见问题速查表问题现象可能原因解决办法模型完全不调用函数函数描述不清晰 / System Prompt 互相矛盾重写描述使用触发场景导向语言让 System Prompt 明确要求使用函数报错 Bean 找不到defaultFunctions 名称和 Bean 名称不一致核对名称统一规范命名参数反序列化失败字段类型与模型传参不一致开启日志查看 tool_calls 原始 JSON调整参数类字段类型或增加描述流式输出丢函数结果未开启工具调用会话 / 函数超时过长确认 ChatClient 的流式写法函数内设置更短的超时函数执行报错函数内部异常未捕获捕获所有异常返回兜底结构而非抛出注册多个函数后模型表现变差函数过多、描述过长挤占上下文精简函数数量按需动态注册优化描述长度这些坑几乎每个做 Spring AI Function Call 的团队都会遇到区别只在于你提前踩还是后知后觉。对照速查表排查能省下一大半非理性调试的时间。最后分享一点我个人在实际项目里的体会Function Call 写起来不难难的是函数设计本身。哪些能力该暴露给模型、暴露到什么粒度、描述怎么写模型才不会误解这些比写十行 Java 代码更花心思。我的做法是每设计一个新函数先在调试工具里用 5-10 个典型问题测试一遍看模型是否总能做出正确的调用决策不行就改描述而不是改代码。另外这个能力天然适合往 Agent、NL2SQL 这些方向延伸——函数调用的本质就是让模型学会使用工具而一旦模型学会了用工具很多看起来很聪明的业务功能就都不再是空中楼阁了。