一、前言

本系列仅做个人笔记使用,绝大部分内容基于 LangChain4j 官网 ,内容个人做了一定修改,可能存在错漏,一切以官网为准。


本系列使用 LangChain4j 版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.8.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

本系列完整代码地址 :langchain4j-hwl


传统LLM的核心输出是文本,但“工具调用”让其突破这一局限——能在需要时关联外部功能(即“工具”)。这些工具由开发者定义,形式灵活,比如网页搜索、调用第三方API(如天气查询接口)、执行特定代码(如数学计算脚本)等,本质是让LLM成为“连接AI与外部功能的桥梁”。

LLM本身无法主动触发工具运行,而是通过响应传递“要调用哪个工具、用什么参数”的意图(而非输出纯文本回答)。整个流程需开发者参与:

  • 开发者预先定义工具(如“计算平方根”的代码函数);
  • 用户提问后,LLM判断需用工具,在响应中明确“调用平方根工具,参数为XXX”;
  • 开发者接收该意图,执行工具并获取结果,再将结果反馈给LLM;
  • LLM基于工具结果,最终生成自然语言回答。

需要注意的是 :并非所有LLM都支持 Tool 调用,能力有别,可以从官网提供的地址查看具体支持情况 :https://docs.langchain4j.dev/integrations/language-models


LangChain4j 为 Tools 的设计的两层抽象,以适配不同开发场景的灵活性与效率需求:

  • Low-Level Tool :基于 ChatModel 和 ToolSpecification API,手动定义工具的「元信息」+ 手动处理工具调用流程,对工具的控制粒度最细。
  • High-Level Tool :基于 AI Services 和 @Tool 注解。通过注解简化工具定义,框架自动完成「工具规范转换」和「调用流程管理」,降低开发成本。

二、Low-Level Tool

1. 介绍

Low-Level Tool 调用的起点是两个模型类的方法,负责接收请求并处理工具相关逻辑:

  • ChatModel:提供 chat(ChatRequest) 方法,用于非流式的工具调用交互(即一次性获取完整响应,包括工具调用意图或结果)。
  • StreamingChatModel:提供与 ChatModel 逻辑相似的方法,但支持流式响应(比如工具调用的参数会分片段返回,而非一次性完整返回),适合需要实时反馈的场景。

要让模型“知道可用工具”,需在创建 ChatRequest(模型请求对象)时,传入工具的详细定义——即 ToolSpecification(工具规格对象):

  • ToolSpecification的核心构成:它是工具的“说明书”,必须包含三类信息:
    1. name :让模型明确调用的工具标识;
    2. description :告诉模型该工具的用途、适用场景;
    3. parameters :定义调用工具时需传入的参数格式、要求。

我们需要尽可能完善ToolSpecification的信息,原因是: LLM(大语言模型)无法直接“理解”工具功能,只能通过名称、描述等文本信息判断“是否需要调用该工具”“如何传参”。信息越清晰(如明确参数是否必填、格式要求),模型就越难出现“调用错误工具”“传参格式错误”等问题,工具调用的准确性会显著提升。


2. ToolSpecification 的创建

创建ToolSpecification的两种方式解析如下:

  1. 手动创建

    • 直接通过ToolSpecification.builder()构建器手动设置工具的名称(如"getWeather")、描述(如返回指定城市的天气预报)

    • 通过JsonObjectSchema定义参数

    • 这种方式需要开发者手动配置所有工具细节,灵活性高但代码量较大

    • 官网示例如下:

      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"))
              // 设置这个工具的 city 参数是必填的
              .required("city") // the required properties should be specified explicitly
              .build())
          .build();
      
  2. 使用辅助方法

    • 借助ToolSpecifications类提供的静态方法(如从类、对象或方法生成)

    • 通过在方法上添加@Tool注解定义工具描述,在参数上用@P注解说明参数含义

    • 示例中WeatherTools类的getWeather方法被注解为工具,通过toolSpecificationsFrom(WeatherTools.class)可自动生成对应的ToolSpecification

    • 这种方式通过注解简化配置,减少重复代码,更符合面向对象编程风格

    • 官网示例如下:

      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);
      

两种方式均可生成工具规范,手动方式适合简单场景或需要精细控制的情况,辅助方法更适合基于现有类和方法快速生成工具定义。当我们创建好 ToolSpecification 之后就可以直接在 ChatModel 或者 StreamingChatModel 中直接使用,如下:

