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

}

image

 

第二部分,加入记忆

/**
     * 创建带记忆的 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) {}
}

 

 

posted @ 2026-01-09 13:19  蔡徐坤1987  阅读(2)  评论(0)    收藏  举报