跳到主要内容

工具(函数调用)

除了生成文本外,部分 LLM 还能触发操作。

备注

所有支持工具的 LLM 可在此处找到(参见 "Tools" 列)。

备注

并非所有 LLM 都同样好地支持工具。 理解、选择和正确使用工具的能力在很大程度上取决于具体模型及其能力。 有些模型可能完全不支持工具,而另一些可能需要仔细的提示工程 或额外的系统指令。

有一个称为“工具”或“函数调用”的概念。 它允许 LLM 在必要时调用一个或多个可用工具(通常由开发者定义)。 工具可以是任何东西:网络搜索、对外部 API 的调用,或执行特定代码片段等。 LLM 实际上无法自行调用工具;相反,它们在响应中表达 调用特定工具的意图(而不是以纯文本回复)。 作为开发者,我们随后应以提供的参数执行该工具,并将 工具执行结果回报回去。

例如,我们知道 LLM 本身并不擅长数学。 若你的用例偶尔涉及数学计算,可能希望为 LLM 提供“数学工具”。 通过在发给 LLM 的请求中声明一个或多个工具, 它在认为合适时可以决定调用其中一个。 给定一道数学题以及一组数学工具,LLM 可能决定为了正确回答问题, 应先调用其中一个提供的数学工具。

让我们看看实际如何工作(有工具与无工具):

无工具的消息交换示例:

Request:
- messages:
- UserMessage:
- text: What is the square root of 475695037565?

Response:
- AiMessage:
- text: The square root of 475695037565 is approximately 689710.

接近,但不正确。

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

@Tool("Sums 2 given numbers")
double sum(double a, double b) {
return a + b;
}

@Tool("Returns a square root of a given number")
double squareRoot(double x) {
return Math.sqrt(x);
}
Request 1:
- messages:
- UserMessage:
- text: What is the square root of 475695037565?
- tools:
- sum(double a, double b): Sums 2 given numbers
- squareRoot(double x): Returns a square root of a given number

Response 1:
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)


... here we are executing the squareRoot method with the "475695037565" argument and getting "689706.486532" as a result ...


Request 2:
- messages:
- UserMessage:
- text: What is the square root of 475695037565?
- AiMessage:
- toolExecutionRequests:
- squareRoot(475695037565)
- ToolExecutionResultMessage:
- text: 689706.486532

Response 2:
- AiMessage:
- text: The square root of 475695037565 is 689706.486532.

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

这是一个非常强大的功能。 在这个简单示例中,我们给了 LLM 基础数学工具, 但想象一下,如果我们给它例如 googleSearchsendEmail 工具, 以及类似“我的朋友想了解 AI 领域的最新新闻。请将简短摘要发送到 friend@email.com”的查询, 那么它可以用 googleSearch 工具查找最近新闻, 然后用 sendEmail 工具总结并发送摘要。

备注

为提高 LLM 以正确参数调用正确工具的几率, 我们应提供清晰且无歧义的:

  • 工具名称
  • 工具做什么以及何时应使用的描述
  • 每个工具参数的描述

一条经验法则:如果人类能理解工具的用途及如何使用, 那么 LLM 也很有可能理解。

LLM 经过专门微调,以检测何时调用工具以及如何调用。 有些模型甚至可以一次调用多个工具,例如 OpenAI

备注

请注意并非所有模型都支持工具。 要查看哪些模型支持工具,请参阅页面上的 "Tools" 列。

备注

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

两层抽象

LangChain4j 为使用工具提供了两层抽象:

  • 底层:使用 ChatModelToolSpecification API
  • 高层:使用 AI Services 和带 @Tool 注解的 Java 方法

底层工具 API

在底层,你可以使用 ChatModelchat(ChatRequest) 方法。 StreamingChatModel 中也有类似方法。

创建 ChatRequest 时可指定一个或多个 ToolSpecification

ToolSpecification 是包含工具全部信息的对象:

  • 工具的 name
  • 工具的 description
  • 工具的 parameters 及其描述
  • 工具的 metadata。 默认情况下不会发送给 LLM 提供商,创建 ChatModel 时必须显式指定应发送哪些元数据键。 目前工具元数据仅由 langchain4j-anthropic 模块支持。 当工具由 McpToolProvider 提供时, metadata 可包含 MCP 特定条目。

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

创建工具规范

有两种方式创建 ToolSpecification

  1. 手动
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.description("Returns the weather forecast for a given city")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city", "The city for which the weather forecast should be returned")
.addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
.required("city") // the required properties should be specified explicitly
.build())
.build();

有关 JsonObjectSchema 的更多信息见此处

  1. 使用辅助方法:
  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools { 

@Tool("Returns the weather forecast for a given city")
String getWeather(
@P("The city for which the weather forecast should be returned") String city,
TemperatureUnit temperatureUnit
) {
...
}
}

List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);

JSON 序列化

ToolSpecification 可使用 toJson()fromJson() 方法序列化为 JSON 并反序列化回来。 例如,当你想将工具规范存储在数据库中或通过网络传输时,这会很有用。

String json = toolSpecification.toJson();

ToolSpecification deserialized = ToolSpecification.fromJson(json);

默认使用专用的 Jackson ObjectMapper 进行 JSON 转换。 你可以通过实现 ToolSpecificationJsonCodecFactory 并在 META-INF/services/dev.langchain4j.spi.agent.tool.ToolSpecificationJsonCodecFactory 中注册, 经由 SPI 提供自己的 ToolSpecificationJsonCodec 实现。

使用 ChatModel

一旦有了 List<ToolSpecification>,就可以调用模型:

ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();

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

每个 ToolExecutionRequest 应包含:

  • 工具调用的 id。请注意某些 LLM 提供商(例如 Google、Ollama)可能省略此 ID。
  • 要调用的工具的 name,例如:getWeather
  • arguments,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }

你需要使用 ToolExecutionRequest(s) 中的信息手动执行工具。

若要将工具执行结果发送回 LLM, 需要创建 ToolExecutionResultMessage(每个 ToolExecutionRequest 一个) 并将其与所有先前消息一起发送:


String result = "It is expected to rain in London tomorrow.";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
.messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
.toolSpecifications(toolSpecifications)
.build();
ChatResponse response2 = model.chat(request2);

多模态工具结果

ToolExecutionResultMessage 也可以携带图像等非文本内容。 除了使用 text(),还可以使用带 contents() 的构建器:

ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.builder()
.id(toolExecutionRequest.id())
.toolName(toolExecutionRequest.name())
.contents(
TextContent.from("Here is the photo"),
ImageContent.from(Image.builder()
.base64Data(base64Data)
.mimeType("image/png")
.build())
)
.build();
备注

并非所有 LLM 提供商都支持多模态工具结果。 有关提供商支持的详情,参见返回图像和多模态内容

