深入解析 Spring AI 系列:解析函数调用
我们之前讨论并实践过通过常规的函数调用来实现 AI Agent 的设计和实现。但是,有一个关键点我之前并没有详细讲解。今天我们就来讨论一下,如何让大模型只决定是否调用某个函数,但是Spring AI 不会在内部处理函数调用,而是将其代理到客户端。然后,客户端负责处理函数调用,将其分派到相应的函数并返回结果。
好的,我们开始。
函数调用
核心代码
函数调用是开发AI Agent的关键组成部分,它使得AI能够与外部系统、数据库或其他服务进行交互,从而提升了其功能性和灵活性。所以开发必须要适用于支持函数调用的聊天模型,在Spring AI中处理函数调用也仅仅是一行代码,核心代码如下,我们看下:
if (!isProxyToolCalls(prompt, this.defaultOptions)
&& isToolCall(response, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()))) {
var toolCallConversation = handleToolCalls(prompt, response);
return this.internalCall(new Prompt(toolCallConversation, prompt.getOptions()), response);
}
假设我们已经开发并集成了一个天气查询函数,当我们向大模型提出类似“长春天气咋样”这样的请求时,大模型会自动识别并选择调用相应的函数。在这个过程中,handleToolCalls 方法通过反射机制来动态地调用正确的天气查询方法,接着该方法会递归调用 internalCall 方法,继续处理后续的逻辑。需要注意的是,关于反射机制和递归调用的具体实现细节,在前文中已经有所说明,因此此处不再赘述。
判断是否是函数
protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
var finishReason = (generation.getMetadata().getFinishReason() != null)
? generation.getMetadata().getFinishReason() : "";
return generation.getOutput().hasToolCalls() && toolCallFinishReasons.stream()
.map(s -> s.toLowerCase())
.toList()
.contains(finishReason.toLowerCase());
}
isToolCall的核心逻辑就是要判断大模型返回的信息是否正确,OpenAI的API文档如下:

重写判断
如果你的大模型返回的格式不一样,那么重写方法即可,比如minimax就重写了,我们看下:
protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
if (!super.isToolCall(generation, toolCallFinishReasons)) {
return false;
}
return generation.getOutput()
.getToolCalls()
.stream()
.anyMatch(toolCall -> org.springframework.ai.minimax.api.MiniMaxApiConstants.TOOL_CALL_FUNCTION_TYPE
.equals(toolCall.type()));
}
他在原有的基础上又再次判断了一下toolCall.type是否为function,因为minimax不仅支持function类型的type,还支持web_search,看下官方文档,如图所示:

不要细究为什么他会有这个类型,只需要明白你可以根据不同大模型接口重写isToolCall方法判断即可!
函数自动调用开关
前面提到之所以会默认调用函数并再次进行大模型调用以进行润色并返回参考结果,关键原因在于 isProxyToolCalls 参数默认设置为 false。这个参数充当了一个控制开关,用来决定是由用户自行处理相关逻辑,还是由 Spring AI 自动进行处理并进行润色。
具体而言,用户可以通过设置该开关来选择是手动管理流程,还是让系统自动完成这一过程。以下是该控制开关的核心代码示例:
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder().withProxyToolCalls(true).build();
此时一旦你打开此开关,你就需要自己进行处理本次结果了。大模型将仅返回调用的参数以及其思考过程的输出,具体内容如下所示:

