
Function Calling这个名字看着唬人其实就是让大模型在需要的时候调用你写好的Java方法而不是只会对着聊天窗口说一堆正确的废话。我做SpringAI项目时第一次跑通Function Calling最大的感受是这才是把LLM从“聊天机器人”变成“业务系统一部分”的关键一步。这篇博文基于我自己的SpringAI实战项目讲透Function Calling怎么用、背后发生了什么以及哪些坑我已经替你先踩过了。这个功能适合谁后端Java工程师想在Spring Boot项目里快速接入AI能力尤其是需要让模型实时查数据库、调接口、算数据的团队。不需要你精通机器学习会写Java方法就行。下面我直接从项目角度拆解不是官方文档复读机保证你能照着做。1. 项目背景与核心概念拆解1.1 为什么需要Function Calling先说一个最朴素的场景你问GPT“杭州今天要不要带伞”如果模型只靠训练数据它多半会说“我不能实时获取天气信息”。普通Prompt再怎么写模型也碰不到你数据库里的订单、用户、库存这些私有数据。Function Calling要解决的就是这个断层——让模型在对话过程中主动申请调用你的Java方法由你的方法返回真实数据模型再基于这些数据组织回答。这里有个关键点模型本身不执行任何代码。它只是根据你的“函数说明书”生成一段结构化的JSON调用请求比如“我要调用getWeather参数是city杭州”。真正执行getWeather的是Spring Boot应用执行完把结果塞回给模型模型再整理成用户能看懂的自然语言。这个解耦非常重要意味着你所有函数都可以复用现有业务代码不用为了AI单独重写一遍。从我实际做过项目的角度看Function Calling最大的价值有三个。第一实时性模型终于能拿到“此刻”的数据第二准确性通过函数强约束格式避免模型瞎编第三可操作性函数执行的是你的系统行为比如下单、查询、计算AI从“建议者”变成了“操作员”。这三点直接决定了它是我做SpringAI项目时优先级最高的能力。1.2 SpringAI里Function Calling的整体机制SpringAI对Function Calling的封装核心思路是把“模型如何知道函数、如何传参”这些繁琐细节通过注解和自动配置藏起来。我用的版本是SpringAI 1.0.0如果你还在用0.8.xAPI差异不大后面我会标注需要注意的差异点。整个调用链路大致是这样的用户在聊天界面提问问题进入ChatClient。SpringAI把你注册的Java方法转换成模型能理解的function列表连同用户消息一起发给模型。模型判断这个问题需要调用函数就在响应里返回一个tool_call指令包含函数名和参数JSON。SpringAI收到这个指令后在本机调用对应的Java方法拿到执行结果。SpringAI把函数结果作为附加消息再次发送给模型。模型结合函数结果生成最终的对话回复给用户。注意第3步模型“是否调用函数”是一个概率决策不是每次都会走函数。如果你发现模型时不时自己直接回答最简单的办法是在System Prompt里明确告诉它“当涉及天气时必须调用getWeather不要自己编造”。这一步属于Prompt层面的引导配合函数注册一起用可靠性会高很多。SpringAI帮你处理了最麻烦的JSON Schema生成、参数绑定、多轮tool_call消息组装。你只需要定义普通Java方法加上Tool注解然后注册到ChatClient剩下的交给框架。但框架也不是万能的参数类型设计不好、函数返回内容过大等问题它不会替你兜底。这些我放到后面的排查章节细说。2. 环境准备与依赖配置2.1 创建Spring Boot工程并引入SpringAI我建议直接用start.spring.io生成一个最小工程Java 17以上Spring Boot 3.3.x或3.4.x都行。SpringAI 1.0.0对Spring Boot版本有明确要求最好用3.2.0以上的版本避免依赖冲突。在pom.xml里面引入核心依赖时有两点要注意。第一SpringAI的依赖管理并不在Spring Boot的BOM里你需要额外引入spring-ai-bom或者像我一样直接写上版本号。第二如果你只用OpenAI模型引入spring-ai-openai-spring-boot-starter就够它会把OpenAI相关的自动配置全部带进来。下面是能用的最小配置dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency如果你是0.8.x的老项目groupId是一模一样的但版本号会不一样。升级到1.0.0后最大的改变是ChatClient成了推荐的主入口原来OpenAiChatClient这种直接new的方式还在但官方已经不鼓励了。如果你在网上的老教程里看到一堆ChatOptions.builder()加手动OpenAiChatClient的写法建议尽量迁移到ChatClient上来。2.2 配置OpenAI与ChatClient这一步很简单就是在application.yml里写模型访问参数。注意我只配置了模型接入最基本的key和模型名其他像temperature、maxTokens这类参数可以放到代码里针对不同业务单独设置不建议全局写死。spring: application: name: springai-function-calling ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o环境变量OPENAI_API_KEY我建议放在启动脚本或者CI的secrets里千万别直接提交到Git仓库。模型名我用的是gpt-4o如果你有成本压力换gpt-4o-mini也可以Function Calling能力两者都支持但复杂参数场景下gpt-4o的理解准确性更高。项目初期可以用mini先跑通上线前再评估是否升级。然后创建ChatClient的Bean。这个Bean是后面所有对话的入口我选择直接注入ChatClient.Builder而不是缓存一个单一实例因为不同业务可能需要不同的默认系统提示词或不同的函数集合。Configuration public class ChatClientConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder.defaultSystem(你是一个乐于助人的助手数据必须来自提供的函数返回值禁止编造。) .build(); } }2.3 关键参数说明模型、温度和超时可能有人会问配置一个ChatClient为什么这么折腾因为Function Calling对模型参数比普通聊天更敏感。我踩过几个坑分享出来temperature是第一个关键参数。函数调用场景下我建议设置在0.2到0.5之间。温度越高模型越“自由”它可能不按你给的JSON格式走甚至自己脑补函数参数。我做测试时用0.8的temperature跑天气查询有一次模型把“浙江杭州”传成了“杭州市浙江省”把结构化参数完全搞乱了。后来统一降到0.3这类情况几乎消失。maxTokens也要注意。函数调用的中间过程会消耗不少token尤其是函数返回结果很长的时候。如果你设置太小模型可能来不及把整个工具调用过程走完就被截断。我的习惯是至少给到1024如果业务复杂需要函数返回大列表数据建议2048以上。当然这会增加成本需要在体验和费用之间平衡。还有超时配置。默认情况下如果模型迟迟不返回结果HTTP调用会一直挂着。在application.yml里加上超时控制会稳妥很多。spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o temperature: 0.3 max-tokens: 20483. SpringAI Function Calling实战一个天气查询功能3.1 编写工具方法并声明Tool理论说了半天直接上一个我自己项目里跑通的功能天气查询。这个例子的好处是足够简单、人人都能理解而且它恰好体现Function Calling“模型拿实时数据”的核心。先定义一个request的record用来接收模型传入的参数。很多新手会忽略这一步直接写普通方法参数但SpringAI在生成JSON Schema时依赖这个类型的结构所以建议规范化定义参数对象public record WeatherRequest(String city) { }然后定义返回结果。同样的返回值类型也会影响模型理解函数意图名字要直观。我这里用一个简单record包住天气描述public record WeatherResponse(String city, String weather, int temperature) { }核心工具方法如下。注意Tool注解里的描述这个描述是给模型看的一定要写清楚“什么时候用、参数是什么含义”。import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; Component public class WeatherTools { Tool(description 根据城市名获取当前天气参数city是城市中文名例如杭州、上海) public WeatherResponse getWeather(WeatherRequest request) { // 这里应该是真实调用天气服务实际项目中可以写RestTemplate调用外部API // 示例直接返回模拟数据让你先跑通链路 if (request.city().contains(杭州)) { return new WeatherResponse(request.city(), 小雨, 24); } return new WeatherResponse(request.city(), 晴, 27); } }有几点必须说明。Tool方法默认只支持public方法私有方法会被忽略。返回值类型、参数类型必须能被Jackson正确序列化/反序列化也就是要有无参构造或默认构造record天然满足条件。如果你用传统POJO记得加getter/setter和无参构造否则SpringAI在参数绑定环节会抛异常这个是我见过的最高频报错。3.2 将Function注册到ChatClient方法定义好了光有Component还不行SpringAI不会自动扫描所有Tool方法你必须显式告诉ChatClient“这个函数我要用”。这里有两种方式按场景选择。第一种是直接方法引用注册适合函数不多、且每个调用场景固定的时候RestController public class WeatherController { private final ChatClient chatClient; public WeatherController(ChatClient.Builder builder, WeatherTools weatherTools) { this.chatClient builder .defaultFunction(getWeather, weatherTools) .build(); } }第二种是使用defaultFunctions注册多个函数适合一次对话里模型需要从多个工具中选择的情况this.chatClient builder .defaultFunctions(getWeather, getStockPrice, calcDistance) .build();我实际项目里更常用第一种因为.defaultFunction(String, Object)会自动把对象里的Tool方法解析成函数定义而且在同一个对象里定义多个相关工具非常方便。比如我可以把WeatherTools里放两个方法一个getWeather一个getWind, 然后用.defaultFunction注册两次指向同一个对象实例。3.3 完整调用代码与运行效果注册好之后调用就非常平淡了就是普通ChatClient接口PostMapping(/chat) public String chat(RequestBody String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); }启动工程后用Postman请求“杭州今天天气怎么样”你会在日志里看到整个Function Calling的真实调用过程先是模型请求调用getWeather然后SpringAI执行你的方法再把结果回传模型。最终用户看到的是类似“杭州市今天小雨气温24摄氏度”的自然语言回答。如果不走HTTP接口直接写个CommandLineRunner做验证也很快Component public class FunctionCallingRunner implements CommandLineRunner { private final ChatClient chatClient; public FunctionCallingRunner(ChatClient.Builder builder, WeatherTools weatherTools) { this.chatClient builder .defaultFunction(getWeather, weatherTools) .build(); } Override public void run(String... args) { String answer chatClient.prompt() .user(杭州今天要不要带伞) .call() .content(); System.out.println(answer); } }第一次看到模型只会输出“我需要调用工具查询天气”这类中间消息时不用慌那是内部机制的一部分。SpringAI在框架层面已经自动完成了多轮消息组装你看到的最终结果一定已经是整合后的自然语言回复。4. 核心机制与实现原理4.1 Tool与JSON Schema的生成过程很多人用Function Calling很顺利却不理解为什么模型能准确知道Java方法的参数结构。这是因为SpringAI在启动时通过反射扫描Tool注解方法把你的方法签名转换成OpenAI定义的tools格式其中包含function的name、description和parameters的JSON Schema。我打个比方你写了一个getWeather方法SpringAI就好比是你的“接口文档生成器”自动把你的Java方法翻译成一份模型能读懂的说明书。说明书里写着“这个函数叫getWeather作用是获取天气它接收一个对象对象里有个city字段字段类型是string”。模型看了这份说明书才知道要用什么格式调用你。这段JSON Schema在框架内部会自动生成但我建议你至少在开发阶段把它打印出来看一眼。方法如下toolCallingManager.getToolDefinitions()或者直接在日志级别配置DEBUG观察请求体。这会让你快速理解为什么参数命名要规范、为什么中文描述会有用。我第一次打印出来时发现一个Java方法里的布尔类型参数会被翻译成boolean打开方式不直观后来为了模型理解而改成了字符串“true/false”准确率反而更高。4.2 一次调用中模型和Java方法如何协作完整的协作过程在SpringAI内部是分两步走的但被封装成了一个连贯的调用。第一步模型收到用户消息后判断是否需要函数调用。如果它判断需要就会返回一个ChatResponse里面携带toolCalls列表每个toolCall包含一个id、要调用的函数名以及一串参数JSON。这个过程中模型是没有产生最终文本回复的或者只会产生类似“让我查一下”的过渡文本。第二步框架根据toolCall里的函数名从当前ChatClient注册的Function列表里找到对应方法然后使用JSON反序列化工具把参数转换成Java对象执行方法拿到返回值。这个返回值会被包装成ToolResponseMessage再次发给模型。模型读到返回值后生成最终回复。我特别强调一下这里的方法执行是在你的Java进程里完成的不是模型服务器上。用户问“杭州天气”模型服务器只负责调度真正去查天气的是你自己的服务。这也意味着你的方法完全可以访问数据库、Redis、甚至调用其他公司的API。4.3 多Function并存时模型的决策依据实际项目里一个ChatClient往往不止一个函数。比如我可以同时注册getWeather、getStockPrice、getRoomPrice三个函数。这时候模型怎么知道该选哪个模型主要看两个东西函数描述和参数Schema。描述越清晰决策越准。我做过一次对比实验把函数描述写得很模糊时10次里有3次模型选错函数把描述改精确后10次只错了1次。这个误差不是SpringAI的问题是大模型本身的能力边界。你无法彻底消除但可以通过优化描述来降低到可接受水平。另外注册函数的顺序也有微小影响。OpenAI的API里tools列表的顺序会影响模型的注意力。我现在的习惯是简单、高频的函数放前面复杂、低频的放后面。这个没有官方理论支持是我自己试验的规律但在多函数场景下实测确实有效。也可以限定每个场景的ChatClient只暴露相关函数比如天气对话页就只注册天气函数不要图省事把一个超级ChatClient到处用函数越少决策越稳定。5. 常见问题与排查技巧5.1 函数参数不匹配与类型转换报错我在实战中最常遇到的是参数类型转换异常。典型报错长这样Cannot deserialize value of type java.lang.String from Array value问题通常出在参数定义上。比如你方法里写的是double temperature但模型返回的JSON里temperature可能是25字符串或者你定义为List 模型却传了一个Integer。OpenAI的模型并不保证每次输出的参数类型和你的Schema完全一致它是在“尽量匹配”而不是“严格匹配”。SpringAI底层通常用Jackson做反序列化遇到不一致就直接抛异常了。我的解决思路有三个。第一参数类型尽量用宽松的包装类型比如Double而不是double允许缺失值。第二所有参数对象字段都提供合理的默认值哪怕模型漏传了某个字段也能兜底。第三写工具方法时不要过度信任模型方法内部加参数校验比如city为空时返回“参数不完整”的固定提示而不是直接NullPointerException。5.2 返回结果过长导致上下文爆炸有一次我做订单查询功能函数返回了最近100条订单明细结果下一次请求的token消耗直接翻倍因为函数返回结果被完整带入了对话上下文。这是Function Calling一个非常隐蔽的问题模型不记得函数内部逻辑它只能把整个返回值当成新的消息继续处理。返回内容越大后续每一轮对话的token成本越高。解决方法是压缩输出。比如订单查询函数不要返回全部字段只返回id、状态、金额这种模型需要用来回答问题的关键字段。我在工具方法末尾会做一次裁剪把不需要的长文本字段去掉。另外函数返回值可以加一层“摘要化”比如返回“共100条订单总金额12000元最近一笔订单号是20250101”模型已经足够回答大部分用户问题。如果用户真的想逐条看再走分页或详情接口。5.3 Function调用没有触发这是新手最容易困惑的问题明明注册了函数模型却自己乱答函数完全不触发。我先说结论这不是SpringAI的bug很大概率是模型觉得“不调用函数也能回答”。排查顺序我建议按下面这个来。第一步确认ChatClient里真的注册了函数别在Controller里new了一个新的ChatClient忘记了注册。第二步看请求日志里发给模型的tools列表是否包含你的函数定义如果没有就是注册链路断了。第三步检查System Prompt如果模型觉得从你的Prompt里能猜到答案它可能选择不调用。比如你问“杭州天气”函数只给了城市参数但你在Prompt里写了“如果查询不到就默认返回晴天”模型就很可能直接返回一个编造的晴天。解决办法是在System Prompt里强制约束比如我常用的一句话是“只有调用工具函数得到的结果才是事实没有工具返回的数据不得编造。当用户询问天气时必须调用getWeather。” 加上这句之后触发率明显提升。5.4 如何有效调试Function Calling链路调试这种多轮调用的链路最直观的方法是打印完整的请求和响应日志。SpringAI支持配置OpenAI的日志级别logging: level: org.springframework.ai: DEBUG打开后你可以在日志里看到发送给模型的tools列表、模型返回的toolCalls内容以及第二次发送时携带的函数结果。这个信息量很大基本一眼就能定位问题是出在“模型没选函数”还是“参数转换报错”还是“返回值解析出错”。我还习惯在工具方法内部加一个日志标记。比如getWeather方法一开始就log.info(函数被调用, city{}, request.city())。这样配合外层日志能明确知道什么时候进入了你的代码方便区分是框架问题还是业务方法问题。曾有一次我发现函数被连续调用了三次后来排查是因为函数返回的数据不满足模型的期望模型又请求调用了一次这在多轮工具调用里是正常现象但要注意避免无限循环最好在System Prompt里说明“如果函数结果不包含答案向用户说明暂时无法获取”。6. 进阶技巧与个人心得6.1 多轮对话中的状态保持Function Calling的默认模型是无状态的每一轮函数调用结束后上下文里只累积消息文本和函数返回结果函数本身的调用顺序并不会被记住。如果你做一个客服场景用户先问“杭州天气”再问“那北京呢”模型大概率会直接调用getWeather(北京)这没问题。但如果用户问“那后天呢”模型可能无法自动推断出要调用带日期的函数除非你上一轮的函数返回结果里面包含了日期这个字段。我的建议是在设计工具函数时尽量返回“完整上下文相关的字段”。比如天气函数接受city和date两个参数即使模型只传了city返回值里也带上当天日期。这样后续对话模型看到函数结果里有日期字段才有依据回答“后天”这类问题。做不到的时候就在Prompt里引导用户补充缺失信息不要硬让模型猜。6.2 在函数内做业务校验与权限控制Function Calling把Java方法暴露给了模型等于把系统的“手”借给了AI。这也会带来安全风险用户可能诱导模型调用一个目的之外的函数或者传入恶意参数。比如你有deleteOrder函数模型可能因为Prompt注入被引导去调用它。我的处理原则很简单所有通过Function Calling暴露的方法都要像处理外部HTTP接口一样做权限校验。首先方法内部必须校验当前用户是否有权执行操作不要依赖模型判断。其次敏感操作函数不要注册到普通的对话ChatClient里得单独起一个限权场景。最后参数不要直接拼SQL或命令比如工具方法里如果涉及数据库查询坚持用PreparedStatement避免模型生成的参数成为注入点。我见过有人把“删除用户”功能直接加Tool注解暴露给通用AI助手这属于给自己埋雷。正确的做法是让函数只做“只读查询”写操作必须经过额外的确认机制。6.3 我优化Function Calling效果的几点经验最后分享几条面对真实业务时摸索出来的小技巧属于不一定写进文档但实际很有用的经验。第一函数名要动词名词清晰可读。模型对函数名的语义理解能力比很多人想象中强命名尽量不要用handler1这种要用getWeatherByCity。第二描述里写清“何时使用”。比如“当用户想问某个商品是否有货时调用不要用于促销活动查询”这能大幅降低误调用。第三不要注册太多函数。我在项目里试过一次注册8个函数结果模型选择准确率明显下降最后拆成两个ChatClient各注册3到4个函数效果才恢复。还有一点是关于SpringAI版本迭代的。SpringAI现在迭代速度很快我早期写的代码在升级版本后出现过不兼容的情况。如果你从0.8.x升到1.0.0重点检查ChatClient的构建方式ToolCallingManager的注入方式以及Tool注解的包名。用IDE全局搜索可以快速定位。升级前建议先看changelog很多API变化在官方迁移文档里都有说明。我个人在实际操作中最深的一个体会是Function Calling不是“加个注解就结束”的玩具它的质量上限由你的工具方法设计质量决定。你花在优化函数描述、精简返回结果、做异常兜底上的时间最终都会反映在用户体验上。如果你做完第一个SpringAI Function Calling功能后发现模型越来越会和你的业务系统“配合”那说明你已经掌握了这个能力的真正用法。接下来可以试试把多个函数链式调用起来做成一个能自主完成复杂任务的AI Agent那会是一个更有意思的方向。