LangChain4j从入门到精通-9-工具调用

本文深入解析了LangChain4j框架中强大的工具调用(Function Calling) 功能,揭秘如何让大语言模型(LLM)突破纯文本生成限制,具备执行外部操作的能力。文章系统对比了底层ChatModel API(手动处理ToolSpecification与ToolExecutionRequest)与高级AI Service API(通过@Tool注解自动转换与执行)两种实现方案,并通过数学计算、网络搜索等实用案例演示了工具集成的完整流程。针对生产环境需求,详细阐述了动态工具加载、并行执行优化、错误处理机制及即时返回策略等高级特性,帮助开发者灵活应对复杂场景。掌握工具调用技术,是构建能“思考”且能“行动”的智能代理应用,实现从对话机器人到自动化工作流跨越的关键一步。

#Java #人工智能 #LangChain4j #工具调用 #AI应用开发

一些大型语言模型除了生成文本外,还能触发操作。

:::注意
并非所有大语言模型对工具的支持程度都相同。模型理解、选择及正确使用工具的能力很大程度上取决于具体模型及其功能特性。有些模型可能完全不支持工具调用,而另一些模型可能需要精心设计的提示词工程或额外的系统指令才能实现。
:::
有一种概念称为“工具”或“函数调用”。
它允许大型语言模型(LLM)在必要时调用一个或多个可用工具,这些工具通常由开发者定义。
工具可以是任何东西:网络搜索、调用外部API,或是执行特定代码片段等。 实际上,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提供了基础的数学工具, 但想象一下,如果我们给它提供诸如googleSearch和sendEmail这样的工具, 以及一个查询请求,比如“我的朋友想了解AI领域的最新动态。将简短摘要发送到friend@email.com”,那么它就可以使用googleSearch工具查找最近的新闻,然后进行摘要,并通过sendEmail工具将摘要通过邮件发送出去。

:::注意
为了提高大语言模型(LLM)使用正确工具和参数的几率, 我们应当提供清晰且明确的:

  • 工具名称
  • 工具功能及使用场景描述
  • 每个工具参数的说明

一个实用的经验法则:如果人类能理解工具的用途及使用方法,那么大型语言模型很可能也能做到。
:::

大型语言模型(LLMs)经过专门微调,能够判断何时调用工具以及如何调用。有些模型甚至可以同时调用多个工具,例如,
OpenAI.

:::注意
请注意,并非所有模型都支持工具功能。
要查看哪些模型支持工具,请参考“工具”列中的页面 this .
:::

两个抽象层次

LangChain4j提供了两个层次的工具使用抽象:

  • 底层实现,使用 ChatModel和 ToolSpecificationAPI
  • 高级别,使用AI服务和@Tool注解的Java方法

底层实现

在底层,你可以使用 ChatModel的 chat(ChatRequest)方法。类似的方法也存在于 StreamingChatModel中。
在创建 ChatRequest时,你可以指定一个或多个 ToolSpecification。
ToolSpecification是一个包含工具所有信息的对象:

  • 工具的名称
  • 工具的描述
  • 工具的参数及其描述
  • 工具的元数据
    默认情况下,元数据不会发送给LLM提供商,您必须在创建ChatModel时明确指定需要发送哪些元数据键名。
    目前,仅langchain4j-anthropic模块支持工具元数据功能。
    当工具由McpToolProvider提供时,metadata字段可包含MCP专用的配置项。 建议尽可能提供关于该工具的详细信息:清晰的名称、全面的描述以及每个参数的说明等。

Creating Tool Specification

有两种方法可以创建一个 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();
  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);

使用 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();

如果大语言模型决定调用工具,返回的 AiMessage将包含 toolExecutionRequests字段的数据。 此时,AiMessage.hasToolExecutionRequests()会返回 true。 根据大语言模型的不同,该字段可能包含一个或多个 ToolExecutionRequest对象(部分大语言模型支持并行调用多个工具)。
每个 ToolExecutionRequest应包含:

  • 工具调用的id。请注意,一些LLM提供商(如Google、Ollama)可能会省略此ID。
  • 工具的name属性表示要调用的工具名称,例如:getWeather
  • 参数: { "city": "London", "temperatureUnit": "CELSIUS" }
    您需要使用来自 ToolExecutionRequest的信息手动执行工具。
    如果你想将工具执行的结果返回给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);

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

  • 可以是静态的或非静态的
  • 可以有任何可见性(公共、私有等)。