2.2.1 ChatModel 方式调用

ChatModel 可以通过如下方式调用 Tool :

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 在执行过程并会自己判断是否需要调用 Tool,若模型认为需要调用工具(比如“查天气”无法直接回答,必须用工具获取实时数据),则AiMessagetoolExecutionRequests字段会包含调用数据,我们可以通过AiMessage.hasToolExecutionRequests()快速判断是否存在调用请求(返回true即需要执行工具)。

每个ToolExecutionRequest包含3个核心信息:

  • Tool 调用的id :一些大语言模型提供商(例如谷歌、Ollama)可能会省略此ID。
  • Tool 名称 :如上面例子中的 getWeather
  • Tool 调用参数 :(如{"city":"London","temperatureUnit":"CELSIUS"}),这些信息是后续执行工具的关键依据。

如果你想将工具执行的结果发送回大语言模型,你需要创建一个ToolExecutionResultMessage(每个ToolExecutionRequest对应一个),并将其与所有先前的消息一同发送:

// 假设这是 Tool 调用返回的结果
String result = "It is expected to rain in London tomorrow.";
// 封装ToolExecutionResultMessage 
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
// 作为上下文发起新的请求
ChatRequest request2 = ChatRequest.builder()
        .messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
        .toolSpecifications(toolSpecifications)
        .build();
ChatResponse response2 = model.chat(request2);

2.2.2 StreamingChatModel 方式调用

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 onPartialToolCall(PartialToolCall partialToolCall) {
        System.out.println("onPartialToolCall: " + partialToolCall);
    }

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

onPartialToolCall(PartialToolCall) 回调通常会被多次调用,之后最终会调用 onCompleteToolCall(CompleteToolCall) 回调,这表明该工具调用的流式传输已完成。

并非所有大语言模型提供商都会流式传输部分工具调用。有些提供商(例如Bedrock、Google、Mistral、Ollama)仅返回完整的工具调用。在这种情况下,onPartialToolCall回调不会被调用,只会调用onCompleteToolCall。

3. 简单示例

如下是一个简单示例, :

  1. 创建一个 天气工具类

    public class WeatherTools {
    
        @Tool("返回给定城市的天气")
        public String getWeather(
                @P("应返回天气的城市") String city,
                TemperatureUnit temperatureUnit) {
    
            if (city.equals("北京")) {
                if (temperatureUnit == TemperatureUnit.CELSIUS) {
                    return "25°C";
                } else if (temperatureUnit == TemperatureUnit.FAHRENHEIT) {
                    return "77°F";
                } else {
                    return "未知温度单位";
                }
            } else if (city.equals("上海")) {
                if (temperatureUnit == TemperatureUnit.CELSIUS) {
                    return "28°C";
                } else if (temperatureUnit == TemperatureUnit.FAHRENHEIT) {
                    return "82°F";
                } else {
                    return "未知温度单位";
                }
            } else {
                return "未支持的城市";
            }
    
        }
    }
    
  2. 发起 LLM 调用,过程中调用 WeatherTools

        @Override
        public String chatByLowLevelApi(String userMessage) {
            List<ChatMessage> chatMessages = new ArrayList<>();
            chatMessages.add(UserMessage.from(userMessage));
    
            // 1. 构建 Tool 信息
            WeatherTools weatherTools = new WeatherTools();
            List<ToolSpecification> toolSpecifications =
                    ToolSpecifications.toolSpecificationsFrom(weatherTools);
            ChatRequest request = ChatRequest.builder()
                    .messages(chatMessages)
                    .toolSpecifications(toolSpecifications)
                    .build();
            // 2. 发起第一次 LLM 调用
            ChatResponse response = chatModel.chat(request);
            AiMessage aiMessage = response.aiMessage();
            // 3. 如果需要调用 tool,需要执行 tool 调用
            if (aiMessage.hasToolExecutionRequests()) {
                // 将AI的工具调用消息添加到消息列表(作为工具结果的前置消息),
                // 否则会提示 Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.",
                chatMessages.add(aiMessage);
                // 4. 执行工具调用
                for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) {
                    ToolExecutor toolExecutor = new DefaultToolExecutor(weatherTools, toolExecutionRequest);
                    String result = toolExecutor.execute(toolExecutionRequest, "1");
                    ToolExecutionResultMessage toolExecutionResultMessages =
                            ToolExecutionResultMessage.from(toolExecutionRequest, result);
                    chatMessages.add(toolExecutionResultMessages);
                }
    
                // 5. 将工具执行结果作为新的消息,发起 LLM 调用
                ChatRequest chatRequest2 = ChatRequest.builder()
                        .messages(chatMessages)
                        .parameters(ChatRequestParameters.builder()
                                .toolSpecifications(toolSpecifications)
                                .build())
                        .build();
                ChatResponse finalChatResponse = chatModel.chat(chatRequest2);
                return finalChatResponse.aiMessage().text();
            } else {
                // 不需要调用工具直接返回
                return aiMessage.text();
            }
        }
    

