LangChain4j 核心介绍

LangChain4j 是专为 Java/Kotlin 开发者打造的大模型应用开发框架,是 Python 生态 LangChain 的 Java 版本,目标是让 Java 技术栈能高效构建 LLM 驱动的应用。


🔹 核心定位

  • 技术栈适配:填补了 Java 生态缺少成熟大模型开发框架的空白,无需切换到 Python 即可开发 AI 应用。
  • 能力对齐:与 LangChain 核心能力对齐,提供模型连接、提示词工程、记忆管理、工具调用等全链路能力。
  • 生产级特性:支持 Spring Boot 集成、多模型兼容、可观测性与性能优化,适合企业级项目落地。

🔹 核心功能

  1. 多模型接入

    • 支持 OpenAI、Anthropic、Gemini、国内大模型(如通义千问、文心一言)及开源模型(Llama 3、Qwen)。
    • 提供统一的 API 接口,切换模型无需大幅修改代码。
  2. 提示词工程

    • 内置提示词模板、动态变量替换、Few-Shot 示例管理,支持复杂场景下的提示词复用与优化。
  3. 记忆管理

    • 提供对话记忆(Conversation Memory)、摘要记忆(Summary Memory)等多种实现,自动维护多轮对话上下文。
  4. 工具调用(Tool Calling)

    • 允许大模型调用外部工具(如数据库查询、API 请求、计算器),实现“思考+执行”闭环。
  5. 文档处理

    • 集成 PDF、Word、Markdown 等格式解析,结合向量数据库(如 Pinecone、Milvus)实现文档问答。
  6. Spring Boot 集成

    • 提供 Starter 依赖,支持注解式开发,可快速嵌入现有 Java 后端服务。

🔹 典型应用场景

  • 智能客服机器人:多轮对话+记忆管理,支持业务知识库检索。
  • 企业内部问答系统:对接内部文档库,实现自然语言查询。
  • 代码生成助手:基于 Java 代码上下文生成或优化代码。
  • 数据分析助手:调用 SQL 工具查询数据库,用自然语言输出分析结果。

🔹 快速上手示例

// 1. 初始化模型
ChatLanguageModel model = OpenAiChatModel.withApiKey("YOUR_API_KEY");

// 2. 构建链
ChatChain chatChain = ChatChain.builder()
    .chatLanguageModel(model)
    .memory(MessageWindowMemory.withMaxMessages(10))
    .build();

// 3. 发起对话
String response = chatChain.sendUserMessage("解释一下 LangChain4j 的核心价值");
System.out.println(response);

LangChain4j 企业级项目集成清单

本清单覆盖环境配置、组件选型、性能优化、生产部署四大核心环节,可直接用于 Java 企业级项目集成 LangChain4j 落地 AI 能力。

一、 环境配置(前置准备)

步骤 具体操作 工具/依赖 注意事项
1 JDK 版本确认 JDK 17+ LangChain4j 推荐 JDK 17,兼容模块化与新特性
2 构建工具配置 Maven/Gradle 引入核心依赖及对应 Starter
3 依赖引入 xml<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai</artifactId><version>0.32.0</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-spring-boot-starter</artifactId><version>0.32.0</version></dependency> 版本需统一,避免依赖冲突
4 API 密钥配置 模型平台密钥(OpenAI/国内大模型) 生产环境通过配置中心存储,禁止硬编码
5 开发/测试环境隔离 Spring Profiles 开发用测试模型,生产用正式模型

二、 核心组件选型(按需组合)

组件类别 可选方案 适用场景 选型建议
大模型接入 1. 闭源模型:OpenAI、Anthropic、文心一言
2. 开源模型:Llama 3、Qwen(本地部署)
1. 闭源:快速上线、追求效果
2. 开源:数据隐私要求高、私有化部署
企业内部系统优先开源+私有化;对外轻量应用选闭源 API
记忆管理 1. 对话记忆:MessageWindowMemory
2. 摘要记忆:SummaryMemory
3. 持久化记忆:Redis/数据库存储
1. 短对话:MessageWindowMemory
2. 长对话:SummaryMemory
3. 跨会话记忆:持久化存储
结合业务场景,长对话需定期清理冗余上下文
工具调用 1. 内置工具:计算器、搜索引擎
2. 自定义工具:数据库查询、内部 API 调用
需模型联动外部系统的场景(如智能查数、工单处理) 自定义工具需添加权限校验,避免越权调用
文档处理 1. 文档解析:Apache Tika、PDFBox
2. 向量存储:Milvus、Pinecone、Redis Stack
企业知识库问答、文档检索场景 中小规模用 Redis Stack;大规模用 Milvus 分布式向量库

三、 性能优化(生产级必备)

优化方向 具体措施 工具/方法 预期效果
模型调用优化 1. 开启请求缓存:缓存高频相同查询
2. 批量请求合并:合并多条相似查询
3. 设置超时与重试:配置 timeout=30s、重试次数=3
Caffeine 缓存、Spring Retry 降低 API 调用成本,提升响应速度
上下文优化 1. 上下文裁剪:只保留关键对话历史
2. 摘要压缩:长对话生成摘要替代原始内容
LangChain4j 内置摘要工具 减少 Token 消耗,降低延迟
并发控制 1. 限制模型调用线程池大小
2. 配置熔断降级:避免模型服务异常拖垮业务系统
Resilience4j、Sentinel 保障高并发下系统稳定性
量化与本地部署优化 1. 开源模型 INT4/INT8 量化
2. 使用 GPU 加速推理
Llama.cpp、Ollama 本地部署模型推理速度提升 50%+

四、 生产部署(合规与稳定保障)

