欢迎光临
我们一直在努力

Java LangChain4j 实战搭建私有 RAG 知识库

一个技术能不能用,先看依赖和代码量。下面是LangChain4j的Maven坐标和50行核心代码,直接跑通一个RAG(检索增强生成)知识库:

<!– pom.xml 核心依赖 –>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai</artifactId>
    <version>0.36.2</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
    <version>0.36.2</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>0.36.2</version>
</dependency>

// 50行核心代码,跑通RAG
@SpringBootApplication
public class RagApplication {
    public static void main(String[] args) {
        SpringApplication.run(RagApplication.class, args);
    }

    @Bean
    public CommandLineRunner demo(EmbeddingModel embeddingModel,
                                   EmbeddingStore<TextSegment> embeddingStore,
                                   ChatLanguageModel chatModel) {
        return args -> {
            // 1. 加载文档
            Document document = FileSystemDocumentLoader.loadDocument(
                Path.of("docs/公司制度.md"));

            // 2. 文本分片,每段500字,重叠100字
            DocumentSplitter splitter = DocumentSplitters.recursive(500, 100);
            List<TextSegment> segments = splitter.split(document);

            // 3. 向量化 + 存入向量库
            List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
            embeddingStore.addAll(embeddings, segments);

            // 4. 构建RAG检索增强器
            EmbeddingStoreContentRetriever retriever =
                EmbeddingStoreContentRetriever.builder()
                    .embeddingStore(embeddingStore)
                    .embeddingModel(embeddingModel)
                    .maxResults(3)        // 检索TopK=3
                    .minScore(0.6)        // 最低相似度阈值
                    .build();

            RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
                .contentRetriever(retriever)
                .build();

            // 5. 组装对话链
            AiServices<RagAssistant> aiService = AiServices.builder(RagAssistant.class)
                .chatLanguageModel(chatModel)
                .retrievalAugmentor(augmentor)
                .build();

            // 6. 问答
            String answer = aiService.chat("年假怎么申请?");
            System.out.println(answer);
        };
    }
}

// 接口定义
interface RagAssistant {
    String chat(@UserMessage String question);
}

你不需要装Python环境,不需要折腾向量数据库(内存跑),不需要写复杂的Pipeline。LangChain4j把整个RAG链路封装成了Builder模式,跟Spring Boot的自动配置一样丝滑。

现在咱们拆开聊,每一行代码背后的原理是什么,出了故障怎么排查。


RAG 完整链路拆解:文档加载 → 文本分片 → 向量化 → 存储 → 检索 → 生成

RAG(Retrieval-Augmented Generation)这个名字看着唬人,说白了就是:先搜索,再生成。你把技术文档扔给系统,用户提问时,系统先从文档里搜出相关段落,然后把段落和问题一起扔给大模型,让大模型"看着资料回答"。

这样做的核心价值:大模型不会瞎编。它回答的内容有据可查,来自你喂给它的文档。

1. 文档加载(Document Loader)

LangChain4j提供了FileSystemDocumentLoader,支持PDF、Markdown、TXT、HTML等格式:

// 加载单个文件
Document doc = FileSystemDocumentLoader.loadDocument(Path.of("docs/产品手册.pdf"));

// 加载整个目录
List<Document> docs = FileSystemDocumentLoader.loadDocuments(Path.of("docs/"));

底层用了Apache Tika做格式解析,PDF里的表格、图片中的文字都能提取出来。如果你有特殊格式,可以自己实现DocumentParser接口:

public class CustomDocumentParser implements DocumentParser {
    @Override
    public Document parse(InputStream inputStream) {
        // 自定义解析逻辑,比如解析Word文档
        String text = new String(inputStream.readAllBytes());
        return Document.from(text);
    }
}

2. 文本分片(Text Splitting)

这是RAG最容易出问题的一环。分片太大,检索精度下降,大模型拿到的上下文噪声多;分片太小,关键信息被切碎,语义不完整。

LangChain4j提供了四种分片策略:

// 1. 递归分片(推荐):按段落→句子→词逐级切分,保证语义完整
DocumentSplitter recursive = DocumentSplitters.recursive(500, 100);

// 2. 按句子分片:适合问答类文档
DocumentSplitter sentence = DocumentSplitters.recursive(300, 50);