Low-Level 的调用需要我们自己执行 Tool 逻辑,灵活性更高,但也更加繁琐,而 High-Level 则更加简单一些。

三、High-Level Tool

LangChain4j通过@Tool注解,将普通Java方法“升级”为LLM可调用的工具,无需开发者手动编写工具描述、参数映射等底层逻辑。

  • 开发者只需给目标Java方法加@Tool注解(可写工具功能描述),并在创建 AI Service 时指定这些方法;
  • AI Service 会自动把带@Tool的方法转为ToolSpecification(包含工具名、描述、参数信息的标准格式),每次和LLM交互时自动携带这些工具信息;
  • 当LLM判断需要调用工具时,AI Service 会自动执行对应的Java方法,并把方法返回值回传给LLM (具体实现在 DefaultToolExecutor 中),无需开发者手动触发执行。

1. @Tool

  • @Tool 修饰的方法可以是静态或非静态,也可以可以具有任何可见性(公共、私有等)

  • @Tool 修饰的方法可以接受任意数量的各种类型的参数

  • 默认情况下,@Tool 修饰的方法参数都被视为必填项。这意味着大语言模型必须为这类参数生成一个值。通过使用@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) {
        ...
    }
    
  • 使用@Tool注解的方法可以返回任何类型,包括void。

    • 如果该方法的返回类型为void,那么当方法成功返回时,会向大语言模型发送“Success”字符串。
    • 如果该方法的返回类型为String,则返回值会原样发送给大语言模型,不进行任何转换。
    • 对于其他返回类型,返回值在发送给大语言模型之前会被转换为JSON字符串。

2. AI services Tool

LangChain4J 允许将一个 AI service(如专注医疗咨询的服务)封装成 “工具”,供另一个 AI service(如负责需求分发的服务)调用,形成 “主服务 - 工具服务” 的协作模式,而非各自独立运行,如下:

public interface LegalExpert {

    @UserMessage("""
                你是一名法律专家。
                请从法律角度分析以下用户请求,并提供尽可能好的答案。
                用户请求是{{request}}。
            """)
    @Tool("一名法律专家")
    String legal( String request);
}

public interface MedicalExpert {

    @UserMessage("""
                你是一名医学专家。
                请从医学角度分析以下用户请求,并提供尽可能好的答案。
                用户请求是{{request}}。
            """)
    @Tool("一名医学专家")
    String medical(String request);
}

public interface TechnicalExpert {

    @UserMessage("""
                你是一名技术专家。
                请从技术角度分析以下用户请求,并提供尽可能好的答案。
                用户请求是{{request}}。
            """)
    @Tool("一名技术专家")
    String technical(String request);
}

public interface ExpertRouterAgent {
    String ask(String request);
}


...

    @Bean
    public ExpertRouterAgent expertRouterAgent(ChatModel chatModel) {
        MedicalExpert medicalExpert = AiServices.builder(MedicalExpert.class)
                .chatModel(chatModel)
                .build();
        LegalExpert legalExpert = AiServices.builder(LegalExpert.class)
                .chatModel(chatModel)
                .build();
        TechnicalExpert technicalExpert = AiServices.builder(TechnicalExpert.class)
                .chatModel(chatModel)
                .build();

        return AiServices.builder(ExpertRouterAgent.class)
                .chatModel(chatModel)
                .tools(medicalExpert, legalExpert, technicalExpert)
                .build();
    }

在实际调用过程中 ExpertRouterAgent 会根据用户实际请求的不同,自动执行不同的 Tools (medicalExpert, legalExpert, technicalExpert),并将结果返回。

