当前位置: 首页 > news >正文

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

http://www.cnnetsun.cn/news/3958412.html

相关文章:

  • Java转大模型:别急着学Prompt,你的工程经验才是真正壁垒
  • 大模型接入调查岗位匹配度
  • 魔兽争霸3终极优化指南:3步免费解锁完整功能体验
  • 图像融合技术全解析:从传统算法到深度学习实战指南
  • AI Agent中间件:从工具管理到系统架构的核心设计
  • Matlab电力储能调频模型开发与优化实践
  • Hadoop+Spark构建股票大数据分析系统实战
  • JavaScript 字符串工具库设计思路
  • OpenRGB:一站式RGB灯光控制平台,终结多软件混乱时代
  • 数字记忆的守护者:让聊天记录成为永恒的生命印记
  • 从Claude Fable 5系统提示词看AI产品工程化:安全、可控与人格塑造
  • 如何快速为Mac双系统安装Boot Camp驱动:Brigadier终极指南
  • SQL注入文件读写实战:从数据库查询到系统入侵的攻防解析
  • 意图共鸣科技《AI协作记忆系统 · 认知架构白皮书》: AI记住更多,是错的
  • State、Session 与 Checkpoint:Agent 如何保存任务现场?
  • 企业存储服务器NAS的选型逻辑与补充路径
  • Python数据分析实战:Pandas数据清洗、处理与聚合核心技巧
  • AI Agent工具链设计:五大核心原则提升LLM工具调用能力
  • macOS Protocol Launcher开发:URL Scheme深度集成指南
  • RAG 八股不必硬背:跟着逆境救活一个“满嘴跑火车”的知识助手
  • 如何实现淘宝多店防关联管理自动化?独占IP+Profile固化,从创建到销毁零关联
  • 炎症“七重奏”全景奏响——IL1b/IL2/IL4/IL5/IL6/IP10/MIP1a七因子Panel解锁慢性炎症与自身免疫研究新维度
  • 半自动图像采集工具:构建定制化计算机视觉训练集实践指南
  • 内层图形转移+层压成型:多层PCB叠层稳定的关键工艺要点
  • ERA5逐小时数据聚合为日数据的三种方法:CDO、NCL与Python实战指南
  • 数字时代创意归属困境:从“窃取idea”到构建可追溯协作流程
  • 照抄对手的GEO打法,是你“自废武功”的开始,如何守住差异化底线?
  • Docker部署Redis全攻略:从单机到生产环境配置
  • AI赋能+全链服务,传播易升级广州候车亭广告投放模式
  • 深入解析TCP三次握手与四次挥手:从原理到实战排查