news 2026/10/19 2:32:33

SpringBoot整合LangChain4j:RAG知识库问答实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot整合LangChain4j:RAG知识库问答实战

去年年底我接了一个内部知识库改造的活儿,几百篇中文技术文档散落在系统里,业务同事天天在群里问“XX模块的配置项在哪个文档里写过”。原来的实现是老一套的关键词搜索,稍微换个问法就翻不到东西。后来我用 LangChain4j 把 RAG 链路接进了 SpringBoot 服务,才真正把“找文档”变成“直接回答”。这篇是 LangChain4j 系列的第二篇,围绕 SpringBoot 整合 RAG 展开,适合已经会 SpringBoot 基础、想在 Java 生态里落地大模型应用的开发者。

我会把整条链路拆开讲,包括数据摄取、向量化、检索、生成这几个核心环节,也会把我真实踩过的坑和调优思路写出来。这篇不会只是配置堆砌,重点放在“为什么这样设计”和“效果不对时怎么排查”上。

1. RAG 架构拆解:先搞清楚检索与生成的边界

1.1 为什么单靠大模型不够用

很多人刚开始做知识库问答时,第一反应是把所有文档塞进 prompt 里,让大模型“读完”再回答。这个方法在小规模场景下可行,但一旦文档量上来就崩了,原因有两点:

第一,大模型的上下文窗口有限,即便某些模型宣称支持超长上下文,实际塞入几十万字后,模型对中间内容的注意力会明显下降,回答质量会从“准确引用”退化成“模糊概括”。第二,大模型的知识有截止时间,它没办法知道你们公司上周新发布的内部规范,更不可能知道你上传到系统里的私有资料。

打个比方,大模型像一个记忆力很强但没看过公司档案的员工,你直接问他业务细节,他只能凭常识瞎猜。RAG 做的事情,是先派一个检索员去档案室把相关文件抽出来,再把文件放到这个员工面前让他照着念。这样既解决了知识时效性问题,也避免了模型不懂装懂。

1.2 RAG 管线的五个关键环节

一条完整的 RAG 链路可以切成五段:文档摄取、文本切分、向量化入库、相似度检索、生成回答。

文档摄取负责把 PDF、Word、Markdown 等不同格式的源文件解析成纯文本。文本切分解决的是“长文档如何拆成适合检索的小块”的问题,这一步直接决定了后续检索能不能命中。向量化入库是把每块文本通过 Embedding 模型转换成高维向量,存储在向量数据库里。相似度检索是把用户问题转成向量后,在库里找出最相近的几个文本片段。最后一步生成,是把检索到的片段和用户问题一起交给大模型,让它基于这些片段组织回答。

这里面最容易被人忽视的是第一步和第二步。很多人把精力花在选模型上,结果数据没处理好,检索出来的内容本身就不相关,再强的生成模型也救不回来。这一点后面我会详细展开。

1.3 LangChain4j 在管线里的定位

LangChain4j 之于 Java 生态,相当于 LangChain 之于 Python 生态,它把上面五个环节抽象成了统一的 Java API。在这个框架里,EmbeddingModel 负责向量化,EmbeddingStore 负责向量存储,DocumentSplitter 负责切分,ContentRetriever 负责检索,ChatLanguageModel 负责生成。每一层都支持替换实现,你在本地调试时可以先用内存版,上线后换成生产级向量数据库,代码改动量很小。

对 SpringBoot 开发者来说,LangChain4j 最大的价值是它的 Spring Boot Starter 把 Bean 装配全部托管了。你只需要在配置文件里声明模型服务商的参数,框架会自动帮你创建好 EmbeddingModel、ChatLanguageModel 等对象,注入到你的业务代码里。相比在 Python 里手动拼链路,Java 这边用起来清爽得多。

管线环节LangChain4j 核心接口说明
文档加载Document / DocumentParser负责把源文件解析成 Document 对象
文本切分DocumentSplitter将长文档拆成多个 TextSegment
向量化EmbeddingModel调用模型服务将文本转为向量
向量存储EmbeddingStore保存向量并支持相似度查询
检索ContentRetriever将问题转向量后召回相关片段
生成ChatLanguageModel基于检索结果生成最终回答

2. SpringBoot 整合:从搭依赖到跑通第一条问答链路

2.1 依赖引入与版本选择