使用 StreamingChatModel

一旦有了 List<ToolSpecification>,就可以调用模型:

ChatRequest request = ChatRequest.builder()
.messages(UserMessage.from("What will the weather be like in London tomorrow?"))
.toolSpecifications(toolSpecifications)
.build();

model.chat(request, new StreamingChatResponseHandler() {

@Override
public void onPartialResponse(String partialResponse) {
System.out.println("onPartialResponse: " + partialResponse);
}

@Override
public void onPartialToolCall(PartialToolCall partialToolCall) {
System.out.println("onPartialToolCall: " + partialToolCall);
}

@Override
public void onCompleteToolCall(CompleteToolCall completeToolCall) {
System.out.println("onCompleteToolCall: " + completeToolCall);
}

@Override
public void onCompleteResponse(ChatResponse completeResponse) {
System.out.println("onCompleteResponse: " + completeResponse);
}

@Override
public void onError(Throwable error) {
error.printStackTrace();
}
});

如果 LLM 决定调用工具,onPartialToolCall(PartialToolCall) 回调 通常会在最终调用 onCompleteToolCall(CompleteToolCall) 之前被多次调用, 后者表示该工具调用的流式输出已完成。

备注

请注意并非所有 LLM 提供商都会流式传输部分工具调用。 某些提供商(例如 Bedrock、Google、Mistral、Ollama)仅返回完整的工具调用。 在这些情况下,不会调用 onPartialToolCall 回调——只会调用 onCompleteToolCall

单个工具调用的流式输出可能如下所示:

onPartialToolCall(index = 0, id = "call_abc", name = "get_weather", partialArguments = "{\"")
onPartialToolCall(index = 0, id = "call_abc", name = "get_weather", partialArguments = "city")
onPartialToolCall(index = 0, id = "call_abc", name = "get_weather", partialArguments = ""\":\"")
onPartialToolCall(index = 0, id = "call_abc", name = "get_weather", partialArguments = "London")
onPartialToolCall(index = 0, id = "call_abc", name = "get_weather", partialArguments = "\"}")
onCompleteToolCall(index = 0, id = "call_abc", name = "get_weather", arguments = "{\"city\":\"London\"}")

如果 LLM 发起多个工具调用,index 会递增,使你能够将不同的 PartialToolCall 彼此关联,并与最终的 CompleteToolCall 关联。

当完整响应流式输出结束并调用 onCompleteResponse(ChatResponse) 时, ChatResponse 内的 AiMessage 将包含流式过程中发生的所有工具调用。

高层工具 API

在高层抽象中,你可以用 @Tool 注解标注任何 Java 方法, 并在创建 AI Service 时指定它们。

AI Service 会自动将这些方法转换为 ToolSpecification, 并在每次与 LLM 交互的请求中包含它们。 当 LLM 决定调用工具时,AI Service 会自动执行相应方法, 方法的返回值(如有)会发送回 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 注解的方法:

  • 可以是静态或非静态的
  • 可以有任意可见性(public、private 等)。

工具方法参数

@Tool 注解的方法可以接受任意数量、各种类型的参数:

  • 原始类型:intdouble
  • 对象类型:StringIntegerDouble
  • 自定义 POJO(可包含嵌套 POJO)
  • enum
  • 多态类型(sealed 接口/类,或带 Jackson @JsonSubTypes / @JsonTypeInfo 注解的类型)——参见多态工具参数
  • List<T>/Set<T>,其中 T 是上述类型之一
  • Map<K,V>(需要在参数描述中用 @P 手动指定 KV 的类型)

也支持无参数的方法。

参数名称

默认情况下,若未指定 @Pname 属性,参数名通过反射获取。 然而,没有 -parameters javac 选项时,反射返回 arg0arg1 等通用名称。 参数的语义含义丢失,可能使 LLM 困惑。

在两种情况下设置 @Pname 很有用:

  1. 缺少 -parameters javac 选项 — 避免 LLM 看到通用的 arg0/arg1 名称。 注意 Quarkus 和 Spring 等框架默认启用 -parameters, 因此实际方法参数名会被保留,使用这些框架时通常无需设置 name
  2. 为 LLM 自定义名称 — 当你希望 LLM 看到与源代码中不同的参数名时 (例如,以匹配特定 API 约定或提供更具描述性的名称)。

示例:

@Tool
void getTemperature(
@P("Temperature value") double value,
@P("Unit of temperature") Optional<String> unit) {
...
}

必需与可选

默认情况下,所有工具方法参数都被视为必需。 这意味着参数列在发送给 LLM 的 JSON schema 的 required 数组中, 指示其生成一个值。 可通过用 @P(required = false) 注解参数使其变为可选:

@Tool
String getTemperature(String location, @P(required = false) Unit unit) {
...
}

或者,可以将参数声明为 Optional<T>

@Tool
String getTemperature(String location, Optional<Unit> unit) {
...
}

复杂参数的字段和子字段默认也被视为必需。 可通过用 @JsonProperty(required = false) 注解字段使其变为可选:

record User(String name, @JsonProperty(required = false) String email) {}

@Tool
void add(User user) {
...
}
备注

请注意与结构化输出一起使用时, 所有字段和子字段默认被视为可选

Required 是建议性的:LLM 仍可能省略参数

required 标志控制发送给 LLM 的 JSON schema(必需参数列在 schema 的 required 数组中)。期望 LLM 遵守这一点,但实际上它仍可能 无视 schema 并省略参数。

在 LangChain4j 1.x 中,这仅对原始类型参数(intlongboolean 等)检测—— 缺失的原始类型会触发 ToolArgumentsErrorHandler(参见下方的错误处理)。 缺失的对象参数不会被验证:即使 schema 将参数标记为必需,也会向 @Tool 注解的方法传递 null

我们计划在 LangChain4j 2.0 中消除这种不对称,使所有必需参数 得到统一验证。若此计划变更会影响你的用例,请 提交 issue,以便在落地前听取反馈。

若希望用真正的回退值代替 null(或代替原始类型参数的错误),请使用 @P(defaultValue = ...)

默认参数值

@P(defaultValue = "...") 声明当 LLM 省略参数时 LangChain4j 替换的值。 这是使参数可选并向工具方法提供合理回退值的最简单方式。

enum SortBy { RELEVANCE, DATE, RATING }

@Tool
List<Article> searchArticles(
String query,
@P(defaultValue = "10") int limit,
@P(defaultValue = "[\"en\"]") List<String> languages,
@P(defaultValue = "RELEVANCE") SortBy sortBy
) {
// When the LLM omits them:
// 'limit' -> 10
// 'languages' -> ["en"]
// 'sortBy' -> SortBy.RELEVANCE
}