这种实现方式存在一定缺陷:

  • 这种实现方式要求 LLL 将用户请求原封不动地复制粘贴作为工具调用,而这可能是一个容易出错的操作。
  • 像调用任何其他工具一样,将另一个大语言模型作为工具调用的大语言模型必须重新处理其响应,这在时间和消耗的令牌方面都可能是一种浪费性的计算。
  • 作为一项完全独立的人工智能服务,智能体工具无法访问调用它的智能体的聊天记忆,因此无法利用聊天记忆来提供更全面的答案。

3. Tool 的核心类

3.1 @Tool

@Tool 注解的核心作用是 标记“可被LLM调用的工具方法”,任何带有@Tool注解且在AI服务构建期间被明确指定的Java方法,都可以由大语言模型执行

@Tool 注解有两个属性:

  • name:工具的自定义名称,不填则默认用方法名(如squareRoot方法默认工具名为squareRoot);
  • value:工具的功能描述(如给add加描述"计算两个整数的和"),虽简单工具(如add(a,b))可省略,但复杂工具需清晰描述,帮助LLM判断“是否该调用此工具”。

在使用 Tool 时,一般会调用多次 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.

当调用mathGenius.ask("求475695037565的平方根")时,流程如下:

  1. 第一次LLM交互:AI服务将“用户问题+可用工具列表(addsquareRoot)”发给LLM,LLM分析后判断“需要调用squareRoot工具”,并返回“工具调用指令”(含参数475695037565);
  2. 工具自动执行:LangChain4j接收LLM的指令,自动调用Calculator类的squareRoot方法,计算出结果(689706.486532);
  3. 第二次LLM交互:AI服务将“工具执行结果”回传给LLM,LLM将结果整理成自然语言(如"475695037565的平方根是689706.486532"),最终返回给用户。

3.2 @P

@P仅用于被@Tool注解的工具方法的参数上,目的是向LLM传递参数的关键信息——比如参数的含义、是否必须提供值,避免LLM因对参数理解模糊而错误调用工具(例如遗漏必填参数、传入不符合预期的参数)。该注解具备两个属性

  • value(必填字段):用于描述参数的“用途”或“含义”,必须手动填写。
    例:若工具方法是getWeather(String city),给city参数加@P("需要查询天气的城市名称,如北京、London"),LLM就能明确该参数需传入“城市名”,而非其他内容(如日期)。
    为什么必填? 因为LLM无法直接识别参数名(如city)的具体语义,需要通过自然语言描述明确其作用。
  • required(可选字段,默认true:控制参数是否为“必填项”,不填时默认该参数必须传入值。
    例:若工具方法参数是@P(value = "温度单位(如摄氏度/华氏度)", required = false) String unit,则LLM可选择不传入unit(此时工具可能用默认单位);若不写required,则LLM必须为unit提供值,否则工具调用会出错。

通过@P注解可以给LLM提供“参数使用说明书”,减少LLM调用工具时的参数错误,确保工具能按预期接收有效输入,最终提升工具调用的准确性。

3.3 @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 enum值上的内容无效,且不会包含在生成的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
}

3.4 InvocationParameters

InvocationParameters是在调用AI服务时向工具传递额外数据的机制,这些数据不会被大语言模型(LLM)知晓,仅在开发层面可见和使用。这一机制为AI服务调用提供了灵活的上下文传递能力,同时避免了向LLM暴露不必要的实现细节。

  1. 使用方式

    • 在AI服务接口(如Assistant)的方法中添加InvocationParameters参数
    • 在工具方法(如getWeather)中同样声明该参数,即可获取传递的数据
    • 通过InvocationParameters.from(Map)创建参数实例并传入
  2. 应用场景:如下示例中通过传递userId,工具可以获取该用户的偏好设置(如温度单位),从而返回个性化的天气信息。

    
    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);
    
  3. 扩展特性

    • 可在多个AI服务组件中访问(如ToolProvider、错误处理器、RAG组件等)
    • 基于线程安全的Map存储,支持在单次调用中实现组件间的数据传递
    • 数据可在工具之间、RAG组件与工具之间流转

3.5 @ToolMemoryId

