Yang's blog Yang's blog
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xiaoyang

编程爱好者
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • SpringCloud

    • 微服务架构介绍
    • SpringCloud介绍
    • Spring Cloud:生产者与消费者
    • Spring Cloud Eureka:构建可靠的服务注册与发现
    • Spring Cloud Ribbon:负载均衡
    • Spring Cloud Fegin:服务调用
    • Spring Cloud Hystrix:熔断器
    • Spring Cloud Zuul:统一网关路由
    • Spring Cloud Config:配置中心
  • Java后端框架

    • LangChain4j

      • 介绍
      • 快速开始
      • Chat and Language Models
      • Chat Memory
      • Model Parameters
      • Response Streaming
      • AI Services
      • Agent
      • Tools (Function Calling)
        • 底层工具API
        • 高级工具 API
          • 工具方法限制
          • 工具方法参数
          • 工具方法返回类型
          • 异常处理
          • @Tool
          • @P
          • @Description
          • @ToolMemoryId
          • 访问执行的工具
          • 程序化指定工具
          • 动态指定工具
        • 相关教程
      • RAG
      • Structured Outputs
      • Classification
      • Embedding (Vector) Stores
      • Image Models
      • Quarkus Integration
      • Spring Boot Integration
      • Kotlin Support
      • Logging
      • Observability
      • Testing and Evaluation
      • Model Context Protocol
  • 八股文

    • 操作系统
    • JVM介绍
    • Java多线程
    • Java集合框架
    • Java反射
    • JavaIO
    • Mybatis介绍
    • Spring介绍
    • SpringBoot介绍
    • Mysql
    • Redis
    • 数据结构
    • 云计算
    • 设计模式
    • 计算机网络
    • 锁核心类AQS
    • Nginx
  • 前端技术

    • 初识Vue3
    • Vue3数据双向绑定
    • Vue3生命周期
    • Vue-Router 组件
    • Pinia 集中式状态存储
  • 中间件

    • RocketMQ
  • 开发知识

    • 请求参数注解
    • 时间复杂度和空间复杂度
    • JSON序列化与反序列化
    • Timestamp vs Datetime
    • Java开发中必备能力单元测试
    • 正向代理和反向代理
    • 什么是VPN
    • 正则表达式
  • Java
  • Java后端框架
  • LangChain4j
xiaoyang
2025-01-15
目录

Tools (Function Calling)

# Tools (Function Calling)

某些大语言模型(LLM)除了生成文本,还可以触发一些操作。

支持工具的所有LLM可以在此处 (opens new window)找到(参见“Tools”列)。

有一个概念叫做“工具”(Tools)或“函数调用”(Function Calling), 它允许LLM在必要时调用由开发者定义的一个或多个工具。 工具可以是任何东西,例如网页搜索、调用外部API或执行特定的代码段等。 LLM本身无法直接调用工具,而是通过其响应中表达调用特定工具的意图(而非普通的文本响应)。 开发者需要根据提供的参数执行工具,并将执行结果反馈给LLM。

例如,我们知道LLM在数学方面表现并不佳。 如果您的用例中偶尔需要数学计算,您可以向LLM提供一个“数学工具”。 通过在请求中声明一个或多个工具,LLM可以在认为合适时决定调用其中之一。 面对一个数学问题及一组数学工具,LLM可能会决定为了正确回答问题, 需要首先调用提供的某个数学工具。

以下展示了如何在实践中使用工具(以及不使用工具时的表现):

没有工具的消息交换示例:

请求:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?

响应:
- AiMessage:
    - text: 475695037565的平方根大约是689710。
1
2
3
4
5
6
7
8

接近,但不正确。

使用以下工具的消息交换示例:

@Tool("计算两个数的和")
double sum(double a, double b) {
    return a + b;
}

@Tool("返回一个数的平方根")
double squareRoot(double x) {
    return Math.sqrt(x);
}
请求1:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?
- tools:
    - sum(double a, double b): 计算两个数的和
    - squareRoot(double x): 返回一个数的平方根

响应1:
- AiMessage:
    - toolExecutionRequests:
        - squareRoot(475695037565)


... 此处我们用参数“475695037565”执行squareRoot方法,并得到结果“689706.486532” ...


请求2:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?
    - AiMessage:
        - toolExecutionRequests:
            - squareRoot(475695037565)
    - ToolExecutionResultMessage:
        - text: 689706.486532

响应2:
- AiMessage:
    - text: 475695037565的平方根是689706.486532。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

正如您所见,当LLM可以访问工具时,它可以在适当的时候决定调用其中一个工具。

这是一个非常强大的功能。 在这个简单的示例中,我们为LLM提供了基础的数学工具, 但可以设想,如果我们为它提供例如googleSearch和sendEmail工具, 并给出一个类似“我的朋友想了解AI领域的最新新闻。请将简要总结发送到friend@email.com”的查询, 它可以使用googleSearch工具查找AI领域的最新新闻, 然后总结内容,并使用sendEmail工具通过电子邮件发送总结。