// 3. 按段落分片:适合制度文档、技术手册
DocumentSplitter paragraph = DocumentSplitters.recursive(1000, 200);

// 4. 固定长度分片:不推荐,容易切断句子
DocumentSplitter fixed = DocumentSplitters.recursive(500, 0);

重叠窗口(Overlap)是分片策略里最容易被忽略的关键参数。假设你设置chunkSize=500,overlap=100,意味着相邻两个分片之间有100个字的重叠。这能防止"年假申请需要满足以下条件:1. 入职满一年 2. 提前三天申请"被切成两段,导致检索时只能命中半个规则。

生产环境调优建议:先拿你的文档做实验。用几个典型问题检索,看返回的分片是否包含了完整答案。如果答案被切断,调大chunkSize或overlap;如果返回的噪声太多,调小chunkSize。

3. 向量化(Embedding)

Embedding是把文字变成一串数字(向量),让计算机能"理解"文字的语义。语义相近的文本,向量距离就近。

// 使用本地Embedding模型(无需联网,免费)
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();

// 文本转向量
Embedding embedding = embeddingModel.embed("年假怎么申请?").content();
// 返回一个384维的浮点数数组:[-0.023, 0.451, …]

LangChain4j支持的Embedding模型:

模型

维度

速度

精度

是否需要联网

AllMiniLmL6V2

384

极快

BgeSmallZh

512

高(中文)

OpenAI text-embedding-ada-002

1536

通义千问 text-embedding-v2

1536

高(中文)

注意:Embedding模型的维度决定了向量库的存储结构。如果你先用384维的模型建了索引,后换成1536维的模型,必须重建索引,否则查询会报维度不匹配的错误。这是生产环境迁移时最常见的坑。

4. 向量存储(Embedding Store)

向量库存储的是"文本→向量"的映射关系。查询时,把用户问题转成向量,在库里找最相似的几个向量,返回对应的文本。

// 内存存储(开发测试用)
EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();

// Milvus存储(生产环境)
EmbeddingStore<TextSegment> store = MilvusEmbeddingStore.builder()
    .host("192.168.1.100")
    .port(19530)
    .collectionName("company_docs")
    .dimension(384)  // 必须与Embedding模型维度一致!
    .build();

// Elasticsearch存储(已有ES集群的场景)
EmbeddingStore<TextSegment> store = ElasticsearchEmbeddingStore.builder()
    .serverUrl("http://es-cluster:9200")
    .indexName("rag_docs")
    .dimension(384)
    .build();

三种存储的选型建议:

  • InMemoryEmbeddingStore:开发测试,重启就没了

  • Milvus:专业向量数据库,支持10亿级向量检索,适合大规模文档

  • Elasticsearch:团队已有ES集群,不想引入新组件,ES 8.x支持向量检索

5. 检索 + 生成

ContentRetriever负责从向量库中检索相关内容,RetrievalAugmentor把检索结果注入到Prompt中:

// 检索器配置
EmbeddingStoreContentRetriever retriever =
    EmbeddingStoreContentRetriever.builder()
        .embeddingStore(embeddingStore)
        .embeddingModel(embeddingModel)
        .maxResults(3)     // 返回Top 3个最相似的分片
        .minScore(0.65)    // 相似度低于0.65的不要
        .build();

maxResults和minScore这两个参数需要配合调优。maxResults=3意味着每次检索返回最多3个分片,如果你每个分片500字,那上下文大约1500字,加上Prompt和用户问题,很难超过大部分模型的上下文窗口(4K~8K)。minScore是相似度阈值,设太低会引入噪声,设太高可能什么都搜不到。我一般从0.6开始,根据实际效果调整。

完整链路总结

用户提问:"年假怎么申请?"
    ↓
问题向量化 → [0.12, -0.34, 0.56, …]
    ↓
Milvus/ES向量检索 → 找到Top 3相关分片
    ↓
分片1: "年假申请条件:入职满一年…"
分片2: "年假天数:1-10年5天,10-20年10天…"
分片3: "申请流程:OA系统→人事审批→…"
    ↓
拼接Prompt: "根据以下资料回答问题:{分片1}{分片2}{分片3}。问题:年假怎么申请?"
    ↓
