一个技术能不能用先看依赖和代码量。下面是LangChain4j的Maven坐标和50行核心代码直接跑通一个RAG检索增强生成知识库!-- pom.xml 核心依赖 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-all-minilm-l6-v2/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.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, EmbeddingStoreTextSegment embeddingStore, ChatLanguageModel chatModel) { return args - { // 1. 加载文档 Document document FileSystemDocumentLoader.loadDocument( Path.of(docs/公司制度.md)); // 2. 文本分片每段500字重叠100字 DocumentSplitter splitter DocumentSplitters.recursive(500, 100); ListTextSegment segments splitter.split(document); // 3. 向量化 存入向量库 ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments); // 4. 构建RAG检索增强器 EmbeddingStoreContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) // 检索TopK3 .minScore(0.6) // 最低相似度阈值 .build(); RetrievalAugmentor augmentor DefaultRetrievalAugmentor.builder() .contentRetriever(retriever) .build(); // 5. 组装对话链 AiServicesRagAssistant 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 完整链路拆解文档加载 → 文本分片 → 向量化 → 存储 → 检索 → 生成RAGRetrieval-Augmented Generation这个名字看着唬人说白了就是先搜索再生成。你把技术文档扔给系统用户提问时系统先从文档里搜出相关段落然后把段落和问题一起扔给大模型让大模型看着资料回答。这样做的核心价值大模型不会瞎编。它回答的内容有据可查来自你喂给它的文档。1. 文档加载Document LoaderLangChain4j提供了FileSystemDocumentLoader支持PDF、Markdown、TXT、HTML等格式// 加载单个文件 Document doc FileSystemDocumentLoader.loadDocument(Path.of(docs/产品手册.pdf)); // 加载整个目录 ListDocument 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是分片策略里最容易被忽略的关键参数。假设你设置chunkSize500overlap100意味着相邻两个分片之间有100个字的重叠。这能防止年假申请需要满足以下条件1. 入职满一年 2. 提前三天申请被切成两段导致检索时只能命中半个规则。生产环境调优建议先拿你的文档做实验。用几个典型问题检索看返回的分片是否包含了完整答案。如果答案被切断调大chunkSize或overlap如果返回的噪声太多调小chunkSize。3. 向量化EmbeddingEmbedding是把文字变成一串数字向量让计算机能理解文字的语义。语义相近的文本向量距离就近。// 使用本地Embedding模型无需联网免费 EmbeddingModel embeddingModel new AllMiniLmL6V2EmbeddingModel(); // 文本转向量 Embedding embedding embeddingModel.embed(年假怎么申请).content(); // 返回一个384维的浮点数数组[-0.023, 0.451, ...]LangChain4j支持的Embedding模型模型维度速度精度是否需要联网AllMiniLmL6V2384极快中否BgeSmallZh512快高中文否OpenAI text-embedding-ada-0021536快高是通义千问 text-embedding-v21536快高中文是注意Embedding模型的维度决定了向量库的存储结构。如果你先用384维的模型建了索引后换成1536维的模型必须重建索引否则查询会报维度不匹配的错误。这是生产环境迁移时最常见的坑。4. 向量存储Embedding Store向量库存储的是文本→向量的映射关系。查询时把用户问题转成向量在库里找最相似的几个向量返回对应的文本。// 内存存储开发测试用 EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); // Milvus存储生产环境 EmbeddingStoreTextSegment store MilvusEmbeddingStore.builder() .host(192.168.1.100) .port(19530) .collectionName(company_docs) .dimension(384) // 必须与Embedding模型维度一致 .build(); // Elasticsearch存储已有ES集群的场景 EmbeddingStoreTextSegment 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这两个参数需要配合调优。maxResults3意味着每次检索返回最多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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.0/version /parent groupIdcom.example/groupId artifactIdrag-demo/artifactId version1.0.0/version properties java.version17/java.version langchain4j.version0.36.2/langchain4j.version /properties dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- LangChain4j 核心 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version${langchain4j.version}/version /dependency !-- 本地Embedding模型无需联网 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-all-minilm-l6-v2/artifactId version${langchain4j.version}/version /dependency !-- OpenAI兼容接口通义千问/DeepSeek都走这个 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version${langchain4j.version}/version /dependency !-- 文档解析 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-document-parser-apache-tika/artifactId version${langchain4j.version}/version /dependency /dependencies /projectapplication.ymlspring: 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 EmbeddingStoreTextSegment embeddingStore() { return new InMemoryEmbeddingStore(); } // 文档分片策略 Bean public DocumentSplitter documentSplitter() { return DocumentSplitters.recursive(500, 100); } }ControllerRestController RequestMapping(/api/rag) public class RagController { private final RagAssistant assistant; public RagController(RagAssistant assistant) { this.assistant assistant; } PostMapping(/chat) public ResponseEntityMapString, 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 EmbeddingStoreTextSegment embeddingStore; private final DocumentSplitter splitter; public DocumentInitializer(EmbeddingModel embeddingModel, EmbeddingStoreTextSegment 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 { ListDocument documents FileSystemDocumentLoader.loadDocuments(docsPath); for (Document doc : documents) { ListTextSegment segments splitter.split(doc); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments); } log.info(文档初始化完成共加载 {} 个文档, documents.size()); } catch (Exception e) { log.error(文档初始化失败, e); } } }线上高频故障复现故障1检索结果完全不相关现象用户问年假怎么申请返回的是加班餐补标准。根因排查// 打印检索结果看相似度分数 ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant( embeddingModel.embed(年假怎么申请).content(), 5, 0.0); for (EmbeddingMatchTextSegment 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 MapString, 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-50050-100知识点密集太大容易混入无关内容制度/法规200-40050-80条款之间相互独立小分片更精准对话记录/工单500-800100-150语义连贯需要完整上下文长篇小说/报告800-1000150-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隐性坑点坑1LangChain4j版本兼容性LangChain4j更新非常快0.35和0.36的API差异能让你编译都过不了0.35.x API0.36.x APIDocumentSplitter.recursive(500, 100)DocumentSplitters.recursive(500, 100)HuggingFaceTokenizerOpenAiTokenizerChatMemoryProviderChatMemoryProvider(接口方法签名变了)避坑方案在pom.xml里用properties统一管理版本号不要混用不同版本的依赖。坑2Embedding模型选择对精度的影响别以为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。坑3InMemoryEmbeddingStore内存泄漏// 错误每次查询都往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 MapString, 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, ListEmbeddingMatchTextSegment 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。JavaAI落地实战生产级的能力完整视频地址https://edu.csdn.net/course/detail/41307但真正上生产时分片策略、相似度阈值、Embedding模型选型这些细节才是决定效果的关键。建议先用本文的Demo跑通自己的数据再用故障复现部分的方法检查效果最后根据生产部署方案做优化。