设置 defaultValue 即意味着在 JSON schema 中为可选 — 无论 @P(required) 如何,该参数不会 列在 schema 的 required 数组中。LLM 被告知可以省略该参数;若省略,LangChain4j 在调用 你的方法前会填入默认值。

支持的类型:

类型格式示例
String原样使用defaultValue = "USD"
原始类型 / 装箱原始类型类型特定转换"10""3.14""true"
enum枚举常量名defaultValue = "EUR"
UUIDUUID.fromString"550e8400-e29b-41d4-a716-446655440000"
BigDecimalBigInteger数字字面量"1.5""100"
List<T> / Set<T> / 数组JSON 数组"[\"a\",\"b\"]", "[1,2,3]"
Map<K,V>JSON 对象"{\"a\":1,\"b\":2}"
POJO(包括嵌套)JSON 对象"{\"name\":\"Klaus\",\"age\":42}"

默认值字符串在 AI Service 注册时解析。若无法 转换为参数类型,AI Service 构建会立即以 IllegalConfigurationException 失败,并指出有问题的参数——拼写错误在 启动时捕获,而不是在首次 LLM 调用时。

默认值仅适用于缺失,不适用于错误值。 若 LLM 提供的参数 类型强制转换失败(例如对 int 提供 "banana"),强制转换错误会照常传播—— 默认值不会用作回退。

默认值在每次调用时重新解析, 因此变异默认 List/Map/POJO 的工具不会污染后续调用:

@Tool
void process(@P(defaultValue = "[\"a\",\"b\"]") List<String> tags) {
tags.add("processed"); // safe — next invocation still receives ["a","b"]
}

限制(注册时以 IllegalConfigurationException 拒绝):

  • defaultValue 不能与 Optional<T> 组合 — Optional 已编码 “缺失”;选择一种机制。
  • defaultValue 不能设置在 LangChain4j 注入的参数上(@ToolMemoryIdInvocationContext 等)——它们不来自 LLM。

多态工具参数

工具参数可以是多态类型 — 具体子类型由 LLM 在调用时决定的基类型。sealed 接口和 sealed 类无需注解即可工作; 普通抽象类和接口必须用 Jackson 的 @JsonSubTypes 声明其子类型。 发送给 LLM 的 schema 包含对允许子类型的 anyOf,每个都有 鉴别器属性(默认为 "type"),以便 LLM 能传达它产生的具体 类型;LangChain4j 在调用工具方法前将 LLM 的参数反序列化为正确的子类型。

这对作为参数的多态类型、多态类型的 List<T> / Set<T>、 以及嵌套在另一个 POJO 参数内的多态类型都有效。

sealed 接口和类 — 无需注解:

sealed interface Animal permits Dog, Cat {}

record Dog(String name, String breed) implements Animal {}

record Cat(String name, boolean indoor) implements Animal {}

class AnimalRegistry {

@Tool("Registers a single animal")
void registerAnimal(Animal animal) { /* dispatched to Dog or Cat */ }

@Tool("Registers a batch of animals")
void registerAnimals(List<Animal> animals) { /* mixed Dog / Cat */ }

@Tool("Registers an owner with their pet")
void registerOwner(Owner owner) { /* Owner.pet is dispatched */ }
}

record Owner(String name, Animal pet) {}

Jackson @JsonSubTypes / @JsonTypeInfo 也受支持,可让你将线上 名称与 Java 类名解耦:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
@JsonSubTypes({
@JsonSubTypes.Type(value = Square.class, name = "square"),
@JsonSubTypes.Type(value = Circle.class, name = "circle")
})
interface Shape {}

class ShapeRegistry {

@Tool("Registers a shape")
void registerShape(Shape shape) { /* dispatched to Square or Circle */ }
}

支持的 @JsonTypeInfo 选项集、鉴别器名称解析顺序、 defaultImpl 行为、visible 标志以及字段冲突检测,在结构化输出下的 多态类型中有详细描述—— 它们同样适用于工具参数。

递归参数

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

工具方法返回类型

@Tool 注解的方法可以返回任何类型,包括 void。 如果方法具有 void 返回类型,方法成功返回时会向 LLM 发送 "Success" 字符串。

如果方法具有 String 返回类型,返回值原样发送给 LLM,不做任何转换。

对于其他返回类型,返回值在发送给 LLM 之前会转换为 JSON 字符串。

返回图像和多模态内容

工具也可以返回图像和其他非文本内容。当工具返回以下类型之一时, 结果作为多模态内容(例如图像)发送给 LLM,而不是序列化为 JSON 文本:

  • Image — 作为单个图像发送
  • ImageContent — 作为单个图像内容发送
  • Content — 作为单个内容元素发送(例如 TextContentImageContent
  • List<Content> — 作为多个内容元素发送
  • Content[] — 作为多个内容元素发送

例如,拍摄照片并返回图像的工具:

@Tool("Takes a photo and returns it")
Image takePhoto() {
byte[] imageBytes = camera.capture();
return Image.builder()
.base64Data(Base64.getEncoder().encodeToString(imageBytes))
.mimeType("image/png")
.build();
}

或者返回文本和图像的工具:

@Tool("Takes a photo and returns it with a description")
List<Content> takePhoto() {
Image image = camera.capture();
return List.of(
TextContent.from("Photo taken at " + LocalDateTime.now()),
ImageContent.from(image)
);
}
备注

并非所有 LLM 提供商都支持多模态工具结果。 当前支持工具结果中图像的提供商包括 Anthropic、Amazon Bedrock 和 Google AI Gemini。 若工具返回非文本内容,其他提供商将抛出 UnsupportedFeatureException

AI 服务作为其他 AI 服务的工具

AI 服务也可以用作其他 AI 服务的工具。这在许多智能体用例中很有用,其中一个 AI 服务可以请求另一个更专业的 AI 服务帮助执行特定任务。例如,定义了以下 AI 服务后:

    interface RouterAgent {

@dev.langchain4j.service.UserMessage("""
Analyze the following user request and categorize it as 'legal', 'medical' or 'technical',
then forward the request as it is to the corresponding expert provided as a tool.
Finally return the answer that you received from the expert without any modification.

The user request is: '{{it}}'.
""")
String askToExpert(String request);
}

interface MedicalExpert {

@dev.langchain4j.service.UserMessage("""
You are a medical expert.
Analyze the following user request under a medical point of view and provide the best possible answer.
The user request is {{it}}.
""")
@Tool("A medical expert")
String medicalRequest(String request);
}

interface LegalExpert {

@dev.langchain4j.service.UserMessage("""
You are a legal expert.
Analyze the following user request under a legal point of view and provide the best possible answer.
The user request is {{it}}.
""")
@Tool("A legal expert")
String legalRequest(String request);
}

interface TechnicalExpert {

@dev.langchain4j.service.UserMessage("""
You are a technical expert.
Analyze the following user request under a technical point of view and provide the best possible answer.
The user request is {{it}}.
""")
@Tool("A technical expert")
String technicalRequest(String request);
}

RouterAgent 可以配置为将另外 3 个特定领域专家 AI 服务用作工具,将用户请求路由到其中一个。

MedicalExpert medicalExpert = AiServices.builder(MedicalExpert.class)
.chatModel(model)
.build();
LegalExpert legalExpert = AiServices.builder(LegalExpert.class)
.chatModel(model)
.build();
TechnicalExpert technicalExpert = AiServices.builder(TechnicalExpert.class)
.chatModel(model)
.build();

RouterAgent routerAgent = AiServices.builder(RouterAgent.class)
.chatModel(model)
.tools(medicalExpert, legalExpert, technicalExpert)
.build();

routerAgent.askToExpert("I broke my leg what should I do");
备注

将 AI 服务用作其他 AI 服务的工具是一个强大的功能,可构建复杂的智能体系统。不过,这种方法也有一些需要注意的相关缺点:

  • 此实现要求 LLM 将用户请求原样复制粘贴为工具调用,这可能是容易出错的操作。
  • 将另一个 LLM 作为工具调用的 LLM 必须重新处理其响应(与任何其他工具调用一样),这在时间和消耗的 token 方面可能是浪费的计算。
  • 作为工具的智能体是完全分离的 AI 服务,无法访问调用它的智能体的聊天记忆,因此无法使用聊天记忆提供更知情的回答。

@Tool

任何带 @Tool 注解且在构建 AI Service 时显式指定的 Java 方法都可由 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)
.chatModel(model)
.tools(new Calculator())
.build();

