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。
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。
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
有两种方式:
- 手动创建
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.description("返回指定城市的天气预报")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "需要返回天气预报的城市")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city") // 必须显式指定必需属性
.build())
.build();
2
3
4
5
6
7
8
9
有关JsonObjectSchema
的更多信息,请参见此处。
- 使用辅助方法:
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);
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();
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);
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();
}
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。
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) {
...
}
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();
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();
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();
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();
};
2
3
4
5
6
一旦我们有了一个或多个(ToolSpecification
,ToolExecutor
)对,我们可以在创建 AI 服务时指定它们:
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.tools(Map.of(toolSpecification, toolExecutor))
.build();
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();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21