我建议直接使用官方提供的 Spring Boot Starter,它会自动配置大部分组件。核心依赖如下:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> <version>1.x</version> </dependency>

如果你还需要特定向量库的支持,比如要用某个开源的关系型数据库向量扩展,就再引入对应的模块依赖。项目里尽量用 Maven BOM 管理版本,避免各个组件版本不一致导致的兼容问题。

这里有个心得:Spring Boot 的版本不要追太高,LangChain4j 对 Spring Boot 版本有一定要求,我用的是 Spring Boot 3.x 的稳定版本,整体兼容性没问题。如果你还在用 2.x,需要确认对应 Starter 的兼容范围,否则会遇到自动配置不生效的问题。

2.2 配置项:三条核心链路参数

以当前主流的配置方式为例,你的 application.yml 里会有几个关键配置块:

langchain4j: chat-model: provider: 某云模型服务商 api-key: ${MODEL_API_KEY} model-name: 对话模型名称 temperature: 0.2 embedding-model: provider: 某云模型服务商 api-key: ${MODEL_API_KEY} model-name: 文本向量模型名称 vector-store: provider: 某个开源向量库 host: localhost port: 5432 database: knowledge_db user: ${DB_USER} password: ${DB_PASSWORD} table: document_chunks dimension: 1024

几个参数重点说明一下:

temperature 建议调低。知识库问答场景要求回答忠实于检索内容,temperature 太高会让模型自由发挥,产生幻觉。我一般设置在 0.1 到 0.3 之间。

embedding 模型的维度必须和向量库表结构一致。很多向量库建表时需要指定向量维度,如果模型输出是 1024 维,你建了 768 维的字段,数据写入直接报错。所以在确定模型后,先查清楚它的输出维度再建表。

同一个项目中,召回和入库必须使用同一个 Embedding 模型。这是新手最容易犯的错——入库用一个模型,检索时换了另一个,两者的向量分布空间完全不同,相似度计算结果毫无意义。

2.3 数据摄取:把文档变成可检索的文本块

数据摄取是整个 RAG 链路里最脏最累的活,但也是回报率最高的部分。我通常的做法是这样的:

