LangChain4j實戰-工具(函數調用)Tools(Function Calling)

Tools(Function Calling)的概念

有一個概念被稱為"工具(Tools)"或者"函數調用(function calling)"。它允許LLM在必要時調用一個或多個可用的工具,這些工具通常由開發人員定義。工具可以是任何東西:網絡搜索,對外部API的調用,或特定代碼的執行等待。LLM實際不能調用工具本身;相反,它們表達的時意圖在響應中調用特定的工具(而不是以純文本響應)。作為開發人員,我們應該使用提供的參數執行此工具並返回工具執行的結果。

當LLM可以訪問工具時,它可以決定在適當的時候調用其中一個工具。

為了增加LLM使用正確參數調用正確工具的機會,我應該提供一個明確而不含糊的:

  • 工具名稱
  • 描述該工具的功能以及何時使用
  • 各工具參數説明

如果一個人能夠理解工具的用途以及如何使用它,LLM很可能也可以。

LLM專門進行了微調,以檢測何時調用工具以及如何調用它們,當然並非所有模型都支持工具,需要參見LangChain4j官網上的語言模型的支持情況

https://docs.langchain4j.dev/integrations/language-models/

LangChain4j使用工具的兩種方式

LangChain4j為使用工具提供了兩種抽象層次

  • Low-level(低階):使用ChatModel和ToolSpecification API
  • High-level(高階):使用AI Services和@Tool註釋的java方法

示例代碼的依賴

物料清單

<properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>21</java.version>
        <!--Spring Boot-->
        <spring-boot.version>3.5.3</spring-boot.version>
        <!--LangChain4J-->
        <langchain4j.version>1.7.1</langchain4j.version>
        <!--LangChain4J community-->
        <langchain4j-community.version>1.7.1-beta14</langchain4j-community.version>
    </properties>
    <!--    <dependencyManagement> 是一個聲明和集中管理依賴版本和配置的機制(物料清單)。它本身並不引入實際的依賴,而是為依賴提供一個“模板”或“藍圖”-->
    <dependencyManagement>
        <dependencies>
            <!--Spring Boot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--LangChain4J-->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--langchain4j-community-->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j-community.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

實際調用的依賴

<dependencies>
        <!--快速構建一個基於 Spring MVC 的 Web 應用程序而預置的一組依賴項的集合-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--一個通過註解在編譯時自動生成 Java 樣板代碼的庫-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--LangChain4J openAI集成依賴(低級API依賴)-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
        </dependency>
        <!--LangChain4J 高級AI服務API依賴-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
        </dependency>
        <!--LangChain4J 響應式編程依賴(AI服務使用Flux)-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
        </dependency>
        <!--hutool Java工具庫-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.40</version>
        </dependency>
        <!--Apache HttpClient http客户端依賴-->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.5</version>
        </dependency>
    </dependencies>

Low Level Tool API(低階工具API)

低階層級可以使用ChatModel的chat(ChatRequest)方法,在創建ChatRequest時,可以指定一個或多個工具規格説明(ToolSpecification)

創建工具規格説明(ToolSpecification)

ToolSpecification是一個包含工具所有信息的對象:

  • name:工具名稱
  • description:工具説明
  • parameters:工具的參數及其描述

有兩種方式可以創建ToolSpecification

1.Manually手動創建

ToolSpecification toolSpecification = ToolSpecification.builder()
                .name("開具發票助手")
                .description("根據用户提供的開票信息,開具發票")
                .parameters(JsonObjectSchema.builder()
                        .addStringProperty("companyName", "公司名稱")
                        .addStringProperty("dutyNumber", "税號")
                        .addStringProperty("amount", "開票金額")
                        .build())
                .build();

2.使用輔助方法創建

  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
/**
 * 發票處理類
 */
@Slf4j
public class InvoiceHandler {
    @Tool(value = "根據用户開票信息進行開票並預報今日天氣")
    public String createInvoice(@P("公司名稱") String companyName,
                                @P("税號") String dutyNumber,
                                @P("報銷金額") String amount){
        log.info("companyName : {}, dutyNumber : {}, amount : {}", companyName, dutyNumber, amount);
        String invoiceStr = "發票信息\n" +
                "發票號碼: " + dutyNumber + "\n" +
                "開票日期: 2025年11月05日\n" +
                "到期日期: 2025年12月16日\n" +
                "公司名稱: " + companyName + "\n" +
                "統一社會信用代碼: 91110000MA001234XY\n" +
                "註冊地址: 北京市海淀區中關村大街1號\n" +
                "聯繫電話: 010-12345678\n" +
                "總計金額: " + amount + "\n";
        return "開票成功:\n" + invoiceStr;
    }
}
List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(InvoiceHandler.class);

