学习代码产出 —> 点击跳转

1. 介绍

LangChain4j 的目标是简化与 Java 应用程序 集成大模型点击跳转官网

在这里插入图片描述

  • 特征
  • 统一 API: LLM提供程序(如 OpenAI 或 阿里百炼)和嵌入(向量)存储(如 redis 或 ES) 使用专有 API。LangChain4j 提供了一个统一的 API,以避免为每个 API 学习和实现特定的 API。 要试验不同的LLMs存储或嵌入的存储,您可以在它们之间轻松切换,而无需重新编写代码。 LangChain4j 目前支持 15+ 热门LLM20+ 嵌入模型
  • jdk版本:v0.35.0可以在jdk1.8, v0.36.0+迁移到jdk17 , 所以最新版本必须jdk17+
  • langchain4j vs springAI
维度 Spring AI LangChain4j
技术栈绑定 强依赖 Spring 生态 无框架依赖,可独立使用
适用场景 SpringBoot应用快速接入单模型 多模型(动态模型)平台

点击查看代码产出

2. 初识(纯java)

  • 导入maven依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.x</groupId>
    <artifactId>LangChain4j_01</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <langchain4j.version>1.11.0</langchain4j.version>
        <dashscope.version>1.11.0-beta19</dashscope.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <!--OPEN AI-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <!--ALI-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope</artifactId>
            <version>${dashscope.version}</version>
        </dependency>
        <!--ollama-->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama</artifactId>
            <version>1.11.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
  • 测试代码
public class demo_01 {
    /**
     * TODO open ai (deepseek差不多)
     * */
    @Test
    public void test01(){
        OpenAiChatModel model = OpenAiChatModel
                .builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .modelName("gpt-4o-mini")
                .build();
        String answer = model.chat("你好,你是谁?");
        System.out.println(answer);
    }
    /**
     * TODO ali的qwen-plus-latest
     * */
    @Test
    public void test02(){
        ChatModel qwenModel = QwenChatModel.builder()
                .apiKey(System.getenv("API_KEY"))
                .modelName("qwen-plus-latest")
                .build();
        String answer = qwenModel.chat("你好,你是谁?");
        System.out.println(answer);
    }
    /**
     * TODO 图像模型
     * */
    @Test
    public void test03() {
        WanxImageModel wanxImageModel = WanxImageModel.builder()
                .modelName("qwen-image-plus")
                .apiKey(System.getenv("API_KEY"))
                .build();
        Response<Image> response = wanxImageModel.generate("生成一张游戏三角洲里边女英雄麦晓雯的图片");
        System.out.println(response.content().url());
    }
    /**
     * TODO 本地ollama的deepseek-r1:1.5b
     * */
    @Test
    public void test04() {
        ChatModel model = OllamaChatModel.builder()
                .baseUrl("http://192.168.0.115:11434")
                .modelName("deepseek-r1:1.5b")
                .build();
        String answer = model.chat("请帮我介绍一下明朝开国皇帝");
        System.out.println(answer);
    }


}

点击 ollama官网

3. 集合spring boot

  • 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.x</groupId>
    <artifactId>demo_02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo_02</name>
    <description>demo_02</description>
    <properties>
        <java.version>17</java.version>
        <langchain4j.version>1.0.0-beta1</langchain4j.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3.1 百炼平台

点击跳转

  • 依赖
<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
  • 配置文件application.properties
#接入别的模型要选择相应的模型的key和名称
langchain4j.community.dashscope.chatModel.apiKey=${API_KEY}
langchain4j.community.dashscope.chatModel.modelName=qwen-plus
  • 代码实现
@Autowired
@Qualifier("qwenChatModel")
 private ChatLanguageModel chatLanguageModel;
 /**
  * TODO 接入百炼
  * */
 @GetMapping("test01")
 public String test01(@RequestParam(defaultValue = "你是谁") String message){
     String chat = chatLanguageModel.chat(message);
     return chat;
 }

3.2 接入ollama

前提

  • 依赖
 <!--本地ollama-->
  <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
      <version>${langchain4j.version}</version>
  </dependency>
  • 配置文件application.properties
#ollama配置
langchain4j.ollama.chat-model.base-url=http://192.168.0.115:11434
langchain4j.ollama.chat-model.model-name=deepseek-r1:1.5b
langchain4j.ollama.chat-model.temperature=0.8
langchain4j.ollama.chat-model.timeout=PT60S
  • 代码实现
 @Autowired
 @Qualifier("ollamaChatModel")
 private OllamaChatModel ollamaChatModel;
 /**
  * TODO 本地ollama
  * */
 @GetMapping("test02")
 public String test02(@RequestParam(defaultValue = "你是谁") String message){
     String chat = ollamaChatModel.chat(message);
     return chat;
 }

3.3 流式输出

  • 因为langchain4j不是spring家族, 所以我们在wen应用中需要引入webflux
 <!--流式输出-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>
  • 配置文件application.properties
# 流式输出
langchain4j.community.dashscope.streaming-chat-model.apiKey=${API_KEY}
langchain4j.community.dashscope.streaming-chat-model.modelName=qwen-plus
  • 代码实现
 @Autowired
 private QwenStreamingChatModel qwenStreamingChatModel;
 /**
  * TODO 流式返回
  * */
 @GetMapping(value = "test03",produces ="text/stream;charset=UTF-8")
 public Flux<String> test03(@RequestParam(defaultValue = "你是谁") String message){
     return Flux.create(sink ->
                     qwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() {
                 @Override
                 public void onPartialResponse(String s) {
                     System.out.println(s);
                     sink.next(s);// 逐次返回部分响应
                 }

                 @Override
                 public void onCompleteResponse(ChatResponse chatResponse) {
                     sink.complete();  // 完成整个响应流
                 }

                 @Override
                 public void onError(Throwable throwable) {
                     sink.error(throwable);  // 异常处理
                 }
             })
             );
 }

3.4 记忆对话

3.4.1 原生方式

原生方式(不推荐),大模型并不会把我们每次的对话存在服务端, 所以他记不住我们说的话

所以每次对话都需要将之前的对话记录,都发给大模型, 这样才能知道我们之前说了什么

  • 这里使用OpenAiChatModel,所以需要引入相关的依赖
 <!--open ai-->
 <dependency>
     <groupId>dev.langchain4j</groupId>
     <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
     <version>${langchain4j.version}</version>
 </dependency>
  • 原生方式
