函數呼叫 API

在 AI 模型中整合函數支援,允許模型請求執行用戶端函數,從而在需要時動態存取必要資訊或執行任務。

Spring AI 目前支援以下 AI 模型的函數調用:

Function calling

您可以向 ChatClient 註冊自訂 Java 函數,並讓 AI 模型智慧地選擇輸出包含引數的 JSON 物件,以呼叫一個或多個已註冊的函數。這讓您可以將 LLM 功能與外部工具和 API 連接。AI 模型經過訓練,可以偵測何時應呼叫函數,並以符合函數簽章的 JSON 回應。

API 不會直接呼叫函數;相反地,模型會產生 JSON,您可以使用它在程式碼中呼叫函數,並將結果傳回給模型以完成對話。

Spring AI 提供了彈性且使用者友善的方式來註冊和呼叫自訂函數。一般而言,自訂函數需要提供函數名稱描述和函數呼叫簽章(以 JSON 結構描述形式),以讓模型知道函數期望的引數。描述有助於模型理解何時呼叫函數。

作為開發人員,您需要實作一個函數,該函數接收從 AI 模型傳送的函數呼叫引數,並將結果回應給模型。您的函數可以進而調用其他第三方服務以提供結果。

Spring AI 使此過程變得非常簡單,只需定義一個 @Bean 定義,該定義會回傳 java.util.Function,並在調用 ChatClient 時或在提示詞請求中動態註冊函數時,提供 bean 名稱作為選項。

在底層,Spring 使用適當的適配器程式碼封裝您的 POJO(函數),以實現與 AI 模型的互動,從而節省您編寫繁瑣的樣板程式碼。底層基礎架構的基礎是 FunctionCallback.java 介面和配套的 Builder 實用程式類別,以簡化 Java 回呼函數的實作和註冊。

運作方式

假設我們希望 AI 模型回應其不具備的資訊,例如,給定位置的目前溫度。

我們可以向 AI 模型提供關於我們自己的函數的中繼資料,以便模型在處理您的提示詞時使用這些函數來檢索該資訊。

例如,如果在處理提示詞期間,AI 模型判斷它需要關於給定位置溫度的額外資訊,它將啟動伺服器端產生的請求/回應互動。AI 模型不會回傳最終回應訊息,而是回傳特殊的工具呼叫請求,提供函數名稱和引數(以 JSON 形式)。用戶端的責任是處理此訊息並執行指定的函數,然後將回應作為工具回應訊息傳回給 AI 模型。

Spring AI 大大簡化了您需要編寫以支援函數調用的程式碼。它為您仲介函數調用對話。您只需將函數定義作為 @Bean 提供,然後在您的提示詞選項中提供函數的 bean 名稱,或直接將函數作為參數傳遞到您的提示詞請求選項中。

您也可以在提示詞中參考多個函數 bean 名稱。

範例用例

讓我們定義一個簡單的用例,我們可以將其作為範例來說明函數調用如何運作。讓我們建立一個聊天機器人,透過呼叫我們自己的函數來回答問題。為了支援聊天機器人的回應,我們將註冊我們自己的函數,該函數接收一個位置並回傳該位置的目前天氣。

當模型需要回答諸如 "波士頓的天氣如何?" 之類的問題時,AI 模型將調用用戶端,並提供位置值作為要傳遞給函數的引數。此類 RPC 類型的資料以 JSON 形式傳遞。

我們的函數調用一些基於 SaaS 的天氣服務 API,並將天氣回應傳回給模型以完成對話。在本範例中,我們將使用一個名為 MockWeatherService 的簡單實作,該實作硬編碼了各個位置的溫度。

以下 MockWeatherService 類別代表天氣服務 API

  • Java

  • Kotlin

public class MockWeatherService implements Function<Request, Response> {

	public enum Unit { C, F }
	public record Request(String location, Unit unit) {}
	public record Response(double temp, Unit unit) {}

	public Response apply(Request request) {
		return new Response(30.0, Unit.C);
	}
}
class MockWeatherService : Function1<Request, Response> {
	override fun invoke(request: Request) = Response(30.0, Unit.C)
}

enum class Unit { C, F }
data class Request(val location: String, val unit: Unit) {}
data class Response(val temp: Double, val unit: Unit) {}

伺服器端註冊

作為 Bean 的函數

Spring AI 提供了多種方法,可以在 Spring 容器中將自訂函數註冊為 bean。

我們先從描述最 POJO 友善的選項開始。

純函數

在此方法中,您可以像定義任何其他 Spring 管理的物件一樣,在應用程式容器中定義 @Bean

