一些大型语言模型(LLMs)除了生成文本外,还可以触发操作。
注意:所有支持工具的LLMs都可以在这里找到(查看“工具”列)。
有一个概念被称为“工具”或“函数调用”。它允许LLM在必要时调用一个或多个可用的工具,这些工具通常由开发人员定义。工具可以是任何东西:网络搜索、调用外部API,或者执行一段特定的代码等。
LLMs本身并不能实际调用工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本形式响应)。作为开发人员,我们应该执行这个工具,并将工具执行的结果报告回来。
例如,我们知道LLMs本身在数学方面并不擅长。如果您的用例涉及偶尔的数学计算,您可能希望为LLM提供一个“数学工具”。通过在请求中声明一个或多个工具,LLM可以决定是否调用其中一个工具。
让我们看看在实际中(有工具和没有工具)的情况:
没有工具的消息交换示例:
Request:
- messages:
- UserMessage:
- text:475695037565的平方根是多少?
Response:
- AiMessage:
- text:475695037565的平方根大约是689710。
接近,但不正确。
有以下工具的消息交换示例:
@Tool("对两个给定的数字求和")
double sum(double a, double b) {
return a + b;
}
@Tool("返回给定数字的平方根")
double squareRoot(double x) {
return Math.sqrt(x);
}
Request 1:
- messages:
- UserMessage:
- text:475695037565的平方根是多少?
- tools:
- sum(double a, double b):对两个给定的数字求和
- squareRoot(double x):返回给定数字的平方根
Response 1:
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
... 这里我们使用“475695037565”参数执行squareRoot方法,并得到“689706.486532”作为结果 ...
Request 2:
- messages:
- UserMessage:
- text:475695037565的平方根是多少?
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
- ToolExecutionResultMessage:
- text:689706.486532
Response 2:
- AiMessage:
- text:475695037565的平方根是689706.486532。
如您所见,当LLM可以访问工具时,它可以在适当的时候决定调用其中一个工具。
这是一个非常强大的功能。在这个简单的例子中,我们给LLM提供了原始的数学工具,但想象一下,如果我们给它例如googleSearch
和sendEmail
工具,并且有一个查询如“我的朋友想了解AI领域的最新新闻。请发送简短摘要到[email protected]”,那么它可以用googleSearch
工具查找最新新闻,然后总结并使用sendEmail
工具通过电子邮件发送摘要。
注意:为了增加LLM调用正确工具和正确参数的可能性,我们应该提供清晰且明确的:
一个好的经验法则是:如果一个人可以理解工具的用途和如何使用它,那么LLM很可能也能做到。
LLMs被特别微调以检测何时调用工具以及如何调用它们。一些模型甚至可以同时调用多个工具,例如OpenAI。
注意:并非所有模型都支持工具。要查看哪些模型支持工具,请参阅此页面上的“工具”列。
注意:工具/函数调用与JSON模式不同。
LangChain4j为使用工具提供了两个抽象级别:
ChatLanguageModel
和ToolSpecification
API@Tool
注解的Java方法在低级中,您可以使用ChatLanguageModel
的chat(ChatRequest)
方法。StreamingChatLanguageModel
中也有类似的方法。
创建ChatRequest
时可以指定一个或多个ToolSpecification
。
ToolSpecification
是一个包含有关工具所有信息的对象:
名称
描述
参数
及其描述建议尽可能多地提供有关工具的信息:清晰的名称、全面的描述,以及每个参数的描述等。
创建ToolSpecification
有两种方式:
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.description("返回给定城市的天气预报")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "应返回天气预报的城市")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city") // 必需属性应明确指定
.build())
.build();
您可以在这里找到有关JsonObjectSchema
的更多信息。
ToolSpecifications.toolSpecificationsFrom(Class)
ToolSpecifications.toolSpecificationsFrom(Object)
ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools {
@Tool("返回给定城市的天气预报")
String getWeather(
@P("应返回天气预报的城市") String city,
TemperatureUnit temperatureUnit
) {
...
}
}
List toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
一旦您有了一个List
,就可以调用模型:
ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("明天伦敦的天气会怎样?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();
如果LLM决定调用工具,返回的AiMessage
将包含toolExecutionRequests
字段中的数据。在这种情况下,AiMessage.hasToolExecutionRequests()
将返回true
。根据LLM的不同,它可以包含一个或多个ToolExecutionRequest
对象(一些LLM支持并行调用多个工具)。
每个ToolExecutionRequest
应包含:
id
(一些LLM不提供此id)名称
,例如:getWeather
参数
,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }
您需要使用ToolExecutionRequest
(s)中的信息手动执行工具(s)。
如果您想将工具执行的结果发送回LLM,您需要为每个ToolExecutionRequest
创建一个ToolExecutionResultMessage
,并将其与所有先前的消息一起发送:
String result = "预计明天伦敦将下雨。";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
.messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response2 = model.chat(request2);
在高级抽象级别,您可以在创建AI服务时注解任何Java方法为@Tool
。AI服务将自动将这些方法转换为ToolSpecification
,并将其包含在每次与LLM交互的请求中。当LLM决定调用工具时,AI服务将自动执行相应的方法,并将方法的返回值(如果有)发送回LLM。您可以在DefaultToolExecutor
中找到实现细节。
一些工具示例:
@Tool("根据查询在Google中搜索相关URL")
public List searchGoogle(@P("搜索查询") String query) {
return googleSearchService.search(query);
}
@Tool("根据URL返回网页内容")
public String getWebPageContent(@P("页面URL") String url) {
Document jsoupDocument = Jsoup.connect(url).get();
return jsoupDocument.body().text();
}
带有@Tool
注解的方法:
带有@Tool
注解的方法可以接受任何数量的各种类型的参数:
int
、double
等String
、Integer
、Double
等enum
枚举List
/ Set
,其中T是上述类型之一Map
(您需要手动指定K和V的类型,在参数描述中使用@P
)也支持无参数的方法。
默认情况下,所有方法参数都被视为必需的/必填的。这意味着LLM必须为这样的参数生成一个值。可以通过用@P(required = false)
注解参数来使其成为可选参数。目前不支持将POJO参数的字段声明为可选。
递归参数(例如,一个Person
类有一个Set
字段)目前仅被OpenAI支持。
带有@Tool
注解的方法可以返回任何类型,包括void
。如果方法具有void
返回类型,则如果方法成功返回,则向LLM发送“成功”字符串。
如果方法具有String
返回类型,则返回的值将按原样发送到LLM,不进行任何转换。
对于其他返回类型,返回的值将在发送到LLM之前转换为JSON字符串。
如果带有@Tool
注解的方法抛出Exception
,则Exception
的消息(e.getMessage()
)将作为工具执行的结果发送到LLM。这允许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)
.chatLanguageModel(model)
.tools(new Calculator())
.build();
String answer = mathGenius.ask("475695037565的平方根是多少?");
System.out.println(answer); // 475695037565的平方根是689706.486532。
当调用ask
方法时,将与LLM进行两次交互,如前面部分所述。在这两次交互之间,将自动调用squareRoot
方法。
@Tool
注解有两个可选字段:
name
:工具的名称。如果未提供此字段,则方法名称将作为工具的名称。value
:工具的描述。根据工具的不同,即使没有描述,LLM也可能很好地理解它(例如,add(a, b)
是显而易见的),但通常最好提供清晰且有意义的名称和描述。这样,LLM可以获得更多关于是否调用给定工具以及如何调用的信息。
@P
方法参数可以可选地用@P
注解。
@P
注解有两个字段:
value
:参数的描述。必填字段。required
:参数是否必需,默认为true
。可选字段。@Description
可以使用@Description
注解指定类和字段的描述:
@Description("要执行的查询")
class Query {
@Description("要选择的字段")
private List select;
@Description("过滤条件")
private List where;
}
@Tool
Result executeQuery(Query query) {
...
}
@ToolMemoryId
如果您的AI服务方法有一个带有@MemoryId
注解的参数,您也可以在带有@Tool
注解的方法的参数上注解@ToolMemoryId
。提供给AI服务方法的值将自动传递给@Tool
方法。此功能在您有多个用户和/或每个用户有多个聊天/记忆时非常有用,您希望在@Tool
方法中区分它们。
如果您希望访问在调用AI服务期间执行的工具,可以轻松地通过将返回类型包装在Result
类中来实现:
interface Assistant {
Result chat(String userMessage);
}
Result result = assistant.chat("取消我的预订123-456");
String answer = result.content();
List toolExecutions = result.toolExecutions();
在流模式下,您可以通过指定onToolExecuted
回调来实现:
interface Assistant {
TokenStream chat(String message);
}
TokenStream tokenStream = assistant.chat("取消我的预订");
tokenStream
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onPartialResponse(...)
.onCompleteResponse(...)
.onError(...)
.start();
在使用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();
对于每个ToolSpecification
,需要提供一个ToolExecutor
实现,该实现将处理由LLM生成的工具执行请求:
ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
Map arguments = fromJson(toolExecutionRequest.arguments());
String bookingNumber = arguments.get("bookingNumber").toString();
Booking booking = getBooking(bookingNumber);
return booking.toString();
};
一旦我们有一个或多个(ToolSpecification
,ToolExecutor
)对,我们可以在创建AI服务时指定它们:
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.tools(Map.of(toolSpecification, toolExecutor))
.build();
在使用AI服务时,工具也可以为每次调用动态指定。可以配置一个ToolProvider
,该提供者将在每次调用AI服务时被调用,并将提供应包含在当前请求中的工具。ToolProvider
接受一个包含UserMessage
和聊天记忆ID的ToolProviderRequest
,并返回一个包含工具的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();
您还可以从MCP服务器导入工具。有关此的更多信息可以在这里找到。