使用LangChain4J實現Agent與Tool呼叫

公众号-JavaEdge發表於2024-09-22

一些LLM除了生成文字,還可觸發操作。

所有支援tools的LLMs可在此處找到(參見“Tools”欄)。

有一個被稱為“工具(tools)”或“函式呼叫(function calling)”的概念。它允許LLM在必要時呼叫一或多個由開發者定義的工具。工具可以是任何東西:網頁搜尋、外部API呼叫、或執行一段特定程式碼等。LLM本身無法實際呼叫這些工具;它們會在響應中表達出呼叫某個工具的意圖(而不是直接生成文字)。我們開發者,則需要根據提供的引數來執行這些工具並報告工具執行結果。

如我們知道LLM本身並不擅長數學運算。若你的應用場景涉及偶爾的數學計算,你可能希望為LLM提供一個“math tool”。透過在請求中宣告一個或多個工具,LLM可以在認為適合時呼叫其中一個。如遇到數學問題並擁有一組數學工具時,LLM可能會決定首先呼叫其中的一個來正確回答問題。

1 有無工具時的效果

1.1 沒有工具的訊息示例

Request:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?

Response:
- AiMessage:
    - text: The square root of 475695037565 is approximately 689710.

接近正確,但不完全對。

1.2 使用以下工具的訊息示例

@Tool("Sums 2 given numbers")
public double sum(double a, double b) {
    return a + b;
}

@Tool("Returns a square root of a given number")
public double squareRoot(double x) {
    return Math.sqrt(x);
}
Request 1:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?
- tools:
    - sum(double a, double b): Sums 2 given numbers
    - squareRoot(double x): Returns a square root of a given number

Response 1:
- AiMessage:
    - toolExecutionRequests:
        - squareRoot(475695037565)


... here we are executing the squareRoot method with the "475695037565" argument and getting "689706.486532" as a result ...


Request 2:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?
    - AiMessage:
        - toolExecutionRequests:
            - squareRoot(475695037565)
    - ToolExecutionResultMessage:
        - text: 689706.486532

Response 2:
- AiMessage:
    - text: The square root of 475695037565 is 689706.486532.

如你所見,當LLM擁有工具時,它可在適當時決定呼叫其中的一個。

這是一個非常強大的功能。這簡單例子,我們給LLM提供原始的數學工具,但可想象如提供如googleSearchsendEmail工具,然後提供一個查詢“我的朋友想知道AI領域的最新訊息。請將簡短的總結髮送到friend@email.com”,那它可用googleSearch工具找到最新訊息,然後總結並透過sendEmail工具傳送總結。

經驗法則

為了增加LLM呼叫正確工具和引數的機率,我們應該提供清晰且明確的:

  • 工具名稱
  • 工具的功能描述以及何時使用
  • 每個工具引數的描述

一個好的經驗法則是:如果人類能理解工具的用途和如何使用,那麼LLM也能理解。

LLM被專門微調,以檢測何時呼叫工具以及如何呼叫它們。某些模型甚至可以一次呼叫多個工具,如OpenAI

注意,工具/函式呼叫與JSON模式不同。

2 兩個抽象層次

LangChain4j 提供兩個使用工具的抽象層:

  • 底層,使用 ChatLanguageModel API
  • 高階,使用AI服務@Tool註解的Java方法

3 底層工具API

3.1 generate

可用ChatLanguageModel#generate(List<ChatMessage>, List<ToolSpecification>)

/**
  * 根據訊息列表和工具規範列表從模型生成響應。響應可以是文字訊息,也可以是執行指定工具之一的請求。通常,該列表包含按以下順序排列的訊息:System (optional) - User - AI - User - AI - User ...
  * messages – 訊息列表
  * toolSpecifications – 允許模型執行的工具列表。該模型自主決定是否使用這些工具中的任何一個
  * return:模型生成的響應
  * AiMessage 可以包含文字響應或執行其中一個工具的請求。
  */
default Response<AiMessage> generate(List<ChatMessage> messages, List<ToolSpecification> toolSpecifications) {
    throw new IllegalArgumentException("Tools are currently not supported by this model");
}