在內部,Spring AI ChatModel 將建立一個 FunctionCallback 的實例,該實例會新增透過 AI 模型調用它的邏輯。@Bean 的名稱用作函數名稱。

  • Java

  • Kotlin

@Configuration
static class Config {

	@Bean
	@Description("Get the weather in location") // function description
	public Function<MockWeatherService.Request, MockWeatherService.Response> currentWeather() {
		return new MockWeatherService();
	}

}
@Configuration
class Config {

	@Bean
	@Description("Get the weather in location") // function description
	fun currentWeather(): (Request) -> Response = MockWeatherService()

}

@Description 註解是選用的,它提供函數描述,以協助模型理解何時呼叫函數。這是一個重要的屬性,需要設定以協助 AI 模型判斷要調用哪個用戶端函數。

提供函數描述的另一個選項是在 MockWeatherService.Request 上使用 @JsonClassDescription 註解

  • Java

  • Kotlin

@Configuration
static class Config {

	@Bean
	public Function<Request, Response> currentWeather() { // bean name as function name
		return new MockWeatherService();
	}
}

@JsonClassDescription("Get the weather in location") // function description
public record Request(String location, Unit unit) {}
@Configuration
class Config {

	@Bean
	fun currentWeather(): (Request) -> Response  { // bean name as function name
		return MockWeatherService()
	}
}

@JsonClassDescription("Get the weather in location") // function description
data class Request(val location: String, val unit: Unit)

最佳實務是在請求物件上註解資訊,以便該函數產生的 JSON 結構描述盡可能具有描述性,以協助 AI 模型選擇正確的函數來調用。

FunctionCallback

註冊函數的另一種方法是建立一個像這樣的 FunctionCallback

  • Java

  • Kotlin

@Configuration
static class Config {

	@Bean
	public FunctionCallback weatherFunctionInfo() {

        return FunctionCallback.builder()
            .description("Get the weather in location") // (2) function description
            .function("CurrentWeather", new MockWeatherService()) // (1) function name and instance
            .inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
            .build();
	}
}
import org.springframework.ai.model.function.withInputType

@Configuration
class Config {

	@Bean
	fun weatherFunctionInfo(): FunctionCallback {

        return FunctionCallback.builder()
            .description("Get the weather in location") // (2) function description
            .function("CurrentWeather", MockWeatherService()) // (1) function name and instance
            // (3) Required due to Kotlin SAM conversion being an opaque lambda
            .inputType<MockWeatherService.Request>()
            .build();
	}
}

它封裝了第三方 MockWeatherService 函數,並將其註冊為 ChatClientCurrentWeather 函數。它還提供了描述 (2) 和選用的回應轉換器,以將回應轉換為模型預期的文字。

預設情況下,回應轉換器會執行回應物件的 JSON 序列化。
FunctionCallback.Builder 在內部根據 MockWeatherService.Request 類別解析函數呼叫簽章。

依 bean 名稱啟用函數

為了讓模型知道並呼叫您的 CurrentWeather 函數,您需要在您的提示詞請求中啟用它

ChatClient chatClient = ...

ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
    .functions("CurrentWeather") // Enable the function
    .call().
    chatResponse();

logger.info("Response: {}", response);

上述使用者問題將觸發對 CurrentWeather 函數的 3 次呼叫(每個城市一次),最終回應將類似於以下內容

Here is the current weather for the requested cities:
- San Francisco, CA: 30.0°C
- Tokyo, Japan: 10.0°C
- Paris, France: 15.0°C

FunctionCallbackWithPlainFunctionBeanIT.java 測試示範了這種方法。

用戶端註冊

除了自動組態之外,您還可以動態註冊回呼函數。您可以使用函數調用或方法調用方法,向您的 ChatClientChatModel 請求註冊函數。

用戶端註冊使您可以預設註冊函數。

函數調用

ChatClient chatClient = ...

ChatResponse response = this.chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
    .functions(FunctionCallback.builder()
            .description("Get the weather in location") // (2) function description
            .function("currentWeather", (Request request) -> new Response(30.0, Unit.C)) // (1) function name and instance
            .inputType(MockWeatherService.Request.class) // (3) input type to build the JSON schema
            .build())
    .call()
    .chatResponse();
即時函數預設為在此請求期間啟用。

這種方法允許根據使用者輸入動態選擇要呼叫的不同函數。

FunctionCallbackInPromptIT.java 整合測試提供了一個完整範例,說明如何向 ChatClient 註冊函數並在提示詞請求中使用它。

方法調用

MethodInvokingFunctionCallback 透過反射啟用方法調用,同時自動處理 JSON 結構描述產生和參數轉換。它特別適用於將 Java 方法整合為 AI 模型互動中的可調用函數。

