资讯动态

Java生态下的RAG知识库实战:LangChain4j与LangGraph4j踩坑记录

发布时间:2026/9/26 7:16:59 来源:尧图企业网站定制
我们直接用Java做完了一整套RAG知识库系统这中间踩的坑比想象中多得多。如果你也在Java生态里做检索增强生成想用LangChain4j和LangGraph4j而不是天天开Python服务这篇文章应该能帮你少走很多弯路。我会从依赖选型讲到图编排再落到切块、去重、多轮对话这些实际工程问题全程给出可复现的代码和参数。1. 为什么在Java生态里自己做RAGLangChain4j与LangGraph4j到底解决什么问题先交代背景。大多数RAG教程都是Python写的LangChain、LlamaIndex、向量数据库一把梭。但很多团队的后端核心是Java尤其是老一点的企业项目为了接入大模型去额外维护一个Python微服务还要处理跨语言调用、部署运维成本并不低。LangChain4j的出现让这件事可以完全在Java/JVM内闭环加上LangGraph4j把可控的Agent流程也带到了Java生态才真正值得正经聊一聊。1.1 从Python到Java的RAG之痛我最初试过用Python写一个独立的RAG服务再让Java业务系统通过HTTP调用。方案能跑通但引入了两个服务之间的网络开销、鉴权、超时和日志追踪问题。而且Python侧依赖管理比较随性打包成镜像体积也不小团队里还要有人专门维护那套环境。换到Java侧后最明显的感受是文档解析、数据库访问、事务控制、监控埋点这些能力直接复用现有技术栈。比如我要把Word、PDF里的内容灌进知识库用Java本身的POI配合其他解析库就行不需要再开一个Python子服务。LangChain4j恰好提供了统一的文档加载、拆分、嵌入、存储、检索API而LangGraph4j则在流程编排层面补上了状态机和条件路由的能力。1.2 LangChain4j的定位与边界LangChain4j不是简单地把Python LangChain翻译成Java。它的核心抽象包括Document、DocumentSplitter、EmbeddingModel、EmbeddingStore、ContentRetriever、ChatMemory、AiServices等。你可以把它理解成一个“胶水层”把不同厂家的模型和向量库以同一方式接入。但它也有边界单轮的“检索-生成”链路通过AiServices很容易实现可一旦你要做多轮对话中的查询改写、判断检索结果是否足够、不够就再查一次这类有状态流程AiServices本身是不够的。这种场景LangGraph4j就更合适。1.3 LangGraph4j把流程变成有向图LangGraph4j的核心理念是让LLM应用中的逻辑流程变成一张显式的图。你定义节点和边每个节点是一个函数边决定下一步走向。节点之间通过一个全局状态对象传递数据支持并行、分支和循环。这个设计让“Agentic RAG”落地变得可控。传统RAG是“问一次、查一次、答一次”而Agentic RAG可以在回答前先判断问题是否需要二次检索、要不要改写查询词、有没有多个子问题需要分别查询。把这些判断逻辑画成一张图你就知道自己每一次调用模型花在什么地方了。2. 动手前的地基项目依赖、Embedding模型选择与本地向量库先说依赖。我用的是MavenSpring Boot 3.2.xJava 17。LangChain4j版本用到了官方BOM来统一管理避免子模块版本漂移。2.1 Maven依赖怎么加properties java.version17/java.version langchain4j.version1.0.0-beta2/langchain4j.version /properties dependencyManagement dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-bom/artifactId version${langchain4j.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId /dependency !-- 按需引入模型和向量库 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-chroma/artifactId /dependency !-- LangGraph4j -- dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-core/artifactId version1.0/version /dependency dependency groupIdorg.bsc.langgraph4j/groupId artifactIdlanggraph4j-langchain4j/artifactId version1.0/version /dependency /dependencies建议直接用BOM不要自己写一堆版本号。我见过好几个项目因为open-ai和chroma模块版本不一致运行时直接NoSuchMethodError。2.2 Embedding模型的选择Embedding模型决定检索质量的上限。如果公司有OpenAI的Key直接用OpenAiEmbeddingModel最省事EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(text-embedding-3-small) .build();如果出于成本、隐私或合规考虑想本地部署可以选择Ollama或者ONNX Runtime加载本地模型。LangChain4j有OllamaEmbeddingModel我本地测试常用nomic-embed-text这种小模型。实测下来本地模型维度低一些检索效果相对OpenAI的embedding会有差距但知识库如果以专业术语为主本地模型的向量仍然够用。2.3 向量存储的选型对比向量存储的选择会影响后续的扩展性和运维复杂度。我整理了一张表方案部署成本持久化适合场景InMemoryEmbeddingStore零部署否原型验证、测试Chroma本地进程是中小型项目、单机PgVector需PostgreSQL插件是复用已有PG事务和向量统一Milvus独立集群是百万级以上向量、高并发我项目里最终选了Chroma因为部署简单不用单独维护一套存储服务。如果你公司已经有用PGPgVectorEmbeddingStore也值得试这样可以用一套数据库同时管业务数据和向量还能用SQL做过滤。3. 核心链路拆解加载-切块-向量化-检索-生成不管用不用LangGraph4jRAG基础链路是绕不开的。这部分串起来的是数据准备和单轮问答也是后续一切流程的地基。3.1 文档加载与切块策略原始文档可能是PDF、Word、Markdown。LangChain4j内置了Document.fromFile和Document.fromInputStream但Word和PDF需要额外配合解析库。我建议先解析成纯文本再交给LangChain4j的DocumentSplitter。毕竟嵌入模型输入的是文本解析成干净文本比在复杂文档结构上做切块更稳定。切块策略直接决定检索精度。我用的是DocumentSplitters.recursiveDocumentSplitter splitter DocumentSplitters.recursive( 500, 100, new NaiveTextSplitter() ); ListTextSegment segments splitter.split(document);这里recursive的意思是先按段落切再按句子切最后按固定窗口切尽量保证语义完整性。500是目标块大小100是重叠部分。块大小要根据你的embedding模型和业务问题长度来调。如果块太小检索到的内容没有上下文支撑太大带入Prompt的噪声多且浪费token。以我的经验一般技术文档用400600字比较合适。如果问答偏向“某段话讲了什么”300字就够如果偏向“总结这一章”可以调到8001000。3.2 构建Embedding并入库切块之后遍历每个TextSegment生成向量存入库中。这里有一个容易忽略的点如果你要用Chroma最好把文档ID或来源URI作为元数据一并存进去否则后续增量更新数据时你不知道哪些向量对应旧文档。ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments);如果是上线项目建议把这段写成一个批处理任务用文档ID做幂等。不要图省事每次构建知识库都全量重刷后期文档多了性能扛不住。3.3 检索器与Prompt组装检索器负责根据用户问题找到最相关的片段。LangChain4j的EmbeddingStoreContentRetriever直接可用ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(4) .minScore(0.6) .build();minScore是相似度阈值设置太低会把不相关内容塞进来。我一般设0.60.7具体看embedding模型的分布。这里要注意不同embedding模型的相似度分数范围不一致OpenAI的基本在0.71.0本地模型可能分散一些一定要先看看实际分布再定阈值。Prompt组装是容易被低估的环节。最简单的是PromptTemplate promptTemplate PromptTemplate.from( 你是知识库助手。请基于以下资料回答用户问题。 资料内容 {{info}} 用户问题{{question}} 如果资料中没有明确答案请直接说不知道不要编造。 );注意要把检索到的文本用分隔符明确隔开并且加上“不要编造”的约束能显著减少幻觉。3.4 用AiServices把问答接口串起来LangChain4j的AiServices可以省掉你手写模型调用和Prompt拼装的代码。先定义一个接口interface Assistant { String answer(UserMessage String userMessage); }然后装配Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatLanguageModel) .contentRetriever(retriever) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build();调用assistant.answer(什么是RRF)即可。ContentRetriever会自动把检索结果注入Prompt。这一套跑通后单轮RAG就能用了。4. 引入LangGraph4j从单轮问答到Agentic RAG只靠AiServices做固定管道处理复杂问题会很吃力。我遇到的实际场景是用户连续追问、问题模糊、或需要多个文档交叉验证这时候要把流程变成图。4.1 为什么简单的“检索生成”不够举一个真实例子用户问“我们公司的报销政策是什么请对比不同部门的额度差异”。如果你只做一次检索可能会得到大量关于报销政策的片段但很难同时命中所有部门的额度描述。更合理的方式是先识别出这是一个对比型问题然后分别检索“A部门报销额度”“B部门报销额度”再汇总答案。这就是Agentic RAG要做的让LLM参与流程决策而不是只当最后的生成器。LangGraph4j允许你把“改写查询-并行检索-判别结果-生成回答”画成一张图每个环节都可控。4.2 LangGraph4j核心概念状态、节点、边LangGraph4j中最关键的是StateGraph。你需要定义状态类型然后注册节点最后设置边的跳转条件。StateGraphRagState graph new StateGraph(RagState::new); graph.addNode(rewrite_query, this::rewriteQuery); graph.addNode(retrieve_docs, this::retrieveDocs); graph.addNode(generate_answer, this::generateAnswer); graph.addNode(check_relevance, this::checkRelevance); graph.setEntryPoint(rewrite_query); graph.addEdge(rewrite_query, retrieve_docs); graph.addEdge(retrieve_docs, check_relevance); graph.addConditionalEdge(check_relevance, state - state.isRelevant() ? generate_answer : rebuild_query, Map.of(generate_answer, generate_answer, rebuild_query, rewrite_query) ); graph.setFinishingPoint(generate_answer);这里的状态类可以包含用户原始问题、改写后的问题、检索到的片段、生成结果等字段。每个节点方法接收状态返回部分状态更新。4.3 一个带查询改写和自愈评测的RAG图怎么搭我的实现思路是收到用户问题后先让LLM判断问题是否需要重写。比如“它的实施步骤是什么”这种指代不明的需要结合历史对话改写为“Java项目中集成LangChain4j的实施步骤是什么”。改写后去做检索。检索结果回来后再加一个“相关性评估”节点让LLM判断检索到的资料是否足以回答当前问题。如果不够则触发二次检索或换查询词重新走一遍。这个“重试”机制就是LangGraph4j相比固定管道最大的优势——你可以在图上显式建模循环。下面是一个简化的节点实现public MapString, Object rewriteQuery(RagState state) { String rephrased chatModel.generate( 请把这个问题改写成适合检索的查询词只输出改写结果不要解释\n state.getQuestion() ); return Map.of(finalQuery, rephrased); }checkRelevance节点实现类似用LLM输出一个布尔评分。要注意控制重试次数我拿一个计数器存在状态里最多重试两次否则容易陷入死循环。实测下来这种带自愈评测的流程对复杂问题的准确率提升明显但延迟会上升因为多了一次甚至几次LLM调用。如果你在乎响应速度可以只在用户开启“深度查询”时走完整图默认走简单链路。5. 实测踩坑记录切块大小、去重逻辑、上下文溢出与响应质量这一节全是实战中遇到的问题每一个都是我花时间调过的。5.1 切块大小对答案质量的影响之前我图省事把所有文档切成了固定1000字结果检索出来的片段经常跨章节。用户问“LangGraph4j的状态如何定义”返回的内容可能包含了节点和边的定义但关键的StateGraph初始化代码被切到了下一个块里导致答案缺胳膊少腿。后来改成递归切块重叠100字后情况好了很多但还不够。我发现对代码教程类内容最好在切块时保留代码块完整性。LangChain4j的DocumentSplitters.recursive支持自定义分界符我把代码块标记也加入分界符列表尽量不让代码块被拦腰切断。5.2 去重逻辑的坑热词里提到“langchain 和 langchain4j 的默认 rrf 实现去重逻辑存在缺陷”我也踩过。当多个检索器比如向量检索和BM25关键词检索合并结果时RRF算法会给出综合排序。LangChain4j默认结果集如果包含相同文本片段的不同chunk可能会同时返回两段高度重复的内容。我的处理方案是在检索结果合并后、组装Prompt之前增加一个去重步骤ListTextSegment deduplicated retrievedSegments.stream() .collect(Collectors.toMap( seg - normalize(seg.text()), Function.identity(), (a, b) - a, LinkedHashMap::new )) .values() .stream() .toList();normalize函数去掉空格和换行把语义上相同的文本视为重复。这个方法简单但有效。另外RRF里的k参数默认是60如果你觉得结果排序不够准可以调小试一下我调到30后精确度感觉更好。5.3 多轮对话中的上下文管理用MessageWindowChatMemory.withMaxMessages(10)简单粗暴地保留最近10轮但这样会带来一个问题中间的历史消息可能包含与当前问题无关的内容且每次都把完整历史塞给模型token消耗大。RAG场景更适合的做法是仅把第一轮用户明确描述的上下文作为历史压缩记忆检索时使用当前轮改写后的查询。我在LangGraph4j状态里单独保存了originQuestionHistory当前轮的问题由LLM结合历史改写后再去检索。这样既保留了指代消解能力又不会让历史消息污染检索权重。5.4 性能与成本延迟与token消耗一次完整RAG响应的延迟主要来自三块embedding检索、LLM生成、额外LLM调用。embedding检索一般是毫秒级LLM生成才是大头。如果用了LangGraph4j的自愈流程延迟会变成多次串行LLM调用之和。我在实际线上环境中做过分流策略普通问题走AiServices快速链路复杂问题走LangGraph4j完整链路。判断逻辑就是一个简单的关键词长度规则没有额外模型调用成本可控。另外OpenAiEmbeddingModel跑批处理时一定要控制并发和批次大小不然会被限流。我习惯把“文档切片→embedding→入库”写成独立的Job不在用户请求链路里做实时入库。6. Spring AI还是LangGraph4j我的选型建议很多Java程序员会纠结现在官方有Spring AI社区有LangChain4j和LangGraph4j到底选哪个我不能替你决定但可以把我的观察讲清楚。6.1 两者定位区别Spring AI更像是一个“Spring官方对AI能力的抽象层”。它倾向于和Spring生态无缝整合提供了ChatClient、EmbeddingModel等接口但它在Agent编排和复杂流程上目前还比较基础。它的特点是稳、标准化适合已经在Spring全家桶里深耕的团队。LangGraph4j则更接近于Python LangGraph的思路把应用逻辑画成图节点之间显式传状态。你可以精细控制循环、分支、并行。代价是要自己理解图执行引擎的细节学习成本比单纯用Spring AI的RestTemplate高不少。6.2 不同场景的取舍如果你的需求是快速给业务加一个“智能客服”简单检索固定提示词就够了那么Spring AI或LangChain4j的AiServices都合适。真正需要LangGraph4j的时候往往具备以下特征需要对检索过程做多次改写和验证需要根据用户意图动态选择不同工具需要并行处理多个检索子问题。我的项目最终是LangGraph4j和LangChain4j混用底层文档处理和向量操作用LangChain4j上层复杂流程编排用LangGraph4j。两者不冲突langgraph4j-langchain4j这个模块就是为桥接而生的。6.3 从维护成本和团队能力看选型也要看团队的Java能力。LangGraph4j的图模型要求开发者对状态机、异步处理有概念写起来比普通CRUD更像在写算法。如果团队是刚转Java的建议还是先从LangChain4j的AiServices起步跑通了再上LangGraph4j。否则一旦流程复杂调试状态流转就够折腾一壶。我在实际中写LangGraph4j节点时每个节点尽量保持纯函数风格只依赖传入的状态、只返回需要修改的字段这样单元测试非常好写。这也是我想分享的最有价值的一条实践。最后再分享一个小技巧给RAG系统加上缓存和反馈线上跑了两个月后我发现很多用户的问题其实是重复问法。与其每次都走一遍LLM和检索不如给RAG加一层结果缓存。缓存键用归一化后的问题文本缓存值用生成结果命中缓存直接返回。知识库更新时需要手动或者定时清理相关缓存不然老答案会一直驻留。另外推荐在答案里带上引用的文档ID和片段文字方便用户核对。这一点在RAG系统里极其重要——用户信任度和答案准确性一样重要。搭建这套系统的过程让我印象最深的一点是真正决定RAG上线效果的不是用什么模型、用什么向量库而是你对数据切分、流程控制和错误处理的细致程度。用Java统一技术栈确实能省掉很多跨服务协调的麻烦但工程细节一个都绕不过去。希望上面这些记录能让你少踩几个我踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