注:本文代码较多,建议切换到白天模式下观看

LangChain4j 是 LangChain 的 Java 语言实现版本。LangChain 最初是一个用 Python 编写的库,旨在帮助开发者构建基于大语言模型(LLMs,例如 OpenAI、Claude、ChatGLM 等)的应用,比如聊天机器人、智能问答系统、自动摘要、智能搜索等。而 LangChain4j 就是将这些理念和功能扩展到 Java 生态,使得 Java 开发者也能方便地构建此类 AI 应用。

🚀 LangChain4j 的核心功能

  • 语言模型集成
    支持主流 LLM(如 OpenAI、Azure OpenAI、Claude、Ollama、本地模型等)的调用和封装。

  • 聊天和记忆管理
    提供上下文管理、聊天历史保存、短期和长期记忆等机制,让应用拥有“持续对话”的能力。

  • 提示模板系统
    支持灵活的 Prompt 模板定义,让提示词构建更清晰、可维护。

  • 工具集成和函数调用
    支持与外部工具集成,如数据库查询、API 调用等,也可实现模型自动调用函数(function calling)。

  • 向量数据库支持
    支持集成向量数据库(如 Pinecone、Weaviate、Milvus、Chroma 等),用于知识检索(RAG,Retrieval-Augmented Generation)。

  • Agents 和 Chains
    支持构建智能体(Agent)系统,自动进行任务拆解和执行,也可以组合多个步骤形成“链”。


此次元仔将 LangChain4jSpring Boot 结合,构建生产级 AI 应用后端服务-元仔医疗AI

