# java-ai-langchain4j **Repository Path**: zhaomou123/java-ai-langchain4j ## Basic Information - **Project Name**: java-ai-langchain4j - **Description**: 基于LLM+RAG+Agent+Faction Calling的智慧医疗系统 - **Primary Language**: Java - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2025-09-06 - **Last Updated**: 2025-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, SpringBoot, mangodb, MyBatis ## README # 1.创建`Springboot`项目 ## 添加依赖 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.6 org.example langchain4j 0.0.1-SNAPSHOT langchain4j A Spring Boot project for LangChain integration 17 17 UTF-8 3.2.6 4.3.0 1.0.0-beta3 3.5.11 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} dev.langchain4j langchain4j-open-ai 1.0.0-beta3 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import dev.langchain4j langchain4j-bom ${langchain4j.version} pom import org.springframework.boot spring-boot-maven-plugin ``` `Knife4j` 是一个 **增强版的 `Swagger UI`**,用于在 `Java` 项目(尤其是 `Spring Boot`)中自动生成 **接口文档**。 启动之后访问 http://localhost:8080/doc.html 查看程序能否成功运行并显示如下页面 ![image-20250423104051494](./assets/image-20250423104051494.png) ## 测试`Langchain4j` 写一个测试类 ```java @SpringBootTest class Langchain4jApplicationTests { @Test void contextLoads() { //初始化模型 OpenAiChatModel OpenAiChatModel model = OpenAiChatModel.builder() //阿里云千问大模型 .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1") .apiKey("") //设置模型apiKey .modelName("qwen-plus") //设置模型名称 .build(); //向模型提问 String answer = model.chat("你好"); //输出结果 System.out.println(answer); } } ``` ## Springboot集成 对于 OpenAI(`langchain4j-open-ai`),依赖项名称将是`langchain4j-open-ai-spring-boot-starter`: ```xml dev.langchain4j langchain4j-open-ai-spring-boot-starter 1.0.0-beta3 ``` 然后,您可以在文件中配置模型参数,`application.properties`如下所示: ```ini # 设置语言模型的API密钥和模型名称 langchain4j.open-ai.chat-model.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1 langchain4j.open-ai.chat-model.api-key=你的key langchain4j.open-ai.chat-model.model-name=qwen-plus # 开启日志 langchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true #启用日志debug级别 logging.level.root=debug ``` 在这种情况下,将自动创建一个实例`OpenAiChatModel`(一个实现),并且您可以在需要的地方注入该实例 ```java @Autowired private OpenAiChatModel openAiChatModel; @Test public void testSpringBoot() { //向模型提问 String answer = openAiChatModel.chat("你好"); //输出结果 System.out.println(answer); } ``` # 2.接入其他大模型 `LangChain4j`支持接入的大模型: https://docs.langchain4j.dev/integrations/language-models/ ## 接入DeepSeek 访问官网: https://www.deepseek.com/ 注册账号,获取`base_url`和`api_key`,充值 然后`application.properties`配置 ```ini #DeepSeek langchain4j.open-ai.chat-model.base-url=https://api.deepseek.com langchain4j.open-ai.chat-model.api-key=${DEEP_SEEK_API_KEY} #DeepSeek-V3 langchain4j.open-ai.chat-model.model-name=deepseek-chat #DeepSeek-R1 推理模型 #langchain4j.open-ai.chat-model.model-name=deepseek-reasoner ``` ## 接入阿里百炼平台 官网注册获取APIKEY 添加依赖 ```xml dev.langchain4j langchain4j-community-dashscope-spring-boot-starter dev.langchain4j langchain4j-community-bom ${langchain4j.version}< pom import ``` 配置参数 ```ini #阿里百炼平台 langchain4j.community.dashscope.chat-model.api-key=${DASH_SCOPE_API_KEY} langchain4j.community.dashscope.chat-model.model-name=qwen-plus ``` 测试 ```java @Autowired private QwenChatModel qwenChatModel; @Test public void testDashScopeQwen() { //向模型提问 String answer = qwenChatModel.chat("你好"); //输出结果 System.out.println(answer); } ``` # 3.人工智能服务`AIService` ## 引入依赖 ```xml dev.langchain4j langchain4j-spring-boot-starter ``` ## 创建接口 `@AiService`定义如下 ```java @Service @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AiService { AiServiceWiringMode wiringMode() default AiServiceWiringMode.AUTOMATIC; String chatModel() default "";//绑定聊天模型 String streamingChatModel() default ""; String chatMemory() default "";//绑定聊天记忆 String chatMemoryProvider() default "";//绑定聊天记忆隔离和持久化 String contentRetriever() default "";//绑定内容检索器 String retrievalAugmentor() default ""; String moderationModel() default ""; String[] tools() default {};//绑定工具 } ``` 使用`@AiService`注解,它可能用于标记一个接口,使其被框架(如 `langchain4j`)自动处理,生成 AI 服务的实现。 - **动态代理**:框架会基于该接口生成代理类,处理方法调用(如 `chat(String message)`)。 - **依赖注入**:标记的接口会被 Spring 容器管理,允许通过 `@Autowired` 或其他方式注入。 - **AI 功能集成**:注解会将接口与 AI 模型(如 OpenAI 或其他语言模型)绑定,自动处理请求和响应。 ```java @AiService //如果你有很多AI模型实例,可用自定义绑定哪个模型 //@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel") public interface Assistant { String chat(String message); } ``` ## 测试 ``` @Autowired private Assistant assistant; @Test public void testAssistant() { String answer = assistant.chat("Hello"); System.out.println(answer); } ``` ## 工作原理 `AiServices`会组装`Assistant`接口以及其他组件,并使用**反射机制**创建一个实现`Assistant`接口的代理对象。 这个代理对象会**处理输入和输出**的所有转换工作。在这个例子中,`chat`方法的输入是一个字符串,但是大 模型需要一个 `UserMessage` 对象。所以,代理对象将这个字符串转换为 `UserMessage` ,并调用聊天语 言模型。`chat`方法的输出类型也是字符串,但是大模型返回的是 `AiMessage` 对象,代理对象会将其转换 为字符串。 # 4.聊天记忆 `Chat memory` ## 使用`ChatMemory`实现聊天记忆 `ChatMemory`接口的定义如下: ```java public interface ChatMemory { Object id(); //添加消息 void add(ChatMessage var1); //消息集合,存储历史消息 List messages(); //清空消息 void clear(); } ``` `ChatMemory`有两个实现 - `MessageWindowChatMemory`:基于消息数量的滑动窗口,保留最近的 `N` 条消息。 - `TokenWindowChatMemory`:基于 `token` 数量的滑动窗口,保留最近的 `N` 个 `token`。 以下是`MessageWindowChatMemory`使用示例: ```java @Test public void testChatMemory3() { //创建chatMemory MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10); //创建AIService Assistant assistant = AiServices .builder(Assistant.class) .chatLanguageModel(openAiChatModel) .chatMemory(chatMemory) .build(); //调用service的接口 String answer1 = assistant.chat("我是环环"); System.out.println(answer1); String answer2 = assistant.chat("我是谁"); System.out.println(answer2); } ``` ## 结合`AIService`实现聊天记忆 ### 创建记忆对话智能体 当`AIService`由多个组件(大模型,聊天记忆等)组成的时候,我们就可以称他为 **智能体** 了 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel", chatMemory = "chatMemory" ) public interface MemoryChatAssistant { String chat(String message); } ``` ### 配置ChatMemory ```java @Configuration public class MemoryChatAssistantConfig { @Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.withMaxMessages(10); } } ``` ### 测试 ```java @Autowired private MemoryChatAssistant memoryChatAssistant; @Test public void testChatMemory4() { String answer1 = memoryChatAssistant.chat("我是环环"); System.out.println(answer1); String answer2 = memoryChatAssistant.chat("我是谁"); System.out.println(answer2); } ``` ## 隔离聊天记忆 隔离不同的聊天,比如我想开启一个新的聊天 ### 创建记忆隔离对话智能体 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 //chatMemory = "chatMemory",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProvider"//找到对应的bean进行绑定 ) public interface SeparateChatAssistant { String chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` **不能同时使用 `chatMemory` (单一会话内存)和 `chatMemoryProvider`(多会话内存)** 为每个用户或会话提供独立的 `ChatMemory` 实例,根据提供的 `memoryId`(通常是用户 ID 或会话 ID)返回对应的 `ChatMemory` 实例。 - `@MemoryId` 注解用于标识方法参数,该参数的值将作为 `memoryId` 传递给 `chatMemoryProvider`,以获取对应的 `ChatMemory` 实例。 - `@UserMessage` 注解用于标识方法参数,该参数的值将作为用户消息发送给大语言模型(LLM)。 ### 配置`ChatMemoryProvider` ```java @Configuration public class SeparateChatAssistantConfig { @Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(10) .build(); //看不懂这种写法的,可以看下面这种 return new ChatMemoryProvider() { @Override public ChatMemory get(Object memoryId) { return MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(10) .build(); } } } } ``` `ChatMemoryProvider`:这是一个函数式接口,定义了如何根据 `memoryId`(通常是用户 ID 或会话 ID)提供对应的 `ChatMemory` 实例。定义如下: ```java @FunctionalInterface public interface ChatMemoryProvider { ChatMemory get(Object var1); } ``` 每当有新的对话请求时,LangChain4j 会调用 `chatMemoryProvider` 的 `get` 方法,传入当前的 `memoryId`,以获取对应的 `ChatMemory` 实例。 这意味着,对于每个不同的 `memoryId`,都会有一个独立的对话记忆实例,确保多用户或多会话场景下的对话上下文不会混淆。 ### 测试 ```java @Autowired private SeparateChatAssistant separateChatAssistant; @Test public void testChatMemory5() { 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); } ``` # 5.持久化聊天记忆 `Persistence` 默认情况下,聊天记忆**存储在内存**中。如果需要持久化存储,可以将其存储在数据库中,然后数据库的选型就需要根据具体的业务场景。 ## 数据库的选择 `MySQL` - 特点:关系型数据库。支持事务处理,确保数据的一致性和完整性,**适用于结构化数据的存储和查询**。 - 适用场景:如果**聊天记忆数据结构较为规整**,例如包含固定的字段如对话 ID、用户 ID、时间 戳、消息内容等,且**需要进行复杂的查询和统计分析**,如按用户统计对话次数、按时间范围查询特定对话等,`MySQL` 是不错的选择。 `Redis` - 特点:**内存数据库,读写速度极高**。它适用于存储热点数据,并且支持多种数据结构,如字符 串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。 - 适用场景:**对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具**,`Redis` 可以快 速存储和获取最新的聊天记录,以提供流畅的聊天体验。 `MongoDB` - 特点:文档型数据库,数据以`BSON`的文档形式存储,具有高度的灵活性和可扩展性。**它不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。** - 适用场景:当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者**消息格式可能会频繁变化时,`MongoDB` 能很好地适应这种灵活性**。例如,一些社交应用中用户可能会发送各种格式的消息,使用 `MongoDB` 可以方便地存储和管理这些不同类型的数据。 ## `MongoDB` 安装 使用`docker`进行安装,简单方便 ```ini docker pull mongodb docker run -d \ --name mongodb \ -p 27017:27017 \ -e MONGO_INITDB_ROOT_USERNAME=admin \ -e MONGO_INITDB_ROOT_PASSWORD=123456 \ mongo:latest ``` 使用`Navicat`连接MongoDB![image-20250423143512507](./assets/image-20250423143512507.png) ## 整合`Springboot` ### 添加依赖 ```xml org.springframework.boot spring-boot-starter-data-mongodb ``` ### 配置文件中添加如下配置 ```ini # 设置mongodb数据库的配置 spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 spring.data.mongodb.username=admin spring.data.mongodb.password=123456 spring.data.mongodb.authentication-database=admin ``` ### 测试 创建实体类:映射`MongoDB`中的文档(相当与MySQL的表) ```java @Data @Document("chat_messages") public class ChatMessages { //唯一标识,映射到 MongoDB 文档的 _id 字段 @Id private ObjectId messageId; private String content; //存储当前聊天记录列表的json字符串 } ``` 创建测试类 ```java @SpringBootTest public class MongoCrudTest { @Autowired private MongoTemplate mongoTemplate; @Test public void testCreateChatMessage() { ChatMessages chatMessage = new ChatMessages(); chatMessage.setMessageId(new ObjectId()); chatMessage.setContent("{\"message\":\"Hello, world!\"}"); ChatMessages savedMessage = mongoTemplate.save(chatMessage); assertNotNull(savedMessage.getMessageId()); System.out.println("Created ChatMessage: " + savedMessage); } @Test public void testReadChatMessage() { ObjectId id = new ObjectId(); // Replace with an existing ID in your database ChatMessages chatMessage = mongoTemplate.findById(id, ChatMessages.class); assertNotNull(chatMessage); System.out.println("Read ChatMessage: " + chatMessage); } @Test public void testUpdateChatMessage() { ObjectId id = new ObjectId(); // Replace with an existing ID in your database ChatMessages chatMessage = mongoTemplate.findById(id, ChatMessages.class); assertNotNull(chatMessage); chatMessage.setContent("{\"message\":\"Updated content\"}"); ChatMessages updatedMessage = mongoTemplate.save(chatMessage); assertEquals("{\"message\":\"Updated content\"}", updatedMessage.getContent()); System.out.println("Updated ChatMessage: " + updatedMessage); } @Test public void testDeleteChatMessage() { ObjectId id = new ObjectId(); // Replace with an existing ID in your database ChatMessages chatMessage = mongoTemplate.findById(id, ChatMessages.class); assertNotNull(chatMessage); mongoTemplate.remove(chatMessage); ChatMessages deletedMessage = mongoTemplate.findById(id, ChatMessages.class); assertNull(deletedMessage); System.out.println("Deleted ChatMessage with ID: " + id); } } ``` `new ObjectId()` 会返回一个 **新的 MongoDB `ObjectId`** 实例。`ObjectId` 是 `MongoDB` 默认使用的主键类型,它是一个 **12 字节**的 `BSON` 类型,通常用作 `_id` 字段。`ObjectId` 的值是一个有效的 `24 字符`的十六进制字符串。`12*8=24*4=96` `ObjectId` 的值由以下部分组成: 1. **4 字节**:当前时间戳(秒级,表示创建的时间) 2. **5 字节**:机器标识符和进程ID(唯一) 3. **3 字节**:计数器(递增值,用于确保在同一毫秒内创建多个 `ObjectId` 时的唯一性) `new ObjectId()`默认使用当前时间戳,当然你也可以传入一个时间戳 ## 持久化聊天 ### 优化消息实体类 ```java @Data @Document("chat_messages") public class ChatMessages { //唯一标识,映射到 MongoDB 文档的 _id 字段 @Id private ObjectId id; private int messageId; private String content; //存储当前聊天记录列表的json字符串 } ``` ### 创建持久化类 创建一个类实现`ChatMemoryStore`接口,`ChatMemoryStore`定义如下 ```java public interface ChatMemoryStore { List getMessages(Object var1); void updateMessages(Object var1, List var2); void deleteMessages(Object var1); } ``` `ChatMemoryStore` 接口定义了以下三个方法: 1. `List getMessages(Object memoryId)` - 根据 `memoryId`(通常是用户 ID 或会话 ID)检索对应的聊天消息列表。 2. `void updateMessages(Object memoryId, List messages)` - 更新指定 `memoryId` 的聊天消息列表。每当有新的消息添加到聊天内存中时,`LangChain4j` 会调用此方法 3. `void deleteMessages(Object memoryId)` - 删除指定 `memoryId` 的所有聊天消息。 这些方法允许你**实现自定义的持久化逻辑**,以满足特定的存储需求。 具体的实现类为: ```java @Component public class MongoChatMemoryStore implements ChatMemoryStore { @Autowired private MongoTemplate mongoTemplate; @Override public List getMessages(Object objectId) { Criteria criteria = Criteria.where("id").is(objectId); Query query = new Query(criteria); ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class); if (chatMessages == null) { return new LinkedList<>(); } return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent()); } @Override public void updateMessages(Object objectId, List messages) { Criteria criteria = Criteria.where("id").is(objectId); Query query=new Query(criteria); Update update=new Update(); update.set("content", ChatMessageSerializer.messagesToJson(messages)); // 使用 upsert 方法,如果不存在则插入新文档 mongoTemplate.upsert(query, update, ChatMessages.class); } @Override public void deleteMessages(Object objectId) { Criteria criteria = Criteria.where("id").is(objectId); Query query=new Query(criteria); mongoTemplate.remove(query, ChatMessages.class); } } ``` | 类 | 作用 | | ------------------------- | ------------------------------------------------------------ | | `Criteria` | 构建查询条件(`where`) | | `Query` | 封装查询请求(条件 + 分页 + 排序等) | | `ChatMessageSerializer` | 是 `LangChain4j` 中的一个工具类,用于将聊天消息对象序列化为 `JSON` 字符串。 | | `ChatMessageDeserializer` | 是 `LangChain4j` 中的一个工具类,用于将 `JSON` 字符串反序列化为聊天消息对象。 | ### 更改配置类 **在`SeparateChatAssistantConfig`中,添加`MongoChatMemoryStore`对象的配置** ```java @Configuration public class SeparateChatAssistantConfig { @Autowired private MongoChatMemoryStore mongoChatMemoryStore; @Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(10) .chatMemoryStore(mongoChatMemoryStore)//配置持久化存储 .build(); } } ``` ### 测试 ```java @Autowired private SeparateChatAssistant separateChatAssistant; @Test public void testChatMemory5() { 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); } ``` ![image-20250423163857411](./assets/image-20250423163857411.png) # 6.提示词 Prompt ## 系统提示词 `@SystemMessage` 设定角色,塑造AI助手的专业身份,明确助手的能力范围 配置`@SystemMessage` 在`SeparateChatAssistant`类的`chat`方法上添加`@SystemMessage`注解 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemory = "chatMemory",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProvider"//找到对应的bean进行绑定 ) public interface SeparateChatAssistant { @SystemMessage("你是一个智能助手,请用湖南话回答问题。") String chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` `@SystemMessage` 的内容将在后台转换为 `SystemMessage` 对象,并与 `UserMessage` 一起发送给大语 言模型(`LLM`) ## 测试 ```java @SpringBootTest public class PromptTest { @Autowired private SeparateChatAssistant separateChatAssistant; @Test public void testSystemMessage() { String answer = separateChatAssistant.chat(3, "今天几号"); System.out.println(answer); } } ``` 请求体的内容如下: ``` [{ "model" : "qwen-plus", "messages" : [ { "role" : "system", "content" : "你是一个智能助手,请用湖南话回答问题。" }, { "role" : "user", "content" : "今天几号" } ], "stream" : false }] ``` ## 从资源文件中加载提示模板 `@SystemMessage` 注解还可以从资源文件中加载提示模板 创建`resources/prompts/assistant.txt` ```ini 你是一个智能助手,请用湖南话回答问题。 Current time: {{time}} ``` `{{time}}` 你要传入的变量。 修改`SeparateChatAssistant`类 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemory = "chatMemory",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProvider"//找到对应的bean进行绑定 ) public interface SeparateChatAssistant { // @SystemMessage("你是一个智能助手,请用湖南话回答问题。") @SystemMessage(fromResource = "prompts/assistant.txt") String chat(@MemoryId int memoryId, @UserMessage String userMessage, @V("time")String time); } ``` `@SystemMessage(fromResource = "...")` 表示从资源文件中加载提示。 `@V("time")` 自动填入提示模板中的对应占位符。 请求体为: ``` [{ "model" : "qwen-plus", "messages" : [ { "role" : "system", "content" : "你是一个智能助手,请用湖南话回答问题。\nCurrent time: 2025-04-24T09:46:14.332756\n" }, { "role" : "user", "content" : "今天几号" } ], "stream" : false }] ``` ## 用户提示词 `@UserMessage`:获取用户输入 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemory = "chatMemory"//找到对应的bean进行绑定 ) public interface MemoryChatAssistant { @UserMessage(fromResource = "prompts/assistant.txt") String chat(String time); } ``` 当只有一个参数时,不需要使用`@V`注解,但是需要同名,可以自动绑定 测试 ```java @Autowired private MemoryChatAssistant memoryChatAssistant; @Test public void testUserMessage() { String answer = memoryChatAssistant.chat(LocalDateTime.now().toString()); System.out.println(answer); } ``` 请求体: ``` [{ "model" : "qwen-plus", "messages" : [ { "role" : "user", "content" : "你是一个智能助手,请用湖南话回答问题。\nCurrent time: 2025-04-24T10:01:40.622852\n" } ], "stream" : false }] ``` # 7.项目实战-创建小智 这部分实现硅谷小智的基本聊天功能,包含聊天记忆、聊天记忆持久化、提示词 ## 创建硅谷小智 创建`xiaozhi-prompt-template.txt` ``` 你的名字是“小智”,你是一家名为“北京协和医院”的智能客服。 你是一个训练有素的医疗顾问和医疗伴诊助手。 你态度友好、礼貌且言辞简洁。 1、请仅在用户发起第一次会话时,和用户打个招呼,并介绍你是谁。 2、作为一个训练有素的医疗顾问: 请基于当前临床实践和研究,针对患者提出的特定健康问题,提供详细、准确且实用的医疗建议。请同时考虑可能的病 因、诊断流程、治疗方案以及预防措施,并给出在不同情境下的应对策略。对于药物治疗,请特别指明适用的药品名 称、剂量和疗程。如果需要进一步的检查或就医,也请明确指示。 3、作为医疗伴诊助手,你可以回答用户就医流程中的相关问题,主要包含以下功能: AI分导诊:根据患者的病情和就医需求,智能推荐最合适的科室。 AI挂号助手:实现智能查询是否有挂号号源服务;实现智能预约挂号服务;实现智能取消挂号服务。 4、你必须遵守的规则如下: 在获取挂号预约详情或取消挂号预约之前,你必须确保自己知晓用户的姓名(必选)、身份证号(必选)、预约科室 (必选)、预约日期(必选,格式举例:2025-04-14)、预约时间(必选,格式:上午 或 下午)、预约医生(可 选)。 当被问到其他领域的咨询时,要表示歉意并说明你无法在这方面提供帮助。 5、请在回答的结果中适当包含一些轻松可爱的图标和表情。 6、今天是 {{current_date}}。 ``` **配置持久化和记忆隔离,**创建一个`chatMemoryProviderXiaozhi`,使用`mongoChatMemoryStore`进行消息持久化 ```java @Configuration public class XiaozhiAgentConfig { @Autowired private MongoChatMemoryStore mongoChatMemoryStore; public ChatMemoryProvider chatMemoryProviderXiaozhi() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(mongoChatMemoryStore) .build(); } } ``` 创建`XiaozhiAgent` ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProviderXiaozhi"//找到对应的bean进行绑定 ) public interface XiaozhiAgent { @SystemMessage(fromResource = "prompts/xiaozhi-prompt-template.txt") String chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` 创建聊天数据传输对象`ChatFormDTO` ```java @Data public class ChatFormDTO { //会话id private int memoryId; //用户消息 private String userMessage; } ``` 最后创建`XiaozhiController` ```java @Tag(name = "小智") @RestController @RequestMapping("/xiaozhi") public class XiaozhiController { @Autowired private XiaozhiAgent xiaozhiAgent; @Operation(summary = "对话") @PostMapping("/chat") public String chat(@RequestBody ChatFormDTO chatFormDTO) { return xiaozhiAgent.chat(chatFormDTO.getMemoryId(), chatFormDTO.getUserMessage()); } } ``` ## 启动时遇到的问题 我创建了两个`ChatMemoryProvider`类型的`Bean`:`chatMemoryProvider`和`chatMemoryProviderXiaozhi` 所以在@`AiService`中需要指定`chatMemoryProvider`用哪个`ChatMemoryProvider` ``` @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProviderXiaozhi"//找到对应的bean进行绑定 ) ``` 但是我有一个`Assistant`什么都没指定 ``` @AiService() public interface Assistant { String chat(String message); } ``` 当你使用 `@AiService()` 而不指定任何参数时,框架会尝试自动注入所需的组件,如 `ChatModel`、`ChatMemory` 或 `ChatMemoryProvider`。 所以当上下文中存在多个相同类型的 Bean(例如多个 `ChatMemoryProvider`),框架将无法确定使用哪个 Bean,从而抛出 `IllegalConfigurationException` 异常。 这个时候把`Assistant`删掉就行,这个只是之前测试用的 # 8.`Function Calling` 函数调用 `Function Calling` 函数调用 也叫`Tools` 工具 ## 入门案例 大语言模型本身并不擅长数学运算。如果应用场景中偶尔会涉及到数学计算,我们可以为他提供 一个 “数学工具”。当我们提出问题时,**大语言模型会判断是否使用某个工具**。 ### 创建工具类 通过在方法上添加 `@Tool` 注解,并在构建 AI 服务时显式指定这些工具,LLM 可以根据用户的请求决定是否调用相应的工具方法。 ```java @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Tool { String name() default ""; String[] value() default {""}; } ``` - `name`(可选):指定工具的**名称**。如果未提供,默认使用方法名。 - `value`(可选):提供工具的**描述**,有助于 LLM 更好地理解工具的用途。 此外,可以使用 `@P` 注解为**方法参数添加描述**,增强 LLM 对参数含义的理解。 - `value`:参数的描述信息,这是必填字段。 - `required`:表示该参数是否为必需项,默认值为 true ,此为可选字段。 `@ToolMemoryId`注解用于在**工具方法的参数上**指定用于关联对话上下文的内存标识符(`memoryID`),提供给`AIService`方法的`memoryID`将自动传递给 `@Tool` 方法 调用流程如下: 1. `LLM` 接收用户输入。 2. 判断是否需要调用工具方法。(第一次调用大模型) 3. 如果需要,调用相应的 `@Tool` 方法,并获取返回结果。(第二次调用大模型) 4. 将工具方法的返回结果作为对话的一部分,继续与用户交互。 ```java @Component public class CalculatorTools { @Tool(name = "加法", value = "返回两个参数相加之和") double sum(@ToolMemoryId int memoryId, @P(value = "加数1", required = true) double a,@P(value = "加数2", required = true) double b) { System.out.println("调用加法运算 " + memoryId); return a + b; } @Tool(name = "平方根", value = "返回给定参数的平方根") double squareRoot(@ToolMemoryId int memoryId, double x) { System.out.println("调用平方根运算 " + memoryId); return Math.sqrt(x); } } ``` ### 为`Agent`配置工具类 修改`SeparateChatAssistant`类 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 // chatMemory = "chatMemory",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProvider",//找到对应的bean进行绑定 tools = "calculatorTools"//找到对应的bean进行绑定 ) public interface SeparateChatAssistant { // @SystemMessage("你是一个智能助手,请用湖南话回答问题。") @SystemMessage(fromResource = "prompts/assistant.txt") String chat(@MemoryId int memoryId, @UserMessage String userMessage, @V("time")String time); } ``` ### 测试 ```java @Autowired private SeparateChatAssistant separateChatAssistant; @Test public void testCalculatorTools() { String answer = separateChatAssistant.chat(4, "1+2等于几,475695037565的平方根是多少?", LocalDateTime.now().toString()); System.out.println(answer); } ``` 可以在控制台看一下调用的流程 # 9.项目实战-优化小智 ## 预约业务的实现 这部分我们实现硅谷小智的**查询订单、预约订单、取消订单**的功能 ### 安装`MySQL` 使用docker进行安装 ```ini docker pull mysql:8.0 docker run -d \ --name my-mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ -p 3306:3306 \ -v ~/mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_general_ci ``` 然后使用`Navicat`连接`MySQL` ### 创建数据库表 ```sql -- 创建数据库 CREATE DATABASE IF NOT EXISTS `xiaozhi` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 使用数据库 USE `xiaozhi`; -- 创建预约表 CREATE TABLE `appointment` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '预约人姓名', `id_card` VARCHAR(18) NOT NULL COMMENT '身份证号', `department` VARCHAR(50) NOT NULL COMMENT '预约科室', `date` VARCHAR(10) NOT NULL COMMENT '预约日期(格式:yyyy-MM-dd)', `time` VARCHAR(10) NOT NULL COMMENT '预约时间(格式:HH:mm)', `doctor_name` VARCHAR(50) DEFAULT NULL COMMENT '医生姓名', PRIMARY KEY (`id`) ) COMMENT='预约信息表'; ``` ### 引入依赖 ```xml com.mysql mysql-connector-j com.baomidou mybatis-plus-spring-boot3-starter ${mybatis-plus.version} com.baomidou mybatis-plus-generator 3.5.9 org.freemarker freemarker 2.3.31 ``` ### 配置文件中添加mysql配置 ```ini # mysql配置 # MySQL 数据库连接信息 spring.datasource.url=jdbc:mysql://localhost:3306/xiaozhi?useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ``` ### 使用代码生成器生成实体类、mapper、xml文件 ```java public class CodeGenerator { public static void main(String[] args) { // 使用 FastAutoGenerator 快速配置代码生成器 FastAutoGenerator.create("jdbc:mysql://localhost:3306/xiaozhi?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8", "root", "123456") .globalConfig(builder -> { builder.author("liu bo") // 设置作者 .outputDir("src/main/java"); // 输出目录(Java 文件) }) .packageConfig(builder -> { builder.parent("org.example.langchain4j") // 设置父包名 .entity("entity") // 设置实体类包名 .mapper("mapper") // 设置 Mapper 接口包名 .xml("mapper") // 设置 Mapper XML 文件包名 .pathInfo(Collections.singletonMap(OutputFile.xml, "src/main/resources/mapper")) .build();// 设置 Mapper XML 文件的输出路径 }) .strategyConfig(builder -> { builder.addInclude("appointment") // 设置需要生成的表名 .entityBuilder() .enableLombok() // 启用 Lombok .enableTableFieldAnnotation() // 启用字段注解 .controllerBuilder().disable() // 禁用 Controller 生成 .serviceBuilder().disable() // 禁用 Service 生成 .disableServiceImpl(); // 禁用 ServiceImpl 生成 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用 Freemarker 模板引擎 .execute(); // 执行生成 } } ``` ### 创建服务类 ```java public interface AppointmentService extends IService { public Appointment getOne(Appointment appointment); } @Service public class AppointmentServiceImpl extends ServiceImpl implements AppointmentService { /** * 查询订单是否存在 */ @Override public Appointment getOne(Appointment appointment) { LambdaQueryWrapper 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()); return baseMapper.selectOne(queryWrapper); } } ``` ### 创建`Tools` ```java @Component public class AppointmentTools { @Autowired private AppointmentService appointmentService; @Tool(name = "预约挂号", value = "根据参数,先执行工具方法queryDepartment查询是否可预约," + "并直接给用户回答是否可预约,并让用户确认所有预约信息,用户确认后再进行预约。") public String bookAppointment(Appointment appointment) { //查找数据库中是否包含对应的预约记录 Appointment appointmentDB = appointmentService.getOne(appointment); if (appointmentDB == null) { appointment.setId(null);//防止大模型幻觉设置了id if (appointmentService.save(appointment)) { return "预约成功,并返回预约详情"; } else { return "预约失败"; } } return "您在相同的科室和时间已有预约"; } @Tool( name = "取消预约挂号", value = "根据参数,查询预约是否存在;如果存在则删除预约记录并返回“取消预约成功”,否则返回“取消预约失败”" ) public String cancelAppointment(Appointment appointment) { if (appointment == null) { return "参数无效,无法取消预约"; } Appointment appointmentDB = appointmentService.getOne(appointment); if (appointmentDB != null) { boolean removed = appointmentService.removeById(appointmentDB.getId()); return removed ? "取消预约成功" : "取消预约失败"; } return "您没有预约记录,请核对预约科室、时间等信息"; } @Tool( name = "查询是否有号源", 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.printf("科室名称:%s,日期:%s,时间:%s,医生名称:%s%n", name, date, time, doctorName); // TODO: 查询医生排班信息,以下是伪代码逻辑说明 if (doctorName == null || doctorName.isEmpty()) { // 未指定医生,查询该科室、日期、时间是否有可预约医生 // return true if any doctor is available // 示例:return schedulingService.hasAvailableDoctor(name, date, time); return true; // 示例返回 } else { // 指定了医生 // 检查医生是否有排班 boolean hasSchedule = true; // 示例:schedulingService.hasSchedule(doctorName, date, time); if (!hasSchedule) { return false; } // 检查排班是否已约满 boolean isFull = false; // 示例:schedulingService.isFullyBooked(doctorName, date, time); return !isFull; } } } ``` ### 测试 ``` { "memoryId": 11, "userMessage": "我要挂今天下午妇产科的号,我叫刘波,身份证号是:000000000000000000" } 太好了!您的挂号已经成功啦 😊。以下是您的预约详情: - **科室**:妇产科 - **日期**:2025-04-24 - **时间**:下午 - **医生**:系统将为您分配一位合适的医生 请您按时前往医院就诊,祝您身体健康!如果有任何问题,请随时联系我们哦 😊。 ``` ![image-20250424164001253](./assets/image-20250424164001253.png) # 10.检索增强生成 RAG ## RAG的过程 ![image-20250425165837431](./assets/image-20250425165837431.png) ## 处理文档 ### 文档加载器`Document Loader` LangChain4j 提供了多种文档加载器,适用于不同的文档来源: - **`FileSystemDocumentLoader`**:从本地文件系统加载文档。 - **`UrlDocumentLoader`**:通过 `URL` 加载文档。 - **`AmazonS3DocumentLoader`**:从 `Amazon S3` 存储桶加载文档。 - **`AzureBlobStorageDocumentLoader`**:从 `Azure Blob` 存储加载文档。 - **`GoogleCloudStorageDocumentLoader`**:从 `Google Cloud Storage` 加载文档。 - **`GitHubDocumentLoader`**:从 `GitHub` 仓库加载文档。 - **`TencentCOSDocumentLoader`**:从腾讯云对象存储加载文档。 **测试** ```java xxxxxxxxxx1 1 @Test public void testReadDocument() { //使用FileSystemDocumentLoader读取指定目录下的知识库文档 // 并使用默认的文档解析器TextDocumentParser对文档进行解析 Document document = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/测试.txt"); System.out.println(document.text()); } ``` ### 文档解析器`Document Parser` LangChain4j 提供了多种内置的文档解析器,适用于不同的文件格式: 1. `TextDocumentParser` - **用途**:解析纯文本文件,如 `.txt`、`.md`、`.html` 等。 - **特点**:轻量级,适用于结构简单的文本内容。 2. `ApachePdfBoxDocumentParser` - **用途**:解析 PDF 文件。 - **特点**:能够提取 PDF 中的文本和元数据。 3. `ApachePoiDocumentParser` - **用途**:解析 Microsoft Office 文件,如 `.doc`、`.docx`、`.xls`、`.xlsx`、`.ppt`、`.pptx` 等。 - **特点**:支持提取 Office 文档中的文本内容。 4. `ApacheTikaDocumentParser` - **用途**:通用解析器,支持多种文件格式。 - **特点**:能够自动检测文件类型并解析,适用于处理多种格式的文档。 假设如果我们想解析`PDF`文档,那么原有的 `TextDocumentParser` 就无法工作了,我们需要引入 `langchain4j-document-parser-apache-pdfbox` ```xml dev.langchain4j langchain4j-document-parser-apache-pdfbox ``` **测试** ```java @Test public void testParsePDF() { Document document = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/医院信息.pdf", new ApachePdfBoxDocumentParser()); System.out.println(document); } ``` ### 文档分割器 `Document Splitter` 1. `DocumentByParagraphSplitter` - **功能**:**将文档按段落进行分割**,段落通常由两个或更多连续的换行符定义。 - **特点**:适用于结构清晰、段落分明的文档,如新闻文章、博客等。 2. `DocumentBySentenceSplitter` - **功能**:**基于句子进行分割**,通常依赖于句子检测器(如 OpenNLP)来识别句子边界。 - **特点**:适用于需要精细语义控制的场景,如问答系统、摘要生成等。 - **注意**:需要引入相应的句子检测库作为依赖。 3. `RecursiveCharacterTextSplitter` - **功能**:**递归地按字符进行分割**,优先在自然的分隔符(如段落、句子、空格)处进行分割,以保持语义完整性。 - **特点**:是推荐的默认分割器,适用于大多数通用文本。 4. `CharacterTextSplitter` - **功能**:**按固定的字符数进行分割,**适用于结构简单、语义不太复杂的文本。 - **特点**:实现简单,但可能会打断语义完整性。 5. `TokenTextSplitter` - **功能**:**基于标记(Token)进行分割**,适用于需要控制模型输入长度的场景。 - **特点**:有助于防止超过语言模型的上下文窗口限制。 6. `MarkdownHeaderTextSplitter` - **功能**:**基于 Markdown 文档的标题结构进行分割**,保留标题元数据。 - **特点**:适用于结构化的 Markdown 文档,便于上下文感知的处理。 ### `embedding`生成和向量存储 这里先使用`langchain4j`自带的`RAG`的简单实现,**后面我们探讨`embedding`模型的选型以及向量数据库的选型** **添加依赖** ``` dev.langchain4j langchain4j-easy-rag 1.0.0-beta3 ``` `langchain4j-easy-rag` 是 LangChain4j 提供的一个模块,该模块**封装了文档解析、分割、嵌入生成和向量存储**等复杂流程,使开发者能够更快速地搭建 RAG 系统。 **测试** ```java @Test public void testReadDocumentAndStore() { // 加载文档 Document document = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/人工智能.md"); // 创建内存向量存储 InMemoryEmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); // 文档分割与嵌入生成 EmbeddingStoreIngestor.ingest(document, embeddingStore); // 查看向量存储内容 System.out.println(embeddingStore); } ``` `InMemoryEmbeddingStore` 是 LangChain4j 提供的一个轻量级、基于内存的向量存储实现 `EmbeddingStoreIngestor.ingest(document, embeddingStore);` 方法执行了以下操作: 1. **文档分割**:默认使用递归分割器(`RecursiveCharacterTextSplitter`),将文档分割为多个文本片段(`TextSegment`)。每个片段的最大长度为 **300 个 token**,且相邻片段之间有 **30 个 token 的重叠**,以保持语义连贯性。 2. **嵌入生成**:使用内置的轻量级嵌入模型(如 `BgeSmallEnV15QuantizedEmbeddingModel`:一个量化的英文嵌入模型,具有较小的向量维度,适合快速处理。)将每个文本片段转换为向量表示。 3. **向量存储**:将生成的向量和对应的文本片段存储到内存中的向量存储(`InMemoryEmbeddingStore`)中。 # 11.项目实战-在小智中实现RAG ## 创建`ContentRetriever` 在`xiaozhiAgentConfig`中添加`ContentRetriever` `ContentRetriever` 的核心功能 - **输入**:用户的查询(`Query`)。 - **输出**:与查询相关的内容列表(`List`),目前主要是文本片段(`TextSegment`)。 - **数据源**:可以是嵌入存储(如 `InMemoryEmbeddingStore`)、全文搜索引擎、Web 搜索引擎、知识图谱、SQL 数据库等。 ```java @Bean public ContentRetriever contentRetrieverXiaozhi() { Document document1 = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/医院信息.md"); Document document2 = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/科室信息.md"); Document document3 = FileSystemDocumentLoader.loadDocument("src/main/resources/knowledge/神经内科.md"); List documents = Arrays.asList(document1, document2, document3); //使用内存向量存储 InMemoryEmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>(); //使用默认的文档分割器 EmbeddingStoreIngestor.ingest(documents, embeddingStore); //从嵌入存储(EmbeddingStore)里检索和查询内容相关的信息 return EmbeddingStoreContentRetriever.from(embeddingStore); } ``` `EmbeddingStoreContentRetriever`是`ContentRetriever`实现类 **输入**:用户的查询(`Query`)。 **处理**:使用指定的嵌入模型(默认是`BgeSmallEnV15QuantizedEmbeddingModel`)将查询转换为向量。 **输出**:返回与查询最相关的内容列表(`List`),通常是文本片段(`TextSegment`)。 ## 添加检索配置 在 `XiaozhiAgent` 中添加 `contentRetriever` 配置 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProviderXiaozhi",//找到对应的bean进行绑定 tools = "appointmentTools",//找到对应的bean进行绑定 contentRetriever = "contentRetrieverXiaozhi" ) public interface XiaozhiAgent { @SystemMessage(fromResource = "prompts/xiaozhi-prompt-template.txt") String chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` ## 修改工具的`value`提示 ```java @Tool(name = "预约挂号", value = "根据参数,先执行工具方法queryDepartment查询是否可预约," +"并直接给用户回答是否可预约,并让用户确认所有预约信息,用户确认后再进行预约。" + "如果用户没有提供具体的医生姓名,请从向量存储中找到一位医生。") public String bookAppointment(Appointment appointment) { //查找数据库中是否包含对应的预约记录 Appointment appointmentDB = appointmentService.getOne(appointment); if (appointmentDB == null) { appointment.setId(null);//防止大模型幻觉设置了id if (appointmentService.save(appointment)) { return "预约成功,并返回预约详情"; } else { return "预约失败"; } } return "您在相同的科室和时间已有预约"; } ``` ## 测试 在`controller`中测试 ``` 请求: { "memoryId": 12, "userMessage": "我想要挂今天下午骨科的号,我的姓名是刘波,身份证号是111111111111111111" } 响应: 太棒了!刘波先生,您的预约已经成功啦 😊。以下是您的预约详情: - **就诊科室**:骨科 - **预约日期**:2025-04-26(今天) - **预约时间**:下午 - **医生姓名**:彭斌教授 请您记得在就诊当天携带身份证和医保卡(如有),并提前到北京协和医院东单院区新门诊楼各楼层挂号/收费窗口取号哦。如果需要取消预约,请尽早通知我,以便释放号源给其他有需要的患者。 祝您身体健康!有任何问题随时联系我哦 💕。 ``` ![image-20250426150212541](./assets/image-20250426150212541.png) # 12.向量模型和向量存储 ## 向量模型 `Langchain4j`支持的向量模型:https://docs.langchain4j.dev/category/embedding-models 这里选用阿里云百炼`text-embedding-v3` 添加依赖,之前添加过就不用添加了 ```xml dev.langchain4j langchain4j-community-dashscope-spring-boot-starter dev.langchain4j langchain4j-community-bom ${langchain4j.version}< pom import ``` 配置文件`application.properties`添加 ```ini # 配置阿里通义千问向量模型 langchain4j.community.dashscope.embedding-model.api-key=你的key langchain4j.community.dashscope.embedding-model.model-name=text-embedding-v3 ``` 测试 ```java @SpringBootTest public class EmbeddingTest { @Autowired private EmbeddingModel embeddingModel;//注入千问embeddingModel @Test public void testEmbeddingModel() { Response embed = embeddingModel.embed("你好"); System.out.println("向量维度:" + embed.content().vector().length); System.out.println("向量输出:" + embed.toString()); } } ``` ## 向量存储 `Langchain4j`支持的向量数据库:https://docs.langchain4j.dev/category/embedding-stores ### 主流向量数据库的对比 | 数据库 | 特点 | 优劣势简述 | | ------------ | ------------------------------------------------------------ | ------------------------- | | **FAISS** | Facebook 开源,支持 CPU/GPU,精度高,支持各种索引结构 | 仅支持内存,适合离线分析 | | **Milvus** | 全功能开源向量 DB,支持 ANN、多种索引、元数据过滤、多租户 | 功能丰富,复杂度稍高 | | **Weaviate** | 内置嵌入模型,支持 GraphQL 查询,可与 OpenAI 接入 | 云原生友好,嵌入+存储一体 | | **Pinecone** | 云服务,主打生产级别向量检索 + 元数据过滤,适合 RAG | 非开源,依赖其服务 | | **Qdrant** | Rust 编写,支持 payload 过滤、高性能搜索,内存+磁盘混合存储 | 性能优,使用门槛低 | | **Vespa** | 支持文本检索 + 向量检索 + ranking pipeline | 架构重,适合大型搜索 | | **Chroma** | 面向 LLM 应用,极简部署,开箱即用,集成 LangChain/LlamaIndex | 功能轻量级,适合开发初期 | **研发 / 原型阶段** - 推荐:**FAISS(本地),Chroma** - 优点:轻量、易用、社区丰富 **构建 Web 应用 / 小中型系统** - 推荐:**Qdrant,Weaviate,Milvus-lite** - 优点:支持 `REST/gRPC/客户端SDK`,带元数据过滤,可集成 `LangChain` **大规模生产部署 / 高并发** - 推荐:**Milvus(完整集群),Pinecone(托管),Vespa(超大规模)** - 优点:高可扩展性,多副本,支持异构资源 ### 集成`Pinecone` 获取`APIKEY` [官网](https://app.pinecone.io/organizations/-OOlruIX0RDNVV7Bar2G/keys) **添加依赖** ```xml dev.langchain4j langchain4j-pinecone ``` **配置向量存储对象** 在 LangChain4j 中,`EmbeddingStore` 接口提供了**统一的 API**,使得开发者可以方便地切换不同的向量数据库实现。 `EmbeddingStore` 的主要功能包括: - **存储嵌入向量**:将文本或其他数据转换为嵌入向量后,存储到向量数据库中。 - **相似度搜索**:根据输入的查询向量,检索与之相似的嵌入向量,实现语义搜索。 - **关联原始数据**:可以将嵌入向量与原始的 `TextSegment` 数据一起存储,便于在检索时获取完整的上下文信息。 ```java @Configuration public class EmbeddingStoreConfig { @Autowired private EmbeddingModel embeddingModel; @Bean public EmbeddingStore embeddingStore() { return PineconeEmbeddingStore.builder() .apiKey("pcsk_64ZGQr_52GGBHxfVxFadDcXCf9igB7E1qN3MAeyQwXrCdJzjwTntNxrhoYzavGR7ab31ps") .index("xiaozhi-index")//如果指定的索引不存在,将创建一个新的索引 .nameSpace("xiaozhi-namespace")//如果指定的名称空间不存在,将创建一个新的名称空间 .createIndex(PineconeServerlessIndexConfig.builder() .cloud("AWS") .region("us-west-1") .dimension(embeddingModel.dimension()) .build()) .build(); } } ``` **测试存储** ```java @Test public void testPineconeEmbeddingStore() { TextSegment segment1 = TextSegment.from("我喜欢羽毛球"); Embedding embedding1 = embeddingModel.embed(segment1).content(); embeddingStore.add(embedding1, segment1); TextSegment segment2 = TextSegment.from("今天天气很好"); Embedding embedding2 = embeddingModel.embed(segment2).content(); embeddingStore.add(embedding2, segment2); } ``` ![image-20250426200828653](./assets/image-20250426200828653.png) **测试检索** 接收请求获取问题,将问题转换为向量,在 `Pinecone` 向量数据库中进行相似度搜索,找到最相似的文本 片段,并将其文本内容返回给客户端 ```java @Test public void testEmbeddingSearch(){ Embedding queryEmbedding = embeddingModel.embed("你最喜欢的运动是什么?").content(); EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(1) .build(); EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest); EmbeddingMatch embeddingMatch = searchResult.matches().get(0); System.out.println("匹配的分数:" + embeddingMatch.score()); System.out.println("匹配的内容:" + embeddingMatch.embedded().text()); } ``` `EmbeddingSearchRequest` 的核心作用是构建一个搜索请求,包含以下关键参数: - **queryEmbedding**:待搜索的查询向量,通常由嵌入模型(如 `EmbeddingModel`)生成。 - **filter**(可选):用于根据元数据(如作者、标签等)对搜索结果进行过滤。 - **maxResults**:指定返回的最大结果数量。 - **minScore**(可选):设置结果的最小相似度得分阈值,低于该值的结果将被排除。 # 13.项目实战-在小智中整合向量数据库 ## 上传知识库到`Pinecone` 创建`UploadKnowledgeLibraryService`和 `UploadKnowledgeLibraryServiceImpl` ```java public interface UploadKnowledgeLibraryService { public void uploadKnowledgeLibrary(MultipartFile[] files); } @Service public class UploadKnowledgeLibraryServiceImpl implements UploadKnowledgeLibraryService { @Autowired private EmbeddingStore embeddingStore; @Autowired private EmbeddingModel embeddingModel; @Override public void uploadKnowledgeLibrary(MultipartFile[] files) { List documents = new ArrayList<>(); for (MultipartFile file : files) { if (!file.isEmpty()) { try { // 保存为临时文件 File tempFile = File.createTempFile("upload-", "-" + file.getOriginalFilename()); file.transferTo(tempFile); // 根据文件类型选择适当的解析器 String fileName = file.getOriginalFilename(); Document document; if (fileName != null && fileName.toLowerCase().endsWith(".pdf")) { // 针对PDF文件使用专用解析器 document = FileSystemDocumentLoader.loadDocument(tempFile.getAbsolutePath(), new ApachePdfBoxDocumentParser()); } else { // 其他文件使用默认解析器 document = FileSystemDocumentLoader.loadDocument(tempFile.getAbsolutePath()); } documents.add(document); // 删除临时文件 tempFile.delete(); } catch (IOException e) { throw new RuntimeException("处理文件失败: " + file.getOriginalFilename(), e); } } } // 将文档存入向量数据库 EmbeddingStoreIngestor .builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .build() .ingest(documents); } } ``` 创建上传的`UploadKnowledgeLibraryController` ```java @Tag(name = "上传知识库") @RestController @RequestMapping("/documents") public class UploadKnowledgeLibraryController { @Autowired private UploadKnowledgeLibraryService uploadKnowledgeLibraryService; @PostMapping("/upload") public String uploadKnowledgeLibrary(MultipartFile[] files) { uploadKnowledgeLibraryService.uploadKnowledgeLibrary(files); return "上传成功"; } } ``` 上传 ![image-20250426212227991](./assets/image-20250426212227991.png) ## 修改`XiaozhiAgentConfig` 添加基于`Pinecone`向量存储的检索器 ```java @Autowired private EmbeddingModel embeddingModel; @Autowired private EmbeddingStore embeddingStore; //基于Pinecone向量存储的检索器 @Bean public ContentRetriever contentRetrieverPinecone(){ return EmbeddingStoreContentRetriever .builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .maxResults(1) .minScore(0.8) .build(); } ``` ## 修改`XiaozhiAgent` 修改`contentRetriever`的配置为`contentRetrieverXiaozhiPincone` ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, chatModel = "openAiChatModel",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProviderXiaozhi",//找到对应的bean进行绑定 tools = "appointmentTools",//找到对应的bean进行绑定 contentRetriever = "contentRetrieverPinecone"//找到对应的bean进行绑定 ) public interface XiaozhiAgent { @SystemMessage(fromResource = "prompts/xiaozhi-prompt-template.txt") String chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` # 14.项目实战-改造流式输出 大模型的流式输出是指大模型在生成文本或其他类型的数据时,不是等到整个生成过程完成后再一次性 返回所有内容,而是生成一部分就立即发送一部分给用户或下游系统,以逐步、逐块的方式返回结果。 这样,用户就不需要等待整个文本生成完成再看到结果。通过这种方式可以改善用户体验,因为用户不 需要等待太长时间,几乎可以立即开始阅读响应。 ## 添加依赖 ```xml org.springframework.boot spring-boot-starter-webflux dev.langchain4j langchain4j-reactor ``` ## 配置流式输出模型 在`application.properties`中配置流式输出大模型 ```ini #集成阿里通义千问-流式输出 langchain4j.community.dashscope.streaming-chat-model.api-key=你的apikey langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-plus ``` ## 修改`XiaozhiAgent` 注释`chatModel`,启用`streamingChatModel`;把修改`chat`方法的返回值 ```java @AiService( wiringMode = AiServiceWiringMode.EXPLICIT, // chatModel = "openAiChatModel",//找到对应的bean进行绑定 streamingChatModel = "qwenStreamingChatModel",//找到对应的bean进行绑定 chatMemoryProvider = "chatMemoryProviderXiaozhi",//找到对应的bean进行绑定 tools = "appointmentTools",//找到对应的bean进行绑定 contentRetriever = "contentRetrieverPinecone"//找到对应的bean进行绑定 ) public interface XiaozhiAgent { @SystemMessage(fromResource = "prompts/xiaozhi-prompt-template.txt") Flux chat(@MemoryId int memoryId, @UserMessage String userMessage); } ``` ## 修改`XiaozhiController` 修改`chat`方法的返回值 修改`@PostMapping`,添加`produces = "text/stream;charset=utf-8"`,使其流式输出且不会乱码 ```java @Tag(name = "小智") @RestController @RequestMapping("/xiaozhi") public class XiaozhiController { @Autowired private XiaozhiAgent xiaozhiAgent; @Operation(summary = "对话") @PostMapping(value = "/chat",produces = "text/stream;charset=utf-8") public Flux chat(@RequestBody ChatFormDTO chatFormDTO) { return xiaozhiAgent.chat(chatFormDTO.getMemoryId(), chatFormDTO.getUserMessage()); } } ``` # 15.项目实战-运行前端工程 安装`Node.js` ```sh cd xiaozhi-ui npm i npm run dev ``` 前端我修改了一下,使得输出的内容支持`markdown`语法 后续我会将我自己的代码上传到GitHub中 ![image-20250427164032237](./assets/image-20250427164032237.png)