類似方法也存於StreamingChatLanguageModel

3.2 ToolSpecification

package dev.langchain4j.agent.tool;

// 包含工具所有資訊
public class ToolSpecification {
    // 工具的`名稱`
    private final String name;
    // 工具的`描述`
    private final String description;
    // 工具的`引數`及其描述
    private final ToolParameters parameters;

推薦儘可能提供關於工具的所有資訊:清晰的名稱、詳盡的描述和每個引數的描述等。

3.2.1 建立ToolSpecification

① 手動
ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("getWeather")
    .description("返回指定城市的天氣預報")
    .addParameter("city", type("string"), description("應返回天氣預報的城市"))
    .addParameter("temperatureUnit", enums(TemperatureUnit.class)) // 列舉 TemperatureUnit { 攝氏, 華氏 }
    .build();
② 使用輔助方法
  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools { 
  
    @Tool("Returns the weather forecast for a given city")
    String getWeather(
            @P("The city for which the weather forecast should be returned") String city,
            TemperatureUnit temperatureUnit
    ) {
        ...
    }
}

List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);

一旦你擁有List<ToolSpecification>,可呼叫模型:

UserMessage userMessage = UserMessage.from("倫敦明天的天氣如何?");
Response<AiMessage> response = model.generate(List.of(userMessage), toolSpecifications);
AiMessage aiMessage = response.content();

若LLM決定呼叫工具,返回的AiMessage將包含toolExecutionRequests欄位中的資料。此時,AiMessage.hasToolExecutionRequests()將返回true。根據LLM不同,它可包含一或多個ToolExecutionRequest物件(某些LLM支援並行呼叫多個工具)。

每個ToolExecutionRequest應包含:

public class ToolExecutionRequest {
  	// 工具呼叫的`id`(某些LLM不提供)
    private final String id;
  	// 要呼叫的工具名稱,例如:`getWeather`
    private final String name;
  	// 工具的`引數`,例如:`{ "city": "London", "temperatureUnit": "CELSIUS" }`
    private final String arguments;

你要用ToolExecutionRequest中的資訊手動執行工具。

如希望將工具執行的結果發回LLM,你要為每個ToolExecutionRequest建立一個ToolExecutionResultMessage並與之前的所有訊息一起傳送:

String result = "預計明天倫敦會下雨。";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
List<ChatMessage> messages = List.of(userMessage, aiMessage, toolExecutionResultMessage);
Response<AiMessage> response2 = model.generate(messages, toolSpecifications);

4 高階工具API

高層,你可為任何Java方法新增@Tool註解,並將其與AI服務一起使用。

AI服務會自動將這些方法轉換為ToolSpecification,並在每次與LLM的互動中包含它們。當LLM決定呼叫工具時,AI服務將自動執行相應的方法,並將方法的返回值(如果有)傳送回LLM。實現細節可以在DefaultToolExecutor中找到。

@Tool("Searches Google for relevant URLs, given the query")
public List<String> searchGoogle(@P("search query") String query) {
    return googleSearchService.search(query);
}

@Tool("Returns the content of a web page, given the URL")
public String getWebPageContent(@P("URL of the page") String url) {
    Document jsoupDocument = Jsoup.connect(url).get();
    return jsoupDocument.body().text();
}

4.1 @Tool

任何用@Tool註解並在構建AI服務時明確指定的Java方法,都可以被LLM執行

interface MathGenius {
    
    String ask(String question);
}

class Calculator {
    
    @Tool
    public double add(int a, int b) {
        return a + b;
    }

    @Tool
    public double squareRoot(double x) {
        return Math.sqrt(x);
    }
}

MathGenius mathGenius = AiServices.builder(MathGenius.class)
    .chatLanguageModel(model)
    .tools(new Calculator())
    .build();

String answer = mathGenius.ask("What is the square root of 475695037565?");

System.out.println(answer); // The square root of 475695037565 is 689706.486532.

呼叫ask方法時,會發生兩次與LLM的互動,如前文所述。互動期間,會自動呼叫squareRoot方法。

@Tool註解有兩個可選欄位:

  • name: 工具的名稱。如果未提供,方法名將作為工具名稱。
  • value: 工具的描述。

根據具體工具,即使不提供描述,LLM也可能理解其用途(例如,add(a, b)很明顯),但通常最好提供清晰且有意義的名稱和描述。這樣,LLM在決定是否呼叫工具以及如何呼叫時會有更多資訊。

4.2 @P

方法的引數可以使用@P註解。

@P註解有兩個欄位:

  • value: 引數的描述,此欄位是必填的。
  • required: 引數是否是必需的,預設值為true,此欄位為可選。

4.3 @ToolMemoryId

如果AI服務方法的某個引數使用了@MemoryId註解,則可以在@Tool方法的引數上使用@ToolMemoryId進行註解。這樣,提供給AI服務方法的值將自動傳遞給@Tool方法。這對於多個使用者和/或每個使用者有多個聊天或記憶的場景非常有用,可以在@Tool方法中區分它們。

4.4 訪問已執行的工具

如果你希望訪問AI服務呼叫過程中執行的工具,可以透過將返回型別封裝在Result類中輕鬆實現:

interface Assistant {

    Result<String> chat(String userMessage);
}

Result<String> result = assistant.chat("取消我的預訂 123-456");

String answer = result.content();
List<ToolExecution> toolExecutions = result.toolExecutions();

4.5 以程式設計方式指定工具

在使用AI服務時,也可以透過程式設計方式指定工具。這種方法非常靈活,因為工具可以從外部資源(如資料庫和配置檔案)載入。

工具名稱、描述、引數名稱和描述都可以使用ToolSpecification進行配置:

ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("get_booking_details")
    .description("返回預訂詳情")
    .addParameter("bookingNumber", type("string"), description("B-12345格式的預訂編號"))
    .build();

對於每個ToolSpecification,需要提供一個ToolExecutor實現來處理LLM生成的工具執行請求:

ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
    Map<String, Object> arguments = fromJson(toolExecutionRequest.arguments());
    String bookingNumber = arguments.get("bookingNumber").toString();
    Booking booking = getBooking(bookingNumber);
    return booking.toString();
};

旦我們擁有一個或多個(ToolSpecificationToolExecutor)對,我們可以在建立AI服務時指定它們:

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(chatLanguageModel)
    .tools(singletonMap(toolSpecification, toolExecutor))
    .build();

4.6 動態指定工具

在使用AI服務時,每次呼叫時也可以動態指定工具。可以配置一個ToolProvider,該提供者將在每次呼叫AI服務時被呼叫,並提供應包含在當前請求中的工具。ToolProvider接受一個包含UserMessage和聊天記憶ID的ToolProviderRequest,並返回包含工具的ToolProviderResult,其形式為ToolSpecificationToolExecutor的對映。

下面是一個示例,展示如何僅在使用者訊息中包含“預訂”一詞時新增get_booking_details工具:

ToolProvider toolProvider = (toolProviderRequest) -> {
    if (toolProviderRequest.userMessage().singleText().contains("booking")) {
        ToolSpecification toolSpecification = ToolSpecification.builder()
            .name("get_booking_details")
            .description("返回預訂詳情")
            .addParameter("bookingNumber", type("string"))
            .build();
        return ToolProviderResult.builder()
            .add(toolSpecification, toolExecutor)
            .build();
    } else {
        return null;
    }
};

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .toolProvider(toolProvider)
    .build();

5 示例

  • 帶工具的示例
  • 帶動態工具的示例

參考:

  • 關於工具的精彩指南

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。

各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統效能最佳化
  • 活動&券等營銷中臺建設
  • 交易平臺及資料中臺等架構和開發設計
  • 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
  • LLM Agent應用開發
  • 區塊鏈應用開發
  • 大資料開發挖掘經驗
  • 推薦系統專案

目前主攻市級軟體專案設計、構建服務全社會的應用系統。

參考:

  • 程式設計嚴選網

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章