MethodInvokingFunctionCallback 實作了 FunctionCallback 介面並提供:

  • 方法參數的自動 JSON 結構描述產生

  • 支援靜態方法和實例方法

  • 任何數量的參數(包括無參數)和回傳值(包括 void)

  • 任何參數/回傳類型(基本類型、物件、集合)

  • ToolContext 參數的特殊處理

您需要 FunctionCallback.Builder 來建立像這樣的 MethodInvokingFunctionCallback

// Create using builder pattern
FunctionCallback methodInvokingCallback = FunctionCallback.builder()
    .description("Function calling description") // Hints the AI to know when to call this method
    .method("MethodName", Class<?>...argumentTypes) // The method to invoke and its argument types
    .targetObject(targetObject)       // Required instance methods for static methods use targetClass
    .build();

以下是一些使用範例

  • 靜態方法調用

  • 具有 ToolContext 的實例方法

public class WeatherService {
    public static String getWeather(String city, TemperatureUnit unit) {
        return "Temperature in " + city + ": 20" + unit;
    }
}

// Usage
FunctionCallback callback = FunctionCallback.builder()
    .description("Get weather information for a city")
    .method("getWeather", String.class, TemperatureUnit.class)
    .targetClass(WeatherService.class)
    .build();
public class DeviceController {
    public void setDeviceState(String deviceId, boolean state, ToolContext context) {
        Map<String, Object> contextData = context.getContext();
        // Implementation using context data
    }
}

// Usage
DeviceController controller = new DeviceController();

String response = ChatClient.create(chatModel).prompt()
    .user("Turn on the living room lights")
    .functions(FunctionCallback.builder()
        .description("Control device state")
        .method("setDeviceState", String.class,boolean.class,ToolContext.class)
        .targetObject(controller)
        .build())
    .toolContext(Map.of("location", "home"))
    .call()
    .content();

OpenAiChatClientMethodInvokingFunctionCallbackIT 整合測試提供了關於如何使用 FunctionCallback.Builder 建立方法調用 FunctionCallback 的其他範例。

工具上下文

Spring AI 現在支援透過工具上下文將額外的上下文資訊傳遞給函數回呼。此功能允許您提供額外的、使用者提供的資料,這些資料可以在函數執行期間與 AI 模型傳遞的函數引數一起使用。

Function calling with Tool Context

ToolContext 類別提供了一種傳遞額外上下文資訊的方式。

使用工具上下文

在函數調用的情況下,上下文資訊作為 java.util.BiFunction 的第二個引數傳遞。

對於方法調用,上下文資訊作為 ToolContext 類型的方法引數傳遞。

函數調用

您可以在建置聊天選項時設定工具上下文,並為您的回呼使用 BiFunction

BiFunction<MockWeatherService.Request, ToolContext, MockWeatherService.Response> weatherFunction =
    (request, toolContext) -> {
        String sessionId = (String) toolContext.getContext().get("sessionId");
        String userId = (String) toolContext.getContext().get("userId");

        // Use sessionId and userId in your function logic
        double temperature = 0;
        if (request.location().contains("Paris")) {
            temperature = 15;
        }
        else if (request.location().contains("Tokyo")) {
            temperature = 10;
        }
        else if (request.location().contains("San Francisco")) {
            temperature = 30;
        }

        return new MockWeatherService.Response(temperature, 15, 20, 2, 53, 45, MockWeatherService.Unit.C);
    };


ChatResponse response = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
    .functions(FunctionCallback.builder()
        .description("Get the weather in location")
        .function("getCurrentWeather", this.weatherFunction)
        .inputType(MockWeatherService.Request.class)
        .build())
    .toolContext(Map.of("sessionId", "1234", "userId", "5678"))
    .call()
    .chatResponse();

在本範例中,weatherFunction 定義為一個 BiFunction,它同時接收請求和工具上下文作為參數。這讓您可以直接在函數邏輯中存取上下文。

這種方法允許您將特定於會話或特定於使用者的資訊傳遞給您的函數,從而實現更具上下文和個人化的回應。

方法調用

public class DeviceController {
    public void setDeviceState(String deviceId, boolean state, ToolContext context) {
        Map<String, Object> contextData = context.getContext();
        // Implementation using context data
    }
}

// Usage
DeviceController controller = new DeviceController();

String response = ChatClient.create(chatModel).prompt()
    .user("Turn on the living room lights")
    .functions(FunctionCallback.builder()
        .description("Control device state")
        .method("setDeviceState", String.class,boolean.class,ToolContext.class)
        .targetObject(controller)
        .build())
    .toolContext(Map.of("location", "home"))
    .call()
    .content();