大模型生成回答:"年假申请需满足入职满一年,天数根据工龄…"


完整 SpringBoot 项目 Demo

下面是一个可以直接跑起来的完整项目,包含 pom.xml 和所有代码:

pom.xml

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>rag-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <langchain4j.version>0.36.2</langchain4j.version>
    </properties>

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

        <!– LangChain4j 核心 –>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>

        <!– 本地Embedding模型(无需联网) –>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>

        <!– OpenAI兼容接口(通义千问/DeepSeek都走这个) –>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>

        <!– 文档解析 –>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-tika</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
    </dependencies>
</project>

application.yml

spring:
  application:
    name: rag-demo

# 大模型配置(这里用通义千问的OpenAI兼容接口)
langchain4j:
  open-ai:
    chat-model:
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      api-key: ${DASHSCOPE_API_KEY:your-api-key}
      model-name: qwen-plus
      temperature: 0.1          # 知识库问答建议低温度,减少幻觉
      max-tokens: 2000
      timeout: 30s

配置类

@Configuration
public class RagConfig {

    // 本地Embedding模型,无需联网
    @Bean
    public EmbeddingModel embeddingModel() {
        return new AllMiniLmL6V2EmbeddingModel();
    }

    // 内存向量存储(生产环境替换为Milvus或ES)
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore() {
        return new InMemoryEmbeddingStore<>();
    }

    // 文档分片策略
    @Bean
    public DocumentSplitter documentSplitter() {
        return DocumentSplitters.recursive(500, 100);
    }
}

Controller

@RestController
@RequestMapping("/api/rag")
public class RagController {

    private final RagAssistant assistant;

    public RagController(RagAssistant assistant) {
        this.assistant = assistant;
    }

    @PostMapping("/chat")
    public ResponseEntity<Map<String, String>> chat(@RequestBody ChatRequest request) {
        String answer = assistant.chat(request.question());
        return ResponseEntity.ok(Map.of("answer", answer));
    }

    public record ChatRequest(String question) {}
}

文档初始化

@Component
public class DocumentInitializer {

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

    public DocumentInitializer(EmbeddingModel embeddingModel,
                                EmbeddingStore<TextSegment> embeddingStore,
                                DocumentSplitter splitter) {
        this.embeddingModel = embeddingModel;
        this.embeddingStore = embeddingStore;
        this.splitter = splitter;
    }

    @PostConstruct
    public void init() {
        // 加载文档目录
        Path docsPath = Path.of("docs");
        if (!Files.exists(docsPath)) {
            return;
        }

        try {
            List<Document> documents = FileSystemDocumentLoader.loadDocuments(docsPath);
            for (Document doc : documents) {
                List<TextSegment> segments = splitter.split(doc);
                List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
                embeddingStore.addAll(embeddings, segments);
            }
            log.info("文档初始化完成,共加载 {} 个文档", documents.size());
        } catch (Exception e) {
            log.error("文档初始化失败", e);
        }
    }
}


线上高频故障复现

故障1:检索结果完全不相关

现象:用户问"年假怎么申请",返回的是"加班餐补标准"。

根因排查:

// 打印检索结果,看相似度分数
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(
    embeddingModel.embed("年假怎么申请?").content(), 5, 0.0);

for (EmbeddingMatch<TextSegment> match : matches) {
    System.out.printf("相似度: %.3f, 内容: %s\\n",
        match.score(), match.embedded().text());
}