String answer = mathGenius.ask("What is the square root of 475695037565?");

System.out.println(answer); // The square root of 475695037565 is 689706.486532.

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

@Tool 注解有这些字段:

  • name:工具名称。若未提供,方法名将作为工具名称。
  • value:工具描述。
  • returnBehavior:详见此处
  • metadata:包含 LLM 提供商特定工具元数据条目的有效 JSON 字符串。 默认情况下不会发送给 LLM 提供商,创建 ChatModel 时必须显式指定应发送哪些元数据键。 目前工具元数据仅由 langchain4j-anthropic 模块支持。

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

继承与工具发现

具体的 @Tool 注解方法从超类和接口继承。当向 AI Service 传递工具对象时,LangChain4j 从对象的类、其所有超类(直到但不包括 Object)以及已实现接口的 defaultstatic 方法中发现 @Tool 方法。

class BaseMathTools {

@Tool("Calculates the sum of two numbers")
int sum(int a, int b) {
return a + b;
}
}

class AdvancedMathTools extends BaseMathTools {

@Tool("Calculates the product of two numbers")
int multiply(int a, int b) {
return a * b;
}
}

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new AdvancedMathTools()) // both "sum" and "multiply" are available
.build();

子类可以覆盖父类的 @Tool 方法。在这种情况下,仅使用子类版本——包括其 @Tool 注解:

class ParentTools {

@Tool("Returns the greeting")
String greet(String name) {
return "Hello, " + name;
}
}

class ChildTools extends ParentTools {

@Override
@Tool(name = "greet_formal", value = "Returns a formal greeting")
String greet(String name) {
return "Good day, " + name;
}
}

这里 LLM 将看到一个名为 greet_formal、描述为 "Returns a formal greeting" 的单个工具。

如果子类声明与父方法同名但参数不同的方法(重载,而非覆盖),两个方法都会被发现。由于工具名称必须唯一且默认为方法名,你必须至少给其中一个显式名称:

class ParentTools {

@Tool(name = "process_text", value = "Processes a text input")
String process(String input) {
return input.toUpperCase();
}
}

class ChildTools extends ParentTools {

@Tool(name = "process_number", value = "Processes a numeric input")
int process(int input) {
return input * 2;
}
}

如果两个方法解析为相同的工具名称,则在 AI Service 创建时抛出 IllegalArgumentException

@P

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

@P 注解有以下可选字段:

  • name:LLM 看到的参数名称。未指定时使用实际方法参数名。
  • description:参数描述(value 的别名)。默认为空。
  • value:参数描述(description 的别名)。默认为空。
  • required:参数是否必需,默认为 true

参数名称

name 属性覆盖 LLM 将看到的参数名称。 在两种情况下设置 name 很有用:

  1. 缺少 -parameters javac 选项。 没有 -parameters javac 选项时,Java 反射返回 arg0arg1 等通用名称。 参数的语义含义丢失,可能使 LLM 困惑。 设置 name 可恢复有意义的名称。 注意 Quarkus 和 Spring 等框架默认启用 -parameters, 因此实际方法参数名会被保留,使用这些框架时通常无需设置 name

  2. 为 LLM 自定义名称。 当你希望 LLM 看到与开发者在源代码中使用的不同的参数名时 (例如,以匹配特定 API 约定或提供更具描述性的名称)。

参数描述

descriptionvalue 可互换 — 它们都设置 LLM 将看到的参数描述。 仅需要描述时,使用简写的 value 形式:

@Tool
void getWeather(@P("The city name") String city) { ... }

当同时需要名称和描述时,使用命名属性:

@Tool
void getWeather(@P(name = "city", description = "The city name") String city) { ... }

@Description

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

@Description("Query to execute")
class Query {

@Description("Fields to select")
private List<String> select;

@Description("Conditions to filter on")
private List<Condition> where;
}

@Tool
Result executeQuery(Query query) {
...
}
备注

请注意,放在 enum 值上的 @Description 没有效果,并且不会包含在 生成的 JSON schema 中:

enum Priority {

@Description("Critical issues such as payment gateway failures or security breaches.") // this is ignored
CRITICAL,

@Description("High-priority issues like major feature malfunctions or widespread outages.") // this is ignored
HIGH,

@Description("Low-priority issues such as minor bugs or cosmetic problems.") // this is ignored
LOW
}

InvocationParameters

若希望在调用 AI Service 时将额外数据传入工具,可以使用 InvocationParameters


interface Assistant {
String chat(@UserMessage String userMessage, InvocationParameters parameters);
}

class Tools {
@Tool
String getWeather(String city, InvocationParameters parameters) {
String userId = parameters.get("userId");
UserPreferences preferences = getUserPreferences(userId);
return weatherService.getWeather(city, preferences.temperatureUnits());
}
}

InvocationParameters parameters = InvocationParameters.from(Map.of("userId", "12345"));
String response = assistant.chat("What is the weather in London?", parameters);

在这种情况下,LLM 不知道这些参数; 它们仅对 LangChain4j 和用户代码可见。

InvocationParameters 也可以在其他 AI Service 组件中访问,例如:

参数存储在可变、线程安全的 Map 中。

数据可以在 AI Service 的单次调用期间,在 AI Service 组件之间通过 InvocationParameters 传递(例如,从一个工具到另一个,或从 RAG 组件到工具)。

InvocationContext

InvocationParameters 类似,@Tool 注解的方法 可以接受 InvocationContext 参数以访问有关 AI Service 调用的信息。

class Tools {
@Tool
String getWeather(String city, InvocationContext context) {
UUID invocationId = context.invocationId();
String aiServiceInterfaceName = context.interfaceName();
...
}
}

在这种情况下,LLM 不知道这些参数; 它们仅对 LangChain4j 和用户代码可见。

@ToolMemoryId

如果 AI Service 方法有用 @MemoryId 注解的参数, 也可以用 @ToolMemoryId 注解 @Tool 方法的参数:

interface Assistant{
String chat(@UserMessage String userMessage, @MemoryId memoryId);
}

class Tools {
@Tool
String addCalendarEvent(CalendarEvent event, @ToolMemoryId memoryId) {
...
}
}

String answer = assistant.chat("Tomorrow I will have a meeting with Klaus at 14:00", "12345");

提供给 AI Service 方法的值会自动传递给 @Tool 方法。 如果你有多个用户和/或每个用户有多个聊天/记忆, 并希望在 @Tool 方法内区分它们,此功能很有用。

并发执行工具

默认情况下,当 LLM 一次调用多个工具时(也称为并行工具调用), AI Service 会顺序执行它们。若希望工具并发执行, 可在构建 AI Service 时调用 executeToolsConcurrently()executeToolsConcurrently(Executor)。 若启用这些选项之一,工具将并发执行(有一个例外——见下文), 使用默认或指定的 Executor

使用 ChatModel 时:

  • 当 LLM 调用多个工具时,它们在单独的线程中使用 Executor 并发执行。
  • 当 LLM 调用单个工具时,它在同一(调用方)线程中执行, 使用 Executor 以避免浪费资源。

使用 StreamingChatModel 时:

  • 当 LLM 调用多个工具时,它们在单独的线程中使用 Executor 并发执行。 每个工具在调用 StreamingChatResponseHandler.onCompleteToolCall(CompleteToolCall) 时立即执行,无需等待其他工具或响应流式输出完成。
  • 当 LLM 调用单个工具时,它在单独的线程中使用 Executor 执行。 我们无法在同一线程中执行它,因为此时 我们尚不知道 LLM 将调用多少个工具。

访问已执行的工具

若希望访问 AI Service 调用期间执行的工具, 可通过将返回类型包装在 Result 类中轻松实现:

interface Assistant {

Result<String> chat(String userMessage);
}

Result<String> result = assistant.chat("Cancel my booking 123-456");

String answer = result.content();
List<ToolExecution> toolExecutions = result.toolExecutions();

ToolExecution toolExecution = toolExecutions.get(0);
ToolExecutionRequest request = toolExecution.request();
String result = toolExecution.result(); // tool execution result as text
List<Content> resultContents = toolExecution.resultContents(); // tool execution result as content list (may include images)
Object resultObject = toolExecution.resultObject(); // actual value returned by the tool

在流式模式下,可通过指定 onToolExecuted 回调来实现:

interface Assistant {

TokenStream chat(String message);
}

TokenStream tokenStream = assistant.chat("Cancel my booking");

tokenStream
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onPartialResponse(...)
.onCompleteResponse(...)
.onError(...)
.start();

以编程方式指定工具

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

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