@Test
void test01() {
    ChatLanguageModel model = OpenAiChatModel
            .builder()
            .baseUrl("http://langchain4j.dev/demo/openai/v1")
            .apiKey("demo")
            .modelName("gpt-4o-mini")
            .build();
    UserMessage userMessage1 = UserMessage.userMessage("你好,我是小明");
    ChatResponse response1 = model.chat(userMessage1);
    AiMessage aiMessage1 = response1.aiMessage(); // 大模型的第一次响应
    System.out.println(aiMessage1.text());
    System.out.println("-------------------------------");

    // 下面一行代码是重点 (上次提的问题 + 上次回答 + 这次问题)都要进行提供
    ChatResponse response2 = model.chat(userMessage1, aiMessage1, UserMessage.userMessage("我叫什么"));
    AiMessage aiMessage2 = response2.aiMessage(); // 大模型的第二次响应
    System.out.println(aiMessage2.text());
}

3.4.2 ChatMemory

使用ChatMemory

原理

  1. 通过AiService创建的代理对象(),调用chat方法
  2. 代理对象会去ChatMemory中获取之前的对话记录(获取记忆)
  3. 将获取到的对话记录合并到当前对话中(此时大模型根据之前的聊天记录肯定就拥有了“记忆”)
  4. 将当前的对话内容存入ChatMemory(保存记忆)

在这里插入图片描述

  • 引入依赖
 <!--langchain4j-->
 <dependency>
     <groupId>dev.langchain4j</groupId>
     <artifactId>langchain4j</artifactId>
     <version>${langchain4j.version}</version>
 </dependency>
  • 创建AiConfig
@Configuration
public class AiConfig2 {
    public interface Assistant {
        String chat(String message);
        // 流式响应
        TokenStream stream(String message);
    }

    @Bean
    public Assistant assistant(ChatLanguageModel qwenChatModel,
                               StreamingChatLanguageModel qwenStreamingChatModel) {
        // 最多十条
        ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); // 需要引入langchain4j包


        Assistant assistant = AiServices.builder(Assistant.class)
                .chatLanguageModel(qwenChatModel)
                .streamingChatLanguageModel(qwenStreamingChatModel)
                .chatMemory(chatMemory)
                .build();

        return  assistant;
    }
}
  • 进行代码测试
    @Autowired
    private AiConfig.Assistant assistant;
    /**
     * TODO 记忆存储 进行记忆储存
     * */
    @GetMapping("memory_chat")
    public String memoryChat(@RequestParam(defaultValue = "我是小明") String message){
        return assistant.chat(message);
    }
    /**
     * TODO 记忆证明
     * */
    @GetMapping(value = "/memory_stream_chat",produces ="text/stream;charset=UTF-8")
    public Flux<String> memoryStreamChat(@RequestParam(defaultValue="我是谁") String message, HttpServletResponse response) {
        TokenStream stream = assistant.stream(message);

        return Flux.create(sink -> {
            stream.onPartialResponse(s -> sink.next(s))
                    .onCompleteResponse(c -> sink.complete())
                    .onError(sink::error)
                    .start();

        });
    }

3.4.3 记忆分离

记忆分离:不同的用户或者不同的对话肯定不能用同一个记忆,要不然对话肯定会混淆,此时就需要进行区分

原理:

  1. 通过AiService创建的代理对象()调用chat方法传入id
  2. 代理对象会去ChatMemory中根据id获取之前的对话记录(获取记忆)
  3. 将获取到的对话记录合并到当前对话中(此时大模型根据之前的聊天记录肯定就拥有了“记忆”)
  4. 将当前的对话内容根据id存入ChatMemory(保存记忆)

在这里插入图片描述

  • 配置config
@Configuration
public class AiConfig {
    public interface AssistantUnique {
        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
        // 流式响应
        TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
    }
    @Bean
    public AssistantUnique assistantUnique(ChatLanguageModel qwenChatModel,
                                           StreamingChatLanguageModel qwenStreamingChatModel) {

        AssistantUnique assistant = AiServices.builder(AssistantUnique.class)
                .chatLanguageModel(qwenChatModel)
                .streamingChatLanguageModel(qwenStreamingChatModel)
                .chatMemoryProvider(memoryId ->
                        MessageWindowChatMemory.builder().maxMessages(10)
                                .id(memoryId).build()
                )
                .build();

        return assistant;
    }
}
  • 进行测试
    @Autowired
    private AiConfig.AssistantUnique assistantUnique;
    /**
     * TODO 记忆分离
     * */
    @GetMapping("memory_chat")
    public String memoryChat(@RequestParam(defaultValue = "我是小明") String message,@RequestParam Integer userId){
        return assistantUnique.chat(userId,message);
    }
    /**
     * TODO 记忆证明
     * */
    @GetMapping(value = "/memory_stream_chat",produces ="text/stream;charset=UTF-8")
    public Flux<String> memoryStreamChat(@RequestParam(defaultValue="我是谁") String message,@RequestParam Integer userId) {
        TokenStream stream = assistantUnique.stream(userId,message);

        return Flux.create(sink -> {
            stream.onPartialResponse(s -> sink.next(s))
                    .onCompleteResponse(c -> sink.complete())
                    .onError(sink::error)
                    .start();

        });
    }

3.4.4 持久化对话

持久化对话:可以配置一个ChatMemoryStore

  • 默认是InMemoryChatMemoryStore——通过一个map进行存储
  • 所以如果需要持久化到第三方存储, 可以重新配置ChatMemoryStore
  • 自定义持久化存储
@Slf4j
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {
    @Override
    public List<ChatMessage> getMessages(Object o) {
        // todo 根据memoryId从数据库获取
        log.info("getMessages:{}", o);
        return List.of();
    }

    @Override
    public void updateMessages(Object o, List<ChatMessage> list) {
        // todo 根据memoryId修改、新增记录
        log.info("updateMessages:{},list:{}", o,list);

    }

    @Override
    public void deleteMessages(Object o) {
        // todo 根据memoryId删除
        log.info("deleteMessages:{}", o);
    }
}
  • 进行配置