使用ChatModel

在得到List後就可以調用模型了,如果LLM決定調用該工具,返回的AiMessage將包含數據在toolExecutionRequests字段。需要使用ToolExecutionRequest中的信息手動執行工具

List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(new InvoiceHandler());
        List<ChatMessage> messageList = new ArrayList<>();
        UserMessage userMessage = UserMessage.from("根據以下信息開張發票," +
                "公司:北京深度求索人工智能基礎技術研究有限公司" +
                "税號:192886685Z8LGVYSP2" +
                "金額:80000");
        ChatRequest chatRequest = ChatRequest.builder()
                .messages(userMessage)
                .toolSpecifications(toolSpecifications)
                .build();
        ChatResponse chatResponse = chatModel.chat(chatRequest);
        // 第一次提問如果是開具發票相關的,會把使用工具的參數返回,反之則直接給出回答
        AiMessage aiMessage = chatResponse.aiMessage();
        if (aiMessage.hasToolExecutionRequests()) {
            messageList.add(aiMessage);
            aiMessage.toolExecutionRequests().forEach(t -> {
                DefaultToolExecutor toolExecutor = new DefaultToolExecutor(new InvoiceHandler(), t);
                String result = toolExecutor.execute(t, UUID.randomUUID());
                log.info("工具執行後的結果為:{}", result);
                ToolExecutionResultMessage toolMessage = ToolExecutionResultMessage.from(t, result);
                messageList.add(toolMessage); // 將結果加入消息歷史
            });
        }
        AiMessage finalMessage = chatModel.chat(messageList).aiMessage();

低階使用工具示例(發票助手工具使用)

大模型配置類

/**
 * 大模型配置類
 */
@Slf4j
@Configuration
public class LLMConfig {
    @Bean("qwen")
    public ChatModel chatLanguageModel() {
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("aliyunQwen-apiKey"))
                .modelName("qwen-plus")
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .build();
    }
}

發票處理類

/**
 * 發票處理類
 */
@Slf4j
public class InvoiceHandler {
    @Tool(value = "根據用户開票信息進行開票並預報今日天氣")
    public String createInvoice(@P("公司名稱") String companyName,
                                @P("税號") String dutyNumber,
                                @P("報銷金額") String amount){
        log.info("companyName : {}, dutyNumber : {}, amount : {}", companyName, dutyNumber, amount);
        String invoiceStr = "發票信息\n" +
                "發票號碼: " + dutyNumber + "\n" +
                "開票日期: 2025年11月05日\n" +
                "到期日期: 2025年12月16日\n" +
                "公司名稱: " + companyName + "\n" +
                "統一社會信用代碼: 91110000MA001234XY\n" +
                "註冊地址: 北京市海淀區中關村大街1號\n" +
                "聯繫電話: 010-12345678\n" +
                "總計金額: " + amount + "\n";
        return "開票成功:\n" + invoiceStr;
    }
}

大模型使用工具的控制層接口

@Slf4j
@RestController
@RequestMapping("/chatFunctionCalling")
public class ChatFunctionCallingController {
    @Autowired
    @Qualifier("qwen")
    private ChatModel chatModel;

    /**
     * 低階工具使用 構建工具規格説明(ToolSpecification) + 並手動執行(DefaultToolExecutor) + 大模型分析
     *
     * @return 大模型結合調用工具結果的回答
     */
    @RequestMapping("/lowLevelFunctionCallingChat")
    public String lowLevelFunctionCallingChat() {
        List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(new InvoiceHandler());
        List<ChatMessage> messageList = new ArrayList<>();
        UserMessage userMessage = UserMessage.from("根據以下信息開張發票," +
                "公司:北京深度求索人工智能基礎技術研究有限公司" +
                "税號:192886685Z8LGVYSP2" +
                "金額:80000");
        ChatRequest chatRequest = ChatRequest.builder()
                .messages(userMessage)
                .toolSpecifications(toolSpecifications)
                .build();
        ChatResponse chatResponse = chatModel.chat(chatRequest);
        // 第一次提問會時LLM使用工具,會把使用工具的參數返回
        AiMessage aiMessage = chatResponse.aiMessage();
        if (aiMessage.hasToolExecutionRequests()) {
            messageList.add(aiMessage);
            aiMessage.toolExecutionRequests().forEach(t -> {
                DefaultToolExecutor toolExecutor = new DefaultToolExecutor(new InvoiceHandler(), t);
                String result = toolExecutor.execute(t, UUID.randomUUID());
                log.info("工具執行後的結果為:{}", result);
                ToolExecutionResultMessage toolMessage = ToolExecutionResultMessage.from(t, result);
                messageList.add(toolMessage); // 將結果加入消息歷史
            });
        }
        AiMessage finalMessage = chatModel.chat(messageList).aiMessage();
        log.info("answer is {}", finalMessage);
        return "success :" + DateUtil.now() + "\n" + finalMessage.text();
    }
}