ToolSpecification toolSpecification = ToolSpecification.builder()
.name("get_booking_details")
.description("Returns booking details")
.parameters(JsonObjectSchema.builder()
.properties(Map.of(
"bookingNumber", JsonStringSchema.builder()
.description("Booking number in B-12345 format")
.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();
};

LangChain4j 还提供 DefaultToolExecutor,它可以自动调用 Java 对象上的方法并处理 参数映射:

class BookingTools {
String getBookingDetails(String bookingNumber) {
Booking booking = loadBookingFromDatabase(bookingNumber);
return booking.toString();
}
}

BookingTools tools = new BookingTools();
Method method = BookingTools.class.getMethod("getBookingDetails", String.class);
ToolExecutor toolExecutor = new DefaultToolExecutor(tools, method);

一旦有了一个或多个(ToolSpecificationToolExecutor)对, 我们将每对包装在 AiServiceTool 中,并将列表传给 AI Service:

AiServiceTool tool = AiServiceTool.builder()
.toolSpecification(toolSpecification)
.toolExecutor(toolExecutor)
.build();

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(List.of(tool))
.build();

为编程工具配置立即返回

若需要为工具指定 立即返回行为, 在 AiServiceTool 构建器上设置其 ReturnBehavior

AiServiceTool bookingTool = AiServiceTool.builder()
.toolSpecification(bookingToolSpec)
.toolExecutor(bookingExecutor)
.returnBehavior(IMMEDIATE)
.build();

AiServiceTool closeTool = AiServiceTool.builder()
.toolSpecification(closeToolSpec)
.toolExecutor(closeExecutor)
.returnBehavior(IMMEDIATE_IF_LAST)
.build();

AiServiceTool weatherTool = AiServiceTool.builder()
.toolSpecification(weatherToolSpec)
.toolExecutor(weatherExecutor)
// ReturnBehavior.TO_LLM by default
.build();

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(List.of(bookingTool, closeTool, weatherTool))
.build();

动态指定工具

使用 AI 服务时,也可以为每次调用动态指定工具。 可以配置一个 ToolProvider,每次调用 AI 服务时都会调用它, 并提供应包含在当前发给 LLM 的请求中的工具。 ToolProvider 接受一个 ToolProviderRequest (包含 UserMessage、聊天记忆 ID 和 InvocationParameters) 并返回包含当前 AI Service 调用工具的 ToolProviderResult

以下是仅当用户消息包含单词 "booking" 时添加 get_booking_details 工具的示例:

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

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();

可以在同一次 AI Service 调用中混合静态指定的工具(包括 @Tool 注解的方法和以编程方式配置的工具) 和动态指定的工具。 在这种情况下,所有静态和动态工具会合并在一起。

为动态工具配置立即返回

构建 ToolProviderResult 时,可以使用 ToolProviderResult.builder() 将工具标记为 立即返回add(ToolSpecification, ToolExecutor, ReturnBehavior) 重载 接受 TO_LLMIMMEDIATEIMMEDIATE_IF_LAST 中的任意一个:

ToolProvider toolProvider = (toolProviderRequest) -> {
return ToolProviderResult.builder()
.add(bookingToolSpec, bookingExecutor, ReturnBehavior.IMMEDIATE)
.add(closeToolSpec, closeExecutor, ReturnBehavior.IMMEDIATE_IF_LAST)
.add(weatherToolSpec, weatherExecutor) // ReturnBehavior.TO_LLM by default
.build();
};

工具搜索

当处理大量工具时, 在每个请求中发送所有工具会显著增加 token 使用并降低模型性能。 为解决此问题,LangChain4j 提供了工具搜索机制, 允许 LLM 自身动态发现工具, 而不是预先全部暴露。

核心思想很简单:

  • 最初,LLM 仅接触一个或多个特殊的工具搜索工具
  • LLM 可以调用这些工具来搜索相关工具
  • 一旦找到相关工具,它们会被包含在发给 LLM 的后续请求中

这实现了可扩展、token 高效且由模型驱动的工具发现。

工具搜索如何工作

工具搜索流程通常如下:

  1. 初始请求:
    • LLM 仅看到工具搜索工具(而非完整工具集)
  2. 工具搜索
    • LLM 调用工具搜索工具,描述它需要什么样的工具
    • 工具搜索策略将请求与可用工具进行匹配
  3. 工具暴露
    • 匹配的工具被添加到发给 LLM 的下一个请求中
  4. 工具执行
    • LLM 现在可以正常调用找到的工具

先前找到的工具会在多次工具搜索调用中累积。 每次 LLM 调用工具搜索工具时, 新匹配的工具会添加到 LLM 可见的现有工具集中(合并,而非替换)。 这意味着 LLM 可见的工具列表可以随时间增长。 找到的工具对 LLM 保持可见,直到其对应的 ToolExecutionResultMessageChatMemory 中被逐出,并且至少直到 AI Service 调用结束。

若未配置 ChatMemory,找到的工具仅在 AI 服务调用结束前 对 LLM 保持可见。

ToolSearchStrategy

工具搜索通过 ToolSearchStrategy 接口实现:

@Experimental
public interface ToolSearchStrategy {

List<ToolSpecification> getToolSearchTools(InvocationContext invocationContext);

ToolSearchResult search(ToolSearchRequest toolSearchRequest);
}

ToolSearchStrategy 负责:

  • 向 LLM 暴露工具搜索工具
  • 执行 LLM 生成的工具搜索请求
  • 返回匹配的工具名称,随后会被解析并暴露

LangChain4j 目前提供 2 个开箱即用的实现:

  • SimpleToolSearchStrategy – 基于关键词的匹配
  • VectorToolSearchStrategy – 使用嵌入的语义搜索

更多详情见这些类的 Javadoc。

你也可以实现自定义策略。

在 AI Services 中配置工具搜索

工具搜索在 AI Service 级别配置:

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemory(chatMemory)
.tools(tools) // tool search works for static tools
.toolProvider(mcpToolProvider) // tool search works for tools provided dynamically (e.g., MCP)
.toolSearchStrategy(new SimpleToolSearchStrategy())
.build();

一旦配置:

  • LLM 不再预先看到所有工具
  • 工具发现成为显式的、由模型驱动的步骤
  • token 使用减少,尤其是在大型工具集时

何时使用工具搜索

工具搜索在以下情况特别有用:

  • 你有许多工具(数十或数百个)
  • 工具是领域特定的或很少使用
  • 工具可用性取决于上下文、用户或权限
  • 你希望 LLM 推理它需要哪些工具,而不是从长列表中猜测

如果只有少量工具,或所有工具始终相关, 使用常规方法可能更简单。

始终可见的工具

启用工具搜索时,工具通常对 LLM 隐藏,直到通过工具搜索调用发现它们。 不过,在某些情况下,你可能希望某些工具始终对 LLM 可见。

典型用例:

  • 应始终可访问的核心工具
  • 搜索开销不必要的常用工具
  • 实用工具

LangChain4j 通过 ALWAYS_VISIBLE 工具搜索行为支持这一点。

工作原理

当工具标记为 ALWAYS_VISIBLE 时:

  • 它在非常第一个请求中就暴露给 LLM
  • 不需要通过工具搜索发现
  • 在整个 AI Service 调用期间保持可见
  • 不包含在可搜索的工具候选中

所有其他工具继续遵循正常的工具搜索流程。

使用 @Tool 注解

你可以通过 @Tool 注解将工具标记为始终可见:

@Tool(searchBehavior = ALWAYS_VISIBLE)
String getWeather(String city) {
return weatherService.getWeather(city);
}
使用 McpToolProvider

使用 MCP 工具(通过 McpToolProvider)时,可通过 alwaysVisibleToolNames 配置始终可见的工具:

McpToolProvider.builder()
.mcpClients(mcpClient)
.alwaysVisibleToolNames("getWeather")
.build();
使用 ToolSpecification

若以编程方式配置工具,可以使用 metadata 将它们标记为始终可见:

ToolSpecification toolSpecification = ToolSpecification.builder()
.name("getWeather")
.parameters(JsonObjectSchema.builder()
.addStringProperty("city")
.required("city")
.build())
.metadata(Map.of(ToolSpecification.METADATA_SEARCH_BEHAVIOR, SearchBehavior.ALWAYS_VISIBLE))
.build();

注意事项与限制

备注

工具搜索依赖于 LLM 理解何时以及如何搜索工具的能力。 此功能的有效性在很大程度上取决于所选模型。

备注

工具搜索目前标记为实验性,未来版本可能会演进。

立即返回工具执行请求的结果

默认情况下,工具执行请求的结果会发送回 LLM, LLM 使用该结果并进一步重新处理。不过,在某些情况下, 该工具执行请求产生的结果已经代表 AI 服务调用的预期结果。 在这种情况下,可以将工具配置为立即/直接返回其结果,跳过 LLM 浪费且消耗资源的 重新处理。这可以通过配置 @Tool 注解的 returnBehavior 字段来完成,如下例:

class CalculatorWithImmediateReturn {

@Tool(returnBehavior = ReturnBehavior.IMMEDIATE)
double add(int a, int b) {
return a + b;
}
}

这样,如下所示的 Assistant 服务

interface Assistant {
Result<String> chat(String userMessage);
}

配置为使用上述 CalculatorWithImmediateReturn 工具

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new CalculatorWithImmediateReturn())
.build();

将直接从工具调用返回响应。例如,用以下方式提示助手

Result<String> result = assistant.chat("How much is 37 plus 87?");

将产生一个 Result,其中 Result.content() == null, 而实际响应 124 必须从 result.toolExecutions() 检索。 若没有立即返回,LLM 将不得不重新处理 add 工具执行请求的结果, 从而返回类似这样的响应:The result of adding 37 and 87 is 124.

Result 的 AI Service 方法返回类型

若 AI Service 方法签名不返回 Result 类型,则在存在立即工具调用时, 聊天方法调用可能成功也可能失败,规则如下:

  • 若 AI Service 方法返回类型为 void,请求将成功。
  • 若存在任何非立即工具调用,请求将以 IllegalConfigurationException 失败。
  • 若所有工具执行都有 null(或 void)结果且返回类型不是原始类型,请求将成功且返回值为 null
  • 若有且仅有一个非 null 工具执行结果,且该结果可解析为返回类型,则请求将成功并返回该工具结果。
  • 若有多个非 null 工具执行结果,请求将以 IllegalConfigurationException 失败。
  • 若有一个工具执行结果且无法解析为返回类型,请求将以 IllegalConfigurationException 失败。

单个响应中多个工具调用的立即返回规则

当 LLM 在单个响应中返回多个工具调用时, 循环仅在以下两个条件都成立时立即返回(不将结果发送回 LLM):

  1. 没有工具出错。 任何工具调用中的任何错误都会强制重新处理,以便 LLM 能在下一轮对错误做出反应。
  2. 响应中要么每个工具都是 IMMEDIATE(没有混入 TO_LLM 工具)要么最后一个工具是 IMMEDIATE_IF_LAST(见下一节)。

示例(工具按 LLM 在单个响应中返回的顺序列出; (err) 标记执行出错的工具):

响应中的工具调用结果原因
[IMMEDIATE]立即返回唯一工具是 IMMEDIATE
[IMMEDIATE, IMMEDIATE]立即返回每个工具都是 IMMEDIATE/IMMEDIATE_IF_LAST
[IMMEDIATE, TO_LLM]重新处理TO_LLM 使全部立即规则失效
[IMMEDIATE_IF_LAST]立即返回最后一个工具是 IMMEDIATE_IF_LAST
[TO_LLM, IMMEDIATE_IF_LAST]立即返回最后一个工具是 IMMEDIATE_IF_LAST
[IMMEDIATE_IF_LAST, TO_LLM]重新处理不是最后,且 TO_LLM 使全部立即规则失效
[IMMEDIATE, IMMEDIATE_IF_LAST]立即返回最后一个工具是 IMMEDIATE_IF_LAST
[IMMEDIATE_IF_LAST, IMMEDIATE]立即返回每个工具都是 IMMEDIATE/IMMEDIATE_IF_LAST
[TO_LLM, IMMEDIATE_IF_LAST, IMMEDIATE]重新处理不是最后,且 TO_LLM 使全部立即规则失效
[IMMEDIATE_IF_LAST(err)]重新处理任何错误都会禁用立即返回
[TO_LLM(err), IMMEDIATE_IF_LAST]重新处理任何错误都会禁用立即返回

完整的立即返回与重新处理矩阵见 ReturnBehavior 的 Javadoc。

用于显式结束操作序列的工具的 IMMEDIATE_IF_LAST

ReturnBehavior.IMMEDIATE_IF_LAST 适用于 LLM 用来显式表示 多步操作结束的工具——例如,LLM 在一系列点击、导航等之后追加的 endExecutionAndGetFinalResult 工具。

没有 IMMEDIATE_IF_LAST 时,LLM 通常需要两轮才能结束执行: 一轮将工作工具(TO_LLM)与结束工具混合, 第二轮是 LLM 在看到所有结果后单独调用结束工具。循环仅在第二轮立即返回。

有了 IMMEDIATE_IF_LAST,只要 LLM 将该工具放在响应的最后, 循环就会立即返回——每次调用节省一整轮 LLM 往返。

class ScreenAutomation {

@Tool
String leftMouseClick(int x, int y) { /* ... */ }

@Tool
String typeText(String text) { /* ... */ }

@Tool(returnBehavior = ReturnBehavior.IMMEDIATE_IF_LAST)
String endExecutionAndGetFinalResult(String summary) { return summary; }
}

对于 LLM 响应 [leftMouseClick, typeText, endExecutionAndGetFinalResult], 循环在执行完所有三个工具后立即返回。 若 LLM 将结束工具放在非最后的位置 (例如 [endExecutionAndGetFinalResult, leftMouseClick]), 循环会继续并将所有结果发送回 LLM。

IMMEDIATE_IF_LAST 也计入 IMMEDIATE 的全部立即规则: 仅由 IMMEDIATE 和/或 IMMEDIATE_IF_LAST 工具组成的响应会立即返回, 无论哪一个在最后(仍受无错误规则约束)。

IMMEDIATE 一样,IMMEDIATE_IF_LAST 仅允许用于返回类型为 Result<T> 的 AI 服务。

错误处理

处理工具名称错误

LLM 可能在工具调用上产生幻觉, 换句话说,它可能要求使用名称不存在的工具。 在这种情况下,默认情况下 LangChain4j 会抛出异常报告问题, 但可以为 AI 服务配置在此情况下使用的不同策略。

此策略是 Function<ToolExecutionRequest, ToolExecutionResultMessage> 的实现,定义对于包含调用不可用工具请求的 ToolExecutionRequest 应产生哪个 ToolExecutionResultMessage 作为结果。例如,可以配置 AI 服务使用一种策略,向 LLM 返回一个响应,希望推动它重试不同的工具调用,因为先前要求的工具不存在,如下例:

AssistantHallucinatedTool assistant = AiServices.builder(AssistantHallucinatedTool.class)
.chatModel(chatModel)
.tools(new HelloWorld())
.hallucinatedToolNameStrategy(toolExecutionRequest -> ToolExecutionResultMessage.from(
toolExecutionRequest, "Error: there is no tool called " + toolExecutionRequest.name()))
.build();

处理工具参数错误

默认情况下,当工具参数有问题时(例如,LLM 生成了无效 JSON 或省略了必需参数),AI Service 将无法执行工具,因此会 以异常失败。

建议:将参数错误反馈给 LLM

当前默认(抛出)很少是你想要的。 参数错误通常来自 LLM,而 LLM 在获得清晰的 错误消息时通常可以自我纠正。配置一个返回错误文本的 ToolArgumentsErrorHandler,以便 LLM 能 用纠正后的参数重试。

我们计划在 LangChain4j 2.0 中将默认改为这种行为。若此计划变更 会影响你的用例,请 提交 issue, 以便在落地前听取反馈。

你可以通过在 AI Service 上配置 ToolArgumentsErrorHandler 来自定义此行为:

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler((error, errorContext) -> ...)
.build();

目前,ToolArgumentsErrorHandler 内有两种处理错误的方式:

  • 返回将发送回 LLM 的文本消息(例如错误描述), 允许其适当响应(例如,纠正错误并重试)。
  • 抛出异常:这将停止 AI 服务流程。

推荐(让 LLM 重试):

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler((error, errorContext) -> ToolErrorHandlerResult.text(error.getMessage()))
.build();

严格模式(任何参数错误都停止流程):

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler((error, errorContext) -> { throw MyCustomException(error); })
.build();

try {
assistant.chat(...);
} catch (MyCustomException e) {
// handle e
}
访问原始异常

当工具抛出包装另一个异常的异常时(例如包装 SecurityExceptionToolGuardrailException), LangChain4j 通过 getCause() 提取内部原因并将其作为 error 参数传递。 使用 errorContext.rawError() 访问最初抛出的外部异常——当 包装器类型(而非原因)决定如何处理错误时很有用。

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler((error, errorContext) -> {
if (errorContext.rawError() instanceof MyCriticalException) {
throw (MyCriticalException) errorContext.rawError();
}
return ToolErrorHandlerResult.text(error.getMessage());
})
.build();

处理工具执行错误

默认情况下,当带 @Tool 注解的方法抛出 Exception 时, Exception 的消息(e.getMessage())会作为工具执行结果发送给 LLM。 这允许 LLM 纠正其错误并在认为必要时重试。

建议:在生产环境中不要将原始异常消息发送给 LLM

当前默认将原始异常消息发送回 LLM。在生产环境中,这可能泄露 内部应用数据:堆栈跟踪、文件路径、嵌入在错误字符串中的凭据、 下游 API 响应、错误消息中的 PII 等。 一旦馈送给 LLM,此内容可能流入响应、聊天历史、可观测性管道, 以及 LLM 提供商的日志。

配置一个返回通用消息或经过整理/清理的 失败描述的 ToolExecutionErrorHandler,并将底层细节依赖日志和可观测性事件。

我们计划在 LangChain4j 2.0 中将默认改为“抛出异常并中止 AI Service 调用”。 若此计划变更会影响你的用例,请 提交 issue,以便在落地前听取反馈。

你可以通过在 AI Service 上配置 ToolExecutionErrorHandler 来自定义此行为:

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler((error, errorContext) -> ToolErrorHandlerResult.text("Tool execution failed."))
.build();

ToolArgumentsErrorHandler 一样,ToolExecutionErrorHandler 中有两种处理错误的方式: 返回文本消息或抛出异常。决定如何处理时,可以使用 errorContext.rawError() 在原因解包之前检查 原始错误。

补偿工具操作

当 AI Service 使用多个工具完成任务时,其中一个工具的失败 可能使系统处于不一致状态——某些工具已成功执行, 而其他尚未执行。例如,在银行转账中,LLM 可能 先贷记收款人账户,然后因资金不足而未能从付款人账户扣款, 导致收款人多出资金。

要处理这种情况,可以启用工具错误时的补偿。启用后, 若任何工具执行失败,所有声明了补偿操作且先前成功的工具调用 会按相反顺序自动撤销。

使用 @CompensateFor 声明补偿操作

在方法上使用 @CompensateFor 注解,将其声明为某个 @Tool 的补偿 操作。value 必须匹配暴露给 LLM 的工具名称—— 若设置了其 @Tool(name = ...) 属性则用该值,否则用 @Tool 方法名(默认用作 工具名称)。 补偿方法必须要么具有与工具相同的参数类型, 要么接受单个 ToolExecution 参数。

选项 1:相同参数类型 — 补偿方法接收与传给原始工具相同的 参数:

class BankAccountService {

@Tool("credits money to a bank account")
void credit(String name, double amount) {
accounts.merge(name, amount, Double::sum);
}

@CompensateFor("credit")
void uncredit(String name, double amount) {
accounts.merge(name, -amount, Double::sum);
}

@Tool("withdraws money from a bank account")
void withdraw(String name, double amount) {
if (accounts.getOrDefault(name, 0.0) < amount) {
throw new RuntimeException("Insufficient funds");
}
accounts.merge(name, -amount, Double::sum);
}

@CompensateFor("withdraw")
void unwithdraw(String name, double amount) {
accounts.merge(name, amount, Double::sum);
}
}

选项 2:ToolExecution 参数 — 补偿方法接收完整的 ToolExecution,可访问原始参数和工具的 返回值。当撤销操作需要原始执行产生的信息时(例如交易 ID)很有用:

class BankAccountService {

@Tool("credits money to a bank account")
String credit(String name, double amount) {
accounts.merge(name, amount, Double::sum);
return createTransactionRecord(name, amount); // e.g. "TX-42"
}

@CompensateFor("credit")
void uncredit(ToolExecution toolExecution) {
String transactionId = toolExecution.result(); // "TX-42"
reverseTransaction(transactionId);
}
}

启用补偿

构建 AI Service 时调用 .compensateOnToolErrors(true)

Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new BankAccountService())
.compensateOnToolErrors(true)
.build();

