Spring Boot集成AI大模型:Java RAG检索增强生成源码设计

发布时间:2026/9/8 19:14:22
Spring Boot集成AI大模型:Java RAG检索增强生成源码设计 简介面向需要将大模型能力接入企业级Java应用的开发者这套基于Java与AI大模型的Spring项目检索增强生成(RAG)设计源码围绕“构建专家知识库并提升问答准确性”给出了可运行的工程实现。压缩包共46个文件核心为21个Java源文件辅以7张PNG图片、FTL模板、CSS样式、properties配置、Maven构建文件和命令行脚本等整体仅2.52MB结构紧凑便于对照学习与二次改造。目前已有614人学习适合具备Spring基础、希望掌握RAG落地方案的中高级工程师。源码覆盖后端逻辑、前端展示、数据处理与模型部署等环节Java代码用于实现检索与生成的主流程FTL与CSS负责界面呈现配置文件定义项目参数readme和图片素材能帮助快速理解目录结构读者可借助Maven包装器直接启动工程并通过RAG机制让大模型在回答时引用知识库内容提高结果的权威性与可用性。 最近我把公司内部一个基于Spring Boot的工单系统接入了AI大模型问答能力过程中最大的感触是很多人拿到大模型API就直接往业务里怼结果模型对内部知识一无所知回答全靠编。后来切换成RAG检索增强生成方案把企业知识库、工单历史、产品文档全部接进去效果才真正可用。这个项目标题是“基于Java和AI大模型的Spring项目检索增强生成(RAG)设计源码”核心就是解决一个非常现实的问题让大模型在回答问题之前先检索你给它准备的资料把检索结果作为上下文再生成答案而不是凭空捏造。这类的设计源码适合谁两类人最需要一类是已经在Java后端做业务开发、想在企业内部落地AI应用的技术同学另一类是面试前想搞懂RAG底层链路、想看懂Spring AI相关源码的求职者。本文我会从整体架构、核心环节、工程化落地、源码封装思路、踩坑排查五个维度展开尽量把RAG从理论到可运行的代码链路讲透并且把我实测下来的参数和取舍一并给出你可以直接拿去当参考模板。1. 项目整体架构与设计思路1.1 为什么选Spring AI而不是裸调HTTP接口很多人一开始会纠结一个问题既然大模型只有HTTP接口那我直接用RestTemplate或者OkHttp调用不就行了为什么还要引入Spring AI我的实际感受是如果只是做一个Demo裸调确实够了但一旦进入生产环境哪怕功能再简单你都需要面对几个绕不开的问题多模型切换、Prompt模板管理、向量库接入、上下文窗口控制、流式输出、可观测性。这些东西如果全部自己封装工作量相当于重新造一个轮子。Spring AI这个框架的核心价值在于它把“模型对话”和“向量检索”这两件事抽象成了统一接口。比如你想用ChatClient不管是接OpenAI还是通义千问还是本地部署的模型代码层面的调用方式几乎没有变化。对于做Java项目的人来说这种抽象带来的最大好处是业务代码不被某个具体模型厂商绑定换模型的时候不用改业务逻辑只改配置。这一点在国产模型百花齐放的当下尤其重要今天用这个模型效果好明天换成另一个更便宜、更快的你要做的只是调整Spring配置文件里的模型地址和Key。另外一个容易被忽略的点是Spring AI的自动装配机制和Spring Boot的生态天然契合。你不需要写一堆初始化代码去管理向量库连接池、Embedding模型实例直接用Spring的Bean机制管理即可。我们的项目里向量数据库和Embedding模型都注册成Spring Bean其他地方直接注入使用省去了手动关连接的隐患。1.2 RAG链路的核心环节拆解RAG这个名词听起来高大上实际上拆开之后就是一条非常清晰的数据流水线。它的本质是给大模型外挂一个知识库先检索后生成。整体链路可以划分为四个核心环节知识入库、向量化检索、重排序、增强生成。知识入库阶段做的事情是把PDF、Word、Markdown、数据库里的文本内容读出来按照一定策略切分成小块每一块经过Embedding模型变成向量写入向量数据库。向量化检索阶段做的事情是当用户输入一个问题时把问题也用同一个Embedding模型变成向量然后去向量数据库里做相似度搜索取回最相关的TopK个文本块。重排序阶段则是把召回的结果再做一次精细化排序把真正和问题相关的内容排到前面无关的过滤掉。增强生成阶段是把这些检索到的文本块和用户的原始问题拼装成一个Prompt交给大模型让模型基于检索到的资料给出答案并要求它标注引用来源。我在设计这个项目的初期犯过一个典型的错误把RAG简单理解为“问一句、搜一次、答一次”忽略了知识入库这个前半段。结果就是系统跑起来之后检索到的内容质量非常差模型经常答非所问。实际上RAG的效果上限由入库阶段决定检索只是把这个上限尽量兑现。你入库时候切分策略不合理、Embedding模型选得不合适后面再怎么调Prompt都是白费。1.3 项目模块划分与源码目录设计做源码设计的时候我强烈建议按职责边界拆模块不要让RAG相关代码和业务代码混在一起。我习惯把项目拆成五个Maven模块rag-common通用工具类、统一返回体、异常定义。rag-ingestion文档解析、切片、向量化、入库管道。rag-retriever检索服务包含向量检索、关键词检索、多路召回、重排序。rag-generatorPrompt模板管理、大模型调用、流式输出、引用标注。rag-web对外提供接口比如知识库上传接口、问答接口、检索测试接口。这种模块划分的好处是每一层的职责单一而且可以独立测试。比如你觉得召回效果不好可以直接写一个测试类调用rag-retriever模块的接口单独验证检索结果而不需要启动整个Web服务。我从实际开发体验来说这种独立模块的设计对排查问题尤其友好因为RAG的链路太长如果所有代码堆在一个工程里出了问题你根本不知道是解析的问题、向量化的问题还是模型生成的问题。2. 核心环节解析知识接入与切片策略2.1 文档解析层的选型与实现要点文档解析是RAG项目里最容易被忽视但实际最影响体验的环节。我们的项目需要支持PDF、Word、Markdown这几种常见格式。PDF解析我用的是PDFBox加自研的后处理逻辑Word用的POIMarkdown直接读源文件再做格式清理。每条经验都是踩坑踩出来的PDF用PDFBox提取出来的文本经常存在错误的换行和空格比如一段正常的中文句子被拆成好几个碎片Word文档用POI提取时要注意表格内容和正文内容的顺序不然提取出来的文本顺序是乱的Markdown要注意去掉图片链接和代码块标记否则这些符号会污染后续的切片效果。解析这个环节没有太多高深的技术核心就是耐心和脏数据处理能力。我建议在解析完成之后强制做一次文本清洗包括去掉不可见字符、合并异常换行、规范标点符号、去掉无意义的页眉页脚。我写了一个TextCleaner工具类里面按规则对文本做正则清洗效果很直观。还有一个容易被忽略的点文档的元信息一定要保留比如文档名称、章节标题、页码这些在后续生成回答时用来标注引用来源非常有用用户看到答案来自哪个文档的哪一页信任感会强很多。2.2 切片策略为什么不能一刀切按固定字数切切片是决定检索质量的命门。很多人图省事直接按固定长度切比如512个字符一刀切切完就完事。我一开始也是这么干的后来发现效果不行因为固定长度切片会丧心病狂地切断语义完整的段落。比如一个“如果客户在七天内申请退款商家需要在三个工作日内处理”的句子可能刚好被切到下一片里检索时根本匹配不到。我实测下来比较靠谱的做法是“结构优先长度兜底”。具体来说如果文档有明确的章节结构优先按章节标题切分每个二级标题下的内容作为一个最大的候选块如果某个章节太长再按照段落边界、句子边界做二次切割最后检查每个块的长度如果超过模型上下文容忍的上限比如单块超过1200个字符再强制按长度截断同时保持相邻块之间有一定重叠。下面我给出一段我项目里的切片核心逻辑用递归的方式实现结构感知切分public ListTextChunk splitDocument(Document doc) { ListTextChunk chunks new ArrayList(); // 1. 优先按一级/二级标题切出章节块 ListSections sections SectionSplitter.splitByHeaders(doc.getContent()); for (Section section : sections) { // 2. 如果章节块超长继续按段落和句子切分 if (section.getContent().length() MAX_CHUNK_SIZE) { chunks.addAll(splitByParagraph(section)); } else { chunks.add(new TextChunk(section.getTitle(), section.getContent())); } } // 3. 对相邻块增加重叠字符避免检索时遗漏边界语义 return OverlapMerger.merge(chunks, OVERLAP_CHARS); }注意这里的OVERLAP_CHARS我设为80到120个字符太少了起不到衔接作用太多了会造成大量重复内容白白浪费向量数据库的存储空间。切片之后每一块都需要带上来源元信息比如docId、title、pageNum这样检索结果可以追溯到具体文档位置。2.3 Embedding模型选型与向量化实现向量化是RAG的基石Embedding模型选不好后面全白费。我实际测试下来的经验是如果是英文场景OpenAI的text-embedding-3-small或者bge-large-en都很好用如果是中文场景强烈推荐BGE系列或者智源的text2vec-large-chinese它们的语义理解能力明显优于把英文模型直接迁移过来的效果。我项目里用的是BGE-M3模型这个模型的特点是支持中文和英文并且支持稠密检索、稀疏检索、多向量三种检索模式灵活性很强。部署方式是用本地推理服务跑起来Spring AI这边通过EmbeddingModel接口对接。如果你的机器没有GPU可以用CPU跑只是向量化速度会慢一些但入库场景通常是离线的慢一点问题不大。还有一个细节做向量化的时候问题和文档内容应当使用同一个模型而且模型版本不能随意更换。如果中途换了Embedding模型向量维度可能变了或者语义空间完全对不上之前入库的全部向量都要重新生成。我就踩过这个坑上线后调过一次模型结果没有重新跑入库任务检索结果直接变成垃圾查了很久才定位到是向量空间不一致导致。3. 检索引擎与重排序的实现细节3.1 多路召回向量检索加关键词检索的组合打法如果只靠向量检索效果在很多时候是不够的尤其是用户问题里包含明确的专有名词或者生僻词时向量检索可能召不回关键片段。比如用户问“工单编号T20240618的退款进度”这个问题里的T20240618是一个精确标识符语义向量化之后反而会被“稀释”而关键词检索能精准命中。我在项目里采用的是多路召回策略向量检索和关键词检索并行各自取得TopK结果然后合并去重。向量检索我用的是Spring AI的VectorStore抽象底层存储用的是Milvus。为什么选Milvus因为它对中文语义检索的支持比较好而且支持标量过滤比如可以按文档类型、业务线过滤后再做向量搜索这个功能在工单系统里尤其好用。检索的核心代码大致如下public ListRetrievedChunk hybridRetrieve(String query, int topK) { // 第一路向量检索 ListDocument vectorResults vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(topK) .filter(bizLine AFTER_SALE) .build() ); // 第二路关键词检索基于倒排索引实现 ListDocument keywordResults keywordSearchService.search(query, topK); // 合并去重保留分数 return MergeStrategy.mergeByDocId(vectorResults, keywordResults); }这里的MergeStrategy我采用的是RRFReciprocal Rank Fusion算法简单说就是给每一路的排名一个倒数权重再求和排序。RRF的好处是不需要归一化分数避免不同召回源的分数尺度不一致的问题。实测下来用RRF融合之后检索的召回率比单路向量检索提升了大概15%到20%这个提升幅度在RAG链路里已经算很明显了。3.2 Re-rank重排把真正的相关内容顶上去多路召回之后候选结果里会混入一些相关性一般的内容这是不可避免的。这时候就需要一个重排层来精修排序。我的做法是用一个轻量级的Cross-Encoder模型做重排比如bge-reranker-large把用户的query和每一个候选文本块拼接起来输入模型计算相关性分数然后按分数重新排序最后只取前TopK参与生成。为什么要用Cross-Encoder而不是直接用向量相似度排序因为向量检索是“先各自编码再算相似度”信息和语义在编码过程中有损失Cross-Encoder是“一起编码再打分”query和文档的交互信息更充分效果自然更好。当然Cross-Encoder的缺点是速度慢所以我只在候选集比较小的时候使用比如多路召回取回50条重排后只保留5条这个计算成本完全可控。重排这一步我强烈建议保留在RAG链路里因为它在成本和效果之间取得了非常好的平衡。你不需要每一步都让大模型去判断相关性因为大模型慢而且贵用一个专门的、小体量的重排模型做粗筛已经足够把检索质量提升一个档次。3.3 Prompt模板设计让大模型学会引用来源检索质量上来了最后一步的Prompt设计也不能拉胯。Prompt模板的核心有两点第一明确告诉模型只能基于给定的资料回答问题资料中没有的信息要直接说不知道不要脑补第二要求模型在回答中根据资料附上引用来源增加可解释性。我实际用下来的Prompt模板大致长这样你是一个企业知识库智能助手。请严格基于“参考资料”中的内容回答用户问题。 要求 1. 如果参考资料中有明确答案请直接回答并在答案末尾标注来源文档名称。 2. 如果参考资料中找不到答案请明确回复“根据现有资料无法回答该问题”不要自行编造。 3. 回答时尽量保持原文的关键信息不要过度发挥。 参考资料 {context} 用户问题 {question}这里有一个容易忽略的细节{context}里面的文本块顺序要按重排后的相关性分数从高到低排列而不是按入库顺序。因为大模型对输入顺序是有偏好的相关度高的内容放在前面模型的注意力更集中答案的准确率会更高。这个经验是我在对比实验里发现的调整顺序后同一批测试问题的准确率提升了几个百分点。4. 工程化落地Spring Boot集成与性能优化4.1 异步流水线与缓存设计知识入库是一个耗时操作文档解析加向量化一个几百KB的文档可能要几十秒如果让HTTP请求同步等待用户根本受不了。我的设计是把入库操作提交到线程池异步执行立刻返回一个任务ID给前端前端轮询任务状态。任务状态存在Redis里这样即使服务重启也能恢复部分状态。具体到线程池我用的是Spring的Async加自定义线程池配置核心线程数按CPU核心数设置队列容量根据文档大小估算。入库链路里最耗时的是Embedding模型的调用如果是远程HTTP调用一定要设置超时和重试机制如果是本地调用要注意线程池大小不能超过模型服务的并发上限否则会造成请求堆积和超时。缓存方面我做了两层缓存第一层是问题级的结果缓存短时间内相同或类似的问题直接返回之前的答案用Redis的SET NX加过期时间实现实测命中率在常见FAQ场景下超过30%第二层是向量检索结果的缓存同一个问题的检索结果在十分钟内复用避免重复调用Embedding模型节省成本。4.2 上下文压缩与Token成本控制RAG生产落地最大的坑之一就是Token成本失控。每次问答都把所有检索结果塞给大模型对话轮数一多上下文越来越长费用蹭蹭往上涨。我的做法是引入上下文压缩机制在组装Prompt之前先检查当前对话的Token占用情况如果超过阈值就把一些不太相关的历史消息压缩成摘要或者直接丢弃早期历史里跟当前问题关联度低的轮次。具体实现上我用tiktoken或jieba分词库估算Token数量估算值乘以一个系数1.2左右作为安全预算。实测下来这个方案能把单次对话的成本降下来大概40%左右。还有一个省钱的技巧检索回来的文本块如果跟问题相关性很低重排阶段就直接过滤掉宁可少喂给模型也不要喂无关内容。大模型在信息冗余的情况下表现得并不好不仅浪费Token还可能被无关信息干扰答非所问。4.3 可观测性与日志追踪RAG链路长跨了文档解析、向量检索、模型生成好几个环节出了问题如果没有可观测性排查起来非常痛苦。我在项目里引入了一套链路追踪方案简单但有效每一个问答请求生成一个traceId通过MDC贯穿所有日志打印日志里记录每个环节的耗时和关键参数比如解析耗时、切片数量、召回数量、重排后的分数、模型生成耗时和Token消耗。实际排查的时候这个日志的价值简直救命。举个真实例子线上反馈某个问题的答案质量变差了我看日志发现该问题的向量检索返回的Top5结果里相关性分数全部低于0.3说明知识库里没有相关内容或者是Embedding模型服务出了问题。于是直接定位到是知识库缺少对应文档而不是Prompt的问题几分钟就处理完了不用瞎猜。5. 源码层面的几个关键封装设计5.1 统一RAG服务接口的抽象整个项目对外暴露的核心接口我设计得尽量简洁让上层业务用起来像调用普通Service一样。核心接口就一个方法public interface RagChatService { RagResponse ask(String userId, String question, String bizLine); StreamRagResponse askStream(String userId, String question, String bizLine); }ask方法走全链路返回完整答案askStream方法用于流式输出适合Web前端打字机效果。这里面的bizLine参数做业务线隔离不同业务线的知识库互相独立避免数据串味。这种抽象的好处是业务方不需要关心RAG内部是怎么检索、怎么增强的只关心输入输出代码侵入性很小。5.2 向量存储适配层向量数据库的可替换性也是我刻意保证的。项目初期我用的Milvus后来在另一个环境里想用PGVector试试如果代码里到处是Milvus的API替换成本极高。所以我定义了一个VectorStorePort接口把保存、删除、相似度搜索三个操作抽象出来Milvus和PGVector各自实现一个适配器通过Spring的ConditionalOnProperty按配置加载对应实现。这个设计对源码阅读者来说也友好大家看代码的时候只需要关注接口定义不用担心底层实现细节。实际替换的时候我只需要改配置文件里的vector.store.type不用改任何业务代码。5.3 配置化驱动不同环境不用改代码我把所有跟环境相关的参数全部外置到application.yml里包括模型地址、模型名称、向量维度、TopK值、相似度阈值、重排开关、缓存策略、线程池大小。这样同一个代码包在测试环境接测试模型、测试向量库在生产环境接生产模型、生产向量库不用改一行Java代码。这一点在微服务或容器化部署场景下尤其重要因为构建产物一般是不可变镜像配置应该跟环境走而不是跟代码走。6. 踩坑记录与常见问题排查6.1 中文检索效果差问题出在哪里这是最多人问的问题。我排查过的案例里中文效果差的原因基本集中在几个地方Embedding模型对中文支持不好、切片时把中文句子切成碎片、向量化文本里混入了大量英文标点和HTML标签。解决办法就是换支持中文的Embedding模型、用结构感知的切片策略并且入库前做彻底清洗。还有一个偏方将问题和文档内容在向量化之前做一次轻量的简繁转换或同义改写在某些领域数据集上有奇效但在通用场景下不推荐因为会引入额外误差。6.2 向量库连接池耗尽导致接口超时导出的表现是系统运行一段时间后问答接口突然超时日志里看到获取连接超时的异常。原因通常是向量数据库的客户端连接池配置太小或者某个环节忘记归还连接。排查后发现是我在异步入库线程里每次创建新客户端没有复用Spring容器里的单例Bean。修正方案很简单把客户端注册成单例Bean连接池大小按并发量调整到50到100之间并设置合理的空闲回收时间。6.3 上下文超长导致大模型报错这个问题的直接原因是检索回来的内容加上历史对话超过了模型的最大上下文窗口。解决思路有两个方向一是减少喂给模型的内容比如缩小TopK、加强重排过滤、压缩历史消息二是升级上下文更长的模型。我的建议是优先做减法因为无论上下文窗口多大喂无关内容都会降低回答质量而且成本更高。6.4 模型总是“胡说八道”怎么处理RAG落地后如果模型还在胡说八道多半不是模型的问题而是检索的内容里有误导信息或者Prompt里没有强约束。我的经验是先在检索侧查一遍召回结果看看给模型提供的参考资料是不是包含正确答案如果资料里没有答案那就是知识库覆盖不全需要补数据如果资料里有答案而模型仍然答错再检查Prompt里的约束语句是否足够强硬。顺序一定不能反先查检索再查生成因为RAG的瓶颈90%都出在检索环节。按照这个方法处理过几个案例之后系统的“幻觉”率从最初的百分之十几降到了百分之三左右这个水平在大多数企业知识问答场景里已经可以接受了。最后再分享一个我个人的体会RAG项目不要迷信大而全的框架也不要一上来就追Agentic RAG、GraphRAG这些新概念先把“入库-检索-重排-生成”这条主干链路用确定性最高的方式跑通效果稳定后再逐步叠加路由、多轮改写、Query分解等高级特性。源码设计也是一样模块边界清晰比用最新框架更重要毕竟项目是给人维护的可读性和可扩展性才是长期价值。本文还有配套的精品资源点击获取