@Configuration
public class AiConfig {
    public interface AssistantUnique {
        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
        // 流式响应
        TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
    }
    @Bean
    public AssistantUnique assistantUniqueStore(ChatLanguageModel qwenChatModel,
                                                StreamingChatLanguageModel qwenStreamingChatModel) {

        PersistentChatMemoryStore store = new PersistentChatMemoryStore();

        ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(10)
                .chatMemoryStore(store)
                .build();

        AssistantUnique assistant = AiServices.builder(AssistantUnique.class)
                .chatLanguageModel(qwenChatModel)
                .streamingChatLanguageModel(qwenStreamingChatModel)
                .chatMemoryProvider(memoryId ->
                        MessageWindowChatMemory.builder().maxMessages(10)
                                .id(memoryId).build()
                )
                .chatMemoryProvider(chatMemoryProvider)
                .build();
        return assistant;
    }
}

3.4.5 Function-call (Tools)

  • 对于基础大模型来说, 只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往被各软件系统存储在自己数据库中:
    • 比如我问大模型:“中国有多少叫xxx的人” 他肯定不知道, 我们就需要去调用政务系统的接口。
    • 比如我现在开发一个智能票务助手, 我现在跟AI说需要退票, AI怎么做到呢? 就需要让AI调用我们自己系统的退票业务方法,进行操作数据库。
  • 那这些都可以通过function-call进行完成,更多的用于实现类似智能客服场景,因为客服需要帮用户解决业务问题(就需要调用业务方法)。

function-call的流程

  • 我现在需要当对话中用户问的是“中国有多少叫xxx的人”的对话, 我需要去我程序中获取
  1. 问大模型 中国有多少叫xxx的人
  2. 大模型在识别到你的问题是: “中国有多少叫xxx的人”
  3. 大模型提取“xxx”
  4. 调用相关方法得到结果
  5. 通过返回的结果再结合上下文再次请求大模型
  6. 响应“中国有多少叫xxx的人"

在这里插入图片描述

  • 加入回调方法
  • ToolsService配置为了一个bean
  • @Tool 用于告诉AI什么对话调用这个方法
  • @P(“姓名”) 用于告诉AI ,调用方法的时候需要提取对话中的什么信息, 这里提取的是姓名

所以, 你如果需要加更多的tool. 只需要在TollsService中加

@Service
@Slf4j
public class ToolsService {
    /**
     * TODO 进行工具调用的简单大模型配置
     * */
    public interface AssistantUniqueByTools {
        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
        // 流式响应
        TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
    }
    @Bean
    public AssistantUniqueByTools assistantUniqueByTools(ChatLanguageModel qwenChatModel,
                                                  StreamingChatLanguageModel qwenStreamingChatModel,
                                                  ToolsService toolsService
    ) {

        AssistantUniqueByTools assistant = AiServices.builder(AssistantUniqueByTools.class)
                .chatLanguageModel(qwenChatModel)
                .streamingChatLanguageModel(qwenStreamingChatModel)
                .tools(toolsService)
                .chatMemoryProvider(memoryId ->
                        MessageWindowChatMemory.builder().maxMessages(10)
                                .id(memoryId).build()
                )
                .build();

        return assistant;
    }
}
  • 接口api
   /**
     * TODO 大模型大处理的时候调用工具
     * */
    @Autowired
    private AiConfig.AssistantUniqueByTools assistantUniqueByTools;
    @GetMapping(value = "/assistantUniqueByTools",produces ="text/stream;charset=UTF-8")
    public Flux<String> assistantUniqueByTools(@RequestParam(defaultValue="河北有多少个小明") String message,@RequestParam Integer userId) {
        TokenStream stream = assistantUniqueByTools.stream(userId,message);

        return Flux.create(sink -> {
            stream.onPartialResponse(s -> sink.next(s))
                    .onCompleteResponse(c -> sink.complete())
                    .onError(sink::error)
                    .start();

        });
    }

3.4.6 预设角色(系统消息SystemMessage)

  • 基础大模型是没有目的性的, 你聊什么给什么, 但是如果我们开发的事一个智能票务助手, 我需要他以一个票务助手的角色跟我对话, 并且在我跟他说"退票"的时候, 让大模型一定要告诉我“车次”和"姓名" ,这样我才能去调用业务方法(假设有一个业务方法,需要根据车子和姓名才能查询具体车票),进行退票。

在这里插入图片描述

在langchain4j中实现也非常简单

  • @SystemMessage 系统消息, 一般做一些预设角色的提示词,设置大模型的基本职责
  • 可以通过{{current_date}} 传入参数, 因为预设词中的文本可能需要实时变化
  • @V(“current_date”), 通过@V传入{{}}中的参数
  • 一旦参数不止一个, 就需要通过@UserMessage设置用户信息

另外:假设大模型不支持系统消息(一般都支持),可以用@UserMessage代替@SystemMessag

  • 进行配置
@Configuration
public class AiConfig {
    /**
     * TODO 进行工具调用的简单大模型配置
     * */
    public interface AssistantUniqueByTools {
        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
        // 流式响应
        TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
        @SystemMessage("""
                您是“xxx”航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
                        您正在通过在线聊天系统与客户互动。 
                        在提供有关预订或取消预订的信息之前,您必须始终从用户处获取以下信息:预订号、客户姓名。
                        请讲中文。
					   今天的日期是 {{current_date}}.
                """)
        TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage,@V("current_date") String currentDate);
    }
    @Bean
    public AssistantUniqueByTools assistantUniqueByTools(ChatLanguageModel qwenChatModel,
                                                  StreamingChatLanguageModel qwenStreamingChatModel,
                                                  ToolsService toolsService
    ) {

        AssistantUniqueByTools assistant = AiServices.builder(AssistantUniqueByTools.class)
                .chatLanguageModel(qwenChatModel)
                .streamingChatLanguageModel(qwenStreamingChatModel)
                .tools(toolsService)
                .chatMemoryProvider(memoryId ->
                        MessageWindowChatMemory.builder().maxMessages(10)
                                .id(memoryId).build()
                )
                .build();

        return assistant;
    }
}

  • 工具准备调用