部署环节 关键操作 工具/规范 合规要求
部署模式选择 1. 公有云:模型 API 调用
2. 私有化:开源模型本地部署
3. 混合云:核心数据本地,非核心调用公有云
Kubernetes、Docker 涉及敏感数据需私有化部署,符合等保三级
监控告警 1. 监控指标:调用成功率、响应延迟、Token 消耗
2. 日志记录:全链路日志(请求/响应/异常)
3. 告警阈值:延迟>200ms、成功率<99.9% 触发告警
Prometheus + Grafana、ELK 日志需脱敏,避免敏感信息泄露
权限控制 1. 基于 RBAC 控制模型调用权限
2. 接口鉴权:API Key + 签名验证
Spring Security 禁止匿名访问,操作日志可追溯
容灾备份 1. 多模型 fallback:主模型异常自动切换备用模型
2. 数据备份:记忆数据、知识库定期备份
定时任务 + 云存储 保障服务不中断,数据不丢失

五、 验收 checklist

  • 环境依赖无冲突,模型 API 调用正常
  • 核心功能(对话/工具调用/文档检索)符合预期
  • 性能指标达标(延迟<200ms,成功率>99.9%)
  • 安全合规(密钥不硬编码、日志脱敏、权限可控)
  • 监控告警配置完成,容灾方案验证通过

以下是可直接运行的 LangChain4j + Spring Boot 项目模板代码,覆盖核心的「大模型对话」「记忆管理」「工具调用」能力,你只需替换 API 密钥即可快速验证效果。

一、项目基础配置

1. pom.xml(Maven 依赖)
<?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>

    <!-- Spring Boot 父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>langchain4j-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>langchain4j-demo</name>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- LangChain4j 核心 + OpenAI 集成 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
            <version>0.32.0</version>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>0.32.0</version>
        </dependency>

        <!-- 工具调用依赖(示例用) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
2. application.yml(配置文件)
spring:
  application:
    name: langchain4j-demo

# LangChain4j 配置(替换为你的 API Key)
langchain4j:
  open-ai:
    chat-model:
      api-key: ${OPENAI_API_KEY:你的OpenAI API Key}
      # 国内大模型可替换为:如通义千问/文心一言,需对应依赖和配置
      # base-url: 自定义模型接口地址(可选)
      temperature: 0.7  # 生成随机性
      max-tokens: 1000  # 最大输出Token数

二、核心功能代码

1. 对话服务(带记忆管理)
package com.example.langchain4jdemo.service;

import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import org.springframework.stereotype.Service;

/**
 * 基础对话服务(带多轮记忆)
 */
@Service
public class ChatService {

    // 定义AI服务接口
    public interface ChatAssistant {
        // 基础对话方法
        String chat(String userMessage);
    }

    private final ChatAssistant chatAssistant;

    // 构造函数注入大模型实例
    public ChatService(ChatLanguageModel chatLanguageModel) {
        // 配置记忆:保留最近10条对话
        this.chatAssistant = AiServices.builder(ChatAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .build();
    }

    // 对外提供对话方法
    public String chatWithMemory(String userMessage) {
        return chatAssistant.chat(userMessage);
    }
}
2. 工具调用示例(计算器工具)
package com.example.langchain4jdemo.service;

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import org.springframework.stereotype.Service;

/**
 * 工具调用服务(示例:计算器)
 */
@Service
public class ToolService {

    // 定义带工具的AI接口
    public interface CalculatorAssistant {
        String solveMathProblem(String problem);
    }

    // 计算器工具(标注@Tool,让模型识别)
    public static class CalculatorTool {
        @Tool("加法运算:计算两个数的和")
        public double add(double a, double b) {
            return a + b;
        }

        @Tool("减法运算:计算两个数的差")
        public double subtract(double a, double b) {
            return a - b;
        }
    }

    private final CalculatorAssistant calculatorAssistant;

    public ToolService(ChatLanguageModel chatLanguageModel) {
        // 绑定工具到AI服务
        this.calculatorAssistant = AiServices.builder(CalculatorAssistant.class)
                .chatLanguageModel(chatLanguageModel)
                .tools(new CalculatorTool())
                .build();
    }

    // 对外提供数学解题方法
    public String solveMath(String problem) {
        return calculatorAssistant.solveMathProblem(problem);
    }
}
3. 控制器(API 接口)
package com.example.langchain4jdemo.controller;

import com.example.langchain4jdemo.service.ChatService;
import com.example.langchain4jdemo.service.ToolService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AiController {

    private final ChatService chatService;
    private final ToolService toolService;

    // 构造函数注入服务
    public AiController(ChatService chatService, ToolService toolService) {
        this.chatService = chatService;
        this.toolService = toolService;
    }

    /**
     * 基础对话接口(带记忆)
     * 示例:http://localhost:8080/chat?message=你好,介绍下LangChain4j
     */
    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatService.chatWithMemory(message);
    }

    /**
     * 工具调用接口(计算器)
     * 示例:http://localhost:8080/calculate?problem=计算100+200的结果
     */
    @GetMapping("/calculate")
    public String calculate(@RequestParam String problem) {
        return toolService.solveMath(problem);
    }
}
4. 启动类
package com.example.langchain4jdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Langchain4jDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(Langchain4jDemoApplication.class, args);
    }
}

三、运行与验证

  1. 替换配置:在 application.yml 中替换 api-key 为你的 OpenAI API Key(或国内大模型的密钥/地址);
  2. 启动项目:运行 Langchain4jDemoApplication 类;
  3. 测试接口
    • 对话接口:访问 http://localhost:8080/chat?message=你好,介绍下LangChain4j,返回模型回答;
    • 多轮对话:连续调用 /chat 接口,模型会记住上下文(如接着问“它和LangChain有什么区别”);
    • 工具调用:访问 http://localhost:8080/calculate?problem=计算100+200的结果,模型会调用计算器工具返回 300。