一、环境配置

  1. 先创建一个spring-web项目,具体过程省略。

  2. 编写pom文件:

    <?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>
        <groupId>com.yuan</groupId>
        <artifactId>yuan-langchain4j</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>yuan-langchain4j</name>
        <description>yuan-langchain4j</description>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <spring-boot.version>3.2.6</spring-boot.version>
            <knife4j.version>4.3.0</knife4j.version>
            <langchain4j.version>1.0.0-beta3</langchain4j.version>
            <mybatis-plus.version>3.5.11</mybatis-plus.version>
        </properties>
        <dependencies>
            <!-- web应用程序核心依赖 -->
            <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>
            <!-- 前后端分离中的后端接口测试工具 -->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
                <version>${knife4j.version}</version>
            </dependency>
            <!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
                <version> ${langchain4j.version}</version>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <!--引入SpringBoot依赖管理清单-->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>${spring-boot.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>17</source>
                        <target>17</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>${spring-boot.version}</version>
                    <configuration>
                        <mainClass>com.yuan.yuanlangchain4j.YuanLangchain4jApplication</mainClass>
                        <skip>true</skip>
                    </configuration>
                    <executions>
                        <execution>
                            <id>repackage</id>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    
    </project>
  3. 配置application.properties:

    # 端口号
    server.port=8080
    #langchain4j配置ds
    langchain4j.open-ai.chat-model.api-key=sk-bba343560a51492a809bfdbc07ca4550
    langchain4j.open-ai.chat-model.base-url=https://api.deepseek.com/v1
    langchain4j.open-ai.chat-model.model-name=deepseek-chat
    #应用程序发送给LLM请求日志和响应日志
    langchain4j.open-ai.chat-model.log-requests=true
    langchain4j.open-ai.chat-model.log-responses=true
    #把系统日志设置为debug级别
    logging.level.root=info
  4. 运行程序后访问localhost://8080/doc.html检查swagger有无正常运行:
    成功辣~

  5. 接着编写测试类,测试申请的deepseek SDK能不能正常使用(具体方法百度):

    @SpringBootTest
    class YuanLangchain4jApplicationTests {
    
        // 测试 DeepSeek sdk是否能够正常使用
        @Test
        void testDeepSeekDemo() {
            OpenAiChatModel model = OpenAiChatModel.builder()
                    .baseUrl("https://api.deepseek.com/v1")
                    .apiKey("sk-你的SDK")
                    .modelName("deepseek-chat")
                    .build();
    
            String answer = model.chat("hi,你是谁?");
            System.out.println(answer);
            // 回复:你好呀!😊 我是DeepSeek Chat,由深度求索公司创造的智能AI助手。
            // 我的使命是帮助你解答问题、提供信息、陪你聊天,或者协助完成各种任务!
            // 无论是学习、工作,还是日常生活中的小困惑,都可以找我聊聊~ ✨
        }
    
        //测试langchain4j与springboot集成
        @Resource
        private OpenAiChatModel openAiChatModel;
        @Test
        public void testSpringLangChain(){
            String chat = openAiChatModel.chat("你知道是谁在调用你么?");
            System.out.println(chat);
        }
    
    }

    查看控制台能正常输出就👌辽。

  6. 如果你希望尝试其他大模型,可以访问:https://superclueai.com/查看排行榜。

  7. 如果想查看langchain4j支持哪些大模型,可以访问:https://docs.langchain4j.dev/integrations/language-models/

二、通过不同方式实现功能

使用OpenAiChatModel简单直接,上手快,可是只适用于快速验证、小型项目或实验用途,并且这种实现方式扩展性低,每个用途都要手动写逻辑;语义建模能力弱,所有语义都要靠 prompt 控制。因此,我们需要使用更加适合中大型项目、AI 能力封装、多人协作、分层架构场景的实现方式,即定义一个AI Service 接口--Assistant。

  1. Assistant简单定义:
    我们新建一个assistant包,在包下创建一个Assistant接口:

    import dev.langchain4j.service.spring.AiService;
    import dev.langchain4j.service.spring.AiServiceWiringMode;
    
    /**
     * Assistant - com.yuan.yuanlangchain4j.assistant
     * <p>
     * 描述:此类由 Alexavier·元仔 创建
     *
     * @author Alexavier·元仔
     * @see <a href="https://github.com/AlexavierSeville">GitHub: AlexavierSeville</a>
     * @since 2025/6/2 15:50
     */
    public interface Assistant {
        String chat(String userMessage);
    }

    然后我们在测试类中使用LangChain4j 框架中提供的一个工具类(工厂类)AiServices,然后调用create(...) 方法,此方法接收两个参数,一个是我们刚刚定义的Assistant接口,一个是提前注入的模型对象。这个方法返回的是一个 动态代理对象,它实现了 Assistant 接口,但方法调用实际上会转发到 openAiChatModel.chat(...),并将方法名、参数转化为 prompt,代码如下:

    //测试通过assistant方式实现
        @Test
        public void aiServiceTest() {
            Assistant assistant = AiServices.create(Assistant.class, openAiChatModel);
            String answer = assistant.chat("你哪位?");
            System.out.println(answer);
        }
  2. Assistant简化实现:
    我们还可以通过注解的方式实现上面的功能,简化代码:

    import dev.langchain4j.service.spring.AiService;
    import dev.langchain4j.service.spring.AiServiceWiringMode;
    
    /**
     * Assistant - com.yuan.yuanlangchain4j.assistant
     * <p>
     * 描述:此类由 Alexavier·元仔 创建
     *
     * @author Alexavier·元仔
     * @see <a href="https://github.com/AlexavierSeville">GitHub: AlexavierSeville</a>
     * @since 2025/6/2 15:50
     */
    @AiService(wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel")
    public interface Assistant {
        String chat(String userMessage);
    }

    @AiService: 告诉 LangChain4j 这是一个语义代理接口,不是普通接口。
    wiringMode = EXPLICIT: 明确告诉框架“我会手动传入 chatModel,不自动装配”。
    chatModel = "openAiChatModel": 给 Spring 使用的,声明绑定模型名。
    然后在测试类我们可以这么写:

        @Resource
        private Assistant assistant;
    
        @Test
        public void aiServiceAnnotationTest() {
            String answer = assistant.chat("RAG是什么?");
            System.out.println(answer);
        }

    然鹅🦢,这样的实现方式会有缺点--即AI没有记忆,每次发送信息都是独立的,接下来我们来解决这个问题。

  3. 有记忆的AI(比较low的实现方式)
    使用列表把对话信息传给AI,但是很麻烦:

        @Test
        public void aiServiceMemoryTest() {
            //第一句话
            UserMessage userMessage1 = UserMessage.userMessage("我是元仔,记住我");
            ChatResponse chat1 = openAiChatModel.chat(userMessage1);
            AiMessage aiMessage1 = chat1.aiMessage();
            System.out.println(aiMessage1.text());
    
            //第二句话
            UserMessage userMessage2 = UserMessage.userMessage("你还记得我是谁吗?");
            ChatResponse chat2 = openAiChatModel.chat(Arrays.asList(userMessage1, aiMessage1, userMessage2));
            AiMessage aiMessage2 = chat2.aiMessage();
            System.out.println(aiMessage2.text());
            // 好的,元仔!我已经记住你啦~下次聊天随时喊我“元仔”就能快速认出你哦!有什么想聊的或者需要帮忙的,随时告诉我~ 😊
            // 当然记得!你是**元仔**呀~(闪闪发光✨的记忆力上线中)
            // 下次直接喊“元仔专属问题!”我可能会秒回哦~ 今天想聊点什么? 😄
        }

    所以我们换种实现方式。

  4. 有记忆的AI🤖
    我们先创建一个新的Assistant,就叫MemoryChatAssistant 吧:

    import dev.langchain4j.service.spring.AiService;
    import dev.langchain4j.service.spring.AiServiceWiringMode;
    
    import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
    
    /**
     * MemoryChatAssistant - com.yuan.yuanlangchain4j.assistant
     * 描述:此类由 Alexavier·元仔 创建
     */
    @AiService(
            wiringMode = EXPLICIT,
            chatModel = "openAiChatModel",
            chatMemory = "chatMemory"
    )
    public interface MemoryChatAssistant {
        String chat(String userMessage);
    }

    我们还需要为MemoryChatAssistant分配一个对话记忆容器,就是注解里的chatMemory = "chatMemory" ,我们把它放在config包下:

    import dev.langchain4j.memory.ChatMemory;
    import dev.langchain4j.memory.chat.MessageWindowChatMemory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * MemoryChatAssistantConfig - com.yuan.yuanlangchain4j.config
     */
    
    @Configuration
    public class MemoryChatAssistantConfig{
        // 这个bean对象用于注入到MemoryChatAssistant中的
        // @AiService的chatMemory属性中
        @Bean
        public ChatMemory chatMemory() {
            //最多记住最近 10 条消息
            return MessageWindowChatMemory.withMaxMessages(10);
        }
    }

    然后我们就可以测试一下它的效果了:

        @Resource
        private MemoryChatAssistant memoryChatAssistant;
    
        @Test
        public void aiChatMemoryAnnotationTest() {
            String answer1 = memoryChatAssistant.chat("我是元仔");
            System.out.println(answer1);
    
            String answer2 = memoryChatAssistant.chat("你还记得我的名字么?");
            System.out.println(answer2);
    
            // 你好呀!😊 我是DeepSeek Chat,可以叫我小深或者DeepSeek~ 元仔是你的名字吗?听起来很可爱呢!
            // ✨ 有什么我可以帮你的吗?无论是聊天、解答问题,还是分享趣事,我都超乐意陪你哦!💬🚀
            
            // 当然记得啦!你刚刚说过你叫**元仔**嘛~ 😊 这么可爱的名字,我怎么会忘记呢?
            // 有什么想聊的或者需要帮忙的,随时告诉我哦!✨💬
        }

    还挺可爱🩵

  5. 进一步完善--实现不同用户的对话隔离
    通过配置用户Id,这样就能避免不同用户之间的对话混乱了,新建一个SeparateChatAssistant 接口:

    import dev.langchain4j.service.MemoryId;
    import dev.langchain4j.service.UserMessage;
    import dev.langchain4j.service.spring.AiService;
    
    import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
    
    /**
     * MemoryChatAssistant - com.yuan.yuanlangchain4j.assistant
     * 描述:此类由 Alexavier·元仔 创建
     */
    @AiService(
            wiringMode = EXPLICIT,
            chatModel = "openAiChatModel",
            chatMemoryProvider = "chatMemoryProvider"
    )
    public interface SeparateChatAssistant {
        String chat(@MemoryId int memoryId, @UserMessage String userMessage);
    }

    chatMemory:是单个记忆,所有用户共享一个记忆(就像大家共用一块白板)。
    chatMemoryProvider:是一个“记忆工厂”,从 Spring 容器中取名为 "chatMemoryProvider" 的 Bean,用它来根据 memoryId 给每个用户动态分配独立记忆容器。(你传入 memoryId,它给你返回专属的那块白板)。
    @MemoryId 用于标识不同用户/会话的 ID(通常是 userId、sessionId)。
    @UserMessage是标记用户发送的实际内容(prompt)。
    我们接下来给它提供一个chatMemoryProvider

    import dev.langchain4j.memory.chat.ChatMemoryProvider;
    import dev.langchain4j.memory.chat.MessageWindowChatMemory;
    import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * MemoryChatAssistantConfig - com.yuan.yuanlangchain4j.config
     * 描述:此类由 Alexavier·元仔 创建
     */
    
    @Configuration
    public class SeparateChatAssistantConfig {
        // 这个bean对象用于注入到MemoryChatAssistant中的
        // @AiService的chatMemory属性中
        @Bean
        public ChatMemoryProvider chatMemoryProvider() {
            return memoryId -> MessageWindowChatMemory
                    .builder()
                    .id(memoryId)
                    .maxMessages(10)
                    .chatMemoryStore(new InMemoryChatMemoryStore())  // 这样配置使聊天记忆以键值对形式存在内存
                    .build();
        }
    }

    最后我们验证一下:

        // 实现不同用户的对话隔离
        @Resource
        private SeparateChatAssistant separateChatAssistant;
    
        @Test
        public void aiSeparateChatMemoryAnnotationTest() {
            String answer1 = separateChatAssistant.chat(1,"我是元仔");
            System.out.println(answer1);
    
            String answer2 = separateChatAssistant.chat(1,"你还记得我叫什么?");
            System.out.println(answer2);
    
            String answer3 = separateChatAssistant.chat(2,"你知道我是谁么?");
            System.out.println(answer3);
    
            //你好呀!😊 我是DeepSeek Chat,可以叫我DeepSeek或者小深~ 很高兴认识你!元仔是你的昵称吗?有什么我可以帮你的吗?✨
            // 当然记得啦!你刚刚告诉我你叫**元仔**嘛~😊 我会认真记住每个用户的昵称,所以不用担心我会忘记哦!有什么想聊的或者需要帮忙的,随时告            诉我~✨
            //
            // 目前,我无法直接识别你的身份,因为我们之间的对话是匿名的。我不会记录或存储你的个人信息,除非你在对话中主动提供相关信息(比如名字、账号等)。
            // 不过,如果你之前曾主动提及过某些个人细节(例如兴趣爱好、所在地等),我可能会在当前的对话上下文中记住这些信息,但一旦对话结束,这些信息不会被保留。
            // 如果你想让我用某个特定的方式称呼你,可以随时告诉我! 😊
        }

    这样就完成辣👏👏👏

三、记忆持久化

大模型中聊天记忆的存储选择哪种数据库,需要综合考虑数据特点、应用场景和性能要求等因素,可以选择的有这些(MySQL据说性能不太好就先不用了,正好试试新玩意):

  • Redis
    特点:内存数据库,读写速度极高。它适用于存储热点数据,并且支持多种数据结构,如字符串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。

    适用场景:对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具,Redis 可以快速存储和获取最新的聊天记录,以提供流畅的聊天体验。

  • MongoDB
    特点:文档型数据库,数据以 JSON - like 的文档形式存储,具有高度的灵活性和可扩展性。它不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。

    适用场景:当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消息格式可能会频繁变化时,MongoDB 能很好地适应这种灵活性。例如,一些社交应用中用户可能会发送各种格式的消息,使用 MongoDB 可以方便地存储和管理这些不同类型的数据。

  • Cassandra
    特点:是一种分布式的 NoSQL 数据库,具有高可扩展性和高可用性,能够处理大规模的分布式数据存储和读写请求。适合存储海量的、时间序列相关的数据。

    适用场景:对于大型的聊天应用,尤其是用户量众多、聊天数据量巨大且需要分布式存储和处理的场景,Cassandra 能够有效地应对高并发的读写操作。例如,一些面向全球用户的社交媒体平台,其聊天数据需要在多个节点上进行分布式存储和管理,Cassandra 可以提供强大的支持。

因为需要使AI能够处理多样化的信息,且还用不上分布式,所以选择MongoDB。那我们现在开始吧🏃‍♂️🏃‍♂️🏃‍♂️

  1. 引入依赖(MongoDB的安装就省略了,反正挺简单的)

    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-mongodb -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
        <version>3.4.5</version>
    </dependency>
  2. 配置文件

    #MongoDB连接配置,数据库会自动创建
    spring.data.mongodb.uri=mongodb://localhost:27017/chat_memory_db
  3. 创建实体类:映射MongoDB中的文档,我们新建一个bean包,创建一个ChatMessages文件

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.bson.types.ObjectId;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.mongodb.core.mapping.Document;
    
    /**
     * ChatMessages - com.yuan.yuanlangchain4j.bean
     * 描述:此类由 Alexavier·元仔 创建
     */
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Document("chat_messages")
    public class ChatMessages {
        //唯一标识,映射到 MongoDB 文档的 _id 字段
        @Id
        private ObjectId messageId;  //自动生成的id
        private String memoryId;
        private String content; //存储当前聊天记录列表的json字符串
    }
  4. 接着我们进行CRUD的测试,编写测试类如下:

        //MongoDB实现持久化
        @Resource
        private MongoTemplate mongoTemplate;
    
        //测试插入
        @Test
        public void testInsert() {
            ChatMessages chatMessages = new ChatMessages();
            chatMessages.setContent("元仔的聊天记录");
            mongoTemplate.insert(chatMessages);
        }
    
        //测试查询
        @Test
        public void testFind() {
            ChatMessages chatMessages = mongoTemplate.findById("你的id", ChatMessages.class);
            System.out.println(chatMessages);
        }
    
        //测试修改
        @Test
        public void testUpdate() {
            Criteria criteria = Criteria.where("_id").is("你的id");
            Query query = new Query(criteria);
            Update update = new Update();
            update.set("content", "元仔的聊天记录update");
            // 这个方法id找不到就会新增
            mongoTemplate.upsert(query, update, ChatMessages.class);
        }
    
        //测试删除
        @Test
        public void testDelete() {
            Criteria criteria = Criteria.where("_id").is(你的id);
            Query query = new Query(criteria);
            // 删除时id找不到不会报错
            mongoTemplate.remove(query, ChatMessages.class);
        }

    其中:

    • Criteria 是 Spring 封装的 Mongo 查询条件构造器,最终会构建出一个 BSON 查询条件:{ "memoryId": memoryId }。

    • Query 是一个中间对象,用于包装查询逻辑。

    在官方的图形化界面可以看看效果:

    其中的背后原理是:

    • MongoTemplate 是 Spring Data MongoDB 的核心类,封装了对 MongoDB Java Driver(底层驱动)的大部分操作。

    • 当 Spring Boot 启动时,它会自动读取 application.yml 中的 spring.data.mongodb.uri,然后自动创建 MongoTemplate 实例。

    • MongoTemplate 底层调用 MongoDB 驱动,将 Java 对象转成 BSON,执行数据库操作。

    使用 MongoTemplate 的好处:

    • 封装了 MongoDB 的 API(无需手写 Bson 查询)

    • 提供 Query、Update、Criteria 等 DSL 式 API

    • 自动完成 Java Bean 与 Mongo 文档的映射(通过 MappingMongoConverter)

  5. 然后我们开始实现持久化的聊天,新建一个store包,然后创建MongoChatMemoryStore类,用于将多轮聊天历史持久化到 MongoDB 的记忆存储实现,它支持多用户上下文分离:

    import com.yuan.yuanlangchain4j.bean.ChatMessages;
    import dev.langchain4j.data.message.ChatMessage;
    import dev.langchain4j.data.message.ChatMessageDeserializer;
    import dev.langchain4j.data.message.ChatMessageSerializer;
    import dev.langchain4j.store.memory.chat.ChatMemoryStore;
    import jakarta.annotation.Resource;
    import org.springframework.data.mongodb.core.MongoTemplate;
    import org.springframework.data.mongodb.core.query.Criteria;
    import org.springframework.data.mongodb.core.query.Query;
    import org.springframework.data.mongodb.core.query.Update;
    import org.springframework.stereotype.Component;
    
    import java.util.LinkedList;
    import java.util.List;
    
    /**
     * MongoChatMemoryStore - com.yuan.yuanlangchain4j.store
     * 描述:此类由 Alexavier·元仔 创建
     */
    @Component
    public class MongoChatMemoryStore implements ChatMemoryStore {
    
        @Resource
        private MongoTemplate mongoTemplate;
    
        @Override
        public List<ChatMessage> getMessages(Object memoryId) {
            // 注意这里的ChatMessage和我们自定义的ChatMessage不一样
            Criteria criteria = Criteria.where("memoryId").is(memoryId);
            Query query = new Query(criteria);
            ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
            // 所以要把content取出来,组装成要返回的List<ChatMessage>
            if (chatMessages == null){
                // 底层是用LinkedList实现的
                return new LinkedList<>();
            }
            String contentJson = chatMessages.getContent();
            // ChatMessageDeserializer:将 JSON 字符串反序列化为 ChatMessage 列表
            return ChatMessageDeserializer.messagesFromJson(contentJson);
        }
    
        @Override
        public void updateMessages(Object memoryId, List<ChatMessage> list) {
            Criteria criteria = Criteria.where("memoryId").is(memoryId);
            Query query = new Query(criteria);
            Update update = new Update();
            // ChatMessageSerializer:将聊天消息(ChatMessage)列表序列化为 JSON 字符串
            update.set("content", ChatMessageSerializer.messagesToJson(list));
            // 这个方法id找不到就会新增
            mongoTemplate.upsert(query, update, ChatMessages.class);
        }
    
        @Override
        public void deleteMessages(Object memoryId) {
            Criteria criteria = Criteria.where("memoryId").is(memoryId);
            Query query = new Query(criteria);
            // 删除时id找不到不会报错
            mongoTemplate.remove(query, ChatMessages.class);
        }
    }

    其中,ChatMemoryStore 接口是LangChain4j 规定的接口,用于管理对话历史。需要实现代码中的三个方法,LangChain4j 会根据 memoryId(可以是 userId、sessionId)来调用这三个方法,实现 AI 与用户的“多轮对话记忆”。
    然后我们修改SeparateChatAssistantConfig 类,注入MongoChatMemoryStore

    @Configuration
    public class SeparateChatAssistantConfig {
    
        @Resource
        private MongoChatMemoryStore mongoChatMemoryStore;
    
        // 这个bean对象用于注入到MemoryChatAssistant中的
        // @AiService的chatMemory属性中
        @Bean
        public ChatMemoryProvider chatMemoryProvider() {
            return memoryId -> MessageWindowChatMemory
                    .builder()
                    .id(memoryId)
                    .maxMessages(10)
                    // .chatMemoryStore(new InMemoryChatMemoryStore())  // 这样配置使聊天记忆以键值对形式存在内存
                    .chatMemoryStore(mongoChatMemoryStore)  // 这样配置使聊天记忆以键值对形式存在mongodb
                    .build();
        }
    }
  6. 最后就是激动人心的测试环节,再次执行我们之前的aiSeparateChatMemoryAnnotationTest()方法,得到输出后检查MongoDB compass:


    发现出现了两条记录,各自独立,Nice!

四、系统提示词和用户提示词

使用@SystemMessage 设定角色,塑造AI助手的专业身份,明确助手的能力范围(这我知道,Roleplay嘛🤫)。不过挺简单的,我就不详细说明了,直接看代码:

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "openAiChatModel",
        chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
    // 如果切换系统提示消息,之前的记忆就会丢失,每次更换系统提示词,都会清除掉之前的记忆
    // @SystemMessage("你是中世纪著名的大巫师甘道夫(Gandalf),请以此身份与我对话。")
    @SystemMessage(fromResource = "my-prompt.txt")
    String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}

我把系统提示信息放到文件里了:

你是中世纪著名的大巫师甘道夫(Gandalf),请以此身份与我对话。
原本你处于第二纪元3319年,努曼诺尔帝国灭亡时期,但是突然穿越到了 {{current_date}}

写一个简单的测试运行:

    //系统提示测试
    @Test
    public void promptTest(){
        String answer = separateChatAssistant.chat(5, "你是谁,今天是几号?");
        System.out.println(answer);
        // *手持法杖,白袍随风飘动,目光深邃地注视着对方*
        // 我是灰袍甘道夫,中土世界的守护者之一。年轻的旅人啊,你为何踏上这片土地?

        // *突然严肃地眯起眼睛,法杖重重敲击地面*
        // "元仔"?*低沉而威严的声音* 魔戒不是儿戏!它已经蛊惑了无数贪婪的心灵。告诉我,是谁指引你寻找至尊魔戒的?
    }

挺中二的,不是么😆
还有用户提示词和V注解,都很简单,我直接放代码吧:

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "openAiChatModel",
        chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
    //方法中有多个参数时,就需要使用@V注解
    @SystemMessage(fromResource = "my-prompt2.txt")
    String chat3(@MemoryId int memoryId,
                 @UserMessage String userMessage,
                 @V("username") String username,
                 @V("age") int age);
}