@ToolMemoryId 的作用是实现AI服务方法与工具方法之间的内存ID传递,用于多用户或多会话场景下的上下文区分。

  1. 工作流程

    • 在AI服务接口(Assistant)的方法参数中,用@MemoryId标记内存ID参数

    • 在工具类(Tools)的@Tool方法中,用@ToolMemoryId标记对应的参数

    • 调用AI服务方法时传入的内存ID(如示例中的"12345")会自动传递到工具方法中

    • 示例如下:

      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");
      
  2. 实际价值

    • 当系统需要处理多个用户或同一用户的多个聊天会话时
    • 工具方法可以通过这个内存ID区分不同的上下文
    • 便于实现会话隔离(如存储/读取不同用户的日历事件)

这种机制简化了多会话场景下的上下文管理,无需手动传递会话标识即可在工具层面实现精准的上下文区分。

4. Tool 的扩展用法

4.1 Tool 的并行调用

当大语言模型(LLM)同时调用多个工具(并行工具调用)时, AI Service默认按顺序执行工具。而构建 AI Service 时,调用以下任一方法可启用工具并发执行,工具将使用默认或指定的Executor执行(存在例外情况):

  • executeToolsConcurrently()
  • executeToolsConcurrently(Executor)

如下:

  // 创建 AI Service 服务
  return AiServices.builder(DynamicAssistant.class)
          .chatModel(chatModel)
          // 声明在执行 tool 时使用 executor 并发执行。
          .executeToolsConcurrently(executor)
          .toolProvider(toolProvider)
          .build();

需要注意的是,在通过 StreamingChatModel (流式调用) 和 ChatModel (非流式调用)的方式来发起一个或多个 Tool 调用时, LangChain4j 的处理并不一致,具体如下:

  • ChatModel (非流式调用) :
    • 调用多个 Tool:Tool在独立线程中通过Executor并发执行。
    • 调用单个 Tool :在调用方线程中执行,不使用Executor,避免资源浪费。
  • StreamingChatModel(流式调用):
    • 调用多个Tool:在独立线程中通过Executor并发执行,StreamingChatResponseHandler.onCompleteToolCall(CompleteToolCall)被调用后,Tool立即执行,无需等待其他Tool或响应流完成。
    • 调用单个Tool:通过Executor在单独线程中执行,因无法提前知晓 LLM 将调用的Tool数量,故不能在调用方线程执行。

对于流式调用来说,工具的执行都是独立线程。

4.2 Tools 的执行过程

当 AI 服务(如示例中的 Assistant)调用工具(比如“取消预订”的工具)时,若想获取“工具执行了哪些操作、传入了什么参数、返回了什么结果”,只需将 AI 服务方法的返回类型从普通类型(如 String)改为 Result<T>T 是原返回类型,示例中为 String)。

当你想获取 AI Service 执行过程中调用的 Tool 的详细信息,如:工具执行了哪些操作、传入了什么参数、返回了什么结果。可以将 AIService 服务接口的返回类型改为 dev.langchain4j.service.Result<T>,在 Result 中则包含这些信息。如下:

	public interface CalculatorAssistant {
    	// 返回类型需要是 Result
    	Result<String> chat(String userMessage);
	}

	...
	
    @Override
    public String calculator(String userMessage) {
        Result<String> result = calculatorAssistant.chat(userMessage);
        // 从 result 中获取工具执行的信息
        List<ToolExecution> toolExecutions = result.toolExecutions();
		// 获取调用的第一个 Tool
        ToolExecution toolExecution = toolExecutions.get(0);
        ToolExecutionRequest request = toolExecution.request();
        // 获取工具执行结果的“原始对象”
        Object resultObject = toolExecution.resultObject();
        // 结果需要从 toolExecutions 中获取 :获取工具执行结果的“文本形式”
        return toolExecution.result();
    }

而在流式模式下,AI Service 返回 TokenStream,而非一次性结果,工具执行记录无法通过 Result 类一次性获取,因此通过 onToolExecuted 回调函数,在 Tool 执行完成时“实时捕获”其执行详情。如下:

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

	...
	
    @Bean
    public StreamCalculatorAssistant streamCalculatorAssistant() {
        return AiServices.builder(StreamCalculatorAssistant.class)
                .streamingChatModel(aliQwenStreamingChatModel())
                .tools(new CalculatorWithImmediateReturn())
                .build();
    }

	
	...

	public interface StreamCalculatorAssistant {
	    // 返回类型是 TokenStream
	     TokenStream chat(String userMessage);
	}

	...

    @Override
    public Flux<String> calculatorStream(String userMessage) {
        return Flux.create(emitter ->
                streamCalculatorAssistant.chat(userMessage)
                        // 打印 Tool 信息 :当某个工具执行完成时,会自动触发此回调,参数 `toolExecution` 包含该工具的调用详情(同普通模式的 `ToolExecution`,可获取请求、结果等
                        
                        .onToolExecuted(toolExecution -> emitter.next(toolExecution.result()))
                        .onPartialResponse(emitter::next)
                        .onCompleteResponse(completeResponse -> emitter.complete())
                        .onError(emitter::error)
                        .start());
    }

