langchain4j应用
1.搭建一个最简答的智能问答
第一部分:1.引入langchain 2.阿里官网注册拿到APIKEY 3.启动类加载QwenChatModel 4.通过注解@SystemMessage(“前提条件”)加入系统提示词 5.添加对应接口
<!-- LangChain4j Core --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- LangChain4j DashScope (阿里云) --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-dashscope</artifactId> <version>${langchain4j.version}</version> </dependency>
server:
port: 8080
# 通义千问配置
qianwen:
api-key: sk-xxx
model: qwen-plus
/** * 通义千问(Qwen)配置类 * * @Configuration 标记这是一个配置类,Spring 会在启动时加载 */ @Configuration public class QwenConfig { /** * 从 application.yml 读取 API Key * @Value 注解用于注入配置文件中的值 */ @Value("${qianwen.api-key}") private String apiKey; /** * 从 application.yml 读取模型名称(如 qwen-plus) */ @Value("${qianwen.model}") private String modelName; /** * 创建通义千问聊天模型 Bean * * @Bean 注解表示这个方法返回的对象会被 Spring 管理 * 其他地方可以通过依赖注入使用这个对象 * * @return ChatLanguageModel 聊天模型实例 */ @Bean public ChatLanguageModel chatLanguageModel() { return QwenChatModel.builder() .apiKey(apiKey) // 设置 API Key .modelName(modelName) // 设置模型名称 .build(); } }
/** * 郭德纲风格 AI 助手接口(无记忆版本) * * 这是一个接口,不需要写实现类! * LangChain4j 的 AiServices 会自动创建代理实现 * * 工作原理: * 1. 在 QwenConfig 中用 AiServices.builder() 创建这个接口的实例 * 2. 调用 chat() 方法时,AiServices 会: * - 读取 @SystemMessage 注解的内容作为系统提示词 * - 将用户消息和系统提示词一起发送给 AI * - 返回 AI 的回复 */ public interface GuodeGangAssistant { /** * 与 AI 对话 * * @SystemMessage 设置系统提示词(角色设定) * - 这个注解只有通过 AiServices 创建的代理才会生效 * - 直接在普通类的方法上加这个注解是无效的 * * @param message 用户输入的消息 * @return AI 以郭德纲风格的回复 */ @SystemMessage("假如你是郭德纲,接下来以郭德纲的风格进行对话。") String chat(String message); }
/** * 聊天控制器 - 处理 HTTP 请求 * * @RestController = @Controller + @ResponseBody * - 表示这是一个 REST 风格的控制器 * - 方法返回值会自动转换为 JSON * * @RequestMapping("/api") 设置所有接口的基础路径 */ @RestController @RequestMapping("/api") public class ChatController { // AI 助手(无记忆版本) private final GuodeGangAssistant guodeGangAssistant; /** * POST 请求 - 郭德纲风格对话(无记忆) * * 接口地址: POST http://localhost:8080/api/talk * 请求体: {"message": "你好"} * * @PostMapping("/talk") 处理 POST 请求 * @RequestBody 将请求体的 JSON 自动转换为 ChatRequest 对象 * * @param request 包含用户消息的请求对象 * @return AI 的回复内容 */ @PostMapping("/talk") public String chat(@RequestBody ChatRequest request) { return guodeGangAssistant.chat(request.message()); } }

第二部分,加入记忆
/** * 创建带记忆的 AI 助手 * * chatMemoryProvider 为每个会话(sessionId)创建独立的记忆存储 * MessageWindowChatMemory 是滑动窗口记忆: * - maxMessages(10) 表示最多保留最近10条消息 * - 超过10条时,最早的消息会被丢弃 * * @param chatModel 聊天模型 * @return GuodeGangWithMemory 带记忆的助手实例 */ @Bean public GuodeGangWithMemory guodeGangWithMemory(ChatLanguageModel chatModel) { return AiServices.builder(GuodeGangWithMemory.class) .chatLanguageModel(chatModel) .chatMemoryProvider(sessionId -> MessageWindowChatMemory.builder() .maxMessages(10) // 最多保留10条对话记录 .build()) .build(); }
/** * 郭德纲风格 AI 助手接口(无记忆版本) * * 这是一个接口,不需要写实现类! * LangChain4j 的 AiServices 会自动创建代理实现 * * 工作原理: * 1. 在 QwenConfig 中用 AiServices.builder() 创建这个接口的实例 * 2. 调用 chat() 方法时,AiServices 会: * - 读取 @SystemMessage 注解的内容作为系统提示词 * - 将用户消息和系统提示词一起发送给 AI * - 返回 AI 的回复 */ public interface GuodeGangAssistant { /** * 与 AI 对话 * * @SystemMessage 设置系统提示词(角色设定) * - 这个注解只有通过 AiServices 创建的代理才会生效 * - 直接在普通类的方法上加这个注解是无效的 * * @param message 用户输入的消息 * @return AI 以郭德纲风格的回复 */ @SystemMessage("假如你是郭德纲,接下来以郭德纲的风格进行对话。") String chat(String message); }
/** * POST 请求 - 郭德纲风格对话(带10条记忆) * * 接口地址: POST http://localhost:8080/api/memory * 请求体: {"sessionId": "user123", "message": "我叫张三"} * * 同一个 sessionId 的对话会保留上下文,AI 能记住之前说过的话 * 比如先说"我叫张三",再问"我叫什么",AI 能回答"张三" * * @param request 包含会话ID和消息的请求对象 * @return AI 的回复内容 */ @PostMapping("/memory") public String chatWithMemory(@RequestBody MemoryChatRequest request) { return guodeGangWithMemory.chat(request.sessionId(), request.message()); }
第三部分,引入RAG增强检索
* RAG 工作流程:
* 1. 文档解析:Tika 解析 PDF/Word/Excel 等文件为纯文本
* 2. 文本分块:将长文本切分为小块(便于检索)
* 3. 向量化:将文本块转换为向量(数字表示)
* 4. 存储:将向量存入向量数据库
* 5. 检索:用户提问时,找出最相关的文本块
* 6. 生成:将相关文本块 + 用户问题一起发给 AI 生成回答
引入向量存储和tika解析工具
<!-- LangChain4j 内存向量存储 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> <version>${langchain4j.version}</version> </dependency> <!-- LangChain4j Tika 文档解析器 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-document-parser-apache-tika</artifactId> <version>${langchain4j.version}</version> </dependency>
package com.example.qwen.config; import com.example.qwen.service.DocumentAssistant; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * RAG(检索增强生成)配置类 * * RAG 工作流程: * 1. 文档解析:Tika 解析 PDF/Word/Excel 等文件为纯文本 * 2. 文本分块:将长文本切分为小块(便于检索) * 3. 向量化:将文本块转换为向量(数字表示) * 4. 存储:将向量存入向量数据库 * 5. 检索:用户提问时,找出最相关的文本块 * 6. 生成:将相关文本块 + 用户问题一起发给 AI 生成回答 */ @Configuration public class RagConfig { /** * 创建嵌入模型(Embedding Model) * * 作用:将文本转换为向量(一串数字) * AllMiniLmL6V2 是一个轻量级的本地模型,不需要调用外部 API * * @return EmbeddingModel 嵌入模型实例 */ @Bean public EmbeddingModel embeddingModel() { return new AllMiniLmL6V2EmbeddingModel(); } /** * 创建向量存储(Embedding Store) * * 作用:存储文档的向量表示 * InMemoryEmbeddingStore 是内存存储,重启后数据会丢失 * 生产环境可以换成 Milvus、Pinecone、Redis 等持久化存储 * * @return EmbeddingStore 向量存储实例 */ @Bean public EmbeddingStore<TextSegment> embeddingStore() { return new InMemoryEmbeddingStore<>(); } /** * 创建内容检索器(Content Retriever) * * 作用:根据用户问题,从向量存储中检索最相关的文档片段 * * @param embeddingStore 向量存储 * @param embeddingModel 嵌入模型 * @return ContentRetriever 内容检索器实例 */ @Bean public ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) { return EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) // 最多返回3个相关片段 .minScore(0.5) // 相似度阈值,低于0.5的不返回 .build(); } /** * 创建文档问答助手 * * 关键配置:.contentRetriever(contentRetriever) * - 这会让 AI Service 在回答前先检索相关文档 * - 检索到的内容会自动添加到 AI 的上下文中 * * @param chatModel 聊天模型 * @param contentRetriever 内容检索器 * @return DocumentAssistant 文档问答助手实例 */ @Bean public DocumentAssistant documentAssistant(ChatLanguageModel chatModel, ContentRetriever contentRetriever) { return AiServices.builder(DocumentAssistant.class) .chatLanguageModel(chatModel) .contentRetriever(contentRetriever) // 关键:配置内容检索器 .build(); } }
package com.example.qwen.service; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentParser; import dev.langchain4j.data.document.Metadata; import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; /** * 文档处理服务 * * 负责: * 1. 使用 Tika 解析各种格式的文档(PDF、Word、Excel、PPT等) * 2. 将文档内容分块,并打上文件名标签 * 3. 向量化并存储到向量数据库 * 4. 支持按文件名删除文档 */ @Service public class DocumentService { /** * 元数据 key:文件名 * 用于标记每个文档片段属于哪个文件,方便后续删除 */ private static final String METADATA_FILE_NAME = "file_name"; private final EmbeddingStore<TextSegment> embeddingStore; private final EmbeddingModel embeddingModel; /** * Tika 文档解析器 * 支持解析:PDF、Word、Excel、PPT、HTML、TXT 等多种格式 */ private final DocumentParser documentParser = new ApacheTikaDocumentParser(); public DocumentService(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) { this.embeddingStore = embeddingStore; this.embeddingModel = embeddingModel; } /** * 上传并处理文档 * * 处理流程: * 1. Tika 解析文件内容为纯文本 * 2. 给文档添加 file_name 元数据(用于后续删除) * 3. 将文本按 500 字符分块(重叠 50 字符,保证上下文连贯) * 4. 每个文本块转换为向量 * 5. 存入向量数据库 * * @param file 上传的文件 * @return 处理结果信息 */ public String uploadDocument(MultipartFile file) { try { String fileName = file.getOriginalFilename(); // 1. 使用 Tika 解析文档 Document document = documentParser.parse(file.getInputStream()); // 2. 添加文件名元数据,用于后续按文件名删除 document.metadata().put(METADATA_FILE_NAME, fileName); // 3. 创建文档摄入器(负责分块、向量化、存储) EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .documentSplitter(DocumentSplitters.recursive(500, 50)) .build(); // 4. 执行摄入(分块 -> 向量化 -> 存储) // 分块时会自动继承文档的 metadata ingestor.ingest(document); return "文档上传成功: " + fileName; } catch (IOException e) { return "文档上传失败: " + e.getMessage(); } } /** * 删除指定文件的所有数据 * * 根据上传时记录的 file_name 元数据,删除该文件的所有向量片段 * * @param fileName 要删除的文件名 * @return 删除结果信息 */ public String deleteDocument(String fileName) { try { // 使用元数据过滤器,删除 file_name 等于指定值的所有记录 embeddingStore.removeAll( MetadataFilterBuilder.metadataKey(METADATA_FILE_NAME).isEqualTo(fileName) ); return "文档删除成功: " + fileName; } catch (Exception e) { return "文档删除失败: " + e.getMessage(); } } /** * 清空所有文档数据 * * @return 清空结果信息 */ public String clearAll() { try { embeddingStore.removeAll(); return "所有文档已清空"; } catch (Exception e) { return "清空失败: " + e.getMessage(); } } }
package com.example.qwen.controller; import com.example.qwen.service.DocumentAssistant; import com.example.qwen.service.DocumentService; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** * 文档问答控制器 * * 提供接口: * 1. 上传文档:解析并存储到向量数据库 * 2. 删除文档:按文件名删除 * 3. 清空所有:删除所有文档数据 * 4. 文档问答:基于上传的文档内容回答问题 */ @RestController @RequestMapping("/api/doc") public class DocumentController { private final DocumentService documentService; private final DocumentAssistant documentAssistant; public DocumentController(DocumentService documentService, DocumentAssistant documentAssistant) { this.documentService = documentService; this.documentAssistant = documentAssistant; } /** * 上传文档 * * 接口地址: POST http://localhost:8080/api/doc/upload * 请求方式: form-data,key 为 "file" * * 支持的文件格式(Tika 支持): * - PDF、Word(.doc/.docx)、Excel(.xls/.xlsx) * - PPT(.ppt/.pptx)、TXT、HTML、Markdown 等 * * @param file 上传的文件 * @return 上传结果信息 */ @PostMapping("/upload") public String uploadDocument(@RequestParam("file") MultipartFile file) { return documentService.uploadDocument(file); } /** * 删除指定文档 * * 接口地址: DELETE http://localhost:8080/api/doc/delete?fileName=xxx.pdf * * @param fileName 要删除的文件名(上传时的原始文件名) * @return 删除结果信息 */ @DeleteMapping("/delete") public String deleteDocument(@RequestParam String fileName) { return documentService.deleteDocument(fileName); } /** * 清空所有文档 * * 接口地址: DELETE http://localhost:8080/api/doc/clear * * @return 清空结果信息 */ @DeleteMapping("/clear") public String clearAll() { return documentService.clearAll(); } /** * 基于文档内容问答 * * 接口地址: GET http://localhost:8080/api/doc/ask?question=文档里说了什么 * * @param question 用户的问题 * @return AI 基于文档内容的回答 */ @GetMapping("/ask") public String askDocument(@RequestParam String question) { return documentAssistant.answer(question); } /** * 基于文档内容问答(POST 方式) * * 接口地址: POST http://localhost:8080/api/doc/ask * 请求体: {"question": "文档里说了什么"} * * @param request 包含问题的请求对象 * @return AI 基于文档内容的回答 */ @PostMapping("/ask") public String askDocumentPost(@RequestBody QuestionRequest request) { return documentAssistant.answer(request.question()); } /** * 问题请求对象 */ public record QuestionRequest(String question) {} }
浙公网安备 33010602011771号