工具(函数调用)
在一些大语言模型(LLM)中,除了文本生成之外,还具备触发 工具(tools)或函数调用(function calling) 的能力。
这一机制允许 LLM 在需要时调用一个或多个由开发者预先定义的工具。工具的形式可以非常多样,例如执行网页搜索、调用外部 API、运行特定代码等。
需要强调的是,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)
... 此处我们执行 squareRoot 方法,传入参数 "475695037565",得到结果 "689706.486532" ...
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。”
那么 LLM 就可以先调用 googleSearch
工具获取新闻,
再总结内容,最后调用 sendEmail
工具把总结发送过去。
LLM 已经经过专门微调来检测何时以及如何调用工具。
部分模型甚至能一次调用多个工具,例如
OpenAI 的并行函数调用。
两层抽象级别
LangChain4j 提供了两种使用工具的抽象级别:
- 低级:使用
ChatModel
和ToolSpecification
API - 高级:使用 AI Services 及
@Tool
注解的 Java 方法
低级工具 API
在低级别中,你可以使用 ChatModel
的 chat(ChatRequest)
方法。StreamingChatModel
中也有类似的方法。
在创建 ChatRequest
时,可以指定一个或多个 ToolSpecification
。
ToolSpecification
是一个包含工具全部信息的对象:
- 工具的
name
- 工具的
description
- 工具的
parameters
及其描述
建议尽可能提供详尽信息:清晰的名称、完整的描述、每个参数的说明等。
创建 Tool Specification
有两种方式创建 ToolSpecification
:
- 手动创建
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") // 必填字段需要显式指定
.build())
.build();
关于 JsonObjectSchema
的更多信息见 这里。
- 使用辅助方法
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);
使用 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
。
根据模型不同,它可能包含一个或多个 ToolExecutionRequest
对象 (部分 LLM 支持并行调用多个工具)。
每个 ToolExecutionRequest
应包含:
- 工具调用的
id
(注意:某些 LLM 提供商,如 Google、Ollama,可能省略该 ID) - 要调用的工具名称,例如:
getWeather
- 调用参数,例如:
{ "city": "London", "temperatureUnit": "CELSIUS" }
你需要根据 ToolExecutionRequest
手动执行工具。
如果你想把执行结果返回给 LLM,需要为每个 ToolExecutionRequest
创建一个 ToolExecutionResultMessage
,并与之前的所有消息一起发送:
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);
使用 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)
,表示该工具调用的流式输出结束。
以下是单个工具调用的流式输出示例:
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
在更高层的抽象级别,你可以给任意 Java 方法添加 @Tool
注解,并在创建 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
注解的方法可以接受任意数量、类型的参数:
- 基本类型:
int
、double
等 - 对象类型:
String
、Integer
、Double
等 - 自定义 POJO(可包含嵌套 POJO)
enum
枚举List<T>
/Set<T>
,其中T
是上述类型之一Map<K,V>
(需通过@P
明确指定K
和V
的类型)
不带参数的方法也支持。
必填与可选
默认情况下,所有工具方法参数都被视为 必填。
这意味着 LLM 必须为这些参数生成值。
通过在参数上使用 @P(required = false)
,可以将其设为可选:
@Tool
void getTemperature(String location, @P(value = "Unit of temperature", required = false) Unit unit) {
...
}
复杂参数的字段和子字段默认也为 必填。
你可以通过 @JsonProperty(required = false)
将其设为可选:
record User(String name, @JsonProperty(required = false) String email) {}
@Tool
void add(User user) {
...
}
递归参数(例如 Person
类包含 Set<Person> children
字段)
目前仅 OpenAI 支持。
工具方法返回类型
带有 @Tool
注解的方法可以返回任意类型,包括 void
。
- 如果方法返回类型为
void
,则执行成功后会向 LLM 返回"Success"
字符串。 - 如果返回类型为
String
,则原样返回字符串给 LLM。 - 其他返回类型会在返回前自动转换为 JSON 字符串。
将 AI Service 作为其他 AI Service 的工具
AI Service 还可以作为工具供其他 AI Service 使用。
这在许多 Agentic AI 的应用场景中非常有用:
一个 AI Service 可以请求另一个更专业的 AI Service 来完成特定任务。
例如,定义如下 AI Service:
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 Service 作为工具,并根据用户请求的类别,将请求路由到相应的专家。
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");
异常处理
如果一个使用 @Tool
注解的方法抛出了 Exception
,则异常的消息 (e.getMessage()
) 会作为工具执行的结果传递给 LLM。
这样,LLM 就有机会修正错误并选择是否重试。
@Tool
任何带有 @Tool
注解并在构建 AI Service 时显式指定的方法,都可以被 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 的交互,如前文所述。
在这两次交互之间,squareRoot
方法会被自动调用。
@Tool
注解有两个可选字段:
name
: 工具的名称。如果未提供,则方法名会作为工具名。value
: 工具的描述。
根据工具的不同,LLM 可能即使没有描述也能理解它(例如 add(a, b)
很直观),但通常最好提供清晰且有意义的名称和描述。这样 LLM 才能更好地判断是否使用该工具,以及如何使用。
@P
方法的参数可以选择性地使用 @P
注解。
@P
注解有两个字段:
value
: 参数的描述。必填。required
: 参数是否必填,默认为true
。可选。
@Description
类和字段的描述可以使用 @Description
注解来指定:
@Description("要执行的查询")
class Query {
@Description("要选择的字段")
private List<String> select;
@Description("过滤条件")
private List<Condition> where;
}
@Tool
Result executeQuery(Query query) {
...
}
@ToolMemoryId
如果 AI Service 方法中有使用 @MemoryId
注解的参数,那么你也可以在 @Tool
方法的参数上使用 @ToolMemoryId
。
这样,AI Service 方法提供的值会自动传递给 @Tool
方法。
这在多用户/多会话记忆的场景中非常有用,可以区分不同用户或对话。
并发执行工具
默认情况下,当 LLM 同时调用多个工具时(即并行工具调用),AI Service 会顺序执行这些工具。
如果你希望工具并发执行,可以在构建 AI Service 时调用:
executeToolsConcurrently()
- 或
executeToolsConcurrently(Executor)
启用后,工具会并发执行(有一个例外,见下文),使用默认或指定的 Executor
。
使用 ChatModel
:
- 当调用多个工具时,它们会并发执行,在不同线程中运行。
- 当调用单个工具时,它会在当前线程中执行,不使用
Executor
,以避免浪费资源。
使用 StreamingChatModel
:
- 当调用多个工具时,它们会并发执行,在不同线程中运行。
每个工具在StreamingChatResponseHandler.onCompleteToolCall(CompleteToolCall)
被调用时立即执行,
不必等待其他工具或响应流完成。 - 当调用单个工具时,它也会在独立线程中执行(因为此时尚不确定 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();
在流式模式中,可以通过 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("返回预订详情")
.parameters(JsonObjectSchema.builder()
.properties(Map.of(
"bookingNumber", JsonStringSchema.builder()
.description("预订编号,格式为 B-12345")
.build()
))
.build())
.build();
每个 ToolSpecification
需要配合一个 ToolExecutor
,用于处理 LLM 生成的工具执行请求:
ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
Map<String, Object> arguments = fromJson(toolExecutionRequest.arguments());
String bookingNumber = arguments.get("bookingNumber").toString();
Booking booking = getBooking(bookingNumber);
return booking.toString();
};
然后将 (ToolSpecification, ToolExecutor)
配对传递给 AI Service:
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.tools(Map.of(toolSpecification, toolExecutor))
.build();
动态指定工具
在 AI Services 中,工具也可以动态指定,即在每次调用时由 ToolProvider
决定。
ToolProvider
会接收一个 ToolProviderRequest
,其中包含 UserMessage
和聊天记忆 ID,返回一个 ToolProviderResult
,其中包含工具的映射。
示例:仅当用户消息包含 "booking" 时,才添加 get_booking_details
工具:
ToolProvider toolProvider = (toolProviderRequest) -> {
if (toolProviderRequest.userMessage().singleText().contains("booking")) {
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("get_booking_details")
.description("返回预订详情")
.parameters(JsonObjectSchema.builder()
.addStringProperty("bookingNumber")
.build())
.build();
return ToolProviderResult.builder()
.add(toolSpecification, toolExecutor)
.build();
} else {
return null;
}
};
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();
AI Service 可以同时使用编程方式和动态方式指定工具。
工具幻觉处理策略
LLM 有时可能会“幻觉”出不存在的工具(即调用了未定义的工具)。
默认情况下,LangChain4j 会抛出异常。
但你可以配置一个策略,决定在遇到不存在的工具时返回什么结果。
例如,可以返回一条错误信息,引导 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();
模型上下文协议 (MCP)
你还可以从 MCP 服务器 导入工具。
更多信息请参考 这里。