没返回参数等信息,是因为我把其他信息丢弃了,你可以这样写:
ChatResponse content = this.chatClient
.prompt(systemPrompt)
.user(userInput)
.options(openAiChatOptions)
.advisors(messageChatMemoryAdvisor,myLoggerAdvisor)
.functions("CurrentWeather","TravelPlanning","toDoListFunctionWithContext","myWorkFlowServiceCall")
.call();
// .content();这里只返回string,也就是思考结果
好的。一旦你获得了 ChatResponse 类的实例后,你就可以根据需要自由地操作该对象,并调用其中的各种函数了。你不必从头编写所有的代码,实际上,你可以参考 OpenAI 提供的测试样例,这样会大大简化你的开发过程。以下是一个参考示例:
FunctionCallback functionDefinition = new FunctionCallingHelper.FunctionDefinition("getWeatherInLocation",
"Get the weather in location", """
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["C", "F"]
}
},
"required": ["location", "unit"]
}
""");
@Autowired
private OpenAiChatModel chatModel;
private FunctionCallingHelper functionCallingHelper = new FunctionCallingHelper();
@SuppressWarnings("unchecked")
private static Map<String, String> getFunctionArguments(String functionArguments) {
try {
return new ObjectMapper().readValue(functionArguments, Map.class);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// Function which will be called by the AI model.
private String getWeatherInLocation(String location, String unit) {
double temperature = 0;
if (location.contains("Paris")) {
temperature = 15;
}
else if (location.contains("Tokyo")) {
temperature = 10;
}
else if (location.contains("San Francisco")) {
temperature = 30;
}
return String.format("The weather in %s is %s%s", location, temperature, unit);
}
void functionCall() throws JsonMappingException, JsonProcessingException {
List<Message> messages = List
.of(new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"));
var promptOptions = OpenAiChatOptions.builder().functionCallbacks(List.of(this.functionDefinition)).build();
var prompt = new Prompt(messages, promptOptions);
boolean isToolCall = false;
ChatResponse chatResponse = null;
do {
chatResponse = this.chatModel.call(prompt);
isToolCall = this.functionCallingHelper.isToolCall(chatResponse,
Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(),
OpenAiApi.ChatCompletionFinishReason.STOP.name()));
if (isToolCall) {
Optional<Generation> toolCallGeneration = chatResponse.getResults()
.stream()
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
.findFirst();
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
var functionName = toolCall.name();
String functionArguments = toolCall.arguments();
@SuppressWarnings("unchecked")
Map<String, String> argumentsMap = new ObjectMapper().readValue(functionArguments, Map.class);
String functionResponse = getWeatherInLocation(argumentsMap.get("location").toString(),
argumentsMap.get("unit").toString());
toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName,
ModelOptionsUtils.toJsonString(functionResponse)));
}
ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of());
List<Message> toolCallConversation = this.functionCallingHelper
.buildToolCallConversation(prompt.getInstructions(), assistantMessage, toolMessageResponse);
prompt = new Prompt(toolCallConversation, prompt.getOptions());
}
}
while (isToolCall);
logger.info("Response: {}", chatResponse);
assertThat(chatResponse.getResult().getOutput().getText()).contains("30", "10", "15");
}
这段代码采用了 while 循环来实现默认情况下调用大模型进行润色的逻辑。你可以选择去掉这一部分逻辑,改为直接调用你自己定义的函数,这样就可以绕过大模型的润色过程,直接将结果返回给客户端。通过这种方式,你能够轻松实现类似市面上大多数智能体平台所提供的功能:即在不同场景下,可以选择是否使用固定格式的回答,或是直接采用大模型的回答。
聊天记录维护
这里有几个需要特别注意的关键点。首先,你必须将每次调用后的结果主动封装并更新到历史聊天记录中。如果不这样做,一旦信息顺序或格式出现混乱,系统会直接报错。因此,确保按正确的顺序进行操作是至关重要的。正常的操作流程应遵循如下顺序:

你可以看到测试样例中是有这一步操作的,在这一行代码buildToolCallConversation,代码追到后面就是这样的核心逻辑,代码如下:
protected List<Message> buildToolCallConversation(List<Message> previousMessages, AssistantMessage assistantMessage,
ToolResponseMessage toolResponseMessage) {
List<Message> messages = new ArrayList<>(previousMessages);
messages.add(assistantMessage);
messages.add(toolResponseMessage);
return messages;
}
总结
通过今天的讨论,我们首先了解了如何实现函数调用的基础机制,通过核心代码示例展示了如何在Spring AI中进行函数的动态调用。在此过程中,关键的isToolCall方法和函数自动调用开关的使用,确保了我们可以根据具体需求调整函数调用的方式,甚至完全由客户端来接管函数执行。此外,通过维护聊天记录并精心管理工具调用的顺序,我们能确保AI的行为更为可控和稳定。
总的来说,今天的分享为大家提供了一种新的思路,使得在开发AI Agent时,我们不仅仅依赖大模型的内建能力,还可以通过客户端控制函数的调用和返回结果,从而打造更加灵活和高效的智能系统。这种方式无疑为开发者提供了更多定制化的选择,提升了开发过程的自由度和效率。
我是努力的小雨,一个正经的 Java 东北服务端开发,整天琢磨着 AI 技术这块儿的奥秘。特爱跟人交流技术,喜欢把自己的心得和大家分享。还当上了腾讯云创作之星,阿里云专家博主,华为云云享专家,掘金优秀作者。各种征文、开源比赛的牌子也拿了。
想把我在技术路上走过的弯路和经验全都分享出来,给你们的学习和成长带来点启发,帮一把。
欢迎关注努力的小雨,咱一块儿进步!
深入解析 Spring AI 系列:解析函数调用的更多相关文章
- Spring Boot 系列教程11-html页面解析-jsoup
需求 需要对一个页面进行数据抓取,并导出doc文档 html解析器 jsoup 可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于JQuery的操 ...
- Spring源码解析系列汇总
相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...
- Spring Boot系列(三):Spring Boot整合Mybatis源码解析
一.Mybatis回顾 1.MyBatis介绍 Mybatis是一个半ORM框架,它使用简单的 XML 或注解用于配置和原始映射,将接口和Java的POJOs(普通的Java 对象)映射成数据库中的记 ...
- Spring Boot系列(四):Spring Boot源码解析
一.自动装配原理 之前博文已经讲过,@SpringBootApplication继承了@EnableAutoConfiguration,该注解导入了AutoConfigurationImport Se ...
- Spring Cloud系列(三):Eureka源码解析之服务端
一.自动装配 1.根据自动装配原理(详见:Spring Boot系列(二):Spring Boot自动装配原理解析),找到spring-cloud-starter-netflix-eureka-ser ...
- Spring Cloud系列(四):Eureka源码解析之客户端
一.自动装配 1.根据自动装配原理(详见:Spring Boot系列(二):Spring Boot自动装配原理解析),找到spring-cloud-netflix-eureka-client.jar的 ...
- Spring源码解析-ioc容器的设计
Spring源码解析-ioc容器的设计 1 IoC容器系列的设计:BeanFactory和ApplicatioContext 在Spring容器中,主要分为两个主要的容器系列,一个是实现BeanFac ...
- Spring技术内幕——深入解析Spring架构与设计原理(一)IOC实现原理
IOC的基础 下面我们从IOC/AOP开始,它们是Spring平台实现的核心部分:虽然,我们一开始大多只是在这个层面上,做一些配置和外部特性的使用工作,但对这两个核心模块工作原理和运作机制的理解,对深 ...
- Spring 是如何解析泛型 - ResolvalbeType
Spring 是如何解析泛型 - ResolvalbeType Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Java ...
- Spring Framework框架解析(1)- 从图书馆示例来看xml文件的加载过程
引言 这个系列是我阅读Spring源码后的一个总结,会从Spring Framework框架的整体结构进行分析,不会先入为主的讲解IOC或者AOP的原理,如果读者有使用Spring的经验再好不过.鉴于 ...
随机推荐
- 15-1 OOP概述
目录 核心思想 继承 动态绑定 核心思想 面向对象程序设计(object-oriented programming)的核心思想是 封装:类的接口和实现分离 继承:定义相似的类型并对相似关系建模 动态绑 ...
- 【Azure App Service】在App Service for Windows上验证能占用的内存最大值
问题描述 在创建App Service服务的时候,根据定价层不同,内存使用的最大值也有不同.但在实际测试中,发现内存最大只能占用2GB左右, 而定价层中内存分配明明是大于2GB(比如B3定价层的内存为 ...
- Codeforces Round 975 Div.2 C题 解析
C题 题目链接:Problem - C - Codeforces 题目描述 思路 对于这道题,首先我们能想到最小的组数肯定是数量最多的那个卡片的数量(mx),因为这个值的每一个卡片不能在同一个组里面. ...
- P3523 POI2011 DYN-Dynamite
P3523 POI2011 DYN-Dynamite 小 trick,加双倍经验. 思路 使 \(dis\) 的最大值最小,可以想到二分 \(dis\),然后根据 \(dis\) 判断可行性. 那么可 ...
- php 如何实现 git diff
无意间想到这个问题,如何用php来实现git diff,如果实现了这个功能,岂不是能够使用php对在线编辑文件的功能做更进一步的优化和提升? 查了一下还真有这样的库,话不多说,开始执行 compose ...
- 域渗透之初识Kerberos认证过程
目录 Kerberos协议中的角色 关键名词 Kerberos协议的工作流程 AS_REQ & AS_REP TGS_REQ & TGS_REP AP_REQ PAC 总结 Kerbe ...
- 高性能计算-雅可比算法MPI通信优化(5)
雅可比算法原理:如下图对方阵非边界元素求上下左右元素的均值,全部计算元素的数值计算完成后更新矩阵,进行下一次迭代. 测试目标:用MPI实现对8*8方阵雅可比算法迭代并行计算,用重复非阻塞的通信方式 # ...
- phpredis和predis
一般我们常用的扩展是phpredis和predis两个 phpredis, 它是用c写的php的高效扩展:https://github.com/phpredis/phpredis, predis, 它 ...
- winform窗体无边框拖动
1:引用命名空间 using System.Runtime.InteropServices; 2:想要拖动窗体的控件绑定MouseDown事件 点击查看代码 //窗体移动 [DllImport(&qu ...
- FastExcel 合并单元格(相当的行数据,进行合并)
目录 需求 思路 实现 Excel导出单元格全量合并策略 日期格式转换 接口代码 Service DTO 使用FastExcel数据导出:官网: https://idev.cn/fastexcel/z ...