工具方法参数

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

  • 原始类型: int, double
  • 引用类型: String, Integer, Double
  • 自定义POJO(可包含嵌套POJO)
  • enum
  • List<T>/Set<T>
  • Map<K,V> (你需要在参数描述中手动指定 K和 V的类型,使用 @P)
    同样支持无参数的方法。
必填和可选

默认情况下,所有工具方法的参数都被视为必填项。 这意味着大语言模型必须为这些参数生成一个值。若要将参数设为可选,可使用注解@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,返回值会原样发送给大语言模型,不进行任何转换。 对于其他返回类型,返回值会先转换为 JSON 字符串,然后再发送给大语言模型。

人工智能服务作为其他人工智能服务的工具

人工智能服务也可以作为其他人工智能服务的工具。这在许多代理应用场景中非常有用,其中一个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可以配置为使用其他三个特定领域的专家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注解的 Java 方法,并且在 AI 服务构建过程中被明确指定的,都可以由 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: 工具的说明。
  • metadata: 一个有效的JSON字符串,其中包含LLM提供商特定的工具元数据条目。
    默认情况下,元数据不会发送给LLM提供商,您必须在创建ChatModel时明确指定需要发送哪些元数据键。 目前,只有langchain4j-anthropic模块支持工具元数据功能。
    根据工具的不同,即使没有任何描述,LLM 也可能很好地理解它 (例如,add(a, b)就很直观),但通常最好提供清晰且有意义的名称和描述。 这样,LLM 就能获得更多信息来决定是否调用给定的工具以及如何调用。

@P

方法参数可以选择性地用 @P进行注解。
@P注解有两个字段

  • value: 参数描述。必填字段。
  • required:该参数是否为必填项,默认为 true。可选字段。

@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) {
  ...
}

:::注意
请注意,在枚举值上使用 @Description注解 无效,且不会包含在生成的 JSON 模式中。

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实现:


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服务的单次调用过程中,数据可以通过 InvocationParameters在AI服务组件之间传递
(例如从一个工具到另一个工具,或从RAG组件传递到工具)。
参数存储在一个可变的、线程安全的 Map中。

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

ToolMemoryId

如果你的AI服务方法有一个用@MemoryId注解的参数, 你也可以在@Tool方法的一个参数上使用@ToolMemoryId注解:

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方法。 如果您有多个用户和/或每个用户有多个聊天/记忆,并希望在@Tool方法中区分它们,此功能非常有用。

同时执行工具

默认情况下,当大语言模型(LLM)同时调用多个工具(也称为并行工具调用)时,AI服务会按顺序执行这些工具。若希望工具能并发执行,您可以在构建AI服务时调用executeToolsConcurrently()或executeToolsConcurrently(Executor)方法。 启用其中任一选项后,工具将使用默认或指定的Executor实现并发执行(存在一个例外情况——详见下文说明)。

使用ChatModel时:
  • 当LLM调用多个工具时,它们会在不同的线程中并发执行
  • 使用Executor
  • 当LLM调用单个工具时,它会在同一个(调用者)线程中执行,不使用Executor以避免浪费资源。
使用StreamingChatModel时:
  • 当LLM调用多个工具时,它们会在单独的线程中通过Executor并发执行。 每个工具一旦在StreamingChatResponseHandler.onCompleteToolCall(CompleteToolCall)被调用后就会立即执行,无需等待其他工具或响应流的完成。
  • 当LLM调用单个工具时,会使用Executor在单独的线程中执行。我们不能在同一个线程中执行它,因为在那个时候,我们还不知道LLM会调用多少个工具。

访问已执行工具

如果你想访问在调用AI服务期间执行的工具,只需将返回类型包装在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
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服务时,工具也可以通过编程方式指定。
这种方法提供了极大的灵活性,因为工具可以从外部来源(如数据库和配置文件)加载。 工具名称、描述、参数名称和描述都可以通过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);

一旦我们拥有一个或多个(ToolSpecification,ToolExecutor)配对,就可以在创建AI服务时指定它们:

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .tools(Map.of(toolSpecification, toolExecutor))
    .build();

此外,我们可以传递一个工具名称列表,这些工具应立即/直接返回其执行结果,而无需发送给大语言模型进行重新处理。

Set<String> immediateReturnToolNames = Set.of("get_booking_details");

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .tools(Map.of(toolSpecification, toolExecutor), immediateReturnToolNames)
    .build();