@Service
@Slf4j
public class ToolsService {
    @Tool("描述某个地区有多少叫什么姓名的人")
    public String callToolsName(@P("姓名") String name, @P("地区") String area) {
        // 调用工具
        log.info("在{}地区有多少叫{}姓名的人", area, name);
        return "在"+ area +"地区有10000叫"+ name +"姓名的人";
    }
    @Tool("描述航空公司要求预定或者取消票,要求的姓名和预定号")
    public String callToolsName1(@P("姓名") String name, @P("预定号") String num) {
        // 调用工具
        log.info("姓名为:{}的班机号{}要求更改", name,num);
        return "姓名为:"+ name +"的班机号"+ num +"要求更改";
    }
    
}
  • api接口进行调用
    /**
     * TODO 大模型在使用的时候
     * */
    @GetMapping(value = "/assistantUniqueByToolsUse",produces ="text/stream;charset=UTF-8")
    public Flux<String> assistantUniqueByToolsUse(@RequestParam(defaultValue="G18航班的小明要求退票") String message,@RequestParam Integer userId) {
        TokenStream stream = assistantUniqueByTools.stream(userId,message, String.valueOf(new Date()));
        return Flux.create(sink -> {
            stream.onPartialResponse(s -> sink.next(s))
                    .onCompleteResponse(c -> sink.complete())
                    .onError(sink::error)
                    .start();

        });
    }

4. RAG

  • 检索增强生成(Retrieval-augmented Generation)
  • 对于基础大模型来说, 他只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟, 无法得知一些具体业务数据和实时数据, 这些数据往往在各种文件中(比如txt、word、html、数据库…)
  • 虽然function-call、SystemMessage可以用来解决一部分问题,但是它只能少量, 如果你要提供大量的业务领域信息, 就需要给他外接一个知识库

比如

  1. 我问他退订要多少费用
  2. 这些资料可能都由产品或者需求编写在了文档中:
    • 所以需要现在需求信息存到向量数据库(这个过程叫Embedding, 涉及到文档读取、分词、向量化存入)
  3. 去向量数据库中查询“退订费用相关信息”
  4. 将查询到的数据和对话信息再请求大模型
  5. 此时会响应退订需要多少费用

在这里插入图片描述

4.1 相关概念

4.1.1 向量

  • 向量通常用来做相似性搜索,比如语义的一维向量,可以表示词语或短语的语义相似性。例如,“你好”、“hello”和“见到你很高兴”可以通过一维向量来表示它们的语义接近程度。
    在这里插入图片描述
  • 然而,对于更复杂的对象,比如小狗,无法仅通过一个维度来进行相似性搜索。这时,我们需要提取多个特征,如颜色、大小、品种等,将每个特征表示为向量的一个维度,从而形成一个多维向量。例如,一只棕色的小型泰迪犬可以表示为一个多维向量 [棕色, 小型, 泰迪犬]。

在这里插入图片描述

  • 如果需要检索见过更加精准, 我们肯定还需要更多维度的向量, 组成更多维度的空间,在多维向量空间中,相似性检索变得更加复杂。我们需要使用一些算法,如余弦相似度或欧几里得距离,来计算向量之间的相似性。向量数据库会帮我实现。

4.1.2 文本向量化

  • LangChain4j中来调用向量模型来对一句话进行向量化体验
 @Test
 void test02(){
     // 使用向量模型
     /**
      * TODO QwenEmbeddingModel 类中的 默认对象模型代码
      * this.modelName = Utils.isNullOrBlank(modelName) ? "text-embedding-v2" : modelName;
      * */
     QwenEmbeddingModel model = QwenEmbeddingModel.builder()
             .apiKey(System.getenv("API_KEY"))
             .build();
     // 进行向量存储
     Response<Embedding> embed = model.embed("你好,我是小明");
     log.info(embed.content().toString());// float数组 转换成向量
     log.info(String.valueOf(embed.content().vector().length));// 数组长度
 }
Embedding { vector = [-0.020235416, 0.005272401, ..., 0.007930692] }
1536
  • 从结果可以知道"你好,我是小明"这句话经过OpenAiEmbeddingModel向量化之后得到的一个长度为1536的float数组。注意,1536是固定的,不会随着句子长度而变化。
  • 那么,我们通过这种向量模型得到一句话对应的向量有什么作用呢?非常有用,因为我们可以基于向量来判断两句话之间的相似度,举个例子:
    • 查询跟秋田犬类似的狗, 在向量数据库中根据每个狗的特点进行多维向量, 你会发现秋田犬的向量数值和柴犬的向量数值最接近, 就可以查到类似的狗。

在这里插入图片描述

4.1.3 向量数据库

  • 对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。
  • 在LangChain4j中,EmbeddingStore表示向量数据库,它有支持20+ 嵌入模型
Embedding Store Storing Metadata Filtering by Metadata Removing Embeddings
In-memory
Astra DB
Azure CosmosDB Mongo vCore
Azure CosmosDB NoSQL
Cassandra
Chroma
ClickHouse
Coherence
Couchbase
DuckDB
Elasticsearch
Infinispan
Milvus
MongoDB Atlas
Neo4j
OpenSearch
Oracle
PGVector
Pinecone
Qdrant
Redis
Tablestore
Vearch
Vespa
Weaviate
  • 其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、Pg、Redis。下边主要通过In-memory方式演示使用流程。

演示redis(到那时这里使用的是内存存储,内存比较宝贵推荐使用磁盘存储)

  • Redis也很简单, 你需要先安装redis7.0+的版本:其他的向量数据库不做介绍
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-redis</artifactId>
	<version>${langchain4j.version}</version>
</dependency>
  • 然后需要注意的是,普通的Redis是不支持向量存储和查询的,需要额外的redisearch模块,我这边是直接使用docker来运行一个带有redisearch模块的redis容器的,命令为:
docker run -p 6379:6379 redis/redis-stack-server:latest

注意端口6379不要和你现有的Redis冲突了。

  • 然后就可以使用以下代码把向量存到redis中了:
RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
    .host("127.0.0.1")
    .port(6379)
    .dimension(1536)
    .build();
// 生成向量
Response<Embedding> embed = embeddingModel.embed("我是小明");
// 存储向量
embeddingStore.add(embed.content());
  • dimension表示要存储的向量的维度,所以为1536,如果你不是使用OpenAiEmbeddingModel得到的向量,那么维度可能会不一样。
  • 可以使用以下命令来清空:
redis-cli FT.DROPINDEX embedding-index DD