总结

  1. 该模板基于 Spring Boot 3.x + LangChain4j 0.32.0 构建,兼容主流 Java 企业级项目;
  2. 核心覆盖「对话记忆」「工具调用」两大核心能力,可直接扩展到文档问答、智能客服等场景;
  3. 替换配置中的模型 API 信息,即可适配不同大模型(闭源/开源)。

一、适配国内大模型(通义千问/文心一言)配置模板

1. 新增国内大模型依赖(pom.xml)
<!-- 通义千问集成 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-dashscope</artifactId>
    <version>0.32.0</version>
</dependency>

<!-- 文心一言集成 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ernie</artifactId>
    <version>0.32.0</version>
</dependency>
2. 配置文件修改(application.yml)
spring:
  application:
    name: langchain4j-demo

# 【通义千问配置】
langchain4j:
  dashscope:
    chat-model:
      api-key: ${DASHSCOPE_API_KEY:你的通义千问API Key}
      model-name: qwen-turbo  # 可选:qwen-plus/qwen-max
      temperature: 0.7
      max-tokens: 1000

# 【文心一言配置(二选一)】
# langchain4j:
#   ernie:
#     chat-model:
#       api-key: ${ERNIE_API_KEY:你的文心一言API Key}
#       secret-key: ${ERNIE_SECRET_KEY:你的文心一言Secret Key}
#       model-name: ernie-4.0
#       temperature: 0.7
#       max-tokens: 1000
3. 通用适配改造(无需修改业务代码)

LangChain4j 已封装统一接口,只需确保注入的 ChatLanguageModel 为对应实现类,Spring Boot Starter 会自动根据配置加载,原 ChatService/ToolService 无需改动即可直接使用国内大模型。


二、扩展「文档检索」功能模板

1. 新增文档检索依赖(pom.xml)
<!-- 文档解析(PDF/Word/Markdown) -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-document-loader</artifactId>
    <version>0.32.0</version>
</dependency>
<!-- 向量存储(Redis Stack,轻量易部署) -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-redis</artifactId>
    <version>0.32.0</version>
</dependency>
<!-- PDF解析依赖 -->
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.2</version>
</dependency>
2. Redis 向量存储配置(application.yml)
# Redis 配置(需部署 Redis Stack,开启向量功能)
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ""
      database: 0

# LangChain4j 向量存储配置
langchain4j:
  redis:
    vector-store:
      index-name: document_index  # 向量索引名
      dimension: 768  # 嵌入模型维度(通义千问/文心一言默认768)
3. 文档检索核心代码
package com.example.langchain4jdemo.service;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.dashscope.QwenTextEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.redis.RedisEmbeddingStore;
import org.springframework.stereotype.Service;

import java.nio.file.Paths;
import java.util.List;

/**
 * 文档检索服务(基于向量库)
 */
@Service
public class DocumentRetrievalService {

    private final EmbeddingStore<TextSegment> embeddingStore;
    private final EmbeddingModel embeddingModel;

    // 初始化向量存储和嵌入模型
    public DocumentRetrievalService() {
        // 1. 初始化Redis向量存储
        this.embeddingStore = RedisEmbeddingStore.builder()
                .indexName("document_index")
                .dimension(768)
                .host("localhost")
                .port(6379)
                .build();

        // 2. 初始化通义千问嵌入模型(替换为你的API Key)
        this.embeddingModel = QwenTextEmbeddingModel.builder()
                .apiKey("你的通义千问API Key")
                .build();
    }

    /**
     * 加载本地PDF文档到向量库
     * @param pdfPath PDF文件路径(如:/docs/知识库.pdf)
     */
    public void loadDocumentToVectorStore(String pdfPath) {
        // 1. 加载并解析PDF文档
        Document document = FileSystemDocumentLoader.load(
                Paths.get(pdfPath),
                new ApachePdfBoxDocumentParser()
        );

        // 2. 将文档分片并写入向量库
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(500, 50))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        ingestor.ingest(document);
        System.out.println("文档加载完成:" + pdfPath);
    }

    /**
     * 检索与问题最相关的文档片段
     * @param question 用户问题
     * @param topK 返回最相关的前K条
     * @return 相关文档片段列表
     */
    public List<String> retrieveRelevantContent(String question, int topK) {
        // 1. 将问题转为向量
        Embedding questionEmbedding = embeddingModel.embed(question).content();

        // 2. 向量库检索
        List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(questionEmbedding, topK);

        // 3. 提取文档内容
        return matches.stream()
                .map(match -> match.embeddedObject().text())
                .toList();
    }
}
4. 文档检索控制器
package com.example.langchain4jdemo.controller;

import com.example.langchain4jdemo.service.DocumentRetrievalService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class DocumentController {

    private final DocumentRetrievalService documentRetrievalService;

    public DocumentController(DocumentRetrievalService documentRetrievalService) {
        this.documentRetrievalService = documentRetrievalService;
    }

    /**
     * 加载PDF文档到向量库
     * 示例:http://localhost:8080/load-document?pdfPath=/docs/知识库.pdf
     */
    @GetMapping("/load-document")
    public String loadDocument(@RequestParam String pdfPath) {
        documentRetrievalService.loadDocumentToVectorStore(pdfPath);
        return "文档加载成功!";
    }

    /**
     * 检索相关文档内容
     * 示例:http://localhost:8080/retrieve?question=LangChain4j的核心功能&topK=3
     */
    @GetMapping("/retrieve")
    public List<String> retrieve(@RequestParam String question, @RequestParam(defaultValue = "3") int topK) {
        return documentRetrievalService.retrieveRelevantContent(question, topK);
    }
}

三、运行验证说明

  1. 国内大模型验证:替换 API Key 后,直接调用原 /chat//calculate 接口,即可使用通义千问/文心一言完成对话和工具调用;
  2. 文档检索验证
    • 先部署 Redis Stack(开启向量功能);
    • 调用 /load-document 接口加载本地 PDF 文档;
    • 调用 /retrieve 接口输入问题,即可返回文档中最相关的内容。