有了此配置,若 LLM 调用 credit("Dmytro", 100) 然后 调用失败的 withdraw("Mario", 100),框架将自动调用 uncredit("Dmytro", 100) 以撤销贷记。框架不会抛出异常, 而是为每个工具向 LLM 发送信息性结果消息: 已回滚的工具会收到类似 "Tool 'credit' was executed successfully but was rolled back due to failure of tool 'withdraw'" 的消息,失败的工具会收到其 正常错误消息。这保持 ChatMemory 一致,并让 LLM 决定下一步做什么——重试、通知用户,或采取不同方法。

若没有 .compensateOnToolErrors(true),错误会照常发送回 LLM,即使存在 @CompensateFor 注解也不会发生 补偿。

验证

启用 .compensateOnToolErrors(true) 时,每个 @CompensateFor 都会被验证:

  • 引用的工具必须(按名称)存在于同一对象上。
  • 补偿方法必须具有与工具完全相同的参数类型, 或接受单个 ToolExecution 参数。

若任一检查失败,会立即抛出 IllegalConfigurationException, 因此配置错误在启动时而非运行时被捕获。 若未启用 .compensateOnToolErrors(true)@CompensateFor 注解 会被静默忽略,不执行验证。

注意事项与限制

备注

补偿是尽力而为的:若补偿操作本身抛出异常,会以 WARN 级别记录,其余补偿操作继续执行。

备注

@CompensateFor 方法不会暴露给 LLM — 它们是内部补偿 基础设施,不会出现在工具规范中。

备注

补偿操作始终按相反顺序顺序运行,即使工具 执行通过 .executeToolsConcurrently() 配置为并行运行。

备注

@CompensateFor 方法可以从超类继承,与 @Tool 方法的发现方式一致。

备注

@CompensateFor 仅适用于带 @Tool 注解的方法。以编程方式或 动态定义的工具(例如 MCP 工具、通过 ToolSpecification 注册的工具) 不受支持。

备注

补偿工具操作目前标记为实验性,未来版本可能会演进。

模型上下文协议(MCP)

你也可以从 MCP 服务器导入工具。 更多信息见此处

相关教程

示例