参考文档和资源
LangChain4j官方文档
Tools (Function Calling) | LangChain4j
YouTuBe介绍视频
https://www.youtube.com/watch?v=cjI_6Siry-s&t=1s
工具的概念
工具可以是任何东西,比如网络搜索、调用外部 API 或执行特定代码片段。
使用工具的原因
因为LLM对于一些复杂的任务和数学计算能力薄弱。所以要自己创建一些工具来弥补LLM对于这些薄弱点的处理能力。
LLM如何使用工具
- LLM在接收到请求后,根据需求自己判断是否使用Tools。
- 如果决定使用,就会生成工具调用请求(但是LLM没有权利调用Tools,只是告诉开发者"我决定调用工具,不去执行AI搜索了"),开发者负责接收此请求,在本地或指定服务中执行对应的工具方法,开发者手动执行工具并生成执行结果返回LLM。
- LLM仅仅根据Tools返回的结果生成最终结果(默认情况下不执行本地或者远端搜索),将最终结果返回。(可以理解为AI润色一下返回结果)。
- 我的理解:你相当于一个项目小组长,你给Model分配了一个任务。结果Model分析完任务后跟你说,我不适合做这个任务,Tools擅长干这个任务,你找他干这活,我不干。
官网的使用举例:如果我们给它提供了例如 googleSearch
和 sendEmail
工具, 以及一个查询,如"我的朋友想知道 AI 领域的最新消息。将简短摘要发送到 friend@email.com", 那么它可以使用 googleSearch
工具查找最新消息, 然后总结并通过 sendEmail
工具发送摘要。
声明工具的注意事项
- 显式提供工具名称。
- 提供好清晰尽量简短的工具作用描述。
- 明确工具参数。
- 并不是所有模型都支持工具调用,详见官网文档https://docs.langchain4j.dev/integrations/language-models/
下面我们结合代码理解工具的实现
低级实现
使用ChatLanguageModel的chat(ChatRequest)方法和ToolSpecification的API。
- 注意:默认情况下,低级实现在选择调用工具后不会返回LLM,如果要返回需要手动实现,只有高级实现才会自动返回LLM。
创建工具
- 创建工具类,使用
@Tool
声明工具方法。
public class DashScopeTools {
@Tool("根据城市名称查询天气")
public static String getWeather(@P("城市名称") String city) {
//具体实现可以调用查询天气的API
Map<String, String> weatherData = Map.of(
"北京", "晴,25℃",
"上海", "多云,28℃",
"广州", "阵雨,30℃"
);
return weatherData.getOrDefault(city, "未知城市");
}
@Tool("计算两个数的乘积")
public static double multiply(
@P("第一个乘数") double a,
@P("第二个乘数") double b
) {
return a * b;
}
}
以第一个工具为例:
- 工具参数:@P(“城市名称”) String city 。
- 工具名称:getWeather。
- 工具描述:@Tool(“根据城市名称查询天气”)。
创建聊天模型
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey("your-openai-key")
.modelName("gpt-3.5-turbo")
.temperature(0.3)
.build();
创建工具规范
- 会自动识别出
@Tool
的工具方法并加载。
List<ToolSpecification> toolSpecs = ToolSpecifications.toolSpecificationsFrom(CustomTools.class);
其他识别@Tool
的方法。
ToolSpecifications.toolSpecificationsFrom(Class)
ToolSpecifications.toolSpecificationsFrom(Object)
ToolSpecifications.toolSpecificationFrom(Method)
调用模型
// 用户提问
UserMessage userMessage = UserMessage.from("计算3.5和7.2的和,并告诉我当前时间。");
// 调用模型,传入工具规范
//实现一:将工具绑定到模型
Response<AiMessage> response = model.generate(
singletonList(userMessage),
toolSpecs
);
//实现二:将工具绑定到聊天请求(官网示例)
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 支持并行调用多个工具)。
模型就会根据内容自行判断是否调用工具。
- 如果 LLM 决定调用工具,返回的
AiMessage
将在toolExecutionRequests
字段中包含数据。 在这种情况下,AiMessage.hasToolExecutionRequests()
将返回true
。 toolExecutionRequest
中包含工具调用的ID(某些LLM不支持),工具名称,调用工具的参数。
如果需要再次调用大模型
- 每个
ToolExecutionResult
对应一个ToolExecutionResultMessage
。
String result = "Tools的生成结果";
//将传入数据转化为ToolExecutionResultMessage
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
//构建大模型请求
ChatRequest request2 = ChatRequest.builder()
.messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
.toolSpecifications(toolSpecifications)
.build();
//传入大模型
ChatResponse response2 = model.chat(request2);
userMessage
:提供原始问题,方便模型理解上下文。AiMessage
:包含工具调用请求,告知模型当前处于工具调用中(上一段代码的AiMessage)。ToolExecutionResultMessage
:向模型传递工具执行结果,作为最终回答的依据。
高级实现
工具方法限制
- 只要带有
@Tool
,无论静态非静态,公共私有。
工具方法参数
-
默认情况下,工具方法中的参数都是必须的(不能为空)。
-
基本类型:
int
、double
等。 -
对象类型:
String
、Integer
、Double
等。 -
自定义 POJO(可以包含嵌套 POJO)。
-
enum
(枚举)。 -
List<T>
/Set<T>
,其中T
是上述类型之一。 -
Map<K,V>
(您需要在参数描述中使用@P
手动指定K
和V
的类型)。
指定参数可选方法
@Tool
//表明unit参数不是必须的
void getTemperature(String location, @P(required = false) Unit unit) {
...
}
//复杂参数的字段和子字段默认也是必须的,可以由此设置为非必须
record User(String name, @JsonProperty(required = false) String email) {}
@Tool
void add(User user) {
...
}
- 当与结构化输出一起使用时, 所有字段和子字段默认都被视为可选的。
工具方法返回类型
- 可以返回任何类型包括void。
- String的返回类型会原样发给LLM不做任何转换。
- 其他返回类型会转化为Json字符串再发给LLM。
工具方法异常处理
- 如果带有
@Tool
注解的方法抛出Exception
, 异常的消息(e.getMessage()
)将作为工具执行的结果发送给 LLM。 这允许 LLM 纠正其错误并在认为必要时重试。
工具注解
@Tool
name
:工具名称。如果未提供,方法名将作为工具名称。value
:工具描述。
//请求问题数据
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);
}
}
//构建AI服务
MathGenius mathGenius = AiServices.builder(MathGenius.class)
.chatLanguageModel(model)
.tools(new Calculator())
.build();
String answer = mathGenius.ask("475695037565 的平方根是多少?");
System.out.println(answer); // 475695037565 的平方根是 689706.486532。
@P
value
:参数描述。必填字段。required
:参数是否必需,默认为true
。可选字段。
@Description
- 类和字段的描述可以使用
@Description
注解指定。
@Description("要执行的查询")
class Query {
@Description("要选择的字段")
private List<String> select;
}
- 注意:放在
enum
值上的@Description
没有效果,并且不会包含在生成的 JSON schema。
@ToolMemoryId
- 如果AI 服务方法有一个带有
@MemoryId
注解的参数, 可以使用@ToolMemoryId
注解@Tool
方法的参数。 提供给 AI 服务方法的值将自动传递给@Tool
方法。 如果有多个用户或每个用户有多个聊天/记忆, 可以在@Tool
方法内区分它们。
访问已执行的工具
interface Assistant {
Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("取消我的预订 123-456");
String answer = result.content();
//获取到执行过的工具
List<ToolExecution> toolExecutions = result.toolExecutions();
//流式模式下,通过onToolExecuted实现
tokenStream
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onPartialResponse(...)
.onCompleteResponse(...)
.onError(...)
.start();
以编程方式指定工具
- 更加灵活。
- 可以从外部源(数据库或者配置文件)加载。
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("get_booking_details") // 工具名称(LLM通过此名称调用工具)
.description("返回预订详情") // 工具描述(LLM根据此描述决定是否调用)
.parameters(JsonObjectSchema.builder() // 定义工具参数结构(JSON Schema格式)
.properties(Map.of(
"bookingNumber", JsonStringSchema.builder()
.description("B-12345 格式的预订号") // 参数描述
.build()
))
.build())
.build();
工具幻觉策略
-
可能会发生 LLM 在工具调用上产生幻觉的情况,或者换句话说,它要求使用一个不存在的名称的工具。在这种情况下,默认情况下 LangChain4j 将抛出一个异常报告问题,但可以通过为 AI 服务提供在这种情况下使用的策略来配置不同的行为。
-
这个策略是
Function<ToolExecutionRequest, ToolExecutionResultMessage>
的实现,定义了对于包含请求调用不可用工具的ToolExecutionRequest
应该产生什么ToolExecutionResultMessage
作为结果。例如,可以使用一个策略配置 AI 服务,该策略向 LLM 返回一个响应,希望推动它尝试调用不同的工具,知道之前请求的工具不存在。
AssistantHallucinatedTool assistant = AiServices.builder(AssistantHallucinatedTool.class)
.chatLanguageModel(chatLanguageModel)
.tools(new HelloWorld())
.hallucinatedToolNameStrategy(toolExecutionRequest -> ToolExecutionResultMessage.from(
toolExecutionRequest, "错误:没有名为 " + toolExecutionRequest.name() + " 的工具"))
.build();