动态指定工具

在使用AI服务时,工具也可以为每次调用动态指定。

用户可以配置一个ToolProvider,每次调用AI服务时都会触发该配置,它会提供当前请求中应包含给大语言模型(LLM)的工具。

ToolProvider接收一个ToolProviderRequest (其中包含UserMessage、聊天记忆ID和InvocationParameters), 并返回一个ToolProviderResult,该结果以从ToolSpecification到ToolExecutor的Map形式包含工具。
以下是一个示例,展示如何仅在用户消息包含“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();
配置动态工具中的即时返回功能

在构建 ToolProviderResult时,您可以使用 ToolProviderResult.builder() 标记工具以立即返回

ToolProvider toolProvider = (toolProviderRequest) -> {
    return ToolProviderResult.builder()
        .add(bookingToolSpec, bookingExecutor, ReturnBehavior.IMMEDIATE)
        .add(weatherToolSpec, weatherExecutor)
        .build();
};

你也可以按名称标记多个工具:

ToolProvider toolProvider = (toolProviderRequest) -> {
    return ToolProviderResult.builder()
        .addAll(allTools)
        .immediateReturnToolNames(Set.of("get_booking_details", "cancel_booking"))
        .build();
};

AI服务可以在同一次调用中同时使用程序化指定和动态指定的工具。

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

默认情况下,工具执行请求的结果会返回给调用该结果的LLM(大语言模型),由其进一步处理。但在某些情况下,该工具执行请求生成的结果已代表AI服务调用的预期输出。此时,可以配置工具立即/直接返回其结果,跳过LLM不必要的资源消耗型再处理。这可通过配置@Tool注解中的returnBehavior字段实现,示例如下:

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

:::注意
此功能仅支持返回类型为 Result<T>的 AI 服务。若尝试在返回类型不同的 AI 服务上使用该功能,将产生 IllegalConfigurationException异常。
:::
这样,一个类似以下的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,而实际的响应124必须从result.toolExecutions()中获取。如果不立即返回,LLM将不得不重新处理add工具执行请求的结果,从而返回类似这样的响应:37加87的结果是124。 还需注意的是,如果LLM调用了多个工具且其中至少有一个不是即时返回的,那么就会发生重新处理的情况。
还需注意的是,如果大语言模型调用了多个工具且其中至少有一个不是即时响应的,那么系统将重新进行处理。

:::注意
在使用编程工具时,可以通过向.tools()方法传递一组工具名称来标记工具以便立即返回。当通过ToolProvider使用动态工具时,可以在ToolProviderResult.builder()上使用重载方法.add(ToolSpecification, ToolExecutor, ReturnBehavior)。具体示例请参阅前文相关章节。
:::

错误处理

处理工具名称错误

可能会出现这样的情况:大语言模型(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服务将无法执行该工具,因此会因异常而失败。 你可以通过在AI服务上配置ToolArgumentsErrorHandler来自定义此行为。

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

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

  • 抛出异常:这将中断 AI 服务流程。
  • 返回一条文本消息(例如错误描述),该消息将发送回大语言模型(LLM),使其能够做出适当响应(例如纠正错误并重试)。
    以下是第一种方法的示例:
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
}

以下是第二种方法的示例:

Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(chatModel)
        .tools(tools)
        .toolArgumentsErrorHandler((error, errorContext) -> ToolErrorHandlerResult.text("Something is wrong with tool arguments: " + error.getMessage()))
        .build();
处理工具执行错误

默认情况下,当带有 @Tool注解的方法抛出 Exception时,该异常的提示信息(e.getMessage())将作为工具执行结果发送给大语言模型(LLM)。

这使得LLM在认为必要时能够纠正错误并重试。 您可以通过在AI服务上配置 ToolExecutionErrorHandler来自定义此行为。

Assistant assistant = AiServices.builder(Assistant.class)
        .chatModel(chatModel)
        .tools(tools)
        .toolExecutionErrorHandler((error, errorContext) -> ToolErrorHandlerResult.text("Something is wrong with tool execution: " + error.getMessage()))
        .build();

与 ToolArgumentsErrorHandler类似,处理 ToolExecutionErrorHandler中的错误也有两种方式:抛出异常或返回文本消息。

模型上下文协议(MCP)

你也可以从 MCP 服务器导入工具。 tools from MCP server.

相关教程

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