总结

  1. 国内大模型适配仅需替换依赖和配置,业务代码完全复用,符合“接口统一”的设计原则;
  2. 文档检索功能基于 Redis 向量库实现轻量部署,适配中小规模知识库场景,大规模场景可替换为 Milvus 分布式向量库;
  3. 可将「文档检索结果」拼接至大模型提示词中,实现“基于自有文档的智能问答”(只需在 ChatService 中整合检索结果)。
    以下是「检索+对话」一体化的智能问答代码模板,核心逻辑是先检索文档中与问题相关的内容,再将检索结果作为上下文传给大模型,让模型基于文档内容回答问题,完全复用之前的项目结构,可直接集成使用。

一、核心依赖补充(若未添加)

确保 pom.xml 已包含以下依赖(前文文档检索模板已提供,此处确认):

<!-- 核心依赖:文档加载+向量存储+国内大模型 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-document-loader</artifactId>
    <version>0.32.0</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-redis</artifactId>
    <version>0.32.0</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-dashscope</artifactId>
    <version>0.32.0</version>
</dependency>

二、「检索+对话」一体化服务代码

package com.example.langchain4jdemo.service;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.dashscope.QwenTextEmbeddingModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.redis.RedisEmbeddingStore;
import org.springframework.stereotype.Service;

import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 检索+对话一体化智能问答服务
 * 核心:先检索文档,再基于文档内容回答问题
 */
@Service
public class RAGService {

    // 定义带上下文的AI问答接口
    public interface DocumentQA {
        // prompt模板:指定模型仅基于提供的文档内容回答问题
        String answer(@dev.langchain4j.service.SystemMessage String systemPrompt,
                      @dev.langchain4j.service.UserMessage String userQuestion);
    }

    private final EmbeddingStore<TextSegment> embeddingStore;
    private final EmbeddingModel embeddingModel;
    private final DocumentQA documentQA;

    // 初始化向量存储、嵌入模型、AI问答服务
    public RAGService(ChatLanguageModel chatLanguageModel) {
        // 1. 初始化Redis向量存储(适配通义千问768维嵌入)
        this.embeddingStore = RedisEmbeddingStore.builder()
                .indexName("document_index")
                .dimension(768)
                .host("localhost")
                .port(6379)
                .build();

        // 2. 初始化通义千问嵌入模型(替换为你的API Key)
        this.embeddingModel = QwenTextEmbeddingModel.builder()
                .apiKey("你的通义千问API Key")
                .build();

        // 3. 初始化AI问答服务(带多轮记忆)
        this.documentQA = AiServices.builder(DocumentQA.class)
                .chatLanguageModel(chatLanguageModel)
                .chatMemory(MessageWindowChatMemory.withMaxMessages(5)) // 保留5轮对话记忆
                .build();
    }

    /**
     * 加载PDF文档到向量库(复用文档检索逻辑)
     */
    public void loadDocument(String pdfPath) {
        Document document = FileSystemDocumentLoader.load(
                Paths.get(pdfPath),
                new ApachePdfBoxDocumentParser()
        );
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(500, 50))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        ingestor.ingest(document);
    }

    /**
     * 核心:检索+问答一体化
     * @param userQuestion 用户问题
     * @param topK 检索最相关的前K条文档
     * @return 基于文档内容的回答
     */
    public String ragAnswer(String userQuestion, int topK) {
        // 步骤1:检索相关文档片段
        Embedding questionEmbedding = embeddingModel.embed(userQuestion).content();
        List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(questionEmbedding, topK);
        
        // 步骤2:拼接检索结果为上下文(仅保留文本内容)
        String context = matches.stream()
                .map(match -> match.embeddedObject().text())
                .collect(Collectors.joining("\n\n"));

        // 步骤3:构造系统提示词(约束模型仅基于文档回答)
        String systemPrompt = """
                你是一个基于文档的智能问答助手,仅能使用以下提供的文档内容回答用户问题:
                %s
                如果文档内容无法回答用户问题,请明确说明“无法从文档中找到相关答案”,不要编造信息。
                """.formatted(context.isEmpty() ? "无相关文档内容" : context);

        // 步骤4:调用大模型回答问题
        return documentQA.answer(systemPrompt, userQuestion);
    }
}

三、一体化问答控制器

package com.example.langchain4jdemo.controller;

import com.example.langchain4jdemo.service.RAGService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RAGController {

    private final RAGService ragService;

    public RAGController(RAGService ragService) {
        this.ragService = ragService;
    }

    /**
     * 加载PDF文档到向量库(复用接口)
     * 示例:http://localhost:8080/rag/load-document?pdfPath=/docs/公司产品手册.pdf
     */
    @GetMapping("/rag/load-document")
    public String loadDocument(@RequestParam String pdfPath) {
        ragService.loadDocument(pdfPath);
        return "文档加载成功!已存入向量库";
    }

    /**
     * 检索+对话一体化问答
     * 示例:http://localhost:8080/rag/answer?question=公司产品A的核心功能有哪些&topK=3
     */
    @GetMapping("/rag/answer")
    public String ragAnswer(@RequestParam String question,
                            @RequestParam(defaultValue = "3") int topK) {
        return ragService.ragAnswer(question, topK);
    }
}

四、关键配置说明

  1. 模型适配
    • 若使用文心一言,只需将 QwenTextEmbeddingModel 替换为文心一言的嵌入模型(ErnieTextEmbeddingModel),并修改 API Key 配置;
    • ChatLanguageModel 会自动根据 application.yml 中的配置(通义千问/文心一言)加载,无需修改服务代码。
  2. Redis 依赖:需确保部署了 Redis Stack(开启向量功能),本地测试可直接用 Docker 启动:
    docker run -d -p 6379:6379 redis/redis-stack:latest
    