4.1.4 匹配向量

  • 分别存储了预订航班和取消预订2段说明到向量数据库中,然后通过"退票要多少钱" 进行查询

在这里插入图片描述

  • 代码实现
    /**
     * TODO 匹配向量
     * */
    @Test
    void test03(){
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); // 使用 in-memory
        QwenEmbeddingModel model = QwenEmbeddingModel.builder()
                .apiKey(System.getenv("API_KEY"))
                .build();
        // 利用向量模型进行向量化,然后存储到向量数据库中
        TextSegment textSegment1 = TextSegment.from("""
                预订航班:
                    - 通过我们的网站或移动应用程序预订。
                    - 预订时需要全额付款。
                    - 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
                """);
        Embedding content1 = model.embed(textSegment1).content();
        embeddingStore.add(content1,textSegment1); // 进行向量存储
        // 利用向量模型进行向量化,然后存储到向量数据库中
        TextSegment textSegment2 = TextSegment.from("""
                取消预订:
                    - 最晚在航班起飞前 48 小时取消。
                    - 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
                    - 退款将在 7 个工作日内处理。
                """);
        Embedding content2 = model.embed(textSegment2).content();
        embeddingStore.add(content2,textSegment2); // 进行向量存储

        // 需要查询的内容,向量化
        Embedding content = model.embed("退票需要多少钱").content();
        // 去向量库进行查询
        // 构建查询条件 还可以设置匹配率为多少以上才会被查询出来
        EmbeddingSearchRequest build = EmbeddingSearchRequest.builder()
                .queryEmbedding(content) // 查询内容的向量化
                .maxResults(2) // 最大查询结果数量
                .minScore(0.7) // 设置相似度阈值
                .build();

        EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
        search.matches().forEach(item -> {
            log.info(String.valueOf(item.score())); //匹配率
            log.info(item.embedded().text()); // 匹配的内容
        });

        // ----向量模型已经匹配,下边是叫大模型进行回答输出----
        StringBuilder contextBuilder = new StringBuilder();
        search.matches().forEach(match -> {
            contextBuilder.append(match.embedded().text()).append("\n\n");
        });
        String context = contextBuilder.toString().trim();
        // 大语言模型(用于生成回答)
        QwenChatModel chatModel = QwenChatModel.builder()
                .apiKey(System.getenv("API_KEY"))
                .modelName("qwen-max")  // 或其他模型如 qwen-plus, qwen-max
                .build();
        String systemPrompt = """
            你是一个专业的客服助手。请根据以下提供的参考信息回答用户的问题。
            如果参考信息不足以回答问题,请说明无法回答。
            
            参考信息:
            %s
            """.formatted(context);
        UserMessage userMessage = UserMessage.from("退票需要多少钱");
        SystemMessage from = SystemMessage.from(systemPrompt);
        List<ChatMessage> messages = Arrays.asList(from, userMessage);
        Response<AiMessage> response = chatModel.generate(messages);
        log.info(response.content().text());
    }
  • 结果
0.7146489962563928
取消预订:
    - 最晚在航班起飞前 48 小时取消。
    - 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
    - 退款将在 7 个工作日内处理。

退票费用根据您预订的舱位不同而有所差异:

- 经济舱的取消费用为 75 美元。
- 豪华经济舱的取消费用为 50 美元。
- 商务舱的取消费用为 25 美元。

请注意,这些费用适用于在航班起飞前至少48小时进行取消的情况。如果在这个时间之后取消,可能有不同的政策或无法获得退款。此外,退款将在7个工作日内处理。如果您有其他特殊情况或者疑问,建议直接联系航空公司确认。

4.2 知识库RAG演练

在这里插入图片描述

4.2.1 Document Loaders 文档读取器

分片 -> 向量 -> 存入向量库

在这里插入图片描述

  • 读取为文档
// 读取为文档 (读取文件)
Document document = ClassPathDocumentLoader.loadDocument("./terms-of-service.txt", new TextDocumentParser());
  • Document Parser 文档解析器
    • 如果要开发一个知识库系统, 这些资料可能在各种文件中, 比如word、txt、pdf、image、html等等, 所以langchain4j也提供了不同的文档解析器:
    • TextDocumentParser来自 langchain4j 模块的 TextDocumentParser,它可以解析纯文本格式(e.g. TXT、HTML、MD 等)的文件。
    • ApachePdfBoxDocumentParser来自langchain4j-document-parser-apache-pdfbox ,它可以解析 PDF 文件
    • ApachePoiDocumentParser来自langchain4j-document-parser-apache-poi ,可以解析 MS Office 文件格式(e.g. DOC、DOCX、PPT、PPTX、XLS、XLSX 等)
    • ApacheTikaDocumentParser来自 langchain4j-document-parser-apache-tika 模块中,可以自动检测和解析几乎所有现有的文件格式
  • 在这里我来解析一份这个txt文件, 所以我们用TextDocumentParser随便放在reources/rag/TextDocumentParser
#TextDocumentParser
本服务条款适用于您对 xxxx 的体验。预订航班,即表示您同意这些条款。
1. 预订航班
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
2. 更改预订
- 允许在航班起飞前 24 小时更改。
- 通过在线更改或联系我们的支持人员。
- 改签费:经济舱 50,豪华经济舱 30,商务舱免费。
3. 取消预订
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
// 获取resource里边的文件
Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
TextDocumentParser textDocumentParser = new TextDocumentParser();
// 读取为文档 (读取文件)
Document document1 = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);

4.2.2 DocumentSplitter‌ 文档拆分器

在这里插入图片描述

  • 由于文本读取过来后, 还需要分成一段一段的片段(分块chunk), 分块是为了更好地拆分语义单元,这样在后面可以更精确地进行语义相似性检索,也可以避免LLM的Token限制。
  • langchain4j也提供了不同的文档拆分器:
分词器类型 匹配能力 适用场景
‌DocumentByCharacterSplitter 无符号分割 就是严格根据字数分隔(不推荐,会出现断句)
‌DocumentByRegexSplitter 正则表达式分隔 根据自定义正则‌分隔
‌DocumentByParagraphSplitter 删除大段空白内容 处理连续换行符(如段落分隔)(\\s*(?>\\R)\\s*(?>\\R)\\s*
DocumentByLineSplitte 删除单个换行符周围的空白, 替换一个换行 \\s*\\R\\s*)● ‌示例‌:○ 输入文本:"This is line one.\n\tThis is line two."○ 使用 \s*\R\s* 替换为单个换行符:“This is line one.\nThis is line two.
DocumentByWordSplitter 删除连续的空白字符。 \\s+● ‌示例‌:○ 输入文本:“Hello World” ○ 使用 \s+ 替换为单个空格:“Hello World
‌DocumentBySentenceSplitter 按句子分割 该分割器使用Apache OpenNLP 库中的一个类,用于检测文本中的句子边界。它能够识别标点符号(如句号、问号、感叹号等)是否标记着句子的末尾,从而将一个较长的文本字符串分割成多个句子。

这里选DocumentByLineSplitter‌, 因为内容不多, 所以其实没有特别大的关系, 后面如果大家有兴趣我详细讲解每一种的应用场景。

  • 代码,将第一步读取到的文档进行分割
// 将读取到的文档进行分割
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
        20,// 每段最大长度
        10 // 自然语言最大重叠字数
);
List<TextSegment> split = splitter.split(document1);
  • chunk_size(块大小)指的就是我们分割的字符块的大小;chunk_overlap(块间重叠大小)就是下图中加深的部分,上一个字符块和下一个字符块重叠的部分,即上一个字符块的末尾是下一个字符块的开始。

在这里插入图片描述

  • 在使用按字符切分时,需要指定分割符,另外需要指定块的大小以及块之间重叠的大小(允许重叠是为了尽可能地避免按照字符进行分割造成的语义损失)
#比如
- 最晚在航班起飞前 48 小时取消。取消费用:经济舱 75 美元,豪华经济舱 50 美元,
商务舱 25 美元。退款将在 7 个工作日内处理。


按照chunksize可能会分隔成:   
最晚在航班起飞前 48 小时取消。取消费用:经济舱 7
如果设置了重叠可能会:
-最晚在航班起飞前 48 小时取消。取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。

-取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。退款将在 7 个工作日内处理。
  • 分块流程

在这里插入图片描述

  • 首先按照指定的分割符进行切分,切分过之后,如果块的长度小于 chunk_size 的大小,则进行块之间的合并。在进行合并时,遵循下面的规则:

    1. 如果相邻块加在一起的长度小于或等于chunk_size,则进行合并;否则看你有没有子分割器,如果没有报错。
    2. 在进行合并时,如果块的大小小于或等于chunk_overlap,并且和前后两个相邻块合并后,两个合并后的块均不超过chunk_size,则两个合并后的块允许有重叠
  • 在RAG系统中,文本分块的粒度需要平衡语义完整性与计算效率,并非越细越好。以下是关键考量点:参考分割经验

分隔经验

  1. 过细分块的潜在问题
    • 语义割裂‌: 破坏上下文连贯性,影响模型理解‌
    • 计算成本增加‌:分块过细会导致向量嵌入和检索次数增多,增加时间和算力开销‌。
    • 信息冗余与干扰‌:碎片化的文本块可能引入无关内容,干扰检索结果的质量,降低生成答案的准确性‌。
  2. 分块过大的弊端
    • 信息丢失风险‌:过大的文本块可能超出嵌入模型的输入限制,导致关键信息未被有效编码‌。
    • 检索精度下降‌:大块内容可能包含多主题混合,与用户查询的相关性降低,影响模型反馈效果‌。
‌场景 分块策略 参数参考
微博/短文本 句子级分块,保留完整语义 每块100-200字符‌
学术论文 段落级分块,叠加10%重叠 每块300-500字符‌
法律合同 条款级分块,严格按条款分隔 每块200-400字符‌
长篇小说 章节级分块,过长段落递归拆分为段落 每块500-1000字符‌
  1. 固定长度分块
    • 字符数范围‌:通常建议每块控制在 ‌100-500字符‌(约20-100词),以平衡上下文完整性与检索效率‌
    • 重叠比例‌:相邻块间保留 ‌10-20%的重叠内容‌(如块长500字符时重叠50-100字符),减少语义断层‌
  2. 语义分块
    • 段落或章节‌:优先按自然段落、章节标题划分,保持逻辑单元完整‌
    • ‌动态调整‌:对于长段落,可递归分割为更小单元(如先按段落分块,过长时再按句子拆分)‌
  3. 专业领域调整
    • 高信息密度文本‌(如科研论文、法律文件):采用更细粒度分块(100-200字符),保留专业术语细节‌
    • 通用文本‌(如新闻、社交媒体):适当放宽分块大小(300-500字符)‌

4.2.3 文本向量化

在这里插入图片描述

向量化存储之前在”文本向量化“介绍了,就是通过向量模型进行向量化,下边仍然通过qwen向量模型进行向量化,将之前分割的进行向量化

// 通过向量模型进行向量化
QwenEmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
        .apiKey(System.getenv("API_KEY"))
        .build();
List<Embedding> content = embeddingModel.embedAll(split).content();

4.2.4 存储向量

在这里插入图片描述

  • 选择向量库进行存储
// 使用向量库进行存储
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(content,split);

4.2.5 向量数据库检索

  • 先将文本向量化,然后去向量库查询匹配
// 将查询内容向量化,之后去向量数据库进行查询
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder().queryEmbedding(embed.content()).build();
// 查询
EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
search.matches().forEach(item -> {
    log.info("匹配的内容:{},分数为:{}",item.embedded().text(),item.score()); // 匹配的内容
});

4.2.6 完整代码

    /**
     * TODO 分片
     * */
    @SneakyThrows
    @Test
    void test04(){
        // 获取resource里边的文件
        Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
        TextDocumentParser textDocumentParser = new TextDocumentParser();
        // 读取为文档 (读取文件)
        Document document1 = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);

        // 将读取到的文档进行分割
        DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
                20,// 每段最大长度
                10 // 自然语言最大重叠字数
        );
        List<TextSegment> split = splitter.split(document1);

        // 通过向量模型进行向量化
        QwenEmbeddingModel embeddingModel = QwenEmbeddingModel.builder()
                .apiKey(System.getenv("API_KEY"))
                .build();
        List<Embedding> content = embeddingModel.embedAll(split).content();

        // 使用向量库进行存储
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
        embeddingStore.addAll(content,split);

        // 将查询内容向量化,之后去向量数据库进行查询
        Response<Embedding> embed = embeddingModel.embed("退费费用");
        EmbeddingSearchRequest build = EmbeddingSearchRequest.builder().queryEmbedding(embed.content()).build();
        // 查询
        EmbeddingSearchResult<TextSegment> search = embeddingStore.search(build);
        search.matches().forEach(item -> {
            log.info("匹配的内容:{},分数为:{}",item.embedded().text(),item.score()); // 匹配的内容
        });

    }