为了提高LLM调用正确工具和参数的概率,我们应该提供清晰明确的:

  • 工具名称
  • 工具功能描述以及何时使用的说明
  • 每个工具参数的描述

一个好的规则是:如果人类可以理解工具的用途及使用方法,那么LLM也有很大的可能性可以理解。

LLM经过专门调整,可以检测何时调用工具以及如何调用工具。 某些模型甚至可以同时调用多个工具,例如, OpenAI (opens new window)。

请注意,并非所有模型都支持工具。 要查看支持工具的模型,请参阅此页面 (opens new window)的“Tools”列。

请注意,工具/函数调用与JSON模式并不相同。

# 两个抽象级别

LangChain4j为使用工具提供了两个抽象级别:

  • 底层,使用ChatLanguageModel和ToolSpecification API
  • 高层,使用AI Services和@Tool注解的Java方法

# 底层工具API

在底层,您可以使用ChatLanguageModel的generate(List<ChatMessage>, List<ToolSpecification>)方法。 StreamingChatLanguageModel中也有类似的方法。

ToolSpecification是一个对象,包含工具的所有信息:

  • 工具的name
  • 工具的description
  • 工具的parameters及其描述

建议尽可能多地提供工具信息:清晰的名称、全面的描述以及每个参数的描述等。

创建ToolSpecification有两种方式:

  1. 手动创建
ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("getWeather")
    .description("返回指定城市的天气预报")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("city", "需要返回天气预报的城市")
        .addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
        .required("city") // 必须显式指定必需属性
        .build())
    .build();
1
2
3
4
5
6
7
8
9

有关JsonObjectSchema的更多信息,请参见此处。

  1. 使用辅助方法:
  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools { 
  
    @Tool("返回指定城市的天气预报")
    String getWeather(
            @P("需要返回天气预报的城市") String city,
            TemperatureUnit temperatureUnit
    ) {
        ...
    }
}

List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
1
2
3
4
5
6
7
8
9
10
11
12

完成后,您可以调用模型:

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

如果LLM决定调用工具,返回的AiMessage将包含在toolExecutionRequests字段中的数据。 此时,AiMessage.hasToolExecutionRequests()将返回true。 根据LLM的不同,ToolExecutionRequest对象可能包含一个或多个(某些LLM支持并行调用多个工具)。

每个ToolExecutionRequest应包含:

  • 工具调用的id(某些LLM不提供)
  • 要调用的工具名称,例如:getWeather
  • arguments,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }

您需要根据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);
1
2
3
4

# 高级工具 API

在高层次的抽象中,您可以使用 @Tool 注解标注任何 Java 方法,并在创建 AI 服务 时指定它们。

AI 服务将自动将这些方法转换为 ToolSpecification,并将它们包含在每次与语言模型(LLM)交互的请求中。当 LLM 决定调用工具时,AI 服务将自动执行适当的方法,并将方法的返回值(如果有的话)发送回 LLM。您可以在 DefaultToolExecutor 中找到实现细节。

以下是一些工具示例:

@Tool("根据查询在 Google 中搜索相关 URL")
public List<String> searchGoogle(@P("search query") String query) {
    return googleSearchService.search(query);
}

@Tool("根据 URL 返回网页内容")
public String getWebPageContent(@P("URL of the page") String url) {
    Document jsoupDocument = Jsoup.connect(url).get();
    return jsoupDocument.body().text();
}
1
2
3
4
5
6
7
8
9
10

# 工具方法限制

标注了 @Tool 的方法:

  • 可以是静态的,也可以是非静态的
  • 可以具有任何可见性(public, private 等)

# 工具方法参数

标注了 @Tool 的方法可以接受任意数量的参数,支持多种类型:

  • 基本类型:int、double 等
  • 对象类型:String、Integer、Double 等
  • 自定义 POJO(可以包含嵌套 POJO)
  • enum
  • List<T>/Set<T>,其中 T 是上述类型之一
  • Map<K, V>(您需要使用 @P 手动指定 K 和 V 的类型)

方法也支持没有参数的情况。

默认情况下,所有方法参数都被认为是必需的。这意味着 LLM 必须为这些参数提供值。如果希望某个参数为可选,可以使用 @P(required = false) 来标注。目前不支持声明 POJO 参数字段为可选。

递归参数(例如,Person 类中包含 Set<Person> children 字段)目前仅由 OpenAI 支持。

# 工具方法返回类型

标注了 @Tool 的方法可以返回任何类型,包括 void。

  • 如果方法的返回类型是 void,则如果方法执行成功,将返回 "Success" 字符串给 LLM。
  • 如果方法的返回类型是 String,则返回的值将直接发送给 LLM,无需转换。
  • 对于其他返回类型,返回值将在发送给 LLM 之前转换为 JSON 字符串。

