新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java落地RAG知识库:LangChain4j+LangGraph4j实战指南

发布时间:2026/9/28 9:40:19来源:尧图网络
Java落地RAG知识库:LangChain4j+LangGraph4j实战指南
第一次用 Java 写 RAG 知识库系统的时候心里其实是有点抗拒的——检索增强生成RAG这套东西网上十个教程里九个是 Python剩下一个在教你装环境。真到自己用 Java 落地官方文档翻到吐跑通的 demo 换个模型就报错说多了都是泪。但把整条链路走通之后再回头看RAG 并没有想象中高不可攀。核心就两条管道一条离线的把文档读进来、切块、向量化、存进向量库一条在线的把用户问题向量化、从库里捞相关片段、拼进提示词、交给大模型生成回答。难点全在细节怎么切块不切断语义怎么检索才能命中怎么更新才不会读到旧数据以及当流程复杂到要“先判断该不该检索、检索完要不要自我验证”的时候怎么用状态图把流程编排起来。这篇文章不聊 Python 那套直接用 Java 生态的组合拳LangChain4j提供组件能力和 RAG 基础设施LangGraph4j负责 Agent 编排。全篇以实战为主代码尽量直接能跑坑也尽量提前替你们踩掉适合已经有一定 Java 基础、想在自己业务里落地知识库问答的工程师。1. 为什么用 Java 做 RAG先想清楚再动手1.1 RAG 的本质一条流水线不是玄学我习惯把 RAG 比作开饭店。离线管道是备菜原材料文档经过清洗、切配分块、腌制向量化放进冷库向量库备用在线管道是出菜客人点菜提问后厨快速从冷库翻出配好的料检索 Top-K热锅快炒拼接上下文进 Prompt最后装盘上桌生成回答。RAG 出现的直接原因是大模型的知识有截止日期也不会知道你内部的业务文档。微调一个大模型成本高、周期长、更新慢而 RAG 通过“检索 拼接上下文”的方式把外部知识临时塞给模型。好处很实在知识更新只需要改文档重建索引不需要重新训练模型回答可以追溯到原文来源成本也比微调低一个数量级。在 Java 生态里这条流水线的核心组件有这几类ChatLanguageModel负责最终生成回答可以接各家大模型 API 或本地推理服务。EmbeddingModel把文本变成向量衡量语义相似度。EmbeddingStore向量存储和检索按相关性返回 Top-K 结果。DocumentSplitter把长文档切成适合检索和喂给模型的文本块。RetrievalAugmentor把“检索”和“生成”串成一条自动管道你只管提问它自动增强上下文。1.2 LangChain4j 和 LangGraph4j 到底分工是什么很多人一上来就被这两个项目搞懵了其实分得很清楚。LangChain4j是主框架对标 Python 的 LangChain提供大模型封装、消息记忆、工具调用、文档处理、向量存储抽象、RAG 全套组件。它把“和 AI 对话”这件事变成了 Java 里普通的接口调用你定义一个接口方法AiServices 自动帮你把模型调用、检索增强、Prompt 模板都配好。LangGraph4j则是编排层对标 Python 的 LangGraph核心是一个状态图StateGraph引擎。你可以定义节点Node和边Edge节点之间可以串行、并行、带条件分支甚至循环。这东西在 Agentic RAG 场景下特别有用先判断问题类型再决定走向量检索还是外部搜索生成答案之后再校验一遍不满意就重新检索生成。打个比方LangChain4j 是给你一整套完整厨房设备炉灶、锅铲、冰箱LangGraph4j 是给你一张厨房动线图规定什么时候洗菜、什么时候炒菜、菜咸了怎么回锅加工。1.3 选型对比Spring AI 还是 LangChain4j LangGraph4j我知道现在很多 Java 团队都在纠结这个问题。Spring AI 的优势和劣势都很明显我直接做一个理性的选型对比维度LangChain4j LangGraph4jSpring AI纯手写Spring Boot 直接调 APIRAG 组件完整度很高分块、嵌入、检索、增强开箱即用中等提供了基础抽象但细节要自己补极低全手写Agent/工作流编排高StateGraph 支持条件边、循环低主要靠编程式流程全靠自己设计Spring 生态亲和度中可以很好地集成 Spring Boot高Spring 官方项目高学习曲线中需要理解分层和概念低贴合 Spring 风格高所有细节自己踩适合场景复杂知识库、Agentic RAG、多步推理简单对话、快速封装模型 API对包体积和可控性有极端要求我用过 Spring AI 做简单问答体验不错但只要流程里出现“要不要检索、检索几轮、检索完要不要追问”这种分叉Spring AI 给我的感觉就是在用 if-else 硬写状态机写多了非常累。LangGraph4j 这种把状态流转显式建模的方案反而更清晰。我的建议标准 RAG 用 LangChain4j 足够一旦要上 Agentic RAG、多路由、自我校验就上 LangGraph4j 做编排。2. 从零搭建项目依赖、模型和第一个 RAG 问答2.1 环境准备与依赖选择先说环境。JDK 我用的是 17因为 LangChain4j 和 LangGraph4j 都要求 17 以上而且 record 和 switch pattern 用起来很顺手21 当然更好但很多公司生产环境还停在 17没必要因为这个卡住。Maven 或 Gradle 都行下面以 Maven 为例。依赖引入方式值得专门说一下。LangChain4j 官方提供了 BOM强烈建议先用 BOM 管理版本再引入具体模块防止子模块版本不一致带来一堆诡异错误properties langchain4j.version1.1.0/langchain4j.version langgraph4j.version1.4.0/langgraph4j.version /properties dependencyManagement dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-bom/artifactId version${langchain4j.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后引入核心依赖dependencies !-- LangChain4j 核心 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId /dependency !-- OpenAI 兼容接口能接多种模型服务 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId /dependency !-- 本地嵌入模型BGE 中文小模型 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings-bge-small-zh/artifactId /dependency !-- LangGraph4j 核心和 LangChain4j 集成 -- dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-core/artifactId version${langgraph4j.version}/version /dependency dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-langchain4j/artifactId version${langgraph4j.version}/version /dependency /dependencies版本号建议以你下载时的官方最新稳定版为准。LangGraph4j 的版本迭代比较快1.0 前后 API 差异很大后面会专门说这个坑。这里还要强调一个选型原则嵌入模型不一定要用云端 API。我之前图省事直接用 OpenAI 的 Embedding API一跑测试集花了小几十美元后来换成 BGE small 中文本地嵌入模型速度和成本都舒服多了。本地嵌入对知识库这种批量向量化场景特别友好反正模型跑一次之后也是文件形式落盘。2.2 把第一个文档切块并向量化假设你手上已经有一段文本可能是从 Word、PDF 里抽出来的。第一步是把它封装成 LangChain4j 的 Document 对象String text 公司统一账号体系服务规范第一版发布于2024年适用于所有内部业务系统接入...; Document document Document.from(text);接下来是分块。我常用的分块器是DocumentSplitters.recursive(segmentLength, overlap)它会尝试在段落、句子边界处分隔同时保留前后重叠区域DocumentSplitter splitter DocumentSplitters.recursive(600, 120); ListTextSegment segments splitter.split(document); System.out.println(切块数量: segments.size());为什么是 600 和 120这是我实测下来对中文业务文档比较通用的起点值。块太长检索粒度太粗召回一段里面夹着大量无关内容块太短语义不完整容易把一条规则腰斩成两段。600 字符配 120 字符重叠约 20%保证相邻块之间有一段公共上下文边界处被切掉的句子至少有一半能在邻块里补回来。切完之后就可以向量化并存储EmbeddingModel embeddingModel new BgeSmallZhEmbeddingModel(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); // 批量嵌入这比一条条调 embed() 快很多 ListEmbedding embeddings embeddingModel.embedAll(segments).content(); store.addAll(embeddings, segments); System.out.println(已索引片段: store.countEntities());embedAll是我特别想提醒的一批一批地嵌入而不是 for 循环单个嵌入。我一开始图省事写循环一个 300 段的文档跑了快十分钟改成批量之后不到一半时间。InMemoryEmbeddingStore适合开发调试和 demo生产环境至少要换成 PGVector 或 Milvus 这类持久化向量库LangChain4j 都有对应模块后面会提。2.3 搭一个最简 RAG 管道索引建好了下面把检索和生成串起来。最直观的方式是手动检索再拼 Prompt但这样写几轮之后你就想骂人。LangChain4j 提供了一个非常优雅的抽象AiServices。你先定义一个普通 Java 接口然后让它自动实现内部帮你完成“检索增强 模型调用”的全流程。interface ChatWithDocs { String chat(String userMessage); } ChatLanguageModel chatModel OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(gpt-4o-mini) .build(); ContentRetriever contentRetriever ContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .minScore(0.6) .build(); RetrievalAugmentor augmentor RetrievalAugmentor.builder() .contentRetriever(contentRetriever) .build(); ChatWithDocs assistant AiServices.builder(ChatWithDocs.class) .chatLanguageModel(chatModel) .retrievalAugmentor(augmentor) .build(); String answer assistant.chat(账号服务规范是什么时候发布的); System.out.println(answer);这段代码背后发生的事用户问题先被向量化在向量库里做相似度检索取回 Top-5 命中片段一起拼进系统提示词的{{information}}占位符再带着用户问题调大模型生成回答。这就是 RAG 的主流程你用 LangChain4j 直接用一行assistant.chat()就触发完。这里有个细节值得展开minScore(0.6)。小于阈值的片段会被直接丢弃避免把毫不相干的内容塞给模型。但这个值不是拍脑袋定的取决于你的嵌入模型和文档风格。BGE 模型的相似度分布普遍偏高我通常从 0.5 开始调如果发现检索结果噪声大再往上提到 0.65。3. 把知识库做得更像产品文档处理、检索增强与数据一致性3.1 文档解析不只会读 txt真实业务里知识库的文档不会都乖乖躺在 txt 里。Word、PDF、Markdown、扫描件混着来。Java 生态里处理这类问题Apache Tika是我的首选它有一个AutoDetectParser能自动识别文件类型、抽取正文和元数据配合 POI 还可以处理 Word 里的标题层级。先加依赖dependency groupIdorg.apache.tika/groupId artifactIdtika-parsers-standard-package/artifactId version2.9.2/version /dependency解析 Word 文档的完整代码Parser parser new AutoDetectParser(); BodyContentHandler handler new BodyContentHandler(-1); // -1 表示不限制正文长度 Metadata metadata new Metadata(); try (InputStream in new FileInputStream(需求说明书.docx)) { parser.parse(in, handler, metadata, new ParseContext()); String text handler.toString(); Document document Document.from(text, Metadata.from(Map.of( fileName, 需求说明书.docx, sourceType, word ))); }注意我用Path、Metadata把文件名和类型塞进了 Document 的元数据。这个习惯非常关键后面做引用溯源、按文档维度删除旧数据时全靠这些元数据过滤。千万别省这一步不然后面想只删除某一篇文档的向量你会发现自己无从下手。对于 PDFTika 也能抽文本但遇到扫描版 PDF 就得接 OCR这是另一个话题这里先不展开。3.2 混合检索与 RRF 融合以及那个著名的去重缺陷很多团队做到语义检索就停了其实还不够。语义检索对同义词、概括性表达很友好但对编号、缩写、精确代码片段经常翻车。比如用户问“KB2024-011 号规范”向量检索可能返回一堆“2024年规范”的内容精确的那条反而排不到前面。所以真正能打的知识库都会做混合检索语义检索 关键词检索BM25再把两组排名用 RRF 融合。LangChain4j 里RRF 融合的写法大概是ContentRetriever semanticRetriever ContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) .build(); ContentRetriever bm25Retriever new Bm25ContentRetriever(store, 5); ContentRetriever hybridRetriever ReciprocalRankFusedContentRetriever.builder() .contentRetrievers(List.of(semanticRetriever, bm25Retriever)) .k(60) .build();RRF 的公式不复杂对每个文档在所有检索结果中的排名算一个倒数权重1 / (k rank)然后把多路得分加起来。k 通常取 60作用是把排名差异平滑化——第一名和第二名的差距不会太大防止某一路检索的一己之见压过全局。但这里我要专门泼一盆冷水LangChain4j 默认的 RRF 融合实现在去重逻辑上是有缺陷的。它按TextSegment的相等性去重而 TextSegment 的相等性主要看文本内容本身。结果就是同一个原始文档被切成的多个片段内容各不相同在融合时不会被识别为“同一来源”导致返回结果里可能同一篇文档占了 3 个甚至 4 个位置。这个问题最直接的后果有两个第一知识库的多样性变差用户问一个问题检索回来全是同一篇文档的相邻段落第二浪费上下文窗口又长又重复的内容挤占了其他相关文档的位置。我的做法是写一个包装 Retriever在 RRF 融合之前先按元数据里的documentId分组组内先取最高分的那一个片段参与排名融合等排序完成后再决定要不要把同文档的相邻片段作为窗口补齐。这个思路实现起来不复杂但对效果提升非常明显。3.3 数据一致性文档更新时必须做的事知识库是活的文档隔三差五要修订。很多团队第一版上线后碰到诡异问题文档明明改了问答系统还在一本正经引用旧内容。原因几乎都是同一个只更新了文档本身没清理向量库里旧的向量切片。LangChain4j 支持按元数据过滤删除前提是你索引时把documentId写进了每个切块的 Metadata// 1. 新文档切块时给每块都带上 documentId String documentId req-spec-001; Document updatedDoc Document.from(newText, Metadata.from(Map.of(documentId, documentId))); // 2. 删掉旧的所有切片 MapString, Object filter Map.of(documentId, documentId); store.removeAll(filter); // 3. 再走一遍切块、嵌入、入库 EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(600, 120)) .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(updatedDoc);顺序很重要先删旧、再插新。如果反过来会出现短暂的新旧数据并存查询结果不稳定。对小知识库全量重建也是一种简单粗暴的方案文档不超过几千块时重建一次也就几分钟比维护增量逻辑省心。另一个容易忽视的是一致性边界如果向量库和源文档不在同一事务里删除成功了但写入失败了怎么办我的习惯是引入一个“索引状态表”记录每个documentId的版本号、向量库状态和更新时间重建时先标记“更新中”等向量写入成功再更新状态。这一步操作很轻但能避免很多脏数据带来的诡异问题。4. 进阶到 Agentic RAG用 LangGraph4j 编排状态机4.1 为什么普通 RAG 不够普通 RAG 的流程是固定的问题进来无条件检索把检索结果拼给大模型。这在很多场景下够用但一旦问题变得复杂就露馅了。我举三个真实例子。第一用户问“公司有哪些规定是最近更新的”这压根不需要检索知识库直接根据索引状态回答就行走固定 RAG 反而多此一举。第二用户问“帮我对比一下账号规范和安全规范两篇文档的差异”固定 RAG 只做一次检索很可能两边都捞不全。第三模型生成完回答后你很难判断它有没有瞎编。固定管道没有“自我校验”这一环等到用户反馈说答错了问题才暴露。Agentic RAG 的思路就一句话不要固定流水线让流程根据问题动态决定。先路由分类判断该不该检索、该走哪个检索源检索完生成答案生成后用校验节点检查答案是否有依据没依据就再检索一轮。这套东西用 if-else 写会非常痛苦所以才有 LangGraph4j 这种状态图引擎。4.2 用 StateGraph 建模“路由-检索-生成-校验”流程LangGraph4j 的核心概念是StateGraph你先定义一个状态对象里面放问题、路由结果、检索证据、最终回答这些字段然后定义若干节点每个节点接收当前状态、改状态、返回新状态节点之间用边连接边可以是无条件直连也可以是条件边——根据某个字段的值决定下一步走到哪个节点。一个典型的 Agentic RAG 状态图大概长这样class RagState extends HashMapString, Object { public RagState(MapString, Object data) { super(data); } } public class RagAgentExample { MapString, Object route(MapString, Object state) { String question (String) state.get(question); // 用一个简单分类模型或 LLM 判断需要查向量库还是直接回答 String action classify(question); // direct / vector return Map.of(action, action); } MapString, Object retrieve(MapString, Object state) { EmbeddingStoreTextSegment store ...; // 调用 LangChain4j 检索组件 ListTextSegment evidences search(store, (String) state.get(question)); return Map.of(evidences, evidences); } MapString, Object generate(MapString, Object state) { // 拼接 evidencs 和 question调用 ChatLanguageModel String answer generateAnswer(state); return Map.of(answer, answer); } MapString, Object validate(MapString, Object state) { // 判断 answer 是否在 evidences 中有依据没有则标记 needs_more return Map.of(needs_more, !hasEvidence((String) state.get(answer))); } public void build() { StateGraphRagState graph new StateGraph(RagState::new); graph.addNode(route, this::route); graph.addNode(retrieve, this::retrieve); graph.addNode(generate, this::generate); graph.addNode(validate, this::validate); graph.setEntryPoint(route); graph.addEdge(route, retrieve); // direct 分支可以直接到 generate这里简化为全走检索 graph.addEdge(retrieve, generate); graph.addEdge(generate, validate); // 条件边校验不通过回到 retrieve 再来一轮 graph.addConditionalEdge(validate, state - Boolean.TRUE.equals(state.get(needs_more)) ? retrieve : end, Map.of(retrieve, retrieve, end, end)); CompiledGraphRagState app graph.compile(); MapString, Object result app.invoke(Map.of(question, 账号规范和安全规范有哪些差异)); System.out.println(result.get(answer)); } }注意这里我把校验不通过的节点回到了retrieve形成循环。如果校验连续两次不过你要记得加最大轮数限制否则会死循环烧 token。这是我特别喜欢 LangGraph4j 的一点循环、条件分支在图上表达得很直观代码可读性比散落一地的 if-else 好太
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱 2026/9/28 9:42:31

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱 网站做好了没人访问,这是很多老板最头疼的事。你花大价钱做的官网,设计精美、功能齐全,但打开一看,流量为零,咨询为零。这时候你才意识到,问题不在“做没做”,而在“怎么快速做出来并推向市场”。面…

阅读更多 →
昇腾910B多机分布式推理:从HCCL到MindIE的DeepSeek部署实践 2026/9/28 9:42:24

昇腾910B多机分布式推理:从HCCL到MindIE的DeepSeek部署实践

昇腾910B上跑DeepSeek多机分布式推理,很多人卡在第一眼:MindIE、HCCL、ranktable、hccn_tool,每个词都眼熟,串起来就不是那么回事。实际踩过一圈之后你会发现,真正决定能不能跑起来的不是模型代码,而是通信…

阅读更多 →
从CANoe到TSMaster:车载总线测试工具链迁移实战指南 2026/9/28 9:42:24

从CANoe到TSMaster:车载总线测试工具链迁移实战指南

搞车载总线测试的工程师,电脑里大概率都装着一套CANoe。我最早接触CANoe是刚入行那会儿,跟着前辈在项目里做网络测试,从报文发送、DBC解析到UDS诊断,基本全是靠Vector这套工具撑起来的。说实话,CANoe确实是这个行业的标…

阅读更多 →
从刷榜到用榜:GitHub Trending 的增量逻辑、项目筛选与高效落地 2026/9/28 9:42:23

从刷榜到用榜:GitHub Trending 的增量逻辑、项目筛选与高效落地

1. 日榜的"热度"到底是怎么算出来的先别急着收藏仓库。每天打开 GitHub 的 Trending 页面,你看到的是过去 24 小时内 Star 增量最高的仓库,周榜和月榜则分别看一周、一个月内的增量。官方没有公开完整排序算法,但用久了会发现&…

阅读更多 →
【Java开发MCP】SSE模式开发并集成MCP:TaoToken统一Key接入与SpringAI WebFlux配置骨架 2026/9/28 9:42:23

【Java开发MCP】SSE模式开发并集成MCP:TaoToken统一Key接入与SpringAI WebFlux配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenCompass 高效评测:Partitioner 任务切分与 Runner 执行后端实战指南 2026/9/28 9:42:23

OpenCompass 高效评测:Partitioner 任务切分与 Runner 执行后端实战指南

模型评测人工智能大模型AI 评测 【免费下载链接】opencompass OpenCompass is an LLM evaluation platform, supporting a wide range of models from OpenAI, Anthropic, Gemini, Qwen, GLM, DeepSeek, etc, across 100 datasets covering knowledge, reasoning, coding, scie…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