五、运行验证步骤

  1. 加载文档:访问 http://localhost:8080/rag/load-document?pdfPath=你的PDF文件绝对路径(如 D:/docs/产品手册.pdf);
  2. 智能问答:访问 http://localhost:8080/rag/answer?question=产品手册中产品A的价格是多少&topK=3
    • 若文档中有相关内容,模型会基于文档回答;
    • 若文档中无相关内容,模型会返回“无法从文档中找到相关答案”。

总结

  1. 核心逻辑:通过「检索获取上下文→构造约束性提示词→大模型回答」的流程,确保回答完全基于自有文档,避免编造信息;
  2. 复用性:完全兼容之前的项目结构,可无缝集成到原有 LangChain4j 应用中;
  3. 扩展性
    • 可增加文档类型支持(如 Word/Markdown),只需替换 DocumentParser
    • 可优化检索效果(如调整文档分片大小、嵌入模型维度);
    • 可添加缓存机制,避免重复检索相同问题的文档。

一、行业化提示词模板优化(以金融/医疗为例)

1. 金融行业专属提示词模板(RAGService中修改systemPrompt)
// 金融行业(如理财产品问答)
String systemPrompt = """
        你是专业的金融产品智能问答助手,严格遵守金融行业合规要求,仅基于以下文档内容回答用户问题:
        %s
        回答要求:
        1. 仅引用文档中的理财产品信息,不添加任何主观建议;
        2. 涉及收益相关内容,必须标注“过往收益不代表未来表现”;
        3. 若文档无相关内容,直接回复“暂无该产品相关公开信息”;
        4. 禁止回答文档外的金融产品、理财建议、风险承诺等内容。
        """.formatted(context.isEmpty() ? "无相关文档内容" : context);
2. 医疗行业专属提示词模板
// 医疗行业(如科室/药品问答)
String systemPrompt = """
        你是医疗机构的智能咨询助手,仅基于以下医疗文档内容回答用户问题:
        %s
        回答要求:
        1. 仅提供文档中的科室介绍、药品适应症、就诊流程等信息;
        2. 禁止提供任何诊断建议、用药指导、病情判断等医疗决策类内容;
        3. 结尾必须添加“本回答仅为信息参考,不构成医疗建议,请遵医嘱”;
        4. 若文档无相关内容,直接回复“未查询到相关医疗信息”。
        """.formatted(context.isEmpty() ? "无相关文档内容" : context);

二、批量文档加载功能(支持多文件/文件夹批量导入)

1. 批量加载核心代码(新增到RAGService)
/**
 * 批量加载文件夹下的所有PDF文档到向量库
 * @param folderPath 文件夹路径(如:/docs/金融产品手册/)
 */
public void batchLoadDocuments(String folderPath) {
    File folder = new File(folderPath);
    if (!folder.isDirectory()) {
        throw new IllegalArgumentException("路径不是有效文件夹:" + folderPath);
    }

    // 遍历文件夹下所有PDF文件
    File[] pdfFiles = folder.listFiles((dir, name) -> name.endsWith(".pdf"));
    if (pdfFiles == null || pdfFiles.length == 0) {
        throw new RuntimeException("文件夹中未找到PDF文件:" + folderPath);
    }

    // 批量导入
    for (File pdfFile : pdfFiles) {
        try {
            Document document = FileSystemDocumentLoader.load(
                    pdfFile.toPath(),
                    new ApachePdfBoxDocumentParser()
            );
            // 为文档添加元数据(文件名),方便后续管理
            document.metadata().add("file_name", pdfFile.getName());
            
            EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                    .documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(500, 50))
                    .embeddingModel(embeddingModel)
                    .embeddingStore(embeddingStore)
                    .build();
            ingestor.ingest(document);
            System.out.println("批量加载完成:" + pdfFile.getName());
        } catch (Exception e) {
            System.err.println("加载文件失败:" + pdfFile.getName() + ",原因:" + e.getMessage());
        }
    }
}
2. 批量加载控制器(新增到RAGController)
/**
 * 批量加载文件夹下所有PDF文档
 * 示例:http://localhost:8080/rag/batch-load?folderPath=/docs/金融产品手册/
 */
@GetMapping("/rag/batch-load")
public String batchLoadDocuments(@RequestParam String folderPath) {
    try {
        ragService.batchLoadDocuments(folderPath);
        return "批量加载完成!已处理文件夹下所有PDF文档";
    } catch (Exception e) {
        return "批量加载失败:" + e.getMessage();
    }
}

三、文档更新/删除功能(基于元数据管理)

1. 文档删除核心代码(新增到RAGService)
import dev.langchain4j.store.embedding.filter.MetadataFilter;
import dev.langchain4j.store.embedding.redis.RedisEmbeddingStore;

/**
 * 根据文件名删除向量库中的文档片段
 * @param fileName 要删除的PDF文件名(如:产品A手册.pdf)
 */
public void deleteDocument(String fileName) {
    // 基于元数据过滤(匹配file_name字段)
    MetadataFilter filter = MetadataFilter.builder()
            .key("file_name")
            .value(fileName)
            .build();
    
    // 删除匹配的向量数据
    long deleteCount = ((RedisEmbeddingStore) embeddingStore).deleteByFilter(filter);
    System.out.println("删除完成:共删除 " + deleteCount + " 条文档片段");
}

/**
 * 文档更新(先删除旧文档,再加载新文档)
 * @param oldFileName 旧文件名
 * @param newPdfPath 新PDF文件路径
 */
public void updateDocument(String oldFileName, String newPdfPath) {
    // 步骤1:删除旧文档
    deleteDocument(oldFileName);
    // 步骤2:加载新文档
    loadDocument(newPdfPath);
    System.out.println("文档更新完成:" + oldFileName + " → " + newPdfPath);
}
2. 文档更新/删除控制器(新增到RAGController)
/**
 * 删除向量库中的指定文档
 * 示例:http://localhost:8080/rag/delete?fileName=产品A旧版手册.pdf
 */
