news 2026/8/11 3:37:16

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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();

maxResultsminScore这两个参数需要配合调优。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()); }

通常原因有三个:

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

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

  3. 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)
HuggingFaceTokenizerOpenAiTokenizer
ChatMemoryProviderChatMemoryProvider

(接口方法签名变了)

避坑方案:在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跑通自己的数据,再用"故障复现"部分的方法检查效果,最后根据"生产部署方案"做优化。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 3:34:58

Java转大模型:别急着学Prompt,你的工程经验才是真正壁垒

如果你正准备往大模型方向转&#xff0c;《别急着换赛道&#xff1a;Java经验在 AI 项目里到底值多少&#xff1f;》这类问题别只看热度。更重要的是判断自己该补哪块能力&#xff0c;以及怎么证明你真的会。 摘要 摘要&#xff1a;Java后端转大模型应用开发&#xff0c;最大…

作者头像 李华
网站建设 2026/8/11 3:34:18

大模型接入调查岗位匹配度

计算岗位匹配度 job_router.get(“/resume_submission_detail/{id}”, summary“简历投递详情”) async def resume_submission_detail(id:int): resawait JobService.resume_submission_detail(id) return { “code”: 1, “message”: “查询成功”, “data”: json.loads(re…

作者头像 李华
网站建设 2026/8/11 3:34:00

魔兽争霸3终极优化指南:3步免费解锁完整功能体验

魔兽争霸3终极优化指南&#xff1a;3步免费解锁完整功能体验 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper WarcraftHelper是一款专为魔兽争霸3玩家设…

作者头像 李华
网站建设 2026/8/11 3:33:13

图像融合技术全解析:从传统算法到深度学习实战指南

1. 项目概述&#xff1a;为什么你需要一个“图像融合”大合集如果你正在计算机视觉、遥感、医疗影像或者自动驾驶领域做研究或开发&#xff0c;那么“图像融合”这个词对你来说一定不陌生。简单来说&#xff0c;图像融合就是把来自不同传感器、不同模态、或者同一场景不同焦点的…

作者头像 李华
网站建设 2026/8/11 3:33:11

AI Agent中间件:从工具管理到系统架构的核心设计

1. 从“单兵作战”到“协同作战”&#xff1a;为什么我们需要Agent中间件&#xff1f; 最近在折腾AI Agent项目时&#xff0c;我遇到了一个挺典型的问题。我手头有一个基于Ollama本地模型搭建的推理服务&#xff0c;跑得挺稳&#xff0c;也能处理一些基础的问答和文本生成。但当…

作者头像 李华
网站建设 2026/8/11 3:32:21

Matlab电力储能调频模型开发与优化实践

1. 项目背景与核心价值 电力系统储能调峰调频模型是当前新能源并网背景下的关键技术突破点。去年参与某省级电网储能项目时&#xff0c;我亲眼目睹了传统火电机组在应对风电功率波动时的窘境——每分钟都需要调整出力3-4次&#xff0c;机组寿命急剧衰减。这正是我们开发这套Mat…

作者头像 李华