High Level Tool API(高階工具API)

高階層次可以使用@Tool註解任何Java方法並在創建AI Service(AI服務)時指定這些方法,AI Service會自動將這些方法轉換為ToolSpecification並將他們包括在與LLM每次互動的請求中,當LLM決定調用該工具時,AI Service會自動執行適當的方法,方法的返回值將發送回LLM。

@Tool註解

標記為@Tool的任何Java方法並在構建AI服務時明確指定,可以由LLM執行

interface MathGenius {
    
    String ask(String question);
}

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

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

MathGenius mathGenius = AiServices.builder(MathGenius.class)
    .chatModel(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.

@Tool註解有兩個可選字段:

  • name: 工具名稱。如果沒有提供這個參數,方法的名稱將作為工具的名稱。
  • value: 工具描述

根據工具的不同,即使沒有任何描述,LLM也可能很好地理解它(例如:add(a,b)是含義明顯的),但最好提供清晰而有意義的名稱和描述。這樣LLM有更多的信息來決定是否調用給定的工具,以及如何這樣做。

@P註解

方法參數可以標註為@P,@P註解有兩個字段

  • value: 參數描述,強制性字段。
  • required: 是否為必選參數,默認為true。可選字段。

高階使用工具示例(發票助手工具使用)

AI服務接口

public interface FunctionAssistant {
    String chat(String message);
}

發票處理類(與低階使用工具一致)

/**
 * 發票處理類
 */
@Slf4j
public class InvoiceHandler {
    @Tool(value = "根據用户開票信息進行開票並預報今日天氣")
    public String createInvoice(@P("公司名稱") String companyName,
                                @P("税號") String dutyNumber,
                                @P("報銷金額") String amount) throws Exception {
        log.info("companyName : {}, dutyNumber : {}, amount : {}", companyName, dutyNumber, amount);
        String invoiceStr = "發票信息\n" +
                "發票號碼: " + dutyNumber + "\n" +
                "開票日期: 2025年11月05日\n" +
                "到期日期: 2025年12月16日\n" +
                "公司名稱: " + companyName + "\n" +
                "統一社會信用代碼: 91110000MA001234XY\n" +
                "註冊地址: 北京市海淀區中關村大街1號\n" +
                "聯繫電話: 010-12345678\n" +
                "總計金額: " + amount + "\n";
        JsonNode weatherJsonNode = new WeatherService().getWeather("101010100");
        String weatherStr = weatherJsonNode.toString();
        return "開票成功:\n" + invoiceStr + weatherStr;
    }
}

大模型配置類

/**
 * 大模型配置類
 */
@Slf4j
@Configuration
public class LLMConfig {
    @Bean("qwen")
    public ChatModel chatLanguageModel() {
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("aliyunQwen-apiKey"))
                .modelName("qwen-plus")
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .build();
    }

    @Bean
    public FunctionAssistant functionAssistant(ChatModel chatModel) {
        return AiServices.builder(FunctionAssistant.class)
                .chatModel(chatModel)
                .tools(new InvoiceHandler())
                .build();
    }
}

大模型使用工具的控制層接口

@Slf4j
@RestController
@RequestMapping("/chatFunctionCalling")
public class ChatFunctionCallingController {
    @Autowired
    private FunctionAssistant functionAssistant;

    /**
     * 高階工具使用 使用@Tool註解指定方法,並在構建AI服務實例時添加,大模型會根據情況調用並自動執行工具
     *
     * @return 大模型結合調用工具結果的回答
     */
    @RequestMapping("/highLevelFunctionCallingChat")
    public String highLevelFunctionCallingChat() {
        //如果提問是開具發票相關的則使用工具,反之則不會
        String answer = functionAssistant.chat("根據以下信息開張發票," +
                "公司:北京深度求索人工智能基礎技術研究有限公司" +
                "税號:192886685Z8LGVYSP2" +
                "金額:80000");
        log.info("answer is {}", answer);
        return "success :" + DateUtil.now() + "\n" + answer;
    }
}

參考資料

https://docs.langchain4j.dev/tutorials/tools