工具(函数调用)
一些 LLM 除了生成文本外,还可以触发操作。
有一个被称为”工具”或”函数调用”的概念。 它允许 LLM 在必要时调用一个或多个可用的工具,通常由开发者定义。 工具可以是任何东西:网络搜索、调用外部 API 或执行特定代码片段等。 LLM 实际上不能自己调用工具;相反,它们在响应中表达调用特定工具的意图(而不是以纯文本形式响应)。 作为开发者,我们应该使用提供的参数执行这个工具,并将工具执行的结果反馈回来。
例如,我们知道 LLM 本身在数学计算方面并不擅长。 如果您的用例涉及偶尔的数学计算,您可能希望为 LLM 提供一个”数学工具”。 通过在请求中向 LLM 声明一个或多个工具, 如果它认为合适,它可以决定调用其中一个工具。 给定一个数学问题和一组数学工具,LLM 可能会决定为了正确回答问题, 它应该首先调用提供的数学工具之一。
让我们看看这在实践中是如何工作的(有工具和没有工具的情况):
没有工具的消息交换示例:
1 | 请求: |
接近但不正确。
使用以下工具的消息交换示例:
1 |
|
1 | 请求 1: |
如您所见,当 LLM 可以访问工具时,它可以在适当的时候决定调用其中一个工具。
这是一个非常强大的功能。 在这个简单的例子中,我们给了 LLM 基本的数学工具, 但想象一下,如果我们给它提供了例如 googleSearch 和 sendEmail 工具, 以及一个查询,如”我的朋友想知道 AI 领域的最新消息。将简短摘要发送到 friend@email.com“, 那么它可以使用 googleSearch 工具查找最新消息, 然后总结并通过 sendEmail 工具发送摘要。
备注
为了增加 LLM 调用正确工具并使用正确参数的可能性, 我们应该提供清晰明确的:
- 工具名称
- 工具功能描述以及何时应该使用它
- 每个工具参数的描述
一个好的经验法则是:如果人类能够理解工具的目的和使用方法, 那么 LLM 很可能也能理解。
LLM 经过专门的微调,以检测何时调用工具以及如何调用它们。 有些模型甚至可以同时调用多个工具,例如, OpenAI。
请注意,并非所有模型都支持工具。 要查看哪些模型支持工具,请参考此页面上的”工具”列。
两个抽象级别
LangChain4j 提供了两个抽象级别来使用工具:
- 低级别,使用
ChatLanguageModel和ToolSpecificationAPI - 高级别,使用 AI 服务和带有
@Tool注解的 Java 方法
低级工具 API
低级API的函数调用,本质分三步:
1 | 1️⃣ 告诉模型:我有哪些函数(Tool / Function Schema) |
在低级别,您可以使用 ChatLanguageModel 的 chat(ChatRequest) 方法。 StreamingChatLanguageModel 中也存在类似的方法。
您可以在创建 ChatRequest 时指定一个或多个 ToolSpecification。
ToolSpecification 是一个包含工具所有信息的对象:
- 工具的
name(名称) - 工具的
description(描述) - 工具的
parameters(参数)及其描述
建议提供尽可能多的工具信息: 清晰的名称、全面的描述以及每个参数的描述等。
创建 ToolSpecification 有两种方式:
- 手动创建
1 | ToolSpecification toolSpecification = ToolSpecification.builder() |
您可以在这里找到有关 JsonObjectSchema 的更多信息
- 使用辅助方法:
ToolSpecifications.toolSpecificationsFrom(Class)ToolSpecifications.toolSpecificationsFrom(Object)ToolSpecifications.toolSpecificationFrom(Method)
1 | class WeatherTools { |
一旦您有了 List<ToolSpecification>,您可以调用模型:
1 | ChatRequest request = ChatRequest.builder() |
如果 LLM 决定调用工具,返回的 AiMessage 将在 toolExecutionRequests 字段中包含数据。 在这种情况下,AiMessage.hasToolExecutionRequests() 将返回 true。 根据 LLM 的不同,它可以包含一个或多个 ToolExecutionRequest 对象 (一些 LLM 支持并行调用多个工具)。
每个 ToolExecutionRequest 应包含:
- 工具调用的
id(某些 LLM 不提供) - 要调用的工具的
name,例如:getWeather arguments(参数),例如:{ "city": "London", "temperatureUnit": "CELSIUS" }
您需要使用 ToolExecutionRequest 中的信息手动执行工具。
如果您想将工具执行的结果发送回 LLM, 您需要创建一个 ToolExecutionResultMessage(每个 ToolExecutionRequest 对应一个) 并将其与所有先前的消息一起发送:
1 | String result = "预计明天伦敦会下雨。"; |
案例: 低级API实现 Function Call
1 | //工具函数 |

查看后台,可以发现 LLM 决定调用工具,并传入参数 num : 1

换个提示词呢?

高级工具 API
在高级抽象层面,您可以使用 @Tool 注解任何 Java 方法, 并在创建 AI 服务时指定它们。
AI 服务会自动将这些方法转换为 ToolSpecification, 并在每次与 LLM 交互的请求中包含它们。 当 LLM 决定调用工具时,AI 服务将自动执行相应的方法, 并将方法的返回值(如果有)发送回 LLM。 您可以在 DefaultToolExecutor 中找到实现细节。
几个工具示例:
1 |
|
工具方法限制
带有 @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) 注解使参数成为可选的:
1 |
|
复杂参数的字段和子字段默认也被视为***必需的***。 您可以通过使用 @JsonProperty(required = false) 注解使字段成为可选的:
1 | record User(String name, @JsonProperty(required = false) String email) {} |
工具方法返回类型
带有 @Tool 注解的方法可以返回任何类型,包括 void。 如果方法的返回类型是 void,则在方法成功返回时会向 LLM 发送”Success”字符串。
如果方法的返回类型是 String,则返回值会原样发送给 LLM,不进行任何转换。
对于其他返回类型,返回值会在发送给 LLM 之前转换为 JSON 字符串。
异常处理
如果带有 @Tool 注解的方法抛出 Exception, 异常的消息(e.getMessage())将作为工具执行的结果发送给 LLM。 这允许 LLM 纠正其错误并在认为必要时重试。
@Tool
任何带有 @Tool 注解的 Java 方法, 并且在构建 AI 服务时_明确_指定,都可以由 LLM 执行:
1 | interface MathGenius { |
当调用 ask 方法时,会发生 2 次与 LLM 的交互,如前面部分所述。 在这些交互之间,squareRoot 方法会被自动调用。
@Tool 注解有 2 个可选字段:
name:工具名称。如果未提供,方法名将作为工具名称。value:工具描述。
根据工具的不同,LLM 可能即使没有任何描述也能很好地理解它 (例如,add(a, b) 是显而易见的), 但通常最好提供清晰有意义的名称和描述。 这样,LLM 有更多信息来决定是否调用给定的工具,以及如何调用。
@P
方法参数可以选择使用 @P 注解。
@P 注解有 2 个字段
value:参数描述。必填字段。required:参数是否必需,默认为true。可选字段。
@Description
类和字段的描述可以使用 @Description 注解指定:
1 |
|
备注
请注意,放在 enum 值上的 @Description 没有效果
@ToolMemoryId
如果您的 AI 服务方法有一个带有 @MemoryId 注解的参数, 您也可以使用 @ToolMemoryId 注解 @Tool 方法的参数。 提供给 AI 服务方法的值将自动传递给 @Tool 方法。 如果您有多个用户和/或每个用户有多个聊天/记忆, 并希望在 @Tool 方法内区分它们,这个功能很有用。
访问已执行的工具
如果您希望访问在调用 AI 服务期间执行的工具, 您可以通过将返回类型包装在 Result 类中轻松实现:
1 | interface Assistant { |
在流式模式下,您可以通过指定 onToolExecuted 回调来实现:
1 | interface Assistant { |
以编程方式指定工具
使用 AI 服务时,也可以以编程方式指定工具。 这种方法提供了很大的灵活性,因为工具可以从外部源(如数据库和配置文件)加载。
工具名称、描述、参数名称和描述 都可以使用 ToolSpecification 配置:
1 | ToolSpecification toolSpecification = ToolSpecification.builder() |
对于每个 ToolSpecification,需要提供一个 ToolExecutor 实现, 该实现将处理 LLM 生成的工具执行请求:
1 | ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> { |
一旦我们有了一个或多个(ToolSpecification,ToolExecutor)对, 我们可以在创建 AI 服务时指定它们:
1 | Assistant assistant = AiServices.builder(Assistant.class) |
动态指定工具
使用 AI 服务时,也可以为每次调用动态指定工具。 可以配置一个 ToolProvider,它将在每次调用 AI 服务时被调用, 并提供应包含在当前 LLM 请求中的工具。 ToolProvider 接受一个包含 UserMessage 和聊天记忆 ID 的 ToolProviderRequest, 并返回一个包含工具的 ToolProviderResult,形式为从 ToolSpecification 到 ToolExecutor 的 Map。
以下是一个示例,说明如何仅在用户消息包含”booking”一词时添加 get_booking_details 工具:
1 | ToolProvider toolProvider = (toolProviderRequest) -> { |
AI 服务可以在同一次调用中同时使用以编程方式指定和动态指定的工具。
案例:高级API实现 Function Call
1 | //提供给大模型的工具类 |
效果对比:
- 未使用 Function call

- 使用 Function call