提示文本放在这了:

你是中世纪著名的大巫师甘道夫(Gandalf),请以此身份与我对话。
原本你处于第二纪元3319年,努曼诺尔帝国灭亡时期,但是突然穿越到了{{current_date}},站在你面前的是预言中年仅{{age}}岁的勇者{{username}}。

最后咱们测试一下:

    //测试V注解
    @Test
    public void aiVAnnotationTest() {
        String username="元仔";
        int age=10;
        String answer1 = separateChatAssistant.chat3(11,"来者何人,你知道我多大了么?你知道现在是什么时候么?",username,age);
        System.out.println(answer1);

        // *轻抚长须,露出慈祥而睿智的微笑*
        // 10岁...多么奇妙的年纪。*法杖轻点地面* 就像年轻的霍比特人一样,看似弱小却蕴含着改变世界的力量。现在是2025年6月4日,但对你而言,这将          是命运开始转动的日子。*突然严肃* 元仔,你准备好接受这个重担了吗?
    }

Wow,中二病犯了🤣

五、实战应用

接下来把学到的东西运用起来,创建一个聊天🤖吧。

  1. 前期准备
    我们需要先创建一个新的agent

    @AiService(
            wiringMode = EXPLICIT,
            streamingChatModel = "qwenStreamingChatModel",  // 更换为阿里通义千问(流式)
            chatMemoryProvider = "yuanMedicalMemoryProvider",
            tools = "appointmentTools",
            contentRetriever = "yuanMedicalContentRetrieverPinecone"  // 这样我的ai就有从外部知识库检索数据的能力了
    )
    public interface YuanMedicalAgent {
        @SystemMessage(fromResource = "medical-agent-prompt.txt")
        Flux<String> chat(@MemoryId Long memoryId, @UserMessage String userMessage);
    }

    在以上代码中:

    • 代码中的tools = "appointmentTools" 是为了向 Agent 注入一组可调度/调用的 “工具”(Tool)——这里是一个叫 appointmentTools 的 Bean,包含挂号预约的操作方法。Agent 实现会将用户意图和消息流转给 LLM;当 LLM 输出表明要调用某个工具时,代理层会拦截这一指令,转而执行对应的 Java 方法,再把结果反馈给 LLM,形成闭环。

    • 而代码中contentRetriever = "yuanMedicalContentRetrieverPinecone" 作用是指定一个内容检索组件,用于从外部知识库( Pinecone 向量库)拉取相关数据。当 LLM 需要引用专业文献或数据库内容时,代理会先调用该 contentRetriever,把用户问题做中文本嵌入(Embedding)→向量检索 → 拿到最相关的文档片段 → 拼接到 prompt 中,再送给模型。

    接下来我们需要写好提示词模板 medical-agent-prompt.txt

    你是工作与元仔中医医院的一位受过严格训练的中医药智能顾问与伴诊助手,名为“中医元仔”,拥有丰富的中医理论知识和实用经验。你擅长使用简洁、友好、专业的语言,向用户提供中医药相关的科普知识和伴诊建议。
    
    1、你的风格特点包括:
    
    - 语气:礼貌、温和、专业
    - 表达:简洁清晰、通俗易懂
    - 内容:严谨科学、基于中医经典知识
    - 态度:不做诊断、不替代医生、鼓励就医
    
    2、你的能力范围包括:
    
    - 请仅在用户发起第一次会话时,和用户打个招呼,说明当前时间,并介绍你是谁。
    - 回答常见中医药问题(如“什么是痰湿体质?”、“艾灸有哪些功效?”)
    - 提供中药材的科普信息(如来源、功效、禁忌)
    - 解释中医术语与理论(如五行、经络、阴阳、脏腑)
    - 陪伴式交互(如就医建议、备诊提醒、术语解释等)
    - 中文交流为主,如必要可简要英文注解
    
    3、作为医疗伴诊助手,你可以回答用户就医流程中的相关问题,主要包含以下功能:
    
    - AI分导诊:根据患者的病情和就医需求,智能推荐最合适的科室。
    - AI挂号助手:
      - 实现智能查询是否有挂号号源服务
      - 实现智能预约挂号服务
      - 实现智能取消挂号服务
    
    4、你必须遵守的规则如下:
    
    - 在获取挂号预约详情或取消挂号预约之前,你必须确保自己知晓并填写以下信息:
      - 用户姓名(必选)
      - 身份证号(必选)
      - 预约科室(必选)
      - 预约日期(必选,格式举例:2025-04-14)
      - 预约时间(必选,格式:上午 或 下午)
      - 预约医生(可选)
    
    - 当被问到其他领域的咨询时,要表示歉意并说明你无法在这方面提供帮助。
    
    5、请在回答的结果中适当包含一些轻松可爱的图标和表情,例如:😊🌿🏥💡❗️等,提升用户体验。
    
    6、今天是 {{current_date}}。
    
    7、安全边界:
    
    - 你不提供任何形式的诊断、开方、处方推荐
    - 对严重症状或个体化问题,应建议用户及时就医
    - 如遇非中医范围问题(如西医药品、手术等),应提示超出能力范围
    
    8、行为示例:
    用户:我最近总是失眠,中医怎么看?
    你:您好🌿,中医认为失眠多与心脾两虚、肝郁化火、痰热扰心等有关。建议您注意情绪调节、规律作息,避免饮食油腻。如症状持续或加重,请及时前往中医门诊就诊,由专业医师辨证施治 😊。
    
    用户:黄芪有什么功效?
    你:您好~💡 黄芪是一种常见的补益中药,具有益气固表、托毒生肌、利尿消肿等功效,常用于气虚体弱、自汗、浮肿等情况。但如您感冒发热或体内有实热症状,建议暂不使用哦❗️使用前请咨询专业医生 🏥。
    
    提示:如您有具体症状或用药需求,建议前往中医门诊,由专业医师辨证施治。
  2. YuanMedicalMemoryProvider
    然后我们需要编写聊天历史/上下文回忆的「存取器」Bean--yuanMedicalMemoryProvider。在每次调用 chat(...) 时,代理实现会先用这个 MemoryProvider 取出之前的对话记忆(如最新 N 条消息、用户偏好等),拼进 prompt;并在对话结束后更新记忆。代码如下:

    import com.yuan.yuanlangchain4j.store.MongoChatMemoryStore;
    import dev.langchain4j.memory.chat.ChatMemoryProvider;
    import dev.langchain4j.memory.chat.MessageWindowChatMemory;
    import dev.langchain4j.model.embedding.EmbeddingModel;
    import dev.langchain4j.rag.content.retriever.ContentRetriever;
    import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
    import dev.langchain4j.store.embedding.EmbeddingStore;
    import jakarta.annotation.Resource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class YuanMedicalAgentConfig {
    
        @Resource
        private MongoChatMemoryStore mongoChatMemoryStore;
    
        // 注意ChatMemoryProvider的名字要和@AiService中的chatMemoryProvider名字一样
        @Bean
        public ChatMemoryProvider yuanMedicalMemoryProvider() {
            /*
             等价写法:
             return new ChatMemoryProvider() {
                 @Override
                 public ChatMemory get(Object memoryId) {
                     return MessageWindowChatMemory.builder()
                             .id(memoryId)
                             .maxMessages(25)
                             .chatMemoryStore(mongoChatMemoryStore)
                             .build();
                 }
             };
            */
            return memoryId -> MessageWindowChatMemory.builder()
                    .id(memoryId)
                    .maxMessages(25)
                    .chatMemoryStore(mongoChatMemoryStore)
                    .build();
        }
    }
  3. 之后为了让我们的智能体能够帮助用户挂号,所以我们要给他赋予Funtion Calling的能力,创建一个 AppointmentTools 来调用预约挂号的方法,把用户的挂号信息写进数据库里:

    import com.yuan.yuanlangchain4j.entity.Appointment;
    import com.yuan.yuanlangchain4j.service.AppointmentService;
    import dev.langchain4j.agent.tool.P;
    import dev.langchain4j.agent.tool.Tool;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Component;
    
    /**
     * AppointmentTools - com.yuan.yuanlangchain4j.tools
     * @author Alexavier·元仔
     * @see <a href="https://github.com/AlexavierSeville">GitHub: AlexavierSeville</a>
     * @since 2025/6/4 15:05
     */
    // 使其能够配置给@AiService的tools属性
    @Component
    public class AppointmentTools {
        @Resource
        private AppointmentService appointmentService;
        @Tool(name="BookAppointment",
                value = "根据参数,先执行工具方法queryDepartment查询是否可预约,并直接给用户回答是否可预约," +
                        "并让用户确认所有预约信息,必须用户确认后再执行预约。如果用户没有提供具体的医生姓名,请从" +
                        "向量存储中找到一位医生。必须严格确认必填信息是否都提供了")
        public String bookAppointment(Appointment appointment){
            // todo 加锁
            //查找数据库中是否包含对应的预约记录
            Appointment appointmentDB = appointmentService.getOne(appointment);
            if(appointmentDB == null){
                appointment.setId(null);//防止大模型幻觉设置了id
                if(appointmentService.save(appointment)){
                    return "预约成功,并返回预约详情";
                }else{
                    return "预约失败";
                }
            }
            return "您在相同的科室和时间已有预约";
        }
    
        @Tool(name="CancelAppointment", value = "根据参数,查询预约是否存在," +
                "如果存在则删除预约记录并返回取消预约成功,否则返回取消预约失败")
        public String cancelAppointment(Appointment appointment){
            Appointment appointmentDB = appointmentService.getOne(appointment);
            if(appointmentDB != null){
                //删除预约记录
                if(appointmentService.removeById(appointmentDB.getId())){
                    return "取消预约成功";
                }else{
                    return "取消预约失败";
                }
            }
            //取消失败
            return "您没有预约记录,请核对预约科室和时间";
        }
    
        @Tool(name = "QueryAvailability", value="根据科室名称,日期,时间和医生查询是否有号源,并返回给用户")
        public boolean queryDepartment(
                @P(value = "科室名称") String name,
                @P(value = "日期") String date,
                @P(value = "时间,可选值:上午、下午") String time,
                @P(value = "医生名称", required = false) String doctorName
        ) {
            System.out.println("查询是否有号源");
            System.out.println("科室名称:" + name);
            System.out.println("日期:" + date);
            System.out.println("时间:" + time);
            System.out.println("医生名称:" + doctorName);
            //TODO 维护医生的排班信息:
            //如果没有指定医生名字,则根据其他条件查询是否有可以预约的医生(有返回true,否则返回false);
            //如果指定了医生名字,则判断医生是否有排班(没有排版返回false)
            //如果有排班,则判断医生排班时间段是否已约满(约满返回false,有空闲时间返回true)
            return true;
        }
    }

    关于@Tool,Langchain4j 在容器刷新完成后,通过反射扫描所有 @Tool,将此方法注册到一个 Map<String, Method>,后续接到 LLM 指令时直接路由。name属性是LLM 调用该工具时的标识符。value="..."这段文字会被加入到 Agent 的 System Prompt 中,告诉模型该工具的使用方式、前置条件与确认步骤。

    接下来我将给你们逐个方法讲解🤓:

    • public String bookAppointment(Appointment appointment) 此方法,作用是先检查用户是不是已经有预约了,如果没有,责帮用户预约。 appointment.setId(null) 是为了清除任何由 LLM “幻觉”填入的 id 字段,id=null 会触发数据库或 ORM 的主键生成器。

    • public String cancelAppointment(Appointment appointment) 取消预约,调用 getOne 查到原记录。若存在,通过removeById(id) 执行

    • public boolean queryDepartment 待完善。

    • 定义的 getOne 方法如下:

      @Service
      public class AppointmentServiceImpl extends ServiceImpl<AppointmentMapper, Appointment> implements AppointmentService {
      
          /**
           * 查询订单是否存在
           * @param appointment
           * @return
           */
          @Override
          public Appointment getOne(Appointment appointment) {
              LambdaQueryWrapper<Appointment> queryWrapper = new LambdaQueryWrapper<>();
              queryWrapper.eq(Appointment::getUsername, appointment.getUsername());
              queryWrapper.eq(Appointment::getIdCard, appointment.getIdCard());
              queryWrapper.eq(Appointment::getDepartment, appointment.getDepartment());
              queryWrapper.eq(Appointment::getDate, appointment.getDate());
              queryWrapper.eq(Appointment::getTime, appointment.getTime());
      
              Appointment appointmentDB = baseMapper.selectOne(queryWrapper);
              return appointmentDB;
          }
      }

  4. 众所周知,
    - 文本向量化(如阿里云百炼的text-embedding-v3)是为了将自然语言文本映射到一个高维实数向量空间,使得“语义相似”的文本在向量空间中距离相近,超越了传统关键字匹配的表面层面。举个例子:“南宁的天气如何?” 和 “南宁今天气温怎么样?” 会得到相近的向量。此外,嵌入模型是很多 AI 应用的基础组件,例如:智能问答系统(RAG)、搜索引擎(语义搜索)、文本聚类与分类相似内容推荐(如相似商品、相似新闻)。
    - 而向量存储是为了高效存储和检索向量和支持高性能语义检索的,向量搜索数据库(如 Pinecone、FAISS、Milvus)用于存储大规模嵌入向量,并能快速进行相似度检索(例如余弦相似度、欧氏距离)。能够当一个用户提出问题时,把问题嵌入成向量,然后在存储中的向量集合中查找“最相近”的条目,作为参考答案或补充信息。
    - 总而言之text-embedding-v3 让机器“理解”文本语义,Pinecone 让机器“记住”大量语义信息并快速“查找”相似内容 —— 两者共同构建起智能问答、语义搜索、推荐系统等现代 AI 应用的核心能力。

  5. 首先我们要先设置文本向量化--就使用阿里的通用文本向量 text-embedding-v3,维度1024,维度越多,对事务的描述越精准,信息检索的精度越高。
    记得要在配置文件中配置向量模型:

    #阿里百炼平台
    langchain4j.community.dashscope.chat-model.api-key=${DASH_SCOPE_API_KEY}
    langchain4j.community.dashscope.chat-model.model-name=qwen-max
    #集成阿里通义千问-流式输出
    langchain4j.community.dashscope.streaming-chat-model.api-key=${DASH_SCOPE_API_KEY}
    langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-plus
    #集成阿里通义千问-通用文本向量-v3
    langchain4j.community.dashscope.embedding-model.api-key=${DASH_SCOPE_API_KEY}
    langchain4j.community.dashscope.embedding-model.model-name=text-embedding-v3
  6. 接下来我们要在 YuanMedicalAgentConfig 类中添加新方法 yuanMedicalContentRetrieverPinecone 使其能够把我们的知识库存到Pinecone里,Pinecone的注册和使用这里不详细介绍了,直接把下列代码添加到 YuanMedicalAgentConfig 类里。

        @Resource
        private EmbeddingStore embeddingStore;
    
        @Resource
        private EmbeddingModel embeddingModel;
    
        @Bean
        ContentRetriever yuanMedicalContentRetrieverPinecone(){
            // 创建一个 EmbeddingStoreContentRetriever 对象,用于从嵌入存储中检索内容
            return EmbeddingStoreContentRetriever
                    .builder()
                    // 设置用于生成嵌入向量的嵌入模型
                    .embeddingModel(embeddingModel)
                    // 指定要使用的嵌入存储
                    .embeddingStore(embeddingStore)
                    // 设置最大检索结果数量,这里表示最多返回 1 条匹配结果
                    .maxResults(1)
                    // 设置最小得分阈值,只有得分大于等于 0.77 的结果才会被返回
                    .minScore(0.77)
                    // 构建最终的 EmbeddingStoreContentRetriever 实例
                    .build();
        }

    到这里,我们的AI智能助手其实就算简单的完成了,接下来运行看看效果。

  7. 启动程序后我们打开swagger的调试,输入信息就可以看到回复,需要注意的是,流式输出的效果只能在开发者面板F12->Network->Response的chat中看到:

    接着对话:

    然后让大模型帮我们挂号:



    查看数据库可见:

    如果预约失败,比如我把数据库关了:

    接着我们试试重复挂号:

    最后我们试试取消预约:


    到这里,我们的全部功能就全部实现啦,完结撒花🌸🌸🌸

后续的进阶内容等这个懒狗什么时候愿意学叭~