4.2.7 对话阶段

在这里插入图片描述

在4.2.7完整代码基础上进行编写

// 对话阶段
QwenChatModel qwenChatModel = QwenChatModel.builder().apiKey(System.getenv("API_KEY")).modelName("qwen-max").build();
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
        .embeddingStore(embeddingStore)
        .embeddingModel(embeddingModel)
        .maxResults(5)// 最相似的五个结果
        .minScore(0.6)// 只找相似度0.6以上的
        .build();

// 为Assistant动态代理对象 chat ----> 对话内容存储chatMemory ----> 放入当前对话中
Assistant build1 = AiServices.builder(Assistant.class)
        .chatLanguageModel(qwenChatModel)
        .contentRetriever(retriever)
        .build();
log.info(build1.chat("退费费用"));

Assistant

public interface Assistant {
    String chat(String message);
}
  • AiService向量检索原理

在这里插入图片描述

4.2.8 整合SpringBoot

最终其实还会将查询到的内容, 和对话上下文组合起来, 发给LLM为我们组织语言进行回答。
这一步我们直接整合进SpringBoot进行实战:

  1. 配置一个Content Retriever 内容检索器
    a. 提供向量数据库和向量模型及其他参数
  2. 将内容检索器绑定到AiServices
  3. 当我们进行LLM对话时, 底层会自动为我们检索向量数据库进行回答。
  • 向量模型配置
#langchain4j设置
langchain4j:
  community:
    dashscope:
      chatModel:
        apiKey: ${API_KEY}
        modelName: qwen-plus
      # 流式
      streamingChatModel:
        apiKey: ${API_KEY}
        modelName: qwen-plus
        # 向量模型配置
      embeddingModel:
        apiKey: ${API_KEY}
        # modelName: qwen-plus
  • Assistant配置
/**
 * TODO RAG文档内容检索
 * */
@Bean
public EmbeddingStore embeddingStore(){
    return new InMemoryEmbeddingStore();
}
public interface AssistantRAG {
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);
    // 流式响应
    TokenStream stream(@MemoryId int memoryId, @UserMessage String userMessage);
}

@Bean
public AssistantRAG assistantARG(ChatLanguageModel qwenChatModel,
                                 StreamingChatLanguageModel qwenStreamingChatModel,
                                 ToolsService toolsService,
                                 EmbeddingStore embeddingStore,
                                 QwenEmbeddingModel qwenEmbeddingModel
) {
    EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
            .embeddingStore(embeddingStore)
            .embeddingModel(qwenEmbeddingModel)
            .maxResults(5)// 最相似的五个结果
            .minScore(0.6)// 只要相似大于0.6
            .build();
    AssistantRAG assistant = AiServices.builder(AssistantRAG.class)
            .chatLanguageModel(qwenChatModel)
            .streamingChatLanguageModel(qwenStreamingChatModel)
            .tools(toolsService)
            .chatMemoryProvider(memoryId ->
                    MessageWindowChatMemory.builder().maxMessages(10)
                            .id(memoryId).build()
            )
            .contentRetriever(retriever)
            .build();

    return assistant;
}
  • 当然我们还需要提前存储向量数据到向量数据库,embeddingStore在上边已经放进bean
/**
 * TODO 模拟将内容转为向量存储到向量数据库中
 * */
@SneakyThrows
@Bean
CommandLineRunner ingestTermOfServiceToVectorStore(QwenEmbeddingModel qwenEmbeddingModel,
                                                   EmbeddingStore embeddingStore){
    // 获取resource里边的文件
    Path path = Paths.get(this.getClass().getClassLoader().getResource("rag/terms-of-service.txt").toURI());
    return args -> {
        // 文档读取器
        TextDocumentParser textDocumentParser = new TextDocumentParser();
        // 读取为文档 (读取文件)
        Document document = FileSystemDocumentLoader.loadDocument(path, textDocumentParser);
        // 读取文档进行分割
        DocumentByLineSplitter documentByLineSplitter = new DocumentByLineSplitter(500, 200);
        List<TextSegment> split = documentByLineSplitter.split(document);
        // 向量化
        List<Embedding> content = qwenEmbeddingModel.embedAll(split).content();
        // 存入
        embeddingStore.addAll(content,split);
    };
}
  • controller层进行接口编写
/**
 * TODO RAG测试
 * */
@Autowired
private AiConfig.AssistantRAG assistantRAG;
@GetMapping(value = "/assistantRAG",produces ="text/stream;charset=UTF-8")
public Flux<String> assistantRAG(@RequestParam(defaultValue="退费费用") String message,@RequestParam Integer userId) {
    TokenStream stream = assistantRAG.stream(userId,message);
    return Flux.create(sink -> {
        stream.onPartialResponse(s -> sink.next(s))
                .onCompleteResponse(c -> sink.complete())
                .onError(sink::error)
                .start();

    });
}

5. Chain多个ServiceAI

在一个应用中, 可能需要多个模型共同一起协作完成一个任务。
为什么要这样:

  • 您的LLM可能不需要始终了解您拥有的每个tools。例如,当用户只是向LLM打招呼或说再见时,让 LLM 访问数十或数百个tools的成本很高,有时甚至很危险(LLM 调用中包含的每个tools都会消耗大量token),并且可能会导致意想不到的结果(LLM 可能会产生幻觉或被操纵以使用非预期的输入来调用tools)。
  • 关于 RAG:同样,有时需要为 LLM 提供一些上下文,但并非总是如此,因为它会产生额外的成本(更多上下文 = 更多token)并增加响应时间(更多上下文 = 更高的延迟)。
    关于模型参数:在某些情况下,您可能想不通的对话使用不同的 LLM ,以利用不同LLM的最佳特性。
  • 您可以一个接一个地调用 AI 服务(又称链接-chain)。
  • 您可以使用确定性和 LLM 支持的if/else语句(AI 服务可以返回boolean)。
  • 您可以使用确定性和 LLM 支持的switch语句(AI 服务可以返回enum)。
  • 您可以使用确定性和 LLM 驱动的for/while循环(AI 服务可以返回int和其他数字类型)。
  • 您可以在单元测试中模拟 AI 服务(因为它是一个接口)。
  • 您可以单独地对每个 AI 服务进行集成测试。