需要注意的是:因为 StreamCalculatorAssistant 使用的工具 CalculatorWithImmediateReturn 被标注了 returnBehavior = ReturnBehavior.IMMEDIATE 属性,所以其调用结果不会再交由 LLM 处理,因此这里的 onPartialResponse(emitter::next) 并不会被调用

ReturnBehavior.IMMEDIATE 的内容在本文 【Tool 的结果处理】部分有所说明。

4.3 Tool 的动态选择

4.3.1 ToolSpecification

在使用 AI Services 时,我们可以直接通过 ToolSpecification 来指定工具,Tool 名称、描述、参数等信息都可以由 ToolSpecification 进行配置,而对于每个ToolSpecification,都需要提供一个ToolExecutor实现,用于处理由大语言模型生成的工具执行请求。

某些特殊场景下,所使用的 Tool 的信息可能需要从配置文件,数据库等场景动态加载,此时便可以使用 ToolSpecification 来动态指定 Tool

示例如下:

    @Bean
    public BookAssistant bookAssistant(ChatModel chatModel) {
        // 自定义工具
        ToolSpecification toolSpecification = ToolSpecification.builder()
                .name("bookDetail")
                .description("返回书籍详情")
                .parameters(JsonObjectSchema.builder()
                        .addProperties(Map.of(
                                "bookName", JsonStringSchema.builder()
                                        .description("书籍名称")
                                        .build()
                        ))
                        .build())
                .build();

        // 自定义工具执行的逻辑
        ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
            Map<String, Object> arguments = fromJson(toolExecutionRequest.arguments(), Map.class);
            String bookName = arguments.get("bookName").toString();
            // TODO : 自定义数据获取方式
            if ("三国野史".equals(bookName)) {
                return "诸葛亮老婆黄月英是司马懿假扮的";
            }
            return "未找到书籍";
        };
        
        // 创建 AI Service 服务
        return AiServices.builder(BookAssistant.class)
                .chatModel(chatModel)
                .tools(Map.of(toolSpecification, toolExecutor))
                .build();
    }


4.3.2 ToolProvider

上面的内容还不能满足所有场景,有时候我们可能还需要根据用户提问的信息或者 LLM 信息来动态判断选择执行的 Tool,如用户请求内容包含 “A” 时就交由 ATool 执行,包含 “B” 时就交由 BTool 执行,此时我们可以使用 ToolProvider 接口。

当 Ai Service 被调用时会触发 ToolProvider#provideTools 方法,该方法的入参 ToolProviderRequest 包含 UserMessage、MemoryId 和 InvocationParameters 等信息,基于这些信息我们可以判断并决定使用的 Tool 。

示例如下:

    @Bean
    public DynamicAssistant dynamicAssistant(ChatModel chatModel) {
        ToolProvider toolProvider = request -> {
        	// 根据用户请求消息自主选择使用哪个工具
            if (request.userMessage().singleText().contains("野史")) {
                // TODO : 做一下缓存
                // 自定义工具
                ToolSpecification toolSpecification = ToolSpecification.builder()
                        .name("bookDetail")
                        .description("返回书籍详情")
                        .parameters(JsonObjectSchema.builder()
                                .addProperties(Map.of(
                                        "bookName", JsonStringSchema.builder()
                                                .description("书籍名称")
                                                .build()
                                ))
                                .build())
                        .build();

                // 自定义工具执行的逻辑
                ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
                    Map<String, Object> arguments = fromJson(toolExecutionRequest.arguments(), Map.class);
                    String bookName = arguments.get("bookName").toString();
                    // TODO : 自定义数据获取方式
                    if ("三国野史".equals(bookName)) {
                        return "诸葛亮老婆黄月英是司马懿假扮的";
                    }
                    return "未找到书籍";
                };

                return ToolProviderResult.builder()
                        .add(toolSpecification, toolExecutor)
                        .build();
            } else {
                // TODO : 使用其他的工具
            }

            return null;
        };

        // 创建 AI Service 服务
        return AiServices.builder(DynamicAssistant.class)
                .chatModel(chatModel)
                .toolProvider(toolProvider)
                .build();
    }

