主题
工具(函数调用)
一些 LLM 除了生成文本,还能触发动作。
NOTE
所有支持工具的 LLM 可在这里找到(见"Tools"列)。
NOTE
并非所有 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.接近,但不正确。
一个带以下工具的消息交换示例:
java
@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 一些原始的数学工具,但想象一下,若我们给它的是例如 googleSearch 与 sendEmail 工具,以及类似"我朋友想了解 AI 领域的近期新闻。把简短摘要发到 friend@email.com"的一个查询,那么它就能用 googleSearch 工具查找近期新闻,再做摘要,并用 sendEmail 工具把摘要通过邮件发送出去。
NOTE
为提高 LLM 用正确参数调用正确工具的机会,我们应当提供清晰、无歧义的:
- 工具的名称
- 工具做什么、何时应被使用的描述
- 每个工具参数的描述
一条经验法则:如果一个人能理解一个工具的用途以及如何使用它,那么 LLM 大概率也能。
LLM 被专门微调过,以识别何时调用工具以及如何调用它们。有些模型甚至能一次调用多个工具,例如 OpenAI。
NOTE
请注意,并非所有模型都支持工具。要查看哪些模型支持工具,请参考这个页面的"Tools"列。
NOTE
请注意,工具/函数调用与 JSON 模式不是一回事。
两层抽象
LangChain4j 为使用工具提供两层抽象:
- 低层,使用
ChatModel与ToolSpecificationAPI - 高层,使用 AI 服务 与带
@Tool注解的 Java 方法
低层工具 API
在低层,你可以使用 ChatModel 的 chat(ChatRequest) 方法。StreamingChatModel 中也有类似方法。
你可以在创建 ChatRequest 时指定一个或多个 ToolSpecification。
ToolSpecification 是一个包含该工具全部信息的对象:
- 工具的
name - 工具的
description - 工具的
parameters及其描述 - 工具的
metadata。 默认情况下它不会被发送给 LLM 提供者,你必须创建ChatModel时显式指定哪些元数据键应被发送。目前,工具元数据仅由langchain4j-anthropic模块支持。当工具由 McpToolProvider 提供时,metadata可以含有 MCP 专有的条目。
建议尽可能多地提供工具信息:一个清晰的名字、一段全面的描述、每个参数的描述等。
创建工具规范
有两种方式创建 ToolSpecification:
- 手动
java
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 的更多信息见这里。
- 使用辅助方法:
ToolSpecifications.toolSpecificationsFrom(Class)ToolSpecifications.toolSpecificationsFrom(Object)ToolSpecifications.toolSpecificationFrom(Method)
java
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 可以被序列化/反序列化为 JSON。当你需要通过网络传输工具定义、或把它们持久化到磁盘/数据库时,这会很有用。
java
String json = toolSpecification.toJson();
ToolSpecification deserialized = ToolSpecification.fromJson(json);默认情况下,JSON 转换使用一个专用的 Jackson ObjectMapper。你可以通过实现 ToolSpecificationJsonCodecFactory 并在 META-INF/services/dev.langchain4j.spi.agent.tool.ToolSpecificationJsonCodecFactory 中注册它,经由 SPI 提供你自己的 ToolSpecificationJsonCodec 实现。
使用 ChatModel
一旦你有了 List<ToolSpecification>,就可以调用模型:
java
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 中的信息手动执行工具。
若你想把工具执行的结果发回给 LLM,你需要创建一个 ToolExecutionResultMessage(每个 ToolExecutionRequest 对应一个),并连同此前所有消息一起发送:
java
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() 的 builder:
java
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();NOTE
并非所有 LLM 提供者都支持多模态工具结果。关于提供者支持情况的细节见返回图像与多模态内容。
使用 StreamingChatModel
一旦你有了 List<ToolSpecification>,就可以调用模型:
java
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) 回调被调用,表明该工具调用的流式传输已结束。
NOTE
请注意,并非所有 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 服务 时指定它们。
AI 服务会自动把这类方法转换为 ToolSpecification,并在每次与 LLM 交互的请求中包含它们。当 LLM 决定调用工具时,AI 服务会自动执行相应的方法,且方法的返回值(若有)会被发回给 LLM。实现细节见 DefaultToolExecutor。
几个工具示例:
java
@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 注解的方法可以接受任意数量的各种类型参数:
- 原始类型:
int、double等 - 对象类型:
String、Integer、Double等 - 自定义 POJO(可以含嵌套 POJO)
enum- 多态类型(密封接口/类,或用 Jackson
@JsonSubTypes/@JsonTypeInfo注解的类型)—— 见多态工具参数 List<T>/Set<T>,其中T是上述类型之一Map<K,V>(你需要用@P在参数描述中手动指定K与V的类型)
也支持不带参数的方法。
参数名称
默认情况下,若 @P 的 name 属性未指定,参数名通过反射获取。然而,没有 -parameters javac 选项时,反射返回的是 arg0、arg1 这类通用名称。参数的语义就此丢失,可能会让 LLM 困惑。
在 @P 中设置 name 在两种情况下有用:
- 缺少
-parametersjavac 选项 —— 以避免 LLM 看到通用的arg0/arg1名称。请注意 Quarkus 与 Spring 这类框架默认启用-parameters,因此实际的方法参数名会被保留,使用那些框架时你通常无需设置name。 - 为 LLM 定制名称 —— 当你希望 LLM 看到与源代码中不同的参数名时(例如为了匹配某个特定的 API 约定,或提供更具描述性的名称)。
示例:
java
@Tool
void getTemperature(
@P("Temperature value") double value,
@P("Unit of temperature") Optional<String> unit) {
...
}必填与可选
默认情况下,所有工具方法的参数都被视为**必填**。这意味着该参数会被列入发送给 LLM 的 JSON schema 的 required 数组中,指示它必须产出值。可以用 @P(required = false) 注解参数来使其可选:
java
@Tool
String getTemperature(String location, @P(required = false) Unit unit) {
...
}或者,你可以把参数声明为 Optional<T>:
java
@Tool
String getTemperature(String location, Optional<Unit> unit) {
...
}复杂参数的字段与子字段默认也被视为**必填**。你可以用 @JsonProperty(required = false) 注解字段来使其可选:
java
record User(String name, @JsonProperty(required = false) String email) {}
@Tool
void add(User user) {
...
}NOTE
请注意,当与结构化输出一起使用时,所有字段与子字段默认都被视为**可选**。
::: caution 必填只是建议性的:LLM 仍可能省略参数 "required" 标志控制发送给 LLM 的 JSON schema(必填参数会列在 schema 的 required 数组中)。人们期望 LLM 遵循这一点,但实际上它可能无视 schema 而仍然省略某个参数。
在 LangChain4j 1.x 中,这只对原始类型参数(int、long、boolean 等)做检测——缺失一个原始类型会触发 ToolArgumentsErrorHandler(见下文错误处理)。缺失的对象参数不会被校验:即便 schema 把该参数标为必填,null 仍会被传给带 @Tool 注解的方法。
我们计划在 LangChain4j 2.0 中消除这种不对称,让所有必填参数都被统一校验。若这一计划中的变更会影响你的用例,请提一个 issue,好让我们在落地前听到你的反馈。
若你想要一个真正的回退值(而不是 null,或对原始类型而言不是报错),请使用 @P(defaultValue = ...)。 :::
默认参数值
@P(defaultValue = "...") 声明一个当 LLM 省略参数时 LangChain4j 代入的值。这是让某参数可选、并提供一个你的工具方法能接收的合理回退值的最简单方式。
java
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 中是可选的——该参数不会被列入 schema 的 required 数组,无论 @P(required) 如何。LLM 被告知它可以省略该参数;若它省略了,LangChain4j 会在调用你的方法之前填入默认值。
受支持的类型:
| 类型 | 格式 | 示例 |
|---|---|---|
String | 原样使用 | defaultValue = "USD" |
| 原始类型 / 装箱原始类型 | 按类型特定转换 | "10"、"3.14"、"true" |
enum | 枚举常量名 | defaultValue = "EUR" |
UUID | UUID.fromString | "550e8400-e29b-41d4-a716-446655440000" |
BigDecimal、BigInteger | 数字字面量 | "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 服务注册时被解析。若它无法被转换为该参数的类型,AI 服务构建会立即以 IllegalConfigurationException 失败,并点名出错的参数——因此拼写错误在启动时就被捕获,而不是在第一次 LLM 调用时。
默认值只适用于缺失,不适用于错误值。 若 LLM 提供了一个类型转换失败的参数(例如给 int 传 "banana"),转换错误会照常传播——默认值不会被用作回退。
默认值在每次调用时都会重新解析, 因此某个会修改被设了默认值的 List/Map/POJO 的工具不会污染后续的调用:
java
@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 注入的参数上(@ToolMemoryId、InvocationContext等)——它们不来自 LLM。
多态工具参数
一个工具参数可以是多态类型——一个其具体子类型由 LLM 在调用时决定的基类型。密封接口与密封类无需注解即可工作;普通的抽象类与接口必须用 Jackson 的 @JsonSubTypes 声明其子类型。发送给 LLM 的 schema 含有一个对所许可子类型的 anyOf,每个都带一个判别属性(默认为 "type"),以便 LLM 能表达它产出的是哪个具体类型;LangChain4j 在调用你的工具方法之前会把 LLM 的参数反序列化为正确的子类型。
这对作为参数的多态类型、多态类型的 List<T> / Set<T>,以及嵌套在另一个 POJO 参数内的多态类型都有效。
密封接口与类——无需注解:
java
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 类名解耦:
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 标志以及字段冲突检测,在结构化输出一节的多态类型中有详细描述——它们同样适用于工具参数。
递归参数
递归参数(例如一个 Person 类有一个 Set<Person> children 字段)目前仅由 OpenAI 支持。
工具方法的返回类型
带 @Tool 注解的方法可以返回任何类型,包括 void。若方法是 void 返回类型,当方法成功返回时,一个 "Success" 字符串会被发送给 LLM。
若方法是 String 返回类型,返回的值会不经任何转换原样发送给 LLM。
对于其他返回类型,返回的值会在发送给 LLM 之前被转换为一个 JSON 字符串。
返回图像与多模态内容
工具也能返回图像及其他非文本内容。当工具返回以下某种类型时,结果会作为多模态内容(例如图像)发送给 LLM,而不是被序列化为 JSON 文本:
Image—— 作为单张图像发送ImageContent—— 作为单个图像内容发送Content—— 作为单个内容元素发送(例如TextContent、ImageContent)List<Content>—— 作为多个内容元素发送Content[]—— 作为多个内容元素发送
例如,一个拍照片并返回图像的工具:
java
@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();
}或一个同时返回文本与图像的工具:
java
@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)
);
}NOTE
并非所有 LLM 提供者都支持多模态工具结果。目前在工具结果中支持图像的提供者包括 Anthropic、Amazon Bedrock 与 Google AI Gemini。其他提供者在工具返回非文本内容时会抛出 UnsupportedFeatureException。
AI 服务作为其他 AI 服务的工具
AI 服务也可以作为其他 AI 服务的工具。这在许多智能体用例中很有用,其中一个 AI 服务可以请求另一个更专门的 AI 服务帮忙执行某个特定任务。例如,定义了以下 AI 服务之后:
java
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 服务(各领域专家)作为工具使用,从而把用户请求路由到其中一个。
java
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");NOTE
把一个 AI 服务作为另一个 AI 服务的工具是一个强大的特性,能构建复杂的智能体系统。然而,这种方式也带来几个相关的、值得了解的缺点:
- 这种实现要求 LLM 把用户请求原封不动地复制粘贴为一个工具调用,而这可能是易错的操作。
- 把一个 LLM 作为工具调用的另一个 LLM,必须重新处理它的响应(就像任何工具调用一样),这在时间与消耗的词元两方面都可能是浪费的计算。
- 作为工具的子智能体是一个完全独立的 AI 服务,无法访问调用它的那个智能体的聊天记忆,因此它不能利用聊天记忆来给出一个更知情的答案。
@Tool
任何带 @Tool 注解、且在构建 AI 服务时显式指定的 Java 方法,都可以被 LLM 执行:
java
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 方法被调用时,会发生 2 次与 LLM 的交互,如前节所述。在这两次交互之间,squareRoot 方法会被自动调用。
@Tool 注解有以下字段:
name:工具的名称。若未提供,则用方法名作为工具名。value:工具的描述。returnBehavior:更多细节见这里metadata:一个有效的 JSON 字符串,含有 LLM 提供者专有的工具元数据条目。 默认情况下它不会被发送给 LLM 提供者,你必须在创建ChatModel时显式指定哪些元数据键应被发送。目前,工具元数据仅由langchain4j-anthropic模块支持。
视工具而定,LLM 即便没有任何描述也可能理解得很好(例如 add(a, b) 一目了然),但通常最好提供清晰而有意义的名称与描述。这样 LLM 就有更多信息来决定是否调用给定工具、以及如何调用它。
继承与工具发现
当你把一个工具对象传给 AI 服务时,LangChain4j 会从该对象的类、它的所有父类(到但不包括 Object)中发现 @Tool 方法,以及所实现接口的 default 与 static 方法。
java
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 注解:
java
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" 的工具。
当一个方法覆盖或实现了一个带 @Tool 注解的方法、却没有重复该注解时,它同样是一个工具。当工具在接口中声明、在他处实现时,这很方便:
java
interface WeatherTools {
@Tool("Returns the weather in the given city")
String weather(String city);
}
class OpenMeteoWeatherTools implements WeatherTools {
@Override
public String weather(String city) {
return ...;
}
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new OpenMeteoWeatherTools()) // "weather" is available, described as declared in the interface
.build();除非覆盖方法声明了自己的 @Tool 注解,否则 LLM 看到的是被覆盖方法的那个。
这也是当工具对象被包装在代理中时工具仍能工作原因,例如一个被应用了 @Aspect、@Transactional 或 @Async 的 Spring bean。这样的代理是一个生成的子类,它覆盖了工具方法,而 Java 不会把方法注解复制到覆盖方法上,因此注解会在被代理的那个类上查找。工具是经由代理被调用的,所以拦截器与切面仍会运行。
若子类声明了一个与父类方法同名、但参数不同的方法(重载,而非覆盖),两个方法都会被发现。由于工具名必须唯一且默认取方法名,你必须至少给其中一个显式命名:
java
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 服务创建时抛出 IllegalArgumentException。
@P
方法参数可以选择性地用 @P 注解。
@P 注解有以下可选字段:
name:LLM 所看到的参数名。若未指定,则使用方法实际的参数名。description:参数的描述(value的别名)。默认为空。value:参数的描述(description的别名)。默认为空。required:参数是否必填,默认为true。
参数名称
name 属性会覆盖 LLM 所看到的参数名。设置 name 在两种情况下有用:
缺少
-parametersjavac 选项。 没有-parametersjavac 选项时,Java 反射返回的是arg0、arg1这类通用名称。参数的语义就此丢失,可能会让 LLM 困惑。设置name能恢复一个有意义的名称。请注意 Quarkus 与 Spring 这类框架默认启用-parameters,因此实际的方法参数名会被保留,使用那些框架时你通常无需设置name。为 LLM 定制名称。 当你希望 LLM 看到与开发者在源代码中所用不同的参数名时(例如为了匹配某个特定的 API 约定,或提供一个更具描述性的名称)。
参数描述
description 与 value 可互换——它们都设置 LLM 将看到的参数描述。当只需要描述时,使用简写的 value 形式:
java
@Tool
void getWeather(@P("The city name") String city) { ... }当同时需要名称与描述时,使用具名属性:
java
@Tool
void getWeather(@P(name = "city", description = "The city name") String city) { ... }@Description
类与字段的描述可以用 @Description 注解指定:
java
@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) {
...
}NOTE
请注意,放在某个 enum 值上的 @Description 不起作用,且**不会**被包含在所生成的 JSON schema 中:
java
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 服务时向工具传入额外数据,可以用 InvocationParameters 做到:
java
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 服务的其他组件中被访问,例如:
ToolProvider:在ToolProviderRequest内ToolArgumentsErrorHandler与ToolExecutionErrorHandler:在ToolErrorContext内- RAG 组件:在
Query->Metadata内
参数存储在一个可变、线程安全的 Map 中。
在 AI 服务的单次调用期间,数据可以在各 AI 服务组件之间经由 InvocationParameters 传递(例如从一个工具传到另一个,或从 RAG 组件传到工具)。
InvocationContext
与 InvocationParameters 类似,带 @Tool 注解的方法可以接受一个 InvocationContext 参数,以获取关于 AI 服务调用的信息。
java
class Tools {
@Tool
String getWeather(String city, InvocationContext context) {
UUID invocationId = context.invocationId();
String aiServiceInterfaceName = context.interfaceName();
...
}
}这种情况下,LLM 并不知道这些参数;它们只对 LangChain4j 与用户代码可见。
@ToolMemoryId
若你的 AI 服务方法有一个带 @MemoryId 注解的参数,你也可以给某个 @Tool 方法的参数加上 @ToolMemoryId:
java
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 服务方法的值会被自动传给 @Tool 方法。若你有多个用户和/或每个用户多个会话/记忆,并希望在其中区分它们,这个特性很有用。
并发执行工具
默认情况下,当 LLM 一次调用**多个**工具(也称并行工具调用)时,AI 服务会顺序执行它们。若你想让工具被并发执行,可以在构建 AI 服务时调用 executeToolsConcurrently() 或 executeToolsConcurrently(Executor)。启用其中任一选项后,工具将被并发执行(有一个例外——见下文),使用默认的或你指定的 Executor。
使用 ChatModel 时:
- 当 LLM 调用多个工具时,它们会在各自的线程中并发执行 使用
Executor。 - 当 LLM 调用单个工具时,它会在同一(调用方)线程中执行,
Executor不会被用于避免浪费资源。
使用 StreamingChatModel 时:
- 当 LLM 调用多个工具时,它们会在各自的线程中并发执行 使用
Executor。每个工具在StreamingChatResponseHandler.onCompleteToolCall(CompleteToolCall)被调用时即刻执行,不等待其他工具,也不等待响应流式完成。 - 当 LLM 调用单个工具时,它会使用
Executor在一个独立线程中执行。 我们不能在同一线程中执行它,因为在那时我们还不知道 LLM 将会调用多少个工具。
访问已执行的工具
若你想访问在 AI 服务调用期间执行过的工具,只需把返回类型包装进 Result 类即可轻松做到:
java
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
Map<String, Object> attributes = toolExecution.attributes(); // attributes of the tool execution result, see below在流式模式下,你可以通过指定 onToolExecuted 回调来做到这一点:
java
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();工具结果属性
一次工具执行的结果可以携带属性:一个不被发送给 LLM 的 Map<String, Object>。属性对你应用独需的数据很有用,例如工具所创建记录的 ID,或你的 UI 应当渲染的一个组件。
属性可以由一个自定义 ToolExecutor 设置:
java
ToolExecutor toolExecutor = (toolExecutionRequest, context) -> ToolExecutionResult.builder()
.resultText("Sunny, 22 degrees") // sent to the LLM
.attributes(Map.of("widget", weatherWidget)) // not sent to the LLM
.build();对于 MCP 工具,它们源自工具调用响应的 _meta 字段。对于这类,工具提供者需要用 returnToolResultAttributes(true) 来配置。
属性可以从 ToolExecution 中读取(见上文访问已执行的工具)。它们也会被传播进 ToolExecutionResultMessage.attributes(),因此它们会与消息一起被存储在 ChatMemory 中。
NOTE
若你使用一个会持久化消息的 ChatMemoryStore,它们会被序列化为 JSON。请确保所有属性值都能被序列化为 JSON、且反序列化后仍然有用:
- 一个无法序列化的值(例如
InputStream)会让整个存储操作失败。 - 值会被反序列化为普通的 JSON 类型,所以一个作为属性存储的自定义对象 会以一个
Map的形式回来,且不能再被转型为其原始类型。
属性也会为每条消息存储,所以若会话被持久化,请避免在那里存放很大的值。
以编程方式指定工具
当使用 AI 服务时,工具也可以以编程方式指定。这种方式提供了很多灵活性,因为工具可以从外部来源加载,例如数据库和配置文件。
工具名、描述、参数名与描述都可以用 ToolSpecification 来配置:
java
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 生成的工具执行请求:
java
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 对象上的方法并处理参数映射:
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);一旦我们有一个或多个 (ToolSpecification, ToolExecutor) 对,我们就把每一对包装进一个 AiServiceTool,并把列表传给 AI 服务:
java
AiServiceTool tool = AiServiceTool.builder()
.toolSpecification(toolSpecification)
.toolExecutor(toolExecutor)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(List.of(tool))
.build();为编程式工具配置即时返回
若你需要为某个工具指定即时返回行为,请在 AiServiceTool builder 上设置它的 ReturnBehavior:
java
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 服务调用所用工具的 ToolProviderResult。
下面是一个示例:仅当用户消息含有 "booking" 一词时才添加 get_booking_details 工具:
java
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 服务调用中混合静态(包括 @Tool 注解方法与以编程方式配置的工具)与动态指定的工具。那种情况下,所有静态与动态工具会被合并到一起。
为动态工具配置即时返回
在构建 ToolProviderResult 时,你可以用 ToolProviderResult.builder() 把工具标记为即时返回。add(ToolSpecification, ToolExecutor, ReturnBehavior) 重载接受 TO_LLM、IMMEDIATE 或 IMMEDIATE_IF_LAST 之一:
java
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();
};工具搜索
当处理大量工具时,每次请求都发送所有工具会显著增加词元用量并降低模型表现。为应对这一点,LangChain4j 提供了一个工具搜索机制,让工具能由 LLM 自己动态发现,而不是一开始就全部暴露。
核心思想很简单:
- 起初,LLM 只被暴露一个或多个专门的工具搜索工具
- LLM 可以调用这些工具来搜索相关工具
- 一旦找到相关工具,它们就会被包含进后续对 LLM 的请求
这实现了可扩展、省词元、由模型驱动的工具发现。
工具搜索如何工作
一次工具搜索流程通常像这样:
- 初始请求:
- LLM 只看到工具搜索工具(而非整个工具集)
- 工具搜索
- LLM 调用一个工具搜索工具,描述它需要哪一类工具
- 工具搜索策略把该请求与可用工具做匹配
- 工具暴露
- 匹配到的工具被加入下一次对 LLM 的请求
- 工具执行
- LLM 现在可以正常调用找到的工具
此前找到的工具会跨多次工具搜索调用累积。每次 LLM 调用工具搜索工具,新匹配到的工具都会被加入 LLM 已可见的现有工具集(是合并,而非替换)。这意味着 LLM 可见的工具列表会随时间增长。被找到的工具会一直对 LLM 可见,直到其对应的 ToolExecutionResultMessage 从 ChatMemory 中被淘汰,且至少持续到本次 AI 服务调用结束。
若未配置 ChatMemory,被找到的工具只对 LLM 可见到本次 AI 服务调用结束为止。
ToolSearchStrategy
工具搜索通过 ToolSearchStrategy 接口实现:
java
@Experimental
public interface ToolSearchStrategy {
List<ToolSpecification> getToolSearchTools(InvocationContext invocationContext);
ToolSearchResult search(ToolSearchRequest toolSearchRequest);
}ToolSearchStrategy 负责:
- 向 LLM 暴露工具搜索工具
- 执行由 LLM 生成的工具搜索请求
- 返回匹配的工具名称,它们随后会被解析并暴露
LangChain4j 目前提供 3 种开箱即用的实现:
SimpleToolSearchStrategy—— 基于关键词的匹配VectorToolSearchStrategy—— 使用嵌入的语义搜索DecisionModelToolSearchStrategy—— 由一个决策模型决定哪些工具匹配搜索
更多细节见这些类的 Javadoc。
你也可以实现自定义策略。
在 AI 服务中配置工具搜索
工具搜索在 AI 服务层面配置:
java
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 不再一开始就看到所有工具
- 工具发现成为一个显式的、由模型驱动的步骤
- 词元用量减少,尤其在工具集很大时
何时使用工具搜索
工具搜索在以下情况尤其有用:
- 你有很多工具(几十或几百个)
- 工具是领域专有的或很少被使用
- 工具的可用性取决于上下文、用户或权限
- 你希望 LLM 对它需要哪些工具进行推理,而不是从一个长列表中猜测
若你只有少量工具,或所有工具总是相关,用常规方式可能更简单。
始终可见的工具
当启用工具搜索时,工具通常对 LLM 隐藏,直到它们通过一次工具搜索调用被发现。然而,某些情况下你可能希望某些工具始终对 LLM 可见。
典型用例:
- 应始终可访问的核心工具
- 频繁使用、搜索开销没必要的工具
- 实用工具
LangChain4j 通过 ALWAYS_VISIBLE 工具搜索行为支持这一点。
工作原理
当一个工具被标记为 ALWAYS_VISIBLE 时:
- 它在最开始的第一个请求中就被暴露给 LLM
- 它不需要通过工具搜索来发现
- 它在整个 AI 服务调用期间保持可见
- 它不会被纳入可搜索的候选工具
所有其他工具继续遵循正常的工具搜索流程。
使用 @Tool 注解
你可以通过 @Tool 注解把一个工具标记为始终可见:
java
@Tool(searchBehavior = ALWAYS_VISIBLE)
String getWeather(String city) {
return weatherService.getWeather(city);
}使用 McpToolProvider
当使用 MCP 工具(经 McpToolProvider)时,始终可见的工具可以通过 alwaysVisibleToolNames 配置:
java
McpToolProvider.builder()
.mcpClients(mcpClient)
.alwaysVisibleToolNames("getWeather")
.build();使用 ToolSpecification
若你以编程方式配置工具,可以用 metadata 把它们标记为始终可见:
java
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();说明与限制
NOTE
工具搜索依赖于 LLM 理解何时、以及如何搜索工具的能力。这个特性的效果很大程度上取决于所选的模型。
NOTE
工具搜索目前被标记为实验性,未来版本可能演进。
立即返回一次工具执行请求的结果
默认情况下,一次工具执行请求的结果会被发回给 LLM,由 LLM 使用该结果并进一步重新处理它。然而,某些情形下,那次工具执行请求所产生的结果已经代表了本次 AI 服务调用的期望结果。这种情况下,可以配置该工具立即/直接返回其结果,跳过 LLM 那次浪费且耗资源的重新处理。这可以通过配置 @Tool 注解的 returnBehavior 字段来完成,如下例:
java
class CalculatorWithImmediateReturn {
@Tool(returnBehavior = ReturnBehavior.IMMEDIATE)
double add(int a, int b) {
return a + b;
}
}这样,一个如下的 Assistant 服务
java
interface Assistant {
Result<String> chat(String userMessage);
}被配置为使用上述 CalculatorWithImmediateReturn 工具
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new CalculatorWithImmediateReturn())
.build();会直接从工具调用返回响应。例如,向该助手发出提示
java
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 服务方法返回类型
若你的 AI 服务方法签名不返回 Result 类型,那么当存在即时工具调用时,你的聊天方法调用可能成功也可能失败,规则如下:
- 若 AI 服务方法的返回类型是 void,请求会成功。
- 若存在任何非即时的工具调用,请求会以
IllegalConfigurationException失败。 - 若所有工具执行的结果都为 null(或 void)且返回类型不是原始类型,请求会成功且返回值为
null。 - 若有且仅有一个非 null 的工具执行结果,且该结果能被解析为返回类型,那么请求会成功并返回那个工具结果。
- 若有多个非 null 的工具执行结果,请求会以
IllegalConfigurationException失败。 - 若只有一个工具执行结果且它无法被解析为返回类型,请求会以
IllegalConfigurationException失败。
单次响应含多个工具调用时的即时返回规则
当 LLM 在单次响应中返回多个工具调用时,只有当以下两者都成立,循环才会立即返回(而不把结果发回给 LLM):
- 没有工具出错。 任何工具调用中的任何错误都会强制重新处理,好让 LLM 能在下一轮对该错误作出反应。
- 要么响应中的每个工具都是
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 用来显式表示一个多步动作结束的工具而生——例如一个 endExecutionAndGetFinalResult 工具,LLM 会在一连串点击、导航等操作之后追加它。
没有 IMMEDIATE_IF_LAST 时,LLM 通常需要两轮才能收尾执行:一轮把工作工具(TO_LLM)与收尾工具混在一起,第二轮 LLM 在看到所有结果之后单独调用收尾工具。循环只在第二轮才立即返回。
有了 IMMEDIATE_IF_LAST,一旦 LLM 把该工具放在其响应的最后,循环就立即返回——每次调用省下一整个 LLM 往返。
java
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; }
}对于一个 [leftMouseClick, typeText, endExecutionAndGetFinalResult] 的 LLM 响应,循环在执行完全部三个工具后立即返回。若 LLM 把收尾工具放在非最后的位置(例如 [endExecutionAndGetFinalResult, leftMouseClick]),循环会继续,并把所有结果发回给 LLM。
IMMEDIATE_IF_LAST 也计入 IMMEDIATE 的全即时规则:一个仅由 IMMEDIATE 和/或 IMMEDIATE_IF_LAST 工具构成的响应会立即返回,无论最后是哪个(仍受无错误规则约束)。
与 IMMEDIATE 一样,IMMEDIATE_IF_LAST 只允许用在返回 Result<T> 类型的 AI 服务上。
错误处理
NOTE
下面的默认值适用于同步与 TokenStream 模式。异步与响应式模式的行为已经与默认值的未来计划一致:一个工具执行错误会让调用失败,除非该异常说明了可以告诉 LLM 什么;而一个工具参数解析错误会被发送给 LLM 而不是让调用失败。你显式配置的处理器会被每个模式使用。参见非阻塞与响应式。
现成的处理器
两个处理器接口都带有针对最常见情况的现成实现,因此那些情况下你不必自己写处理器:
| 处理器 | 工具失败时会发生什么 |
|---|---|
ToolArgumentsErrorHandler.sendExceptionMessageToLlm() | 错误的消息会被发送给 LLM,好让它修正参数并重试。AI 服务调用继续。⚠️ 见下方警告。 |
ToolArgumentsErrorHandler.failInvocation() | 错误被重新抛出:AI 服务调用失败,且不有任何内容发送给 LLM。 |
ToolExecutionErrorHandler.sendExceptionMessageToLlm() | 工具所抛异常的消息会被发送给 LLM(或若该异常实现了 ToolErrorVisibleToLlm,则用其 messageForLlm() 的文本),好让它能对其作出反应。AI 服务调用继续。⚠️ 见下方警告。 |
ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm() | 只有实现了 ToolErrorVisibleToLlm 的异常会以其提供的文本展示给 LLM。其他每个异常都会让 AI 服务调用失败。见按异常逐个决定 LLM 看到什么。 |
ToolExecutionErrorHandler.sendGenericMessageToLlmUnlessVisibleToLlm(String) | 实现了 ToolErrorVisibleToLlm 的异常会以其提供的文本展示给 LLM。对每个其他异常,所给的通用消息会被发送给 LLM,且该异常会以 WARN 级别记录。AI 服务调用继续。 |
ToolExecutionErrorHandler.failInvocation() | 异常被重新抛出:AI 服务调用失败,且不有任何内容发送给 LLM。 |
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler(ToolArgumentsErrorHandler.sendExceptionMessageToLlm())
.toolExecutionErrorHandler(ToolExecutionErrorHandler.failInvocation())
.build();对于其他任何需求——在 LLM 看到之前对错误做净化,或依据错误类型来决定——请按后文各节所述自己编写处理器。
把异常消息发送给 LLM 可能泄露敏感数据 异常的消息通常是写给开发者、而非写给 LLM 的:它可能含有应用的内部细节,例如文件路径、SQL、嵌在错误字符串中的凭据、下游服务的响应或个人数据。任何发送给 LLM 的内容都会抵达 LLM 提供者、被存储进聊天记忆,并可能出现在用户读到的答案、日志与可观测性管道中。
只有当你确知你所有工具的异常消息都是带着这一考量而写时,才使用 sendExceptionMessageToLlm()。否则请发送一个通用或经净化的失败描述给 LLM,并把细节留在你的日志里。
NOTE
当一个 AI 服务被配置了工具、但错误处理器未被显式配置时,LangChain4j 会在每个 JVM 记录一次警告。显式配置处理器(用本页的任一选项)能消除该警告,并让你的应用不受默认值未来计划变更的影响。该警告也可以通过把 dev.langchain4j.service.ToolErrorHandlingNotice logger 的日志级别设为 OFF 来关闭。
在 Quarkus 中配置处理器
本页的示例使用 AiServices.builder(...)。在 Quarkus 中,AI 服务用 @RegisterAiService 声明,处理器则用接口上的静态方法配置:
java
@RegisterAiService
public interface Assistant {
String chat(String userMessage);
@HandleToolExecutionError
static ToolErrorHandlerResult onToolError(Throwable error, ToolErrorContext context) {
return ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm().handle(error, context);
}
}对于整个应用,声明一个实现 ToolExecutionErrorHandler 的类并用 @DefaultToolExecutionErrorHandler 注解它(该限定符置于类型上,而非生产者方法上)。
请注意,Quarkus 自行构建它的 AI 服务,因此上面描述的警告在那里不会被记录。Quarkus 也用它自己的默认工具执行错误处理器,它会把异常的消息发送给 LLM 且不查看 ToolErrorVisibleToLlm。要让那个接口被尊重,请像上面展示的那样显式配置 failInvocationUnlessVisibleToLlm() 或 sendExceptionMessageToLlm()。
在 Spring Boot 中配置处理器
用 Spring Boot starter,AI 服务用 @AiService 声明,处理器则声明为 bean:
java
@Configuration
class ToolErrorHandlingConfig {
@Bean
ToolExecutionErrorHandler toolExecutionErrorHandler() {
return ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm();
}
@Bean
ToolArgumentsErrorHandler toolArgumentsErrorHandler() {
return ToolArgumentsErrorHandler.sendExceptionMessageToLlm();
}
}在默认(自动)装配模式下,每个 @AiService 都会使用这些 bean。在显式装配模式下,每个 AI 服务会指名它所用到的 bean:
java
@AiService(
wiringMode = EXPLICIT,
chatModel = "openAiChatModel",
tools = "bookingTools",
toolExecutionErrorHandler = "toolExecutionErrorHandler",
toolArgumentsErrorHandler = "toolArgumentsErrorHandler")
interface Assistant {
String chat(String userMessage);
}关于各装配模式的更多细节,见 Spring Boot 集成。
处理工具名称错误
可能出现 LLM 在工具调用时产生幻觉,或者说它请求使用一个名字并不存在的工具。这种情况下,LangChain4j 默认会抛出一个异常来报告该问题,但你也可以为 AI 服务提供一个用于此种情况的策略,从而配置不同的行为。
这个策略是一个 Function<ToolExecutionRequest, ToolExecutionResultMessage> 的实现,它定义当某个 ToolExecutionRequest 请求调用一个不可用的工具时,应当产出哪个 ToolExecutionResultMessage。例如,可以给 AI 服务配置这样一个策略:它返回给 LLM 一个响应,希望在得知先前所要求的工具并不存在之后,促使它重试调用另一个工具,如下例:
java
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 服务将无法执行该工具,因而会以一个异常失败。
::: caution 推荐:把参数错误反馈给 LLM 当前的默认行为(抛异常)很少是你想要的。参数错误通常来自 LLM,而 LLM 在得到清晰的错误消息时通常能自我纠正。请配置一个把错误文本返回、好让 LLM 能用修正后的参数重试的 ToolArgumentsErrorHandler。
我们计划在未来的某个版本里把默认值改为此行为。若这一计划中的变更会影响你的用例,请提一个 issue,好让我们在落地前听到你的反馈。 :::
你可以通过在 AI 服务上配置一个 ToolArgumentsErrorHandler 来定制这一行为:
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler((error, errorContext) -> ...)
.build();目前,在 ToolArgumentsErrorHandler 内部有两种处理错误的方式:
- 返回一个会被发回给 LLM 的文本消息(例如一段错误描述),让它能恰当地响应(比如纠正错误并重试)。
- 抛出一个异常:这会停止 AI 服务的流程。
推荐(让 LLM 重试):
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler(ToolArgumentsErrorHandler.sendExceptionMessageToLlm())
.build();严格(任何参数错误都停止流程):
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolArgumentsErrorHandler(ToolArgumentsErrorHandler.failInvocation())
.build();若你想让 AI 服务以你自己类型的异常失败,就从处理器中抛出它:
java
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
}访问原始异常
当一个工具抛出一个包装了另一个异常的异常时(例如 ToolGuardrailException 包装了 SecurityException),LangChain4j 会通过 getCause() 提取内部原因,并把它作为 error 参数传入。要用 errorContext.rawError() 访问最初抛出的那个外部异常——当决定如何处理错误的是包装类型(而非原因)时,这很有用。
java
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 的响应、来自错误消息的个人数据等。一旦喂给 LLM,这些内容就可能流入响应、聊天历史、可观测性管道,以及 LLM 提供者的日志。
请配置一个只返回通用消息、或返回对失败的精简/净化描述的 ToolExecutionErrorHandler,底层细节则依赖你自己的日志与可观测性事件。请注意,一旦你配置了处理器,LangChain4j 就不再替你记录工具失败日志——那条日志只为默认行为向你示警而存在。
我们计划在未来的某个版本中把默认值改为 failInvocationUnlessVisibleToLlm():AI 服务调用会失败,除非异常自身说明了可以告诉 LLM 什么。若这一计划中的变更会影响你的用例,请提一个 issue,好让我们在落地前听到你的反馈。
你可以通过在 AI 服务上配置一个 ToolExecutionErrorHandler 来定制这一行为。
推荐(让每个异常自行决定,见下文):
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler(ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm())
.build();严格(任何失败都不会触及 LLM):
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler(ToolExecutionErrorHandler.failInvocation())
.build();让 LLM 对失败作出反应,但不暴露异常消息:
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler((error, errorContext) -> ToolErrorHandlerResult.text("Tool execution failed."))
.build();当前默认(把异常的消息发送给 LLM)——⚠️ 关于暴露敏感数据,见上文警告:
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler(ToolExecutionErrorHandler.sendExceptionMessageToLlm())
.build();与 ToolArgumentsErrorHandler 一样,ToolExecutionErrorHandler 中有两种处理错误的方式:返回一个文本消息,或抛出一个异常。你可以用 errorContext.rawError() 在决定如何处理之前,查看因果解包之前的原始错误。
按异常逐个决定 LLM 看到什么
把每个异常都发给 LLM 是有风险的,而每个异常都失败又剥夺了 LLM 从本可绕过的失败中恢复的能力。通常你想要的是介于两者之间:LLM 应当得知不存在它所编造 ID 的那个订单,但它不该得知你的数据库名叫 ORDERS_V2 且拒绝了该查询。
要表达这一点,让你的异常实现 ToolErrorVisibleToLlm,并在 messageForLlm() 里精确写出应当告诉 LLM 的内容:
java
public class OrderNotFoundException extends RuntimeException implements ToolErrorVisibleToLlm {
private final String orderId;
public OrderNotFoundException(String orderId) {
super("Order " + orderId + " not found");
this.orderId = orderId;
}
@Override
public String messageForLlm() {
return "There is no order with ID " + orderId + ". Ask the user to check the order number.";
}
}若你不想声明一个异常类,那就改抛一个现成的。若你需要原始错误,请自行记录它:它会作为 cause 被保留,但不会被发送给 LLM。
java
@Tool("Returns the status of an order")
String orderStatus(String orderId) {
try {
return orderService.status(orderId);
} catch (SQLException e) {
// the LLM is told only what it needs to know; the cause is not sent to it
log.warn("Could not read the order database", e);
throw ToolErrorVisibleToLlm.from("The order database is temporarily unavailable.", e);
}
}然后配置尊重它的处理器:
java
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(tools)
.toolExecutionErrorHandler(ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm())
.build();现在 OrderNotFoundException 会以 "There is no order with ID ..." 抵达 LLM,而一个 NullPointerException 或一个失败的数据库连接会让 AI 服务调用失败,从而由你而不是 LLM 悄悄向用户道歉来发现它们。
LangChain4j 提供的工具,例如技能、工具搜索与 MCP 工具,会用实现了 ToolErrorVisibleToLlm 的 LlmVisibleToolExecutionException 上报 LLM 能够自行处理的错误(例如缺失参数、未知的技能名),因此用这个处理器,LLM 仍会看到它们。
对于你无法更改的异常——通常是库抛出的那些——请自己编写处理器,在那里决定告诉 LLM 什么:
java
.toolExecutionErrorHandler((error, errorContext) -> {
if (error instanceof EntityNotFoundException) {
return ToolErrorHandlerResult.text("That record does not exist.");
}
throw new RuntimeException(error);
})WARNING
不要把库异常的消息原封不动地传给 LLM:它是写给开发者的,可能含有内部细节,就像你自己异常的消息一样。
NOTE
failInvocationUnlessVisibleToLlm() 正是我们计划在未来某个版本中设为默认的行为。今天就显式配置它,意味着那次变更不会影响你的应用。
补偿工具动作
当一个 AI 服务用多个工具来完成一项任务时,某个工具的失败可能让系统处于不一致状态——有些工具已成功执行,另一些还没有。例如在银行转账中,LLM 可能先给收款人账户入账,随后因余额不足无法从付款人账户出款,导致收款人平白多出一笔钱。
为处理这种情况,你可以启用工具错误补偿。启用后,若任何工具执行失败,所有此前成功、且声明了补偿动作的工具调用会被按逆序自动撤销。
用 @CompensateFor 声明补偿动作
在方法上使用 @CompensateFor 注解,把它声明为某个 @Tool 的补偿动作。value 必须匹配该工具暴露给 LLM 的名称——即其 @Tool(name = ...) 属性(若设置了),否则是 @Tool 方法名(默认被用作工具名)。补偿方法的参数类型必须与该工具完全一致,或接受单个 ToolExecution 参数。
选项 1:相同参数类型 —— 补偿方法接收传给原工具的相同参数:
java
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,可同时访问原始参数与工具的返回值。当撤销某个动作需要信息时很有用
java
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 服务时调用 .compensateOnToolErrors(true):
java
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 注解会被静默忽略,且不执行任何校验。
说明与限制
NOTE
补偿是尽力而为:若某个补偿动作自身抛出异常,它会以 WARN 级别记录,其余的补偿动作继续执行。
NOTE
@CompensateFor 方法不会被暴露给 LLM——它们是内部补偿基础设施,不会出现在工具规范中。
NOTE
补偿动作总是按逆序依次执行,即便工具执行被通过 .executeToolsConcurrently() 配置为并行运行。
NOTE
@CompensateFor 方法可以从父类继承,与 @Tool 方法的发现方式保持一致。
NOTE
@CompensateFor 只对带 @Tool 注解的方法有效。以编程方式或动态定义的工具(例如 MCP 工具、经 ToolSpecification 注册的工具)不受支持。
NOTE
补偿工具动作目前被标记为实验性,未来版本可能演进。
Model Context Protocol(MCP)
你也可以从 MCP 服务器导入工具。更多信息见这里。
NOTE
在异步与响应式 AI 服务模式下,工具的行为不同:它们总是在某个 Executor 上运行,因而默认并发执行(要向 executeToolsConcurrently(Executor) 传入一个单线程 Executor 才能串行化),一个工具执行错误会让调用失败,而工具参数解析错误会被发回给 LLM,且手写的 ToolExecutor 应当实现 executeAsync(...),否则会大声失败而非阻塞线程。参见非阻塞与响应式。