深入解析 Spring AI 系列:解析函数调用

news/2025/1/16 10:08:22/文章来源:https://www.cnblogs.com/guoxiaoyu/p/18666904

我们之前讨论并实践过通过常规的函数调用来实现 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文档如下:

image

重写判断

如果你的大模型返回的格式不一样,那么重写方法即可,比如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,看下官方文档,如图所示:

image

不要细究为什么他会有这个类型,只需要明白你可以根据不同大模型接口重写isToolCall方法判断即可!

函数自动调用开关

前面提到之所以会默认调用函数并再次进行大模型调用以进行润色并返回参考结果,关键原因在于 isProxyToolCalls 参数默认设置为 false。这个参数充当了一个控制开关,用来决定是由用户自行处理相关逻辑,还是由 Spring AI 自动进行处理并进行润色。

具体而言,用户可以通过设置该开关来选择是手动管理流程,还是让系统自动完成这一过程。以下是该控制开关的核心代码示例:

OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder().withProxyToolCalls(true).build();

此时一旦你打开此开关,你就需要自己进行处理本次结果了。大模型将仅返回调用的参数以及其思考过程的输出,具体内容如下所示:

image

没返回参数等信息,是因为我把其他信息丢弃了,你可以这样写:

 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 循环来实现默认情况下调用大模型进行润色的逻辑。你可以选择去掉这一部分逻辑,改为直接调用你自己定义的函数,这样就可以绕过大模型的润色过程,直接将结果返回给客户端。通过这种方式,你能够轻松实现类似市面上大多数智能体平台所提供的功能:即在不同场景下,可以选择是否使用固定格式的回答,或是直接采用大模型的回答。

聊天记录维护

这里有几个需要特别注意的关键点。首先,你必须将每次调用后的结果主动封装并更新到历史聊天记录中。如果不这样做,一旦信息顺序或格式出现混乱,系统会直接报错。因此,确保按正确的顺序进行操作是至关重要的。正常的操作流程应遵循如下顺序:

image

你可以看到测试样例中是有这一步操作的,在这一行代码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 技术这块儿的奥秘。特爱跟人交流技术,喜欢把自己的心得和大家分享。还当上了腾讯云创作之星,阿里云专家博主,华为云云享专家,掘金优秀作者。各种征文、开源比赛的牌子也拿了。

💡 想把我在技术路上走过的弯路和经验全都分享出来,给你们的学习和成长带来点启发,帮一把。

🌟 欢迎关注努力的小雨,咱一块儿进步!🌟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/870010.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【YashanDB知识库】YMP校验从yashandb同步到oracle的数据时,字段timestamp(0)出现不一致

本文内容来自YashanDB官网,原文内容请见 https://www.yashandb.com/newsinfo/7901520.html?templateId=1718516 问题现象 在YMP校验过程中,从yashandb同步到oracle的数据时,字段timestamp(0)出现不一致问题的风险及影响 YMP校验出现数据内容不一致 问题影响的版本 yashandb…

提升团队协作效率:必备的多人协作软件清单

企业团队协作离不开多人协作软件的支持。这些软件各有特色,功能各异,企业可以根据自身的需求和实际情况进行评估和选择。选择一款适合自己的协作软件,不仅能够提升团队效率,还能让团队协作更加顺畅、高效。在快节奏的现代办公环境中,企业团队协作变得尤为重要。为了提升团…

东方通更新HTTPS证书,开启SSL协议

东方通更新HTTPS证书,开启SSL协议1、上传jks证书2、进入tongweb管理页面,打开WEB容器配置->HTTP通道管理。点击tong-http-listener3、修改HTTP为HTTPS,注意端口由80变为443。证书类型改成JKS,证书路径就是上传的路径,还有证书密码

如何调整虚拟主机中网站后台的超时设置

问题描述: 用户在登录网站后台后,短时间内会自动跳转到登录页面,导致无法正常操作。用户询问如何设置或调整网站后台的超时时间。 回答: 为了确保网站后台的稳定性和用户体验,调整超时时间是一个常见的需求。以下是详细的步骤和建议:检查程序池设置: 网站后台的超时问题…

这个指令实际上是的一部分用来指定一个文档的初始或主体在撰写如建这样的长篇文章时通常我们会构建更详细的…………这个指令实际上是HTML的一部分,用来指定一个HTML文档的初始或主体。在撰写如“《建》”这样的长篇文章时,通常我们会构建更详细的结构来实现复杂内容展示,这…

如何让大小不同的图片等比缩放不变形显示在固定大小的div里?写个例子

在前端开发中,等比缩放图片以适配固定大小的 div 容器是一个常见的需求。这通常可以通过 CSS 来实现,确保图片在缩放时不会变形。以下是一个简单的例子,说明如何使用 CSS 来完成这个任务:HTML 结构:首先,创建一个包含图片的 div 容器。 <div class="image-contai…

vulfocus靶场实操tomcat-pass-getshell弱口令漏洞

vulfocus靶场实操之tomcat-pass-getshell弱口令漏洞 提前声明: 本节所有操作都是在vulfocus靶场中进行操作,严禁在无授权的情况下进行任何的渗透操作。 前言: 本节主要对vulfocus靶场中tomcat中间件进行模拟攻击,通过该中间件存在的漏洞进入到后台拿到flag,完成该靶场要求…

Animate 2024 动画设计制作

Animate 2024 动画设计制作 Adobe Animate 2024是一款动画设计制作软件。集成了诸多绘图工具、高级动画特效和灵活的交互设计功能,让用户能够轻松绘制出精美的角色与场景,并通过精细的时间轴控制实现流畅的动画效果。无论是二维动画、HTML5广告、互动网页、游戏元素还是教育课…

SketchUp Pro 2024 3D建模 草图设计大师

SketchUp Pro 2024 3D建模 草图设计大师 SketchUp Pro,是一款3D建模软件草图设计大师,SketchUp Pro mac简单且直观,能迅速准确地进行 3D 建模。借助 3D 模型,设计人员可以做出更明智的决策,传达项目细节,并与同事和客户分享意见以达成共同目标。SketchUp Pro从描绘线条和…

OBS Studio 31 (Linux, macOS, Windows) - 视频录制和直播

OBS Studio 31 (Linux, macOS, Windows) - 视频录制和直播OBS Studio 31 (Linux, macOS, Windows) - 视频录制和直播 Free and open source software for video recording and live streaming 请访问原文链接:https://sysin.org/blog/obs-studio/ 查看最新版。原创作品,转载请…

JASP 0.19 (Linux, macOS, Windows) - 开源免费统计软件

JASP 0.19 (Linux, macOS, Windows) - 开源免费统计软件JASP 0.19 (Linux, macOS, Windows) - 开源免费统计软件 Free and User-Friendly Statistical Software 请访问原文链接:https://sysin.org/blog/jasp/ 查看最新版。原创作品,转载请保留出处。 作者主页:sysin.orgJASP…

代码小白即可完成的刷题脚本 ( Cursor 和 阿里云的 API 的 Python 刷题脚本)

利用 **Cursor** 和 **阿里云的 API** 的 **Python** 刷题脚本,自动化应对大量重复性的练习题,减轻负担。🤖📚 对象可以是代码小白,完全自动化,不需要手动修改该代码。👍 - **小白用户**:大概两个小时可以复刻完成。⏰ - **有一点代码基础的用户**:大概一个小…