@GetMapping("/rag/delete")
public String deleteDocument(@RequestParam String fileName) {
    ragService.deleteDocument(fileName);
    return "文档删除成功!";
}

/**
 * 更新向量库中的文档(先删旧的,再加新的)
 * 示例:http://localhost:8080/rag/update?oldFileName=产品A旧版手册.pdf&newPdfPath=/docs/产品A新版手册.pdf
 */
@GetMapping("/rag/update")
public String updateDocument(@RequestParam String oldFileName,
                             @RequestParam String newPdfPath) {
    try {
        ragService.updateDocument(oldFileName, newPdfPath);
        return "文档更新成功!";
    } catch (Exception e) {
        return "文档更新失败:" + e.getMessage();
    }
}

四、关键优化说明

  1. 提示词模板可配置化

    • 可将行业提示词模板写入配置文件(如application.yml),避免硬编码,示例:
    rag:
      prompt:
        finance: |
          你是专业的金融产品智能问答助手...
        medical: |
          你是医疗机构的智能咨询助手...
    
    • 在代码中通过@Value注入模板,根据行业参数动态选择。
  2. 批量加载容错处理

    • 单个文件加载失败不会中断批量流程,会打印错误日志并继续处理下一个文件。
  3. 文档管理扩展

    • 可新增“查询已加载文档列表”功能,通过Redis的SCAN命令遍历元数据中的file_name去重后返回;
    • 可添加文档版本号元数据,支持按版本管理文档。

总结

  1. 提示词优化:通过行业化约束模板,让回答更贴合业务合规要求,避免违规/编造信息;
  2. 批量加载:支持文件夹级别的文档批量导入,提升海量文档处理效率;
  3. 文档更新/删除:基于元数据过滤实现精准的文档管理,解决“文档变更后向量库数据不一致”的问题;
  4. 扩展性:所有功能均基于原有RAG服务扩展,无需重构核心逻辑,可直接集成到生产环境。

一、文档版本管理(基于元数据+版本号)

核心思路:为文档片段添加「文件名+版本号」元数据,支持按版本检索/删除,保留历史版本或覆盖更新。

1. 改造 RAGService 新增版本管理逻辑
/**
 * 加载文档(带版本号)
 * @param pdfPath PDF文件路径
 * @param version 版本号(如v1.0、20260101)
 */
public String loadDocumentWithVersion(String pdfPath, String version) {
    DocumentLoadProgress progress = DocumentLoadProgress.getInstance();
    progress.reset();
    
    File pdfFile = new File(pdfPath);
    String fileName = pdfFile.getName();
    progress.setCurrentFileName(fileName + " (版本:" + version + ")");

    try {
        Document document = FileSystemDocumentLoader.load(
                Paths.get(pdfPath),
                new ApachePdfBoxDocumentParser()
        );
        // 为文档添加版本元数据
        document.metadata().add("file_name", fileName);
        document.metadata().add("version", version);

        // 预分片统计总数
        List<TextSegment> segments = dev.langchain4j.data.document.splitter.DocumentSplitters
                .recursive(500, 50)
                .split(document);
        progress.setTotalSegments(segments.size());

        // 加载并监听进度
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(500, 50))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .onSegmentIngested(segment -> progress.completeSegment())
                .build();
        ingestor.ingest(document);

        return "文档加载完成(版本:" + version + "):" + fileName;
    } catch (Exception e) {
        return "加载失败:" + e.getMessage();
    }
}

/**
 * 删除指定版本的文档
 * @param fileName 文件名
 * @param version 版本号(不传则删除所有版本)
 */
public String deleteDocumentByVersion(String fileName, String version) {
    MetadataFilter.Builder filterBuilder = MetadataFilter.builder()
            .key("file_name")
            .value(fileName);
    // 若指定版本,添加版本过滤
    if (version != null && !version.isEmpty()) {
        filterBuilder.and(MetadataFilter.builder()
                .key("version")
                .value(version)
                .build());
    }
    MetadataFilter filter = filterBuilder.build();

    long deleteCount = ((RedisEmbeddingStore) embeddingStore).deleteByFilter(filter);
    return "删除完成:共删除 " + deleteCount + " 条文档片段(文件名:" + fileName + ",版本:" + (version == null ? "所有" : version) + ")";
}

/**
 * 查询文档所有版本
 * @param fileName 文件名
 * @return 版本列表
 */
public List<String> listDocumentVersions(String fileName) {
    // 过滤指定文件的所有片段,提取版本号并去重
    MetadataFilter filter = MetadataFilter.builder()
            .key("file_name")
            .value(fileName)
            .build();
    List<TextSegment> segments = ((RedisEmbeddingStore) embeddingStore).findByFilter(filter);
    
    return segments.stream()
            .map(segment -> segment.metadata().get("version"))
            .distinct()
            .sorted()
            .toList();
}

/**
 * 基于指定版本检索问答
 * @param userQuestion 用户问题
 * @param fileName 文件名(可选)
 * @param version 版本号(可选)
 * @param topK 检索数量
 * @return 基于指定版本的回答
 */
