Spring AI 函数调用源码分析并实现Function Calling示例

1,788 阅读7分钟

本文将对 Spring AI 框架对函数调用实现的源码以及背后的思想进行分析,并使用 Spring AI实现函数调用的功能。

函数调用是大模型的基础能力,在ReAct提示词框架、智能体Agent开发,都会用到函数调用,这是一个非常基础的知识点,大家务必掌握。

Added at 2025/02/12

随着 Spring AI 框架的不断完善,Spring AI function calling 废弃,被Tool Calling 概念取代。新入门者可以直接阅读 # Spring AI 框架在升级,Function Calling 废弃,被 Tool Calling 取代

一、大模型为什么要函数调用

  • 预训练, 没有最新信息,GPT将无法感知。比如GPT3.5训练截止日期为2022年4月

    大语言模型(LLMs)是预训练的,而且一次训练的成本非常昂贵。据网络数据说GPT4一次训练大约需要25000个A100GPU。再加上机器、电费等等 需消耗6300万美金。

    函数调用功能可以增强大模型连接到外部数据能力,包括信息检索、数据库操作、知识图谱搜索与推理。弥补大模型最新数据的缺失。

  • 大模型是语言模型,无真逻辑。比如进行数学计算,无法每次准确计算出结果。

    函数调用在弥补大模型无真逻辑问题上,通过注册自己的函数,将大模型连接到外部系统的API,比如调用现成的计算器等工具计算。

因此函数调用能力是大模型连接外部世界的重要手段,弥补大模型的不足。函数调用功能可以增强模型推理效果或进行其他外部操作,包括信息检索、数据库操作、知识图谱搜索与推理、操作系统、触发外部操作等工具调用场景。

可以使用更加形象的一句话总结为:函数调用就像给大模型创造了手脚和眼,可以接触到外部世界数据或者使用外部世界工具!

二、哪些大模型支持函数调用

我们仅看Spring AI框架集成的大模型里,哪些支持函数调用,对于未集成的我们不做讨论。在大模型选型时,大模型是否支持函数调用一般都是需要考虑的重点

  • Open AI.
    • gpt-4o 同时支持并行函数调用
    • gpt-4o-2024-05-13 同时支持并行函数调用
    • gpt-4-turbo 同时支持并行函数调用
    • gpt-4-turbo-2024-04-09 同时支持并行函数调用
    • gpt-4-turbo-preview 同时支持并行函数调用
    • gpt-4-0125-preview 同时支持并行函数调用
    • gpt-4-1106-preview 同时支持并行函数调用
    • gpt-4
    • gpt-4-0613
    • gpt-3.5-turbo
    • gpt-3.5-turbo-0125 同时支持并行函数调用
    • gpt-3.5-turbo-1106 同时支持并行函数调用
    • gpt-3.5-turbo-0613
  • VertexAI Gemini.
  • Azure OpenAI.
  • Mistral AI.
    • Mistral Small
    • Mistral Large
    • Mixtral 8x22B
  • Anthropic Claude.
    • claude-3-5-sonnet-20241022
    • claude-3-opus
    • claude-3-sonnet 
    • claude-3-haiku
  • MiniMax.
  • ZhiPu AI.

三、Spring AI 函数调用框架设计

3.1、框架设计流程

Spring AI 框架函数调用设计框架如下; image.png 将详细介绍一下其执行流程;

  1. 将定义的函数(函数名/方法/参数/方法描述/参数描述))以 Prompt 发送给 AI Model。
  2. AI Model 根据函数相关描述信息以及用户的输入,返回函数所需要的参数。
  3. Spring AI 根据 AI Model 返回的参数进行函数调用(内部函数/外部函数),并将函数执行的结果发送给 AI Model。
  4. AI Model 接收到函数执行结果,通过进一步的加工,最终生成消息返回给客户端。

从整个流程上看 AI Model 本身并不执行函数的调用,而仅仅是根据方法的描述,返回方法所需要的返回值,函数调用仅发生在调用端侧。

3.2、类之间的协作关系

image.png

五、函数调用示例

将演示一个实时查询天气温度的程序。因为对于大模型来说,无法获取最新的数据。本文使用OpenAI大模型进行演示。

5.1、定义函数

为了快速方便快速实现演示,获取天气实现模拟返回气温,此处仅模拟返回北京、天津、南京城市温度,其它城市返回温度为0。

package org.ivy.func;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

import java.util.function.Function;

