资讯动态

Spring Boot整合LangChain4j实现RAG检索增强生成实战

发布时间:2026/10/14 4:05:35 来源:尧图企业网站定制
搞RAG差不多一年了从最早的Python生态摸过来再到后面被强制要求把整套问答能力嵌进现有Java服务体系说实话一开始我是有点抗拒的。但真正把LangChain4j用起来之后我得承认这个组合比我预想的要成熟得多。Spring Boot LangChain4j的RAG方案现在已经是我在新项目里默认推荐的首选架构这篇就从一个实际项目出发把整个RAG检索增强生成的落地过程、踩坑和调优经验完整拆开讲一遍。如果你正好在Java技术栈里想把大模型能力接进自己的业务系统又不想为了一个小功能去单独维护一套Python服务那这篇文章基本覆盖了你从0到1需要知道的所有东西。文档问答、私有知识库、智能客服、内部数据检索这几个场景里Spring Boot配LangChain4j完全够用。1. 项目解剖RAG到底解决什么问题1.1 一个真实痛点场景先说一个我实际做过的模拟项目X需求。某公司内部积累了大几百份产品手册、技术方案、故障排查文档散落在好几个网盘和Wiki里。新员工每次遇到问题都得翻半天文档老员工凭记忆回答问题答案还不一定准。公司想让大模型基于这些内部文档自动回答员工问题但直接拿裸模型肯定不行——模型根本不知道你们公司的产品细节问稍微偏门一点的问题它就一本正经地胡说。这时候就需要RAG也就是检索增强生成。核心思路并不复杂用户提问之后系统先从知识库里检索出相关的文档片段把这些片段和用户问题一起打包塞给大模型让模型基于这些检索到的内容来组织答案。因为真实的、包含业务细节的文档片段已经在Prompt里了模型就能做到有理有据地回答问题而不是凭空编造。这个项目选型时纠结过一阵模型API用哪家向量库用什么编排框架选谁。但最关键的决策点是技术栈。公司后端全是Java不可能为了一个知识库问答单独引入Python服务运维、监控、权限体系全都对不上。所以目标很明确必须找到一个能让Java项目直接落地RAG的方案。这就是LangChain4j进入视野的原因。1.2 Java技术栈该选谁LangChain4j的必要性当时市面上Java生态里能做RAG编排的框架其实不算多。自己写一套也不难无非就是调Embedding接口、调向量库查相似、再调大模型API但真正做起来会发现细节极其繁琐文本要切分、向量要存储、检索结果要排序、Prompt模板要管理、上下文还有长度限制而且多方依赖之间的版本要匹配。自己造轮子不是不行但周期很长踩坑的代价也高。LangChain4j是Java生态里比较成熟的LLM编排框架借鉴了LangChain的设计思想但针对Java语言和生态做了大量适配。它把大模型调用、向量存储、文档切分、Prompt管理这些环节全部抽象成了组件开发的时候只要按需组装就行。更重要的是它内置了Spring Boot的starter也就是说我可以直接在现有的Spring Boot工程里加一个依赖然后像写普通Service一样去写RAG逻辑完全不用改变项目的整体架构。从实际维护角度看这个选择的意义很大。Java项目里的人员协作、代码评审、自动化测试体系都是现成的引入LangChain4j后AI能力变成了项目里的普通模块谁都能接手维护而不像单独挂一个Python服务那样出了问题可能全公司只有一个人会修。2. 整体架构与技术选型2.1 系统链路总览整个系统的逻辑链路是这样跑的用户在前端输入问题请求进入Spring Boot应用。应用先把问题交给Embedding模型转成向量然后用这个向量去向量数据库里做相似度检索召回最相关的几个文本块。接下来系统把用户原始问题和这些检索到的文本块一起按预设的Prompt模板组装成完整提示词发给大模型API。大模型读完这些材料后生成最终答案返回给前端。同时准备知识库还有一个离线的数据加工链路原始文档进来后先做解析和清洗再按策略切分成块每块调用Embedding接口转成向量最后批量写入向量数据库。这两条链路一条是写入一条是查询合起来就是完整的RAG系统。这个链路看起来简单但每个环节都有大量的调优空间。比如切分粒度的大小直接决定了召回效果向量库的索引类型决定了查询延迟Prompt模板的写法决定了模型回答的格式和稳定性。我后面会逐一详细讲。2.2 组件选型对比先交代一下我这次选型的结果给你一个可以照着用的组合组件本次选型备选方案选型理由编排框架LangChain4j Spring Boot Starter自研管线组件全、社区活跃、和Spring集成好向量数据库某开源向量库本地方案某云向量数据库、ES向量检索运维成本低单机即可跑完整个流程Embedding模型某商用Embedding API本地模型如BGE系列效果稳定不用额外部署推理服务大模型商用大模型API开源模型私有化部署按量付费、接入简单、效果最有保障文档解析核心库配合自定义清洗复杂文档用服务化解析接口表格和版式场景按需升级这里特别说一下为什么没选ES。ES本身就是很多团队的标配它的向量检索能力也不差但问题在于ES的向量检索在性能调优和参数配置上其实是另外一个深坑而且RAG场景对它来说只是附加功能不是核心场景。单独用一个专业向量数据库API更简单相似度检索的性能和召回效果也更好维护。等数据量真正大到需要分片和水平扩展时再迁移到云上向量库也不迟接口风格基本兼容。2.3 为什么不用Python方案这个问题的答案本质上取决于项目归属感和维护成本。如果公司本来就有一个独立的AI中台团队Python服务作为基础设施对外提供能力那这套RAG放在Python那边完全合理。但我的实际场景是这个功能属于某个业务系统的一部分要跟用户体系打通要和现有的权限模型绑定要跟业务工单流程联动。这种情况下如果单独上个Python服务意味着要多维护一套部署脚本、一套监控告警、一套依赖管理还要处理跨服务的网络通信和鉴权。Spring Boot里直接集成就没有这些问题包体积虽然大了一点但部署结构还是原来那套上线流程和运维体系完全复用。另外还有一个实操层面的原因LangChain4j对Java开发者的心智负担比LangChain低得多。Python那边你可能要深入理解一大堆隐性的概念和抽象而LangChain4j的API设计比较直接Java开发者看几遍示例代码基本就能上手学习成本低了一大截。3. 从零搭建环境准备与基础配置3.1 依赖引入与版本选择项目基础是Spring Boot 3.x。LangChain4j目前对Spring Boot 3的支持已经很完善了直接用官方提供的starter就行。我的pom.xml里核心依赖长这样dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai-spring-boot-starter/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-embeddings/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-vector-store/artifactId version1.0.0-beta2/version /dependency版本这里多说一句。LangChain4j迭代速度很快不同版本之间的API变动不小尤其是Beta阶段的版本可能一个minor版本升级就会引入破坏性变更。我的建议是一旦选定某个版本上线就不要随便升级等确实需要新功能或者官方发布了稳定版本再考虑迁移。另外Spring Boot的版本也要和LangChain4j的兼容性说明对照一下别装完直接启动报错。配置方面在application.yml里做基础设置。我这里用的是官方OpenAI兼容接口配置结构就是这样langchain4j: open-ai: chat-model: base-url: ${LLM_BASE_URL:https://api.example.com/v1} api-key: ${LLM_API_KEY:your-api-key} model-name: ${LLM_MODEL:qwen-plus} temperature: 0.3 max-tokens: 1024 embedding-model: base-url: ${EMBEDDING_BASE_URL:https://api.example.com/v1} api-key: ${EMBEDDING_API_KEY:your-api-key} model-name: ${EMBEDDING_MODEL:text-embedding-v2}API Key这些敏感信息千万别写死在代码里用环境变量或者配置中心管理才是正规做法。我在项目里是接了配置中心不同环境用不同的配置本地开发默认走测试环境的API线上走生产专属的Key和模型互不干扰。3.2 向量库与模型配置向量库这里我选了本地轻量方案。如果你的项目数据量不大几十万条以内的文档块单机向量库完全能扛住索引加载到内存以后查询速度毫秒级。部署上就是跟着Spring Boot应用一起跑不需要额外维护。配置里主要是设置维度、度量方式和索引参数。我用的Embedding模型输出维度是1024维度量方式用的余弦相似度。初始化的时候建好索引后续写入和查询都走同一个客户端Configuration public class VectorStoreConfig { Bean public EmbeddingStoreTextSegment embeddingStore() { return new InMemoryEmbeddingStore(); } }先用内存版跑通全链路后面数据量上来了再换持久化版本。这里要提醒一个容易踩的坑内存版向量库的数据是存在内存里的服务重启数据就没了所以它只适合开发调试。线上环境要么选持久化能力强的本地向量库要么接独立部署的向量数据库否则每次重启都要重新灌一遍数据那体验就很糟糕了。Embedding模型的选择影响的是向量质量质量又直接决定检索效果。我当时对比过几款模型结论是商用Embedding API的稳定性和泛化能力比较可靠特别是对于混合风格的中文文档官方的正式文档加上员工写的运维笔记文风差距很大商用模型在语义理解上的整体表现更好。如果预算有限本地部署的开源Embedding模型也能用但对中文长文档的支持和细节语义的捕捉能力确实还需要额外调优。4. 核心实现文档处理与向量化4.1 文档解析与清洗RAG系统里流传最广的一句话就是垃圾进垃圾出。很多项目效果差不是模型不行而是文档处理环节没做好。原始文档里大量存在的页眉页脚、目录页码、重复的声明段落、特殊符号这些噪声如果不处理干净切出来的文本块就会特别脏向量化之后检索效果自然一塌糊涂。我在这个项目里文档处理分成三步。第一步是格式解析PDF和Word分别用对应的解析库抽取出纯文本这一步要注意处理表格否则表格内容会变成一堆堆挤在一起的字。第二步是结构化清洗去掉页眉页脚、统一换行符、把多余的空白符号压缩掉还会用正则把类似修订记录这种所有文档都重复出现的内容过滤掉。第三步是格式标准化把全角半角统一把日期格式统一让内容尽量规整。这部分的代码结构大致这样Service public class DocumentProcessor { public ListDocument process(byte[] fileBytes, String fileName) { String rawText extractText(fileBytes, fileName); String cleanedText clean(rawText); String normalizedText normalize(cleanedText); return splitIntoSegments(normalizedText); } }清洗这一步特别花时间但收益非常明显。我调试的时候对比过清洗前后的检索结果同一套测试问题集清洗后的检索命中率提升了大概十几个百分点。可以说这部分是整个项目ROI最高的投入。4.2 文本切分策略文本切分是RAG里最容易被低估的一环。切得太粗每块包含的信息太多和问题的相关性被稀释切得太细每块信息不完整模型根据上下文理解时容易丢失关键前提。我实际对比下来中文文档场景下按固定token数切分设定在300到500之间效果比较理想同时要设置一定比例的重叠避免把一个完整语义单元切碎。LangChain4j里提供了DocumentSplitter接口我用的是DocumentByParagraphSplitter它会优先按段落切段落太长再按句子边界和长度限制二次切分。这样做比纯按字符数硬切要好因为段落天然是语义完整的最小单位。DocumentSplitter splitter new DocumentByParagraphSplitter( maxSegmentSize, // 每个块的最大字符数 maxOverlapSize // 相邻块之间的重叠字符数 ); ListTextSegment segments splitter.split(document);实际配置我一般把maxSegmentSize设在600到800字符之间重叠100字左右。这里的经验是如果你的文档大部分是操作手册、流程说明这种需要前后文连贯的内容块可以稍微大一点如果是知识百科、词条解释这种独立性强的内容块可以小一点。这个参数没有绝对最优上线后要根据召回效果反复调。另外切分之后建议给每个片段打上原始的元数据标记比如文档标题、章节路径、页码。这一步很关键后面回答问题时如果想在答案后面附上引用的文档来源没有元数据就只能自己去全文检索反查非常麻烦。4.3 Embedding与入库切分好的文档块下一步就是向量化入库。逐条调用Embedding接口短文本单条耗时还好但文档量大的时候逐条调用很慢所以我用了批量调用。LangChain4j的EmbeddingModel接口支持传入文本列表批量embedding一次处理几十条吞吐量提升明显。入库环节有个细节我一直提醒自己先查重再写入。知识库文档经常会被重复上传如果不做去重同一个内容可能出现好几份检索时返回的相似片段全是重复的既浪费上下文空间又影响答案质量。我的做法是给每段文本算一个内容哈希入库前先查一下哈希是否已存在存在就直接跳过。public void store(String docId, String text) { String hash DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8)); if (embeddingStore.findByHash(hash) ! null) { return; } TextSegment segment TextSegment.from(text, metadata); Embedding embedding embeddingModel.embed(text).content(); embeddingStore.add(embedding, segment); }入库完成后一套完整的知识库就准备好了。下一步要做的就是把检索和生成这两条链路串起来。5. 核心链路检索增强生成的全流程实现5.1 检索器与向量相似度查询查询侧的第一步是先把用户的提问做向量化然后拿这个向量去向量库里做相似度搜索。LangChain4j里可以用EmbeddingStoreRetriever来实现这一步。它本质上就是一个桥接组件把query向量化、检索、按相关度排序这几件事封装好了Bean public EmbeddingStoreRetriever retriever(EmbeddingStoreTextSegment store, EmbeddingModel embeddingModel) { return EmbeddingStoreRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(5) // 最多召回几条 .minScore(0.65) // 相关度阈值 .build(); }这里有两个参数需要稍微琢磨一下。maxResults控制召回数量太小了可能漏掉相关文档太大了又会往Prompt里塞进一堆噪声还会挤占上下文空间。我的经验值是先设5后续根据答案质量微调。minScore是相似度阈值低于这个分数的片段直接丢弃阈值设太高会召回不到内容设太低又会把一堆不相关的内容全部拉进来。这个值建议你拿自己的知识库多试几轮用一批真实问题跑一遍观察不同阈值下的召回效果再定。检索出来的结果LangChain4j已经按相似度排好序了。这里我要多说一句向量相似度高不代表语义相关性就一定高尤其是涉及到专业术语时模型embedding出来的向量可能会有一些偏差。如果业务对精度要求特别高可以再接一层重排模型Reranker把第一轮召回的结果再做一次精细排序。但这个会增加一个模型的调用成本和响应延迟建议小项目先不加等发现确实需要再上。5.2 Prompt模板与增强生成检索到文档片段之后真正的重头戏是Prompt组装。这一步决定了模型的输出质量上限。我的做法是把这套Prompt设计成三部分角色设定、检索内容和用户问题。角色设定让模型清楚自己的定位和行为规范检索内容作为参考材料用户的原始问题放在最后。String promptTemplate 你是公司的智能知识助手。请基于下面提供的参考文档内容回答用户的问题。 要求 1. 如果参考文档中包含答案请直接给出明确回答并适当引用文档内容。 2. 如果参考文档中不包含答案请直接说明文档库中暂未找到相关信息不要编造。 3. 回答时保持客观、简洁不要加入个人观点。 参考文档 {{documents}} 用户问题 {{question}} ;模板里的{{documents}}和{{question}}是两个变量在运行时替换成实际的检索内容和用户问题。可能有同学会问为什么要把所有检索结果一次性全部塞进Prompt能不能让模型自己决定用哪些我的回答是现阶段主流做法就是全塞进去让模型自己根据上下文选择相关部分。这其实是LLM应用设计上的一个取舍——大模型的输入窗口有限把太多不相关的东西塞进去不仅浪费token还可能导致模型被噪声干扰。所以前一步的maxResults控制很重要精选相关度最高的3到5条按相关度排序后再塞入模板效果最稳定。LangChain4j里提供了Prompt模板的解析能力用起来是这样的PromptTemplate promptTemplate PromptTemplate.from(promptTemplateStr); MapString, Object variables new HashMap(); variables.put(documents, formattedDocs); variables.put(question, userQuestion); Prompt prompt promptTemplate.apply(variables);组装好Prompt之后调用ChatModel生成答案。ChatModel接的是配置里指定的模型。最终形成一个完整的问答ServiceService public class RagService { private final EmbeddingStoreRetriever retriever; private final ChatModel chatModel; private final PromptTemplate promptTemplate; public String answer(String userQuestion) { ListRetrievedDocument docs retriever.retrieve(userQuestion); String formattedDocs formatDocs(docs); MapString, Object variables Map.of( documents, formattedDocs, question, userQuestion ); Prompt prompt promptTemplate.apply(variables); return chatModel.generate(prompt.text()); } }这段代码看起来简洁但要考虑的事情不少。比如formatDocs这个函数里我会把每条文档的标题和来源信息也拼进去这样模型在回答的时候可以引用到具体文档名用户拿到答案后还能去翻原始文档核对体验会好很多。5.3 流式输出与超时控制问答场景如果等模型全部生成完再一次性返回用户的等待时间往往会超过十秒甚至更久体感很差。所以我把答案输出改成了流式Streaming模型每生成一段内容就通过SSEServer-Sent Events推给前端用户看到的是一行一行往外蹦的答案体感上快很多。Spring Boot里接入SSE常见做法是返回SseEmitter。LangChain4j的StreamingChatModel接口就是干这个的它通过TokenStream回调把增量内容吐出来GetMapping(value /api/rag/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamQuery(RequestParam String question) { SseEmitter emitter new SseEmitter(0L); executorService.submit(() - { StreamingChatModel model streamingChatModel; model.generate(promptText) .onPartialResponse(token - { try { emitter.send(SseEmitter.event().name(delta).data(token)); } catch (IOException e) { emitter.completeWithError(e); } }) .onComplete(emitter::complete) .onError(emitter::completeWithError) .start(); }); return emitter; }这里有个坑必须先说SseEmitter的线程管理。因为流式调用是异步的里面不能被Spring的事务管理和拦截器绑定比如你不应该把Transactional标在发起流式调用的方法上。另外前端断开连接时SseEmitter会抛异常这时要确保流式任务的线程也能被正确终止不然会白白浪费一个模型调用。再就是超时控制。流式接口不是一锤子买卖模型生成的时间可能很长所以SseEmitter的超时时间要设置足够大我设的是0也就是不超时但配套要做前端心跳和异常的兜底处理。如果模型API提供商有单次请求的最大时长限制这段时间也要同步调整。6. 常见问题与性能调优实录6.1 典型问题排查速查表把这个项目从头到尾跑通之后我整理了一份如果重新做一遍会直接拿来看的问题速查表正好也分享出来现象排查思路解决办法检索结果完全不相关检查Embedding模型是否统一两处不能不一致检查切分是否过粗检查query预处理是否缺失统一Embedding调整切分参数对用户问题做简单改写补充术语、移除错别字答案中大量出现幻觉检查召回文档是否真的包含答案检查Prompt是否给模型编造的空间调低召回阈值提高minScorePrompt里加强找不到就承认找不到的约束响应延迟太高区分是检索慢还是生成慢分别加日志检索慢查向量库索引类型和查询量生成慢考虑更小的模型、降低max-tokens或启用流式输出知识库更新后线上仍是旧答案检查是否有缓存机制检查向量库是否重建索引给文档加版本号更新时先删旧再插新索引重建后再对外服务同一文件重复入库检查是否存在重复灌库的入口加内容哈希去重入库前先查重上下文窗口溢出一次塞进的文档块过多或单块过大降低maxResults调小maxSegmentSize按相关度排序后只取前几条这些现象里有一些是配置问题有一些是设计问题但绝大多数问题根子都出在文档处理和检索质量这一个环节上模型反而是最后那个背锅的。出了事先查检索出来的文档片段到底有没有质量再去找模型的问题这个顺序一定不要颠倒了。6.2 检索质量调优的三板斧第一板斧还是先评价检索效果。需要一套真实问题的测试集提前准备几十个有标准答案的问题每次改完参数就跑一遍看检索结果的命中率和回答准确率。没有量化指标就只能靠肉眼感受效率太低了。这套测试集我建议你知道RAG要做的时候就开始攒业务人员手里的一些高频问题都可以加进来。第二板斧是优化切分和元数据。我之前提过按照标题和章节路径做元数据标记这在调优阶段非常有价值——当你发现某类问题老是检索不到可以顺着元数据反查这些内容在原始文档里的位置判断是内容本身缺失还是切分方式不对导致语义被破坏。第三板斧是用户的Query也需要处理。实际使用中用户输入往往很口语化比如xxx怎么设置不了而文档里写的是xxx配置失败原因排查两者字面上差很远但语义接近。可以通过Embedding本身来做这种语义匹配但也可以做一个简单的Query改写把口语化的表达改写成更偏向文档术语的表述再去做向量检索。方法不复杂但效果提升往往很直接。调了三轮之后我那个项目的答案准确率从最初的不到六成提到了稳定在八成以上。这些调优工作没有太多玄学成分就是把数据、参数、链路一步步找问题最后就能看到明显的正向结果。6.3 后续扩展建议这个项目做完之后我又在里面加了两个扩展点现在也考虑把它们做成通用的能力沉淀。一个是引用溯源就是把模型回答里的关键句子和检索到的文档片段做映射在答案后面附上具体来源和链接。这个对于企业场景特别重要因为它解决了大模型答案可信任的问题员工看到答案后面带着原始文档出处才敢直接用。另一个是权限过滤。我们知识库里有些文档是有访问权限的不能让所有人查到。这个和RAG的融合点是检索之前先根据当前登录用户的信息过滤掉无权的文档再进入向量检索。LangChain4j的检索器用起来之后加这层过滤逻辑也不复杂核心就是在组装用户输入的阶段同时传入用户的身份上下文。还有一个更远期的方向引入多路召回。单一向量检索在语义匹配上是强项但对关键词精确匹配反而未必有传统搜索好。可以把关键词检索结果和向量检索结果做融合再统一排序这样长尾的词面匹配问题就能得到缓解。这个方案我们还在实验阶段但方向已经验证得很清楚了。一些个人体会回顾这个项目我觉得最核心的一点认知是RAG系统的质量上限是由数据工程和检索质量决定的大模型只是最后那个执行器。你给模型再强的能力检索上来的材料不对答案也不会对。所以做这类项目重心一定要放在前面那段——文档处理、切分、向量化、检索调优这些环节看得见摸得着效果也都是实实在在的。如果你正准备在Spring Boot项目里接入RAG我建议第一步不要急着把各种组件选齐先拿一个最小的闭环跑通就是几十篇文档、一个Embedding API、一个向量库、一个大模型API、一段能回答问题的代码。跑通之后再去逐步优化切分、调Prompt、做流式、加权限。这样出来的系统不会一上来就被各种复杂配置拖垮也不会在第一步就因为追求完美而放弃。最后说点实际的控成本。RAG场景里向量化的费用和模型调用的费用占比都不小千万要注意不能每次提问都重新embedding一遍——一次会话里的用户问题多轮对话每轮都嵌入一次没太大必要合理的做法是对话历史主要靠模型上下文维护向量检索主要还是围绕当前这一轮的问题去做。这样既省钱回答效果也不会打折扣。这个项目后续我还在持续迭代后面有机会再把重排策略和权限过滤的详细实现单独拿出来写一篇。如果你现在也在折腾Spring Boot和LangChain4j的RAG卡在哪个环节了欢迎一起交流。

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

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

免费获取报价 →
↑