@Configuration public class IngestionConfig { @Bean public EmbeddingStore<TextSegment> embeddingStore() { // 生产环境换成对应的向量库实现 return new InMemoryEmbeddingStore<>(); } @Bean public EmbeddingStoreIngestor embeddingStoreIngestor( EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { return EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 80)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); } }

然后写一个摄取服务,把文档内容转换成 Document 对象并写入向量库:

@Service public class KnowledgeIngestService { private final EmbeddingStoreIngestor ingestor; public KnowledgeIngestService(EmbeddingStoreIngestor ingestor) { this.ingestor = ingestor; } public void ingestDocument(String docId, String title, String content) { Document document = Document.from(content); document.metadata().put("docId", docId); document.metadata().put("title", title); document.metadata().put("ingestTime", LocalDateTime.now().toString()); ingestor.ingest(document); } }

切分参数切分块大小 500 字符、重叠 80 字符是我在中文文档场景下试出来比较稳的组合。切分块太大,单块包含多个主题,检索到的内容含大量噪音;太小则单块信息量不足,模型无法给出完整答案。

重叠区域的作用是防止关键信息正好落在两个切块的边界上被截断。这个参数需要在召回率和准确率之间做平衡,重叠越大,信息越不容易丢,但向量库里会有更多冗余内容,检索时可能出现重复片段。

2.4 问答链路:让模型只基于检索结果说话

数据入库之后,问答链路就简单很多了。LangChain4j 的 AiServices 提供了一种接口即服务的开发方式,你定义一个接口,框架自动为你生成实现:

public interface KnowledgeAssistant { @SystemMessage(""" 你是一名企业知识库助手。请严格基于给定的资料内容回答问题。 如果资料中没有相关信息,请直接说“资料库中未找到相关信息”,不要编造。 回答时尽量引用资料中的原文,并标注来源标题。 """) String answer(@UserMessage String userMessage); }

接着在配置类里装配:

@Configuration public class AssistantConfig { @Bean public KnowledgeAssistant knowledgeAssistant( ChatLanguageModel chatModel, ContentRetriever contentRetriever) { return AiServices.builder(KnowledgeAssistant.class) .chatLanguageModel(chatModel) .contentRetriever(contentRetriever) .build(); } @Bean public ContentRetriever contentRetriever( EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore) { return EmbeddingStoreContentRetriever.builder() .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .maxResults(5) .minScore(0.65) .build(); } }

这里最关键的配置是 maxResults 和 minScore。maxResults 表示最多召回多少个文本片段给模型,minScore 是相似度阈值,低于这个值的片段会被过滤掉。

我实测下来的经验是:maxResults 设置在 4 到 6 比较合适,太少可能漏信息,太多会让模型在多个不相关片段里迷失。minScore 需要根据你的 Embedding 模型和业务数据实际调试,不同模型的相似度分数分布差异很大,有些模型的命中分数普遍在 0.7 以上,有些则在 0.5 附近波动。

SystemMessage 里的提示词也很重要。我明确要求模型“资料中没有就直说”,这个约束能大幅减少幻觉。很多初版 RAG 效果差,不是检索有问题,而是模型检索到了相关片段后,仍然忍不住拼上自己训练时见过的不相关内容。

3. 检索质量调优:真正决定 RAG 上限的隐性细节

3.1 切分策略:先按结构切,再按长度切

我在第一节就说过切分决定召回质量,这里展开聊。很多人用固定字符数暴力切分,比如不管什么文档都是“200 个字符一块”。对小短文没问题,但遇到有层级结构的 Markdown 文档、带章节标题的技术手册,这种切法会把完整的知识单元拦腰截断。

我现在的做法是两级切分:先按文档的章节标题切成大段,再按段落把大段切成小块。这样做的好处是,每一小块天然属于同一个主题,检索到它时,模型看到的上下文是连贯的。比如一篇技术手册里有“安装配置”和“故障排查”两个章节,暴力切分可能把“安装配置”的最后两行和“故障排查”的前两行拼进同一块,模型看到这个混合块,回答必然混乱。

框架里内置了多种切分器,我用得最多的是递归式切分器。它会按优先级尝试不同的分隔符切分,优先保段落完整,段落太长才按句子切。

在实际项目里,我会从源头把控文档格式。能拿到 Markdown 源文件就优先用 Markdown,它天然带标题结构,切分器可以按标题层级定位边界。只有 PDF 和 Word 时,就得依赖解析质量了,这一步没有捷径,每个文档类型都要单独验证解析结果。

3.2 topK 与相似度阈值:两个参数必须搭配调

调参时我见过太多人只调 topK,或者只调 minScore,这其实是不够的。两个参数起到的作用不同:

  • maxResults(topK)决定“最多给模型几块内容”。调大它,召回率上升但准确率下降,模型可能被不相关内容干扰。
  • minScore(相似度阈值)决定“低于多少分的直接不要”。调高它,准确率上升但召回率下降,可能导致该搜到的没搜到。

正确的调参顺序是:先把 minScore 调低,让所有潜在相关内容都进得来,观察召回内容是否覆盖了答案所在片段,再逐步调高 minScore 剔除噪声,最后根据 maxResults 看到的重复概率确定最终值。

这里有一个陷阱:相似度分数不是“真理”。它表示的是向量方向上的接近程度,不是内容上的逻辑相关性。两个文本讨论同一个话题但观点相反,向量相似度可能很高。所以 threshold 应该理解为“粗糙过滤器”,真正的内容筛选靠的是切块质量和召回数量。

3.3 混合检索与重排序:向量检索不是万能的

纯向量检索有两个硬伤:第一,它容易忽略精确关键词。用户问“怎么配置 Redis 连接池”,如果文档里写的是“Lettuce 连接参数”,语义上相关,向量检索能命中;但用户问“错误码 10086”,文档里只出现过一次“10086”,向量检索可能因为上下文太少而排到很后面,关键词检索反而能直接命中。第二,向量检索对代码片段和专有名词不敏感,这些内容往往语义信息弱,但字符信息强。

所以生产级方案一定是混合检索:向量检索负责找语义相近的内容,BM25 这类关键词检索负责找字面匹配的内容,两路结果合并后,再用一个重排序模型做精排。

重排序的原理不复杂:把召回的候选片段和用户问题两两拼接,让一个专门的模型重新打分,这个分数通常比单纯的向量相似度更贴近用户真实意图。它的代价是多一次模型调用,但换来的是回答质量肉眼可见的提升。

如果你不想上一套完整的混合检索,最简单的优化是在提示词里要求模型“严格基于给定材料回答”,同时把召回结果按相似度重新排序后塞给模型,这也能缓解一部分排序不准的问题。

3.4 元数据过滤:把“全库检索”变成“定向检索”

很多知识库里的文档是有属性的:所属部门、文档类型、更新时间、业务线。如果不加过滤,用户问“我们部门的报销流程”,系统会把其他部门甚至其他公司的相关资料也召回来。

元数据过滤的思路是在入库时把属性写进 metadata,检索时按条件过滤。比如召回时只看特定部门或特定文档类型的片段:

EmbeddingSearchRequest.builder() .queryEmbedding(embedding) .maxResults(5) .filter(new Filter.And( new Filter.Eq("department", "技术部"), new Filter.Eq("docType", "操作手册") )) .build();

这里需要提醒的是,metadata 的过滤条件必须和入库时写入的 key 完全一致,包括大小写和数据类型。我之前踩过一个坑:入库时写的是字符串类型的时间字段,过滤时用了时间对象去匹配,结果全部被过滤掉了,返回为空,排查了半小时才发现是类型不匹配。

4. 高频踩坑:这些问题我基本都遇过

4.1 回答和资料对不上?先查召回内容

遇到“回答离谱”“回答和资料库内容不符”这类问题,我建议先别查大模型,先查召回。

操作方法是:把用户问题直接拿去调用检索接口,打印出召回的片段内容和相似度分数。如果召回片段本身就不相关,那么模型再聪明也没用。如果召回相关但回答错误,问题才可能在提示词或模型本身。

现象优先排查方向典型原因
回答内容与资料无关召回结果切分不当、阈值过低、向量模型不匹配
召回到相关内容但回答不准提示词与生成提示词未强制要求基于资料回答
完全检索不到任何内容数据摄取文档未入库、metadata 过滤条件错误
回答内容含糊重复topK 与切分召回片段太碎、重叠区域过大
相似度分数普遍很高向量模型向量维度低、模型区分度不够

4.2 中文文本被切碎,向量质量严重下降

默认切分器是按英文标点和空格来识别边界的。中文没有空格分词机制,如果不自定义分隔符,一个完整的中文段落可能被硬切成几个半截句子,向量质量直线下降。

我的解决办法是给切分器明确传中英文分隔符:

DocumentSplitters.recursive(500, 80, List.of("\\n\\n", "\\n", "。", "!", "?", ";", ",", " ", ""));

分隔符列表的顺序也有讲究,框架会按优先级尝试,先按双换行切,不行再按单换行,再按句号切。中文场景把句号、问号、感叹号放在英文逗号前面,能最大程度保住语义完整性。

4.3 更换 Embedding 模型后,旧向量全部失效

不同的 Embedding 模型输出的向量维度可能不同,即使维度相同,向量空间分布也不同。直接换模型会导致旧数据向量和新的查询向量“鸡同鸭讲”,检索结果完全错乱。

这个问题没法热修复,必须全量重建向量库。我的建议是:在上线前就确定好 Embedding 模型,最好选一个在中文场景表现稳定、社区验证较多的模型,后续不要轻易换。如果实在要换,准备一个重建脚本,把源文档重新跑一遍摄取流程,而不是只重新向量化旧向量。

4.4 数据更新了,检索结果却没变化

RAG 系统上线后,文档库是持续更新的。很多团队只做了首次全量导入,后续增量更新没有接入,导致用户问新内容时永远检索不到。

我的习惯是把摄取流程封装成一个幂等接口:同一份文档重复提交不会产生重复向量。具体做法是每次摄取时先按 docId 删除旧向量,再写入新向量。这样既能保证新旧文档都有,又避免向量库里堆积大量过期版本。

还有一个容易被忽略的点:删除旧向量时要用同样的切分规则定位,否则删不干净,会出现旧数据残留。

4.5 一次完整的排查实录:从“答非所问”到定位根因

拿一个我实际遇过的案例说明:某次测试问“服务器内存占用过高怎么处理”,系统回答了一堆跟容器部署相关的内容,明显答非所问。

我按下面的顺序排查:

第一步,直接打印召回结果,发现召回的 5 个片段里,有 3 个讲的是容器调度,1 个讲的是日志采集,只有 1 个和内存排查沾边。这说明不是生成的问题,是召回就被带偏了。

第二步,检查检索阈值,minScore 设置成了 0.5,偏低了。内存占用相关文档的分数在 0.6 左右,但容器调度的片段分数也有 0.55,所以一起进了候选。

第三步,把 minScore 调到 0.65,同时把切分块从 300 字符调整到 500 字符,因为内存排查的步骤是一个完整操作序列,切太碎会导致上下文信息不足。

调整后再测试,召回的 5 个片段集中在内存排查相关文档上,回答恢复正常。

4.6 效果评估:不要用“感觉还行”来验收

RAG 系统最怕的是“试了几个问题感觉差不多”就上线。主观感受很容易被几个精心挑选的测试问题误导。

我会准备一个评估集,包含至少 30 到 50 个真实业务问题,每个问题标注正确答案来源文档和期望的答案要点。每次调整切分参数或检索策略后,用同一批问题回归测试,记录两个指标:召回命中率(标准答案是否出现在召回片段中)和忠实度(生成回答是否只基于召回内容)。

如果召回命中率低,问题在切分和检索;如果命中率高但忠实度低,问题在提示词和生成。这个评估集能让每次改动都有明确结论,而不是“好像好了一点”。


做了几个 RAG 项目之后,我最大的体会是:这类系统的成功不取决于大模型本身有多聪明,而是取决于数据处理的精细程度和检索策略的合理性。切分是否保留了完整语义、阈值是否调到了业务数据的合适位置、召回内容是否真正覆盖了答案,这些远比纠结用哪个对话模型重要。每次效果不对,先怀疑数据和检索,再去怀疑模型,这个排查顺序能帮你省下大量时间。如果你也正在用 LangChain4j 做知识库问答,希望你少踩我踩过的这些坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:31:30

无向量推理式RAG:用倒排索引与逻辑页实现精准文档检索

去年有段时间&#xff0c;我被一个看似普通的问题折腾得够呛&#xff1a;内部知识库问答机器人&#xff0c;用户问“设备采购审批的截止时间是每月几号”&#xff0c;系统抓回来的却是整整三页的《采购管理制度》开篇介绍。理由很简单——那段话里有“采购”“审批”“制度”&a…

作者头像 李华
网站建设 2026/10/19 2:31:11

2026年AI工具实战指南:从选型到工作流与安全

2026年市面上能被称得上“主流”的AI工具&#xff0c;和2024、2025年相比又是一轮洗牌。去年大家还在为了“哪个模型写代码更强”吵架&#xff0c;今年讨论的已经变成“怎么把一堆模型串成一条能自动跑业务的工作流”。我大概是2024年下半年开始重度依赖AI工具做内容生产和自动…

作者头像 李华
网站建设 2026/10/19 2:31:11

足球篮球API接入实战:字段映射与数据标准化全攻略

作为一个常年跟体育数据打交道的开发者&#xff0c;我发现很多人一上来就找足球API接口、篮球API接口&#xff0c;结果不是被字段搞晕&#xff0c;就是被数据延迟坑到怀疑人生。说实话&#xff0c;这类接口的水远比想象中深&#xff1a;光是比赛状态就有十几种写法&#xff0c;…

作者头像 李华
网站建设 2026/10/19 2:30:55

企业级AI Agent落地:多表查询、多文档检索与多轮问答的RAG实践

企业里做AI助手&#xff0c;最容易翻车的地方不是模型不够聪明&#xff0c;而是把“聊天机器人”当成了“知识库问答”来验收。我参与过好几个企业级项目&#xff0c;最后发现核心需求翻来覆去就是三件事&#xff1a;能从多个数据库表里查数&#xff0c;能从一堆文档里找答案&a…

作者头像 李华
网站建设 2026/10/19 2:29:36

从0到1打造健康饮食推荐App:规则引擎、标签体系与Android架构

前阵子我做了个安卓端的健康养生饮食推荐系统App&#xff0c;从需求调研、架构设计到开发上线一路走下来&#xff0c;发现这类项目比想象中要复杂得多。表面上看就是个"推荐菜谱"的工具&#xff0c;但真落地的时候&#xff0c;用户健康档案怎么建、食材营养数据从哪来…

作者头像 李华