public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {

    /**
     * Weather Function request. 该类主要告诉大模型需要从提示词中提取的      * 参数有哪些,然后将提取的参数返回,并调用apply方法。
     */
    @JsonInclude(Include.NON_NULL)
    @JsonClassDescription("Weather API request")
    public record Request(@JsonProperty(required = true, value = "location") @JsonPropertyDescription("The city and state e.g. 北京") String location,
                          @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
    }

    /**
     * Temperature units.
     */
    public enum Unit {
        C, F
    }

    /**
     * Weather Function response. 该类函数执行返回的结果,并将该结果
     * 发送给大模型。
     */
    public record Response(double temp, Unit unit) {
    }

    @Override
    public Response apply(Request request) {
        System.out.println("function called :" + request);
        double temperature = 0;
        if (request.location().contains("北京")) {
            temperature = 15;
        } else if (request.location().contains("天津")) {
            temperature = 10;
        } else if (request.location().contains("南京")) {
            temperature = 30;
        }
        return new Response(temperature, Unit.C);
    }

}

在此处有一点非常重要,Request 中的 @JsonPropertyDescription("The city and state e.g. 北京")中的 e.g 如果你写“北京” 会返回中文城市名称,如果写的beijing,则会返回拼音城市名称。

5.2、将函数注册到 Spring 容器

package org.ivy.config;

import org.ivy.func.MockWeatherService;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {

    @Bean
    public FunctionCallback weatherFunctionInfo() {

        return FunctionCallbackWrapper.builder(new MockWeatherService())
                .withName("WeatherInfo")
                .withDescription("Get the weather in location")
                .withResponseConverter((response) -> "" + response.temp() + response.unit())
                .build();
    }
}

5.3、定义接口

package org.ivy.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.List;
import java.util.Map;

@RestController
public class FunctionCallingController {
    private final OpenAiChatModel openAiChatModel;

    @Value("classpath:weather.st")
    private org.springframework.core.io.Resource weather;

    public FunctionCallingController(OpenAiChatModel openAiChatModel) {
        this.openAiChatModel = openAiChatModel;
    }

    /**
     * 没有函数调用,看看返回结果
     *
     * @return 返回天气情况
     */
    @GetMapping("/noFunc")
    public Flux<String> noFunc(String prompt) {
        ChatClient chatClient = ChatClient.builder(openAiChatModel).build();
        return chatClient.prompt(new PromptTemplate(weather, Map.of("prompt", prompt)).create())
                .stream()
                .content();
    }

    /**
     * 调用函数,看看返回结果
     *
     * @return 天气状况
     */
    @GetMapping("/func")
    public Flux<String> func(String prompt) {
        UserMessage userMessage = new UserMessage(prompt + " 你可以调用函数:'WeatherInfo'");
        ChatClient chatClient = ChatClient.builder(openAiChatModel).build();
        return chatClient.prompt(new Prompt(
                List.of(userMessage),
                        OpenAiChatOptions.builder()
                                .withFunction("WeatherInfo")
                                .build()
                        )
                ).stream()
                .content();
    }
}

定义两个方法,一个无函数调用,一个有函数调用,对比两者效果。

5.4、验证效果

在验证时如果发现无法使用函数调用,首先检查检查代码是否正确,如果代码正确还是不正确的话,然后在确认选择的大模型是否支持函数调用,最后就有一个非常隐蔽的问题:是否使用的代理中转,有些代理中转是无法实现函数调用的。

我们先验证没有函数调用的情况,如下图所示: image.png 开始一通胡说八道,所以对于需要最新的,准确的回答,大模型比较难做到,因为它是基于推理进行的。我们改一下提示词,不知道的时候,让回答不知道

问题: {prompt}, 如果你无法获取到最新的真实有效的数据,请回答:抱歉

image.png 在验证有函数调用的情况,如下图所示: image.png

六、总结

本篇文章主要对大模型中函数调用的概念、作用、解决的问题进行讲解。并对 Spring AI 框架实现的源码进行分析,目前分析的还不透彻,但不耽误开发程序。最后Spring AI接入OpenAI大模型进行演示了无函数与有函数调用的区别。

几个关键的注解:

  • @Description("") 注册函数时使用,告诉大模型函数调用的时机,但是需要大模型非常聪明,像gpt-3.5-turbo 就无法根据描述判断是否函数调用。
  • @JsonClassDescription 定义请求类使用,比如Request,告诉大模型该类的作用。比如 @JsonClassDescription("Weather API request")
  • @JsonPropertyDescription 定义请求类中的请求参数的描述

示例代码:spring-ai-functioncalling-examples