通常原因有三个:

  • Embedding模型不合适:英文模型处理中文文本,语义理解偏差。换成中文模型(BgeSmallZh)立马解决。

  • 分片太大:1000字一个分片,相关信息和大量无关信息混在一起,向量被稀释了。缩小到300-500字。

  • minScore设太高:设了0.85,但你的文档和问题本身语义距离就远,一个都搜不到。

  • 解决方案:先不设minScore,打印Top 10的相似度分数,看实际分布,再定阈值。通常0.5-0.7是一个合理区间。

    故障2:上下文超Token限制

    现象:大模型返回截断的回答,或者直接报错context_length_exceeded。

    根因:maxResults设了10,每个分片1000字,加上系统Prompt和用户问题,总Token超过模型上下文窗口。

    解决方案:控制上下文总量,别超过模型上下文的70%。

    // 方案1:限制检索数量
    .maxResults(3)

    // 方案2:限制每个分片大小
    DocumentSplitters.recursive(300, 50)  // 缩小分片

    // 方案3:使用TokenWindow来截断(高级用法)
    ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
        .embeddingStore(embeddingStore)
        .embeddingModel(embeddingModel)
        .maxResults(5)
        .minScore(0.6)
        .build();

    // 在拼接Prompt时限制总Token数
    String context = matches.stream()
        .map(m -> m.embedded().text())
        .collect(Collectors.joining("\\n\\n"));

    // 如果上下文超过3000字,截断
    if (context.length() > 3000) {
        context = context.substring(0, 3000);
    }


    生产部署方案

    1. 向量缓存层

    Embedding计算是RAG链路中最耗时的环节。文档内容不变的情况下,没必要每次都重新计算向量。

    @Component
    public class EmbeddingCache {

        private final Map<String, Embedding> cache = new ConcurrentHashMap<>();

        public Embedding getOrCompute(String text, EmbeddingModel model) {
            String key = DigestUtils.md5Hex(text);  // 文本MD5做key
            return cache.computeIfAbsent(key, k -> model.embed(text).content());
        }

        public void invalidate(String text) {
            cache.remove(DigestUtils.md5Hex(text));
        }
    }

    2. 分片大小调优

    没有一个通用的分片大小。我在几个项目里的经验值:

    文档类型

    推荐chunkSize

    推荐overlap

    原因

    技术文档/手册

    300-500

    50-100

    知识点密集,太大容易混入无关内容

    制度/法规

    200-400

    50-80

    条款之间相互独立,小分片更精准

    对话记录/工单

    500-800

    100-150

    语义连贯,需要完整上下文

    长篇小说/报告

    800-1000

    150-200

    叙事需要连贯性

    3. 检索TopK调优

    // 动态调整TopK的策略
    public class AdaptiveTopK {

        public static int compute(int contextWindow, int avgChunkTokens) {
            // 预留50%空间给Prompt和回答
            int availableTokens = (int)(contextWindow * 0.5);
            return Math.max(1, availableTokens / avgChunkTokens);
        }
    }

    // 使用示例:qwen-plus上下文8K,每个分片约500 tokens,预留50%
    // availableTokens = 4000, avgChunkTokens = 500 → TopK = 8


    隐性坑点

    坑1:LangChain4j版本兼容性

    LangChain4j更新非常快,0.35和0.36的API差异能让你编译都过不了:

    0.35.x API

    0.36.x API

    DocumentSplitter.recursive(500, 100) DocumentSplitters.recursive(500, 100)
    HuggingFaceTokenizer OpenAiTokenizer
    ChatMemoryProvider ChatMemoryProvider

     (接口方法签名变了)

    避坑方案:在pom.xml里用<properties>统一管理版本号,不要混用不同版本的依赖。

    坑2:Embedding模型选择对精度的影响

    别以为Embedding模型都一样。同一段中文文本,不同模型生成的向量差距巨大:

    // 测试代码:计算两个Embedding模型对同一对文本的相似度差异
    public static void compareEmbeddingModels() {
        EmbeddingModel enModel = new AllMiniLmL6V2EmbeddingModel();  // 英文模型
        EmbeddingModel zhModel = new BgeSmallZhEmbeddingModel();     // 中文模型

        String q = "如何申请年假?";
        String doc = "年假申请需要填写OA表单,经部门经理审批后生效";

        double enScore = cosineSimilarity(enModel.embed(q), enModel.embed(doc));
        double zhScore = cosineSimilarity(zhModel.embed(q), zhModel.embed(doc));

        System.out.printf("英文模型相似度: %.2f, 中文模型相似度: %.2f\\n", enScore, zhScore);
        // 典型输出:英文模型相似度: 0.42, 中文模型相似度: 0.89
    }

    结论:处理中文文档,一定要用中文优化的Embedding模型(BgeSmallZh、text2vec-large-chinese、通义千问Embedding)。

    坑3:InMemoryEmbeddingStore内存泄漏

    // 错误:每次查询都往store里加数据,内存无限增长
    @PostMapping("/add")
    public void addDoc(@RequestBody String text) {
        TextSegment segment = TextSegment.from(text);
        Embedding embedding = embeddingModel.embed(text).content();
        embeddingStore.add(embedding, segment);  // 只增不删,迟早OOM
    }

    // 正确:加上去重逻辑和容量限制
    @PostMapping("/add")
    public void addDoc(@RequestBody String text) {
        String docId = DigestUtils.md5Hex(text);
        // 检查是否已存在
        if (embeddingStore.getAll().stream().anyMatch(e -> e.id().equals(docId))) {
            return;
        }
        TextSegment segment = TextSegment.from(text, Metadata.from("id", docId));
        Embedding embedding = embeddingModel.embed(text).content();
        embeddingStore.add(docId, embedding, segment);
    }

    坑4:文档不更新,知识库成"信息孤岛"

    RAG知识库不会自动更新。文档改了,向量库里的旧数据还在。需要建立文档版本管理机制:

    @Component
    public class DocumentSyncService {

        private final Map<String, String> docVersions = new ConcurrentHashMap<>();

        @Scheduled(fixedDelay = 300_000)  // 每5分钟检查一次
        public void syncDocuments() {
            Path docsPath = Path.of("docs");
            try (var files = Files.list(docsPath)) {
                files.forEach(file -> {
                    String md5 = DigestUtils.md5Hex(Files.readAllBytes(file));
                    String oldMd5 = docVersions.get(file.getFileName().toString());
                    if (!md5.equals(oldMd5)) {
                        // 文档有更新,删除旧向量,重新索引
                        embeddingStore.removeAll(s -> s.metadata().getString("file")
                            .equals(file.getFileName().toString()));
                        reindexDocument(file);
                        docVersions.put(file.getFileName().toString(), md5);
                    }
                });
            }
        }
    }


    运维监控方案

    1. 检索质量监控

    @Component
    public class RagMetrics {

        private final MeterRegistry meterRegistry;

        // 记录每次检索的平均相似度
        public void recordRetrievalScore(double score) {
            meterRegistry.summary("rag.retrieval.score").record(score);
        }

        // 记录检索耗时
        public void recordRetrievalLatency(long millis) {
            meterRegistry.timer("rag.retrieval.latency").record(millis, MILLISECONDS);
        }

        // 记录检索结果为空的情况
        public void recordEmptyRetrieval() {
            meterRegistry.counter("rag.retrieval.empty").increment();
        }
    }

    2. 关键告警指标

    • **检索结果为空率 > 20%**:minScore设太高或者Embedding模型不合适

    • 检索平均耗时 > 500ms:向量库索引需要重建,或者数据量太大需要扩容

    • **大模型返回被截断率 > 10%**:上下文超了,调小maxResults或分片大小

    • Embedding计算耗时 > 200ms:本地模型可能CPU不足,考虑换API模型

    3. 日志规范

    @Slf4j
    public class RagLogger {

        public static void logQuery(String question, List<EmbeddingMatch<TextSegment>> matches,
                                     String answer, long costMs) {
            log.info("RAG查询 | 问题: {} | 检索到{}条 | 最高相似度: {:.3f} | 耗时: {}ms",
                question,
                matches.size(),
                matches.isEmpty() ? 0 : matches.get(0).score(),
                costMs);

            if (matches.isEmpty()) {
                log.warn("RAG检索为空 | 问题: {} | 请检查minScore阈值和Embedding模型", question);
            }
        }
    }


    写在最后

    RAG不是什么高深技术,说白了就是"搜索+生成"。后端程序员搞这个有天然优势:数据库、缓存、API设计这些基本功全都能复用。LangChain4j把整个链路封装得足够好,50行代码就能跑通一个Demo。

    Java+AI落地实战生产级的能力,完整视频地址:https://edu.csdn.net/course/detail/41307

    但真正上生产时,分片策略、相似度阈值、Embedding模型选型这些细节才是决定效果的关键。建议先用本文的Demo跑通自己的数据,再用"故障复现"部分的方法检查效果,最后根据"生产部署方案"做优化。

    赞(0)
    未经允许不得转载:171主机测评 » Java LangChain4j 实战搭建私有 RAG 知识库
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址