第7章:在LangChain中如何调用函数Tools (Function Calling)
本文主要阐述了如何通过LangChain框架基于大模型调用业务系统的接口,从而实现搜索增强,真正解决AI与业务系统集成的问题
主要介绍了LangChain4j中工具(Tools)的概念和使用方法。工具允许语言模型(LLM)在生成文本的同时,触发外部操作(如调用API、执行代码等)。
工具(函数调用)
一些语言模型(LLM)不仅可以生成文本,还可以触发操作。
注意: 支持工具的所有LLM可以在这里找到(查看“Tools”列)。
“工具”或“函数调用”的概念允许LLM在必要时调用一个或多个可用工具,这些工具通常由开发者定义。工具可以是任何东西:一个网络搜索、对外部API的调用,或者执行一段特定代码等。LLM本身无法直接调用工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本形式响应)。作为开发者,我们需要执行这个工具,并将工具执行的结果反馈给LLM。
例如,我们知道LLM本身不擅长数学计算。如果你的用例涉及偶尔的数学计算,你可能希望为LLM提供一个“数学工具”。通过在请求中声明一个或多个工具,LLM可以在认为合适的情况下决定调用其中一个工具。给定一个数学问题和一组数学工具,LLM可能会决定为了正确回答问题,它应该首先调用其中一个提供的数学工具。
让我们看看实际中的工作方式(有工具和没有工具的情况):
没有工具的消息交换示例:
请求:
- 消息:
- UserMessage:
- 文本:475695037565的平方根是多少?
响应:
- AiMessage:
- 文本:475695037565的平方根大约是689710。
接近,但不正确。
有工具的消息交换示例:
@Tool("计算两个给定数字的和")
double sum(double a, double b) {
return a + b;
}
@Tool("返回给定数字的平方根")
double squareRoot(double x) {
return Math.sqrt(x);
}
请求1:
- 消息:
- UserMessage:
- 文本:475695037565的平方根是多少?
- 工具:
- sum(double a, double b): 计算两个给定数字的和
- squareRoot(double x): 返回给定数字的平方根
响应1:
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
... 在这里,我们执行带有“475695037565”参数的squareRoot方法,并得到“689706.486532”作为结果 ...
请求2:
- 消息:
- UserMessage:
- 文本:475695037565的平方根是多少?
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
- ToolExecutionResultMessage:
- 文本:689706.486532
响应2:
- AiMessage:
- 文本:475695037565的平方根是689706.486532。
正如你所看到的,当LLM可以访问工具时,它可以在适当的时候决定调用其中一个工具。
这是一个非常强大的功能。在这个简单的例子中,我们为LLM提供了基本的数学工具,但想象一下,如果我们给它提供了googleSearch和sendEmail工具,以及一个查询“我的朋友想了解人工智能领域的最新消息。请将简要总结发送到xxx@email.com”,那么它可以使用googleSearch工具查找最新消息,然后总结并使用sendEmail工具发送总结。
注意: 为了增加LLM调用正确工具及其正确参数的机会,我们应该提供清晰且无歧义的:
- 工具的名称
- 工具的功能描述以及何时使用
- 每个工具参数的描述
一个好的经验法则是:如果一个人能够理解工具的用途以及如何使用它,那么LLM很可能也能做到。
LLM被特别微调以检测何时调用工具以及如何调用它们。某些模型甚至可以同时调用多个工具,例如OpenAI。
注意: 并非所有模型都支持工具。要查看哪些模型支持工具,请参考此页面的“Tools”列。
注意: 工具/函数调用与JSON模式不是同一件事。
两个抽象层次
LangChain4j提供了两个抽象层次来使用工具:
- 低级抽象:使用ChatLanguageModel和ToolSpecification API。
- 高级抽象:使用AI服务和@Tool注解的Java方法。
在低级抽象中,你可以使用ChatLanguageModel的generate(List, List)方法。StreamingChatLanguageModel中也有类似的方法。
- ToolSpecification是一个包含工具所有信息的对象,包括:
- 工具的名称(name)
- 工具的描述(description)
- 工具的参数及其描述
建议尽可能多地提供工具信息:清晰的名称、全面的描述,以及每个参数的描述等。
创建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<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
一旦你有了List,就可以调用模型:
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应该包含:
- 工具调用的id(某些LLM不提供)
- 要调用的工具的名称,例如:getWeather
- 参数,例如:{ “city”: “London”, “temperatureUnit”: “CELSIUS” }
你需要使用ToolExecutionRequest中的信息手动执行工具。
如果你想将工具执行的结果反馈给LLM,你需要为每个ToolExecutionRequest创建一个ToolExecutionResultMessage,并将其与所有之前的消
如果你想将工具执行的结果反馈给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);
高级抽象:使用@Tool注解的Java方法
在高级抽象中,你可以使用@Tool注解任何Java方法,并在创建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();
}
工具方法的限制
被@Tool注解的方法:
- 可以是静态的(static)或非静态的。
- 可以具有任何可见性(public、private等)。
工具方法的参数
被@Tool注解的方法可以接受任意数量的参数,类型可以是:
- 基本类型:int、double等。
- 对象类型:String、Integer、Double等。
- 自定义POJO(可以包含嵌套的POJO)。
- enums。
- List或Set,其中T是上述提到的类型之一。
- Map<K, V>(需要手动在参数描述中指定K和V的类型)。
无参数的方法也是支持的。
默认情况下,所有方法参数都被视为必填的。这意味着LLM需要为这些参数生成一个值。可以通过@P(required = false)注解将参数设置为可选的。目前还不支持将POJO参数的字段声明为可选的。
递归参数(例如,一个Person类包含一个Set children字段)目前仅支持OpenAI。
工具方法的返回类型
被@Tool注解的方法可以返回任何类型,包括void。如果方法的返回类型是void,则在方法成功返回时,会向LLM发送一个“Success”字符串。
如果方法的返回类型是String,则返回的值将直接发送给LLM,不做任何转换。
对于其他返回类型,返回的值将在发送给LLM之前转换为JSON字符串。
异常处理
如果被@Tool注解的方法抛出了一个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<String> select;
@Description("过滤条件")
private List<Condition> where;
}
@Tool
Result executeQuery(Query query) {
...
}
@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();
在流式模式下,可以通过指定onToolExecuted回调来实现:
interface Assistant {
TokenStream chat(String message);
}
TokenStream tokenStream = assistant.chat("取消我的预订");
tokenStream
.onNext(...)
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onComplete(...)
.onError(...)
.start();
程序化指定工具(Specifying Tools Programmatically)
当使用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<String, Object> arguments = fromJson(toolExecutionRequest.arguments());
String bookingNumber = arguments.get("bookingNumber").toString();
Booking booking = getBooking(bookingNumber);
return booking.toString();
};
动态指定工具
在使用AI服务时,工具也可以动态指定。每次调用AI服务时,都会调用ToolProvider,它将提供应包含在当前请求中的工具。ToolProvider接受一个ToolProviderRequest,其中包含UserMessage和聊天记忆ID,并返回一个ToolProviderResult,其中包含工具的Map,从ToolSpecification到ToolExecutor。
以下是一个示例,仅当用户消息中包含“预订”一词时,才添加get_booking_details工具:
ToolProvider toolProvider = (toolProviderRequest) -> {
if (toolProviderRequest.userMessage().singleText().contains("预订")) {
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();
总结
这篇文章详细介绍了LangChain4j中工具(Tools)的概念和使用方法。工具允许LLM在生成文本的同时,触发外部操作(如调用API、执行代码等)。主要内容包括:
工具的基本概念:工具是LLM可以调用的外部功能,用于增强其能力(如数学计算、网络搜索等)。
工具的声明和使用:通过ToolSpecification和@Tool注解,开发者可以定义工具并将其集成到LLM的交互中。
工具的执行和反馈:LLM可以在响应中表达调用工具的意图,开发者需要执行工具并将结果反馈给LLM。
高级抽象:使用AI服务和@Tool注解的方法,可以更方便地定义和使用工具。
动态工具:通过ToolProvider,可以根据用户消息动态添加工具。
这种工具机制为LLM提供了强大的扩展能力,使其能够处理更复杂的任务。