public String ragAnswerByVersion(String userQuestion, String fileName, String version, int topK) {
    // 1. 构建过滤条件(可选:指定文件+版本)
    MetadataFilter.Builder filterBuilder = MetadataFilter.builder();
    if (fileName != null && !fileName.isEmpty()) {
        filterBuilder.key("file_name").value(fileName);
    }
    if (version != null && !version.isEmpty()) {
        filterBuilder.and(MetadataFilter.builder().key("version").value(version).build());
    }
    MetadataFilter filter = filterBuilder.build();

    // 2. 检索(带版本过滤)
    Embedding questionEmbedding = embeddingModel.embed(userQuestion).content();
    List<EmbeddingMatch<TextSegment>> matches = ((RedisEmbeddingStore) embeddingStore)
            .findRelevant(questionEmbedding, topK, filter); // 传入过滤条件

    // 3. 拼接上下文并回答(逻辑同原有ragAnswer)
    String context = matches.stream()
            .map(match -> match.embeddedObject().text())
            .collect(Collectors.joining("\n\n"));
    String systemPrompt = """
            你是基于指定版本文档的智能问答助手,仅使用以下内容回答:
            %s
            无相关内容时回复“无法从指定版本文档中找到答案”。
            """.formatted(context.isEmpty() ? "无相关内容" : context);

    return documentQA.answer(systemPrompt, userQuestion);
}
2. 版本管理控制器
/**
 * 加载带版本的文档
 * 示例:http://localhost:8080/rag/load-version?pdfPath=/docs/产品手册.pdf&version=v2.0
 */
@GetMapping("/rag/load-version")
public String loadDocumentVersion(@RequestParam String pdfPath, @RequestParam String version) {
    return ragService.loadDocumentWithVersion(pdfPath, version);
}

/**
 * 删除指定版本文档
 * 示例:http://localhost:8080/rag/delete-version?fileName=产品手册.pdf&version=v1.0
 */
@GetMapping("/rag/delete-version")
public String deleteDocumentVersion(@RequestParam String fileName, @RequestParam(required = false) String version) {
    return ragService.deleteDocumentByVersion(fileName, version);
}

/**
 * 查询文档版本列表
 * 示例:http://localhost:8080/rag/list-versions?fileName=产品手册.pdf
 */
@GetMapping("/rag/list-versions")
public List<String> listVersions(@RequestParam String fileName) {
    return ragService.listDocumentVersions(fileName);
}

/**
 * 基于版本的智能问答
 * 示例:http://localhost:8080/rag/answer-version?question=产品A价格&fileName=产品手册.pdf&version=v2.0&topK=3
 */
@GetMapping("/rag/answer-version")
public String answerByVersion(@RequestParam String question,
                              @RequestParam(required = false) String fileName,
                              @RequestParam(required = false) String version,
                              @RequestParam(defaultValue = "3") int topK) {
    return ragService.ragAnswerByVersion(question, fileName, version, topK);
}

二、多格式文档支持(Word/Markdown/TXT)

核心:扩展文档解析器,适配不同格式,统一加载逻辑。

1. 新增依赖(pom.xml)
<!-- Word解析 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>
<!-- Markdown解析 -->
<dependency>
    <groupId>com.vladsch.flexmark</groupId>
    <artifactId>flexmark-all</artifactId>
    <version>0.64.8</version>
</dependency>
2. 通用文档加载方法(支持多格式)
/**
 * 通用文档加载(自动识别格式:PDF/Word/Markdown/TXT)
 * @param filePath 文件路径
 * @param version 版本号
 * @return 加载结果
 */
public String loadAnyDocument(String filePath, String version) {
    DocumentLoadProgress progress = DocumentLoadProgress.getInstance();
    progress.reset();
    
    File file = new File(filePath);
    String fileName = file.getName();
    progress.setCurrentFileName(fileName + " (版本:" + version + ")");

    try {
        Document document;
        // 根据后缀选择解析器
        if (fileName.endsWith(".pdf")) {
            document = FileSystemDocumentLoader.load(file.toPath(), new ApachePdfBoxDocumentParser());
        } else if (fileName.endsWith(".docx")) {
            document = FileSystemDocumentLoader.load(file.toPath(), new dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser());
        } else if (fileName.endsWith(".md")) {
            document = FileSystemDocumentLoader.load(file.toPath(), new dev.langchain4j.data.document.parser.markdown.MarkdownDocumentParser());
        } else if (fileName.endsWith(".txt")) {
            document = FileSystemDocumentLoader.load(file.toPath(), new dev.langchain4j.data.document.parser.plain.PlainTextDocumentParser());
        } else {
            return "不支持的文件格式:" + fileName;
        }

        // 添加元数据
        document.metadata().add("file_name", fileName);
        document.metadata().add("version", version);
        document.metadata().add("file_type", getFileType(fileName));

        // 预分片+加载(逻辑同前)
        List<TextSegment> segments = dev.langchain4j.data.document.splitter.DocumentSplitters
                .recursive(500, 50)
                .split(document);
        progress.setTotalSegments(segments.size());

        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .documentSplitter(dev.langchain4j.data.document.splitter.DocumentSplitters.recursive(500, 50))
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .onSegmentIngested(segment -> progress.completeSegment())
                .build();
        ingestor.ingest(document);

        return "多格式文档加载完成:" + fileName + "(格式:" + getFileType(fileName) + ")";
    } catch (Exception e) {
        return "加载失败:" + e.getMessage();
    }
}

// 辅助方法:获取文件格式
private String getFileType(String fileName) {
    if (fileName.endsWith(".pdf")) return "PDF";
    if (fileName.endsWith(".docx")) return "Word";
    if (fileName.endsWith(".md")) return "Markdown";
    if (fileName.endsWith(".txt")) return "TXT";
    return "未知";
}
3. 多格式加载控制器
/**
 * 加载多格式文档(带版本)
 * 示例:http://localhost:8080/rag/load-any?filePath=/docs/产品说明.md&version=v1.0
 */
@GetMapping("/rag/load-any")
public String loadAnyDocument(@RequestParam String filePath, @RequestParam String version) {
    return ragService.loadAnyDocument(filePath, version);
}

三、检索结果排序优化(精准度+相关性)

核心:优化检索策略,结合相似度得分排序+元数据过滤+关键词权重,提升检索精准度。