并且我们可以自由的进行任务编排:
大家平常应该见过一些AI智能体, 由多个(LLM)任务组合编排为一个智能体,

在这里插入图片描述

其实利用langchain4j的chain特性, 也可以完成这种类似的效果, 不过你需要自己完成前端编排以及不同任务的初始化和具体实现。

  • 演示2个模型协调合作,但是实际非常灵活
    /**
     * TODO 多个大模型进行调用
     * */
    ChatLanguageModel qwen;
    OllamaChatModel deepseek;
    @BeforeEach
    public void init(){
        deepseek = OllamaChatModel.builder()
                .baseUrl("http://192.168.xxx.xxx:11434")
                .modelName("deepseek-r1:1.5b")
                .build();
        qwen = QwenChatModel
                .builder()
                .apiKey(System.getenv("API_KEY"))
                .modelName("qwen-max")
                .build();
    }
    interface GreetingExpert {
        @dev.langchain4j.service.SystemMessage("""
        你是任务分类专家。将用户输入分类到以下类别之一:
        - MODIFY_TICKET(修改机票)
        - QUERY_TICKET(查询机票)
        - CANCEL_TICKET(退票)
        - OTHER(其他)
        
        重要:必须只返回枚举值本身(如 MODIFY_TICKET),不要添加任何解释、格式或特殊符号。
        错误示例:\boxed{MODIFY_TICKET} 或 "修改机票"
        正确示例:MODIFY_TICKET
        """)
        // {{it}}:LangChain4j 的占位符,表示方法参数 text
        @dev.langchain4j.service.UserMessage("以下文本是什么任务: {{it}}")
        TaskType isTask(String text);
    }
    interface ChatBot {
        @dev.langchain4j.service.SystemMessage("你是一名航空公司客服代理,请为客户服务:")
        String reply(String userMessage);
    }
    class MilesOfSmiles {

        private GreetingExpert greetingExpert;
        private ChatBot chatBot;

        public MilesOfSmiles(GreetingExpert greetingExpert, ChatBot chatBot) {
            this.greetingExpert = greetingExpert;
            this.chatBot = chatBot;
        }

        public String handle(String userMessage) {
            TaskType task = greetingExpert.isTask(userMessage);

            switch (task) {
                case MODIFY_TICKET:
                case QUERY_TICKET:
                case CANCEL_TICKET:
                    return task.getName() + "调用service方法处理";
                case OTHER:
                    return chatBot.reply(userMessage);
            }
            return null;
        }
    }

    @Test
    void test05(){
        GreetingExpert greetingExpert = AiServices.create(GreetingExpert.class, deepseek);
        ChatBot chatBot = AiServices.create(ChatBot.class, qwen);
        MilesOfSmiles milesOfSmiles = new MilesOfSmiles(greetingExpert, chatBot);
        log.info(milesOfSmiles.handle("你好,请问你是谁"));
    }

  • 枚举
public enum TaskType {
    MODIFY_TICKET("修改机票"),
    QUERY_TICKET("查询机票"),
    CANCEL_TICKET("退票"),
    OTHER("其他");
    private String name;
    TaskType(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

6. MCP

mcp其实很简单, 就是tools的一种外部调用的方式(既然要外部调用,肯定就需要遵循一种通信协议, 这里的协议就MCP,利用一种json-rpc2.0的json格式告知用有哪些tools什么参数, 调用哪个tool, 返回什么数据)。之前我们在自己程序中实现了tools, 但是这种tools无法提供给其他应用调用, 形成了应用孤岛, 无法提供外部共享

点击查询mcp服务

在这里插入图片描述

langchain4j 没有提供mcp server的实现, 但是提供的mcp client的实现:
当然mcpserver哪怕纯java也可以单独实现,下次有时间单独给大家讲解(如果有兴趣的话)

  • 依赖包
<!--mcp-->
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-mcp</artifactId>
  <version>${langchain4j.version}</version>
</dependency>
  • 代码实现
    /**
     * TODO 调用mcp
     * */

    @Test
    void test06() {
        // 1. 构建模型
        QwenChatModel qwenChatModel = QwenChatModel.builder()
                .apiKey(System.getenv("API_KEY"))
                .modelName("qwen-max")
                .build();

        // 2. 构建 MCP 传输层(stdio 方式)
        StdioMcpTransport transport = new StdioMcpTransport.Builder()
                .command(List.of("cmd", "/c", "npx", "-y", "howtocook-mcp"))
                .logEvents(true)
                .build();

        // 3. 构建 MCP 客户端
        DefaultMcpClient mcpClient = new DefaultMcpClient.Builder()
                .transport(transport)
                .build();

        try {
            // 4. 获取 MCP 提供的工具
            List<ToolSpecification> tools = mcpClient.listTools();
            log.info("🤖 Agent 已加载 {} 个工具:", tools.size());
            tools.forEach(tool -> log.info("   📦 {} - {}",
                    tool.name(),
                    tool.description()));
            // 5. 构建工具提供者
            McpToolProvider toolProvider = McpToolProvider.builder()
                    .mcpClients(List.of(mcpClient))
                    .build();

            // 6. 构建 AI 服务(关键:启用工具支持)
            Bot bot = AiServices.builder(Bot.class)
                    .chatLanguageModel(qwenChatModel)
                    .toolProvider(toolProvider)
                    .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) // 添加记忆
                    .build();

            // 7. 执行对话
            String response = bot.chat("我想吃西红柿炒鸡蛋");
            log.info("AI 回复:{}", response);

        } catch (Exception e) {
            log.error("MCP 调用失败", e);
        } finally {
            // 8. 清理资源
            mcpClient.close();
        }
    }

    interface Bot {
        // 简单对话接口,工具由 MCP 自动提供
        String chat(String userMessage);
    }
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