# 异常处理

如果标注了 @Tool 的方法抛出异常,则异常消息(e.getMessage())将作为工具执行结果发送给 LLM。这使得 LLM 能够修正错误并在必要时重试。

# @Tool

任何标注了 @Tool 并在 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)
    .chatLanguageModel(model)
    .tools(new Calculator())
    .build();

String answer = mathGenius.ask("475695037565 的平方根是多少?");

System.out.println(answer); // 475695037565 的平方根是 689706.486532。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

当调用 ask 方法时,会发生 2 次与 LLM 的交互,如前所述。在这些交互之间,squareRoot 方法会自动调用。

@Tool 注解有 2 个可选字段:

  • name:工具的名称。如果未提供,方法名将作为工具的名称。
  • value:工具的描述。

根据工具的不同,即使没有描述,LLM 也可能能很好地理解它(例如,add(a, b) 很显然),但通常最好提供清晰且有意义的名称和描述。这样,LLM 可以获得更多信息来判断是否调用给定的工具,以及如何调用。

# @P

方法参数可以选择性地使用 @P 注解。

@P 注解有 2 个字段:

  • value:参数描述。必填字段。
  • required:参数是否必需,默认为 true。可选字段。

# @Description

可以使用 @Description 注解指定类和字段的描述:

@Description("要执行的查询")
class Query {

  @Description("要选择的字段")
  private List<String> select;

  @Description("筛选条件")
  private List<Condition> where;
}

@Tool
Result executeQuery(Query query) {
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# @ToolMemoryId

如果您的 AI 服务方法具有标注 @MemoryId 的参数,您也可以在 @Tool 方法的参数中使用 @ToolMemoryId 注解。传递给 AI 服务方法的值将自动传递给 @Tool 方法。如果您有多个用户和/或每个用户多个聊天/记忆,并且希望在 @Tool 方法中区分它们时,这个功能非常有用。

# 访问执行的工具

如果您希望访问在调用 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();
1
2
3
4
5
6
7
8
9

在流模式下,您可以通过指定 onToolExecuted 回调来访问:

interface Assistant {

    TokenStream chat(String message);
}

TokenStream tokenStream = assistant.chat("取消我的预订");

tokenStream
    .onNext(...)
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
    .onComplete(...)
    .onError(...)
    .start();
1
2
3
4
5
6
7
8
9
10
11
12
13

# 程序化指定工具

在使用 AI 服务时,工具也可以通过编程方式指定。这种方法提供了很大的灵活性,因为可以从外部来源(如数据库和配置文件)加载工具。

工具的名称、描述、参数名称和描述都可以通过 ToolSpecification 配置:

ToolSpecification toolSpecification = ToolSpecification.builder()
        .name("get_booking_details")
        .description("返回预订详情")
        .parameters(JsonObjectSchema.builder()
                .properties(Map.of(
                        "bookingNumber", JsonStringSchema.builder()
                                .description("预订号,格式为 B-12345")
                                .build()
                ))
                .build())
        .build();
1
2
3
4
5
6
7
8
9
10
11

对于每个 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();
};
1
2
3
4
5
6

一旦我们有了一个或多个(ToolSpecification,ToolExecutor)对,我们可以在创建 AI 服务时指定它们:

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(chatLanguageModel)
    .tools(Map.of(toolSpecification, toolExecutor))
    .build();
1
2
3
4

# 动态指定工具

在使用 AI 服务时,工具也可以在每次调用时动态指定。可以配置一个 ToolProvider,每次调用 AI 服务时都会调用它,并提供当前请求中应包含的工具。ToolProvider 接受一个 ToolProviderRequest,该请求包含 UserMessage 和聊天记忆 ID,并返回一个 ToolProviderResult,该结果包含一个从 ToolSpecification 到 ToolExecutor 的工具映射。

下面是一个如何仅在用户消息中包含“预订”一词时添加 get_booking_details 工具的示例:

ToolProvider toolProvider = (toolProviderRequest) -> {
    if (toolProviderRequest.userMessage().singleText().contains("booking")) {
        ToolSpecification toolSpecification = ToolSpecification.builder()
            .name("get_booking_details")
            .description("返回预订详情")
            .parameters(JsonObjectSchema.builder()
                .addStringProperty("bookingNumber")
                .build())
            .build();
        return ToolProviderResult.builder()
            .add(toolSpecification, toolExecutor)
            .build();
    } else {
        return null;
    }
};

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .toolProvider(toolProvider)
    .build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 相关教程

  • 工具指南 (opens new window) 由 Tales from the jar side (Ken Kousen) (opens new window)
编辑 (opens new window)
上次更新: 2025/04/01, 01:48:12

← Agent RAG→

最近更新
01
操作系统
03-18
02
Nginx
03-17
03
后端服务端主动推送消息的常见方式
03-11
更多文章>
Theme by Vdoing | Copyright © 2023-2025 xiaoyang | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式