1. 排序优化核心代码
/**
 * 优化后的检索方法(按相似度+关键词权重排序)
 * @param userQuestion 用户问题
 * @param fileName 文件名(可选)
 * @param version 版本号(可选)
 * @param topK 检索数量
 * @return 排序后的相关内容
 */
public List<String> retrieveOptimized(String userQuestion, String fileName, String version, int topK) {
    // 1. 构建过滤条件
    MetadataFilter.Builder filterBuilder = MetadataFilter.builder();
    if (fileName != null && !fileName.isEmpty()) {
        filterBuilder.key("file_name").value(fileName);
    }
    if (version != null && !version.isEmpty()) {
        filterBuilder.and(MetadataFilter.builder().key("version").value(version).build());
    }
    MetadataFilter filter = filterBuilder.build();

    // 2. 基础检索(带相似度得分)
    Embedding questionEmbedding = embeddingModel.embed(userQuestion).content();
    List<EmbeddingMatch<TextSegment>> rawMatches = ((RedisEmbeddingStore) embeddingStore)
            .findRelevant(questionEmbedding, topK * 2, filter); // 先取2倍数量,再二次排序

    // 3. 二次排序:相似度得分 + 关键词匹配权重
    List<EmbeddingMatch<TextSegment>> sortedMatches = rawMatches.stream()
            .sorted((m1, m2) -> {
                // 3.1 基础相似度得分(Redis返回的score,值越大越相关)
                double score1 = m1.score();
                double score2 = m2.score();
                
                // 3.2 关键词匹配权重:问题关键词出现在文档开头/结尾,权重+0.1
                String text1 = m1.embeddedObject().text().toLowerCase();
                String text2 = m2.embeddedObject().text().toLowerCase();
                String questionLower = userQuestion.toLowerCase();
                
                double weight1 = getKeywordWeight(text1, questionLower);
                double weight2 = getKeywordWeight(text2, questionLower);
                
                // 综合得分 = 相似度 + 关键词权重
                double total1 = score1 + weight1;
                double total2 = score2 + weight2;
                
                return Double.compare(total2, total1); // 降序排列
            })
            .limit(topK) // 最终取topK条
            .toList();

    // 4. 提取结果
    return sortedMatches.stream()
            .map(match -> {
                // 附加相似度得分,方便调试
                return String.format("【相似度:%.2f】%s", match.score(), match.embeddedObject().text());
            })
            .toList();
}

// 辅助方法:计算关键词匹配权重
private double getKeywordWeight(String docText, String question) {
    double weight = 0.0;
    // 拆分问题关键词(简单分词,可替换为IK分词等)
    String[] keywords = question.split("[,。!?\\s]+");
    for (String keyword : keywords) {
        if (keyword.length() < 2) continue; // 过滤短关键词
        if (docText.contains(keyword)) {
            weight += 0.05; // 包含关键词,权重+0.05
            // 关键词在开头/结尾,额外+0.05
            if (docText.startsWith(keyword) || docText.endsWith(keyword)) {
                weight += 0.05;
            }
        }
    }
    return weight;
}

/**
 * 优化后的RAG问答(基于排序后的检索结果)
 */
public String ragAnswerOptimized(String userQuestion, String fileName, String version, int topK) {
    // 1. 优化检索
    List<String> relevantContent = retrieveOptimized(userQuestion, fileName, version, topK);
    String context = String.join("\n\n", relevantContent);

    // 2. 构造提示词并回答
    String systemPrompt = """
            你是智能问答助手,基于以下优化排序的文档内容回答问题:
            %s
            回答需优先引用相似度高的内容,无相关内容时回复“未找到相关答案”。
            """.formatted(context.isEmpty() ? "无相关内容" : context);

    return documentQA.answer(systemPrompt, userQuestion);
}
2. 排序优化控制器
/**
 * 优化检索接口
 * 示例:http://localhost:8080/rag/retrieve-optimized?question=产品A价格&fileName=产品手册.pdf&version=v2.0&topK=3
 */
@GetMapping("/rag/retrieve-optimized")
public List<String> retrieveOptimized(@RequestParam String question,
                                      @RequestParam(required = false) String fileName,
                                      @RequestParam(required = false) String version,
                                      @RequestParam(defaultValue = "3") int topK) {
    return ragService.retrieveOptimized(question, fileName, version, topK);
}

/**
 * 优化后的RAG问答
 * 示例:http://localhost:8080/rag/answer-optimized?question=产品A价格&fileName=产品手册.pdf&version=v2.0&topK=3
 */
@GetMapping("/rag/answer-optimized")
public String answerOptimized(@RequestParam String question,
                              @RequestParam(required = false) String fileName,
                              @RequestParam(required = false) String version,
                              @RequestParam(defaultValue = "3") int topK) {
    return ragService.ragAnswerOptimized(question, fileName, version, topK);
}

关键优化说明

  1. 文档版本管理
    • 通过元数据存储版本号,支持按版本检索/删除,解决“文档更新后旧版本残留”问题;
    • 可扩展:添加版本备注、版本对比、历史版本恢复功能。
  2. 多格式支持
    • 自动识别文件后缀,适配PDF/Word/Markdown/TXT,统一加载逻辑;
    • 扩展建议:支持Excel(Apache POI)、HTML(Jsoup)等格式。
  3. 检索排序优化
    • 先取2倍数量的候选结果,再结合“相似度得分+关键词权重”二次排序,提升精准度;
    • 关键词权重:优先匹配文档开头/结尾的关键词,符合用户阅读习惯。

总结

  1. 版本管理:核心是元数据扩展,低成本实现文档版本的全生命周期管理;
  2. 多格式支持:通过替换解析器实现,LangChain4j已封装主流格式解析器,无需重复造轮子;
  3. 排序优化:二次排序策略兼顾“向量相似度”和“关键词匹配”,大幅提升检索结果的相关性。

实现文档内容高亮匹配关键词检索结果分页自定义排序规则(如按时间/文档类型)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