4.4 Tool 的结果处理

默认情况下, Tool 执行的结果还会交由 LLM 来进行进一步的处理,然后在某些情况下,Tool 返回的结果可能已经是我们所期望的值,没有必要再调用 LLM 进行处理,此时便可以将 Tool 调用的结果直接返回。这种方式可以通过配置 @Tool 注解的returnBehavior 字段来实现。

ReturnBehavior 属性取值有两个,默认为 TO_LLM,即交由 LLM 处理,IMMEDIATE 表示结果直接返回。

如下:

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

...

public interface CalculatorAssistant {
    // 返回类型需要是 Result
    Result<String> chat(String userMessage);
}

...
    @Bean
    public CalculatorAssistant calculatorAssistant(ChatModel chatModel) {
        return AiServices.builder(CalculatorAssistant.class)
                .chatModel(chatModel)
                .tools(new CalculatorWithImmediateReturn())
                .build();
    }

...

    @Override
    public String calculator(String userMessage) {
        Result<String> result = calculatorAssistant.chat(userMessage);
        // 结果需要从 toolExecutions 中获取
        return result.toolExecutions().get(0).result();
    }

这里需要注意 :

  1. 此功能依赖的 Ai Service 接口的返回类型必须是 Result<T>(否则会异常)。有关Result<T>的更多信息,请参见返回类型
  2. 调用后 Result<T> 的 content() 为 null,实际结果需从 result.toolExecutions() 中提取。
  3. 若 LLM 同时调用多个工具,且存在非即时工具,则仍会触发 LLM 重处理。

4.5 Tool 异常处理

在调用 Tool 的过程中可能会出现一些异常,如下:

  • Handling Tool Name Errors :LLM 可能会在工具调用时产生幻觉,它可能会请求使用一个不存在的工具。在这种情况下,LangChain4j 默认会抛出一个异常来报告该问题。我们可以通过 hallucinatedToolNameStrategy 为 AI Service 配置一种在这种情况下使用的策略来设置不同的行为。
  • Handling Tool Arguments Errors :当工具参数存在问题(如 LLM 生成无效 JSON)时,AI 服务无法执行工具,会抛出异常导致失败。我们可以通过 toolArgumentsErrorHandler 来配置自定义的处理策略。
  • Handling Tool Execution Errors : 当被@Tool注解的方法抛出Exception时,默认会将异常的消息(e.getMessage())作为工具执行结果发送给 LLM,LLM 可据此纠正错误并在必要时重试。我们可以通过 toolExecutionErrorHandler 来配置自定义的错误处理策略。

示例如下:

    @Bean
    public CalculatorAssistant calculatorAssistant(ChatModel chatModel) {
        return AiServices.builder(CalculatorAssistant.class)
                .chatModel(chatModel)
                .tools(new CalculatorWithImmediateReturn())
                .hallucinatedToolNameStrategy(toolExecutionRequest -> ToolExecutionResultMessage.from(
                        toolExecutionRequest, "Error: there is no tool called " + toolExecutionRequest.name()))
                .toolArgumentsErrorHandler((error, context) -> ToolErrorHandlerResult.text("调用参数错误"))
                .toolExecutionErrorHandler((error, context) -> ToolErrorHandlerResult.text("调用过程错误"))
                .build();
    }

在 ToolArgumentsErrorHandler 和 ToolExecutionErrorHandler 的情况存在两种处理错误的方式:

  • 直接抛出异常 :该操作会直接终止 AI Service,我们可以在调用时捕获对应异常。如下:

    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
    }
    
  • 返回一条文本消息(例如,错误描述),该文本内容会交由 LLM 来进行处理,并使其能够做出适当回应(例如,纠正错误并重试)。如下:

    Assistant assistant = AiServices.builder(Assistant.class)
            .chatModel(chatModel)
            .tools(tools)
            .toolArgumentsErrorHandler((error, errorContext) -> 
            	ToolErrorHandlerResult.text("工具调用出现了错误: " + error.getMessage()))
            .build();
    

四、参考内容

  1. LangChain4j 官网
  2. 豆包
Logo

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

更多推荐