Spring AI 全家桶我把 RAG 这口锅从生米煮成熟饭做 Java 后端的朋友应该都有感觉这半年 AI 的热度快把咱们的老家底给掀翻了。Python 那边 LangChain、LlamaIndex 玩得飞起我们这边能拿得出手的东西一直不多。直到 Spring AI 出来总算是把 Java 生态在 AI 应用开发这块的短板补上了一大截。我自己在几个项目里把 RAG检索增强生成这套链路完整跑了一遍从最开始的文件解析、向量化到后面的 Prompt 增强、上下文召回再到调优阶段各种看似正常但结果就是不对的玄学问题踩坑踩到怀疑人生。这篇文章我就把 Spring AI 里 RAG 实践的全过程拆开揉碎了讲一遍重点不是原理复述而是工程落地上那些实打实的选型、参数、坑点和调优思路。如果你正准备在自己的服务里接入私有知识库问答或者文档辅助写作这类能力这篇文章应该能帮你少走很多弯路。1. 技术选型为什么是 Spring AI 而不是自己拼轮子1.1 先说结论Spring AI 解决的是最后一公里问题很多团队一开始接触 RAG都会下意识地想这不就是文档切分 Embedding 向量检索 拼 Prompt 调大模型吗看起来每个环节都不复杂可一旦真上手就会发现问题全在细节里。比如文档格式千奇百怪PDF 里扫描件和文字版处理方式完全不一样比如向量库选型看着很多但和 Spring 现有的事务、缓存、配置体系能不能顺畅集成又是另一回事再比如同样一个 PDF用不同的解析方式切出来的文本质量天差地别直接影响后面回答的准确率。Spring AI 给的并不是什么革命性的新算法而是一套从模型接入到向量存储再到RAG 流程编排的标准化抽象。它把 OpenAI、通义千问、Ollama 这些模型供应商的差异统一到了ChatClient接口后面把各种向量库统一到了VectorStore接口后面。这意味着你业务代码里只需要面向接口编程换模型、换向量库都是配置层面的改动而不是代码层面的重写。这个设计思路对 Java 后端来说特别重要。我们不像 Python 团队那样可以随手把各种脚本串起来Java 项目讲究的是规范、分层、可维护性。Spring AI 给出了一整套符合 Spring 生态习惯的解决方案让 RAG 能力可以作为一个模块干净地嵌进现有业务系统而不是变成一座孤岛。1.2 向量库选型对比与实测感受我在对比阶段把主流几种向量库都接了一遍说说我自己的实测感受。Spring AI 官方支持的范围挺广包括 Pinecone、Milvus、Redis、PGVector、Chroma、Qdrant 等等还额外支持 Azure 的向量检索服务。如果你团队里已经用了 Redis而且数据量不大几百万条以内直接用 Redis 的向量搜索是最省事的。Spring AI 对 Redis 的封装做得比较完善索引管理和相似度搜索都开箱即用运维成本几乎为零。但 Redis 的向量搜索在高并发、大数据量场景下性能上限比较明显而且内存成本偏高。如果对性能要求比较高或者后面有扩展的打算我建议关注 Milvus 和 Qdrant 这两个。Milvus 更适合大规模生产环境集群能力成熟还支持混合检索——先按标量字段过滤再走向量相似度。Qdrant 我比较喜欢它的过滤器和 payload 机制很多场景下可以省掉一层后处理逻辑。PGVector 适合那种不想引入额外中间件的团队直接在现有 PostgreSQL 上扩展。Spring AI 对 PGVector 的支持也比较稳如果你业务数据本身就在 PG 里这绝对是最省心的方案。我的建议是中小型项目用 Redis 或 PGVector大型项目或者对检索质量要求比较高的用 Milvus。不需要在选型上过度纠结因为 Spring AI 的抽象层已经把这个切换成本降到很低了关键是先把链路跑通。2. 核心链路设计一个可落地的 RAG 服务应该怎么拆2.1 RAG 链路全景拆解一个完整的 RAG 服务本质上是一条数据流水线加一个查询处理逻辑。数据流水线负责把原始文档处理成可检索的向量数据查询逻辑负责把用户的问题转成检索条件然后把检索结果喂给大模型生成回答。在 Spring AI 里这条链路可以被清晰地分成五个核心环节文档加载从不同的数据源读取原始内容可能是本地文件、网络 URL、数据库记录甚至是一个经过 OCR 处理的图片文本。文档切分把长篇文档切割成适合向量化和检索的文本块。切分粒度直接决定了召回质量。向量化用 Embedding 模型把文本块转换成高维向量让语义相似的文本在向量空间里距离更近。向量存储把生成的向量和原始文本、元数据一起写入向量数据库。检索增强生成用户提问时先把问题转成向量进行相似度检索取回 TopK 相关文档片段然后和原始问题一起组成 Prompt 发送给大模型。这五个环节里前四个是离线流程最后一个是在线流程。Spring AI 对离线流程提供了比较完整的工具类支持对在线流程则抽象出了ChatClient加VectorStore的组合使用方式。2.2 明确业务边界你的 RAG 需要哪种形态在做技术实现之前我建议你先想清楚一个问题你的 RAG 是给谁用的要做到什么程度这个听起来有点虚但实际工程中非常关键。如果你是做一个内部知识库问答那核心诉求是准确性和可控性需要比较严格的检索筛选逻辑甚至要考虑多轮对话中的上下文管理。如果你是做智能客服那核心诉求除了准确之外还特别看重响应延迟和并发能力可能需要引入缓存机制和异步处理。如果你是做辅助写作或者内容生成那召回结果的多样性和排序策略可能比单纯的相关性更重要。不同的业务形态会直接影响你的切分策略、检索参数、Prompt 模板设计甚至影响向量库选型。我在一个模拟项目 X 里先做了一版通用 RAG结果发现不同业务场景对参数的要求完全是两个极端——写摘要的时候希望召回粒度大一点做问答的时候希望召回内容更精准。后来我把配置做成了可动态调整的每个业务线可以单独指定自己的切分大小和检索参数才算真正满足需求。3. 实操落地Spring AI RAG 完整实现流程3.1 引入依赖与基础配置第一步在pom.xml里引入 Spring AI 的相关依赖。以 Spring Boot 3.3 为例首先需要引入 BOM 来统一版本管理dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后根据你要用的模型供应商和向量库添加具体依赖。比如我要用 OpenAI 的模型同时接 Pinecone 向量库可以这样加dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pinecone-store-spring-boot-starter/artifactId /dependency如果你是在国内环境或者不想直接调外部 API可以把模型供应商换成 Ollama 或者通义千问依赖和配置方式大同小异Spring AI 把这块抽象做得比较一致。接着在application.yml里配置相关参数spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.3 vectorstore: pinecone: api-key: ${PINECONE_API_KEY} environment: gcp-starter index-name: demo-index namespace: demo-namespace top-k: 4 similarity-threshold: 0.7这里我想强调一个细节temperature在 RAG 场景下建议设置得低一点。RAG 的定位是基于给定的知识内容回答问题而不是让模型自由发挥所以温度太高容易导致模型在检索到的内容之外自行编造信息。我一般设置在 0.2 到 0.4 之间。3.2 文档加载不只是 File 到文本Spring AI 提供了一系列DocumentReader实现包括PagePdfDocumentReader、ParagraphPdfDocumentReader、TikaDocumentReader、JsonDocumentReader等。其中TikaDocumentReader是个通用方案底层集成 Apache Tika可以从 PDF、DOCX、PPT、HTML、甚至邮件文件中提取文本内容适合处理混合格式文档。Configuration public class RagConfig { Bean public VectorStore vectorStore(PineconeVectorStore pineconeVectorStore) { return pineconeVectorStore; } Bean public DocumentReader documentReader() { return new TikaDocumentReader(); } public ListDocument loadDocuments(Resource resource) { DocumentReader reader new TikaDocumentReader(resource); return reader.get(); } }这里有个小坑提醒一下TikaDocumentReader对扫描版 PDF 是无能为力的它只能提取文字层的内容。如果文档是图片扫描件你需要先接 OCR比如 Tesseract把图片转成文字再喂给后续链路。我在一次某用户手册数字化项目中就是没注意这个结果前 20 页纯图片的内容全被跳过了检索出来一堆空白。3.3 文档切分参数选择比想象中重要切分是整个 RAG 链路里最容易被低估的一环。TokenTextSplitter是 Spring AI 提供的默认切分器它基于 Token 数而不是字符数进行切分对中文内容的适配性也比按字符硬切好一些。基础用法如下TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(800) .withChunkOverlap(150) .build(); ListTextChunk chunks splitter.split(document);chunkSize是每个文本块的最大 Token 数chunkOverlap是相邻文本块之间的重叠 Token 数。这两个参数直接影响后续的检索质量。选得太小单个块可能装不下一段完整的意思语义被切碎选得太大每个块包含的信息太杂检索时会返回很多不相关的内容而且会增加向量化成本。我实际测试下来的经验是通用知识类文档 800 到 1000 Token 比较合适代码类或者操作步骤类文档可以适当缩小到 400 到 600。chunkOverlap一般设置为chunkSize的 15% 到 20%这样能保证语义信息在两个块之间不会因为切断而丢失。这里还要特别注意一个点TokenTextSplitter默认使用的是 GPT 的 Tokenizer 来做切分对中文来说一个汉字大概对应 1 到 2 个 Token。所以如果你的文档是纯中文chunkSize: 800实际上能容纳的字符数大概在 400 到 800 之间这是很正常的不要慌。3.4 写入向量库元数据是你的好朋友文档加载、切分完成后需要先通过 Embedding 模型把文本块转成向量然后写入向量库。Spring AI 的VectorStore接口把这一步封装得非常简洁public void buildKnowledgeBase(Resource resource) { DocumentReader reader new TikaDocumentReader(resource); ListDocument documents reader.get(); TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(800) .withChunkOverlap(150) .build(); // 为每个文档添加元数据方便后续过滤 for (Document doc : documents) { doc.getMetadata().put(source, resource.getFilename()); doc.getMetadata().put(category, user-manual); doc.getMetadata().put(timestamp, Instant.now().toString()); } ListDocument chunks splitter.apply(documents); vectorStore.add(chunks); }这里我想重点说下元数据的使用。很多人用向量库时只关注文本和向量本身忽略了元数据的价值。实际上在真实业务场景里元数据几乎是必用的。比如你需要按文档来源过滤、按业务线隔离、按时间范围限制这些都可以通过元数据配合向量库的预过滤来实现能大幅提升检索准确率和性能。Spring AI 的PineconeVectorStore等实现会把Document里的metadata映射到向量库的 payload 或 metadata 字段这样在查询的时候就可以做结构化过滤。这个机制在后面调优阶段非常有用。3.5 在线检索增强核心流程代码与参数解读知识库构建好之后就到了在线问答环节。核心流程是把用户问题转成向量查询从向量库检索 TopK 相关文档把文档内容和用户问题一起组装成 Prompt 发给大模型。代码如下Service public class RagChatService { private final ChatClient chatClient; private final VectorStore vectorStore; public RagChatService(ChatClient chatClient, VectorStore vectorStore) { this.chatClient chatClient; this.vectorStore vectorStore; } public String ask(String question) { // 1. 向量检索 ListDocument documents vectorStore.similaritySearch( SearchRequest.builder() .query(question) .topK(4) .similarityThreshold(0.65) .build() ); // 2. 组装上下文 String context documents.stream() .map(Document::getText) .collect(Collectors.joining(\n\n-----------\n\n)); // 3. 构造增强 Prompt String promptTemplate 你是一个智能助手。请严格基于以下给定的资料内容回答用户的问题。 如果资料中没有相关内容请直接回答抱歉我没有在知识库中找到相关信息。 资料内容 %s 用户问题 %s ; // 4. 调用大模型 return chatClient.call(promptTemplate.formatted(context, question)); } }这段代码看着简单但每个参数背后都有讲究。topK决定了每次检索返回多少文档块太少了可能漏掉关键信息太多了上下文会变得混乱而且会推高 Token 成本。similarityThreshold是相似度阈值低于这个阈值的结果会被过滤掉避免模型在没有足够依据的情况下硬答。我在某跨平台系统的问答模块里实际测试发现topK: 4-6的效果最好。超过 6 个文档块之后模型开始混淆不同来源的信息回答的准确率反而下降。这个规律在不同模型上差别不大你可以直接从 4 开始调。4. 调优策略把 RAG 从能用调到好用4.1 切分参数调优从检索效果反推最优配置RAG 的调优永远要从结果往回推。你不需要看理论上的最优切分策略而是看实际检索出来的内容是否能覆盖答案所需的信息。我的做法是三步走首先准备一组标准的验证问题集每个问题都要有明确的预期答案来源。然后用不同的切分配置跑一遍人工检查检索出来的 TopK 文档和答案质量。最后根据结果反向调整参数。在实际项目里我发现一个规律当检索出来的文档块全都集中在目标内容范围内但答案仍然不完整时大概率是 chunkSize 太小导致上下文被切断了而当检索结果中夹杂了较多不相关内容时大概率是 chunkOverlap 太大或者 topK 太大。你还可以用一些量化指标来辅助判断比如召回率、命中位置、平均相关度等。Spring AI 的SearchRequest支持similarityThreshold参数你可以把它当成一个动态开关调低阈值可以提升召回率但引入噪声调高阈值可以提升精确率但可能漏掉答案。实际使用中阈值通常设置在 0.65 到 0.75 之间具体要看 Embedding 模型对中文语义的敏感度。4.2 Embedding 模型选择与底层原理Spring AI 默认适配 OpenAI 的text-embedding-3-small和text-embedding-3-large这两个模型在英文和代码场景下效果相当好。但对中文场景我实测下来有几个值得注意的倾向。text-embedding-3-small的优势是维度适中、成本低、速度快适合大部分业务场景。text-embedding-3-large的语义理解能力更强对长文档、复杂句式、专业术语的处理更好适合对检索精度要求较高的场景。如果你完全在国内环境可以用阿里云的通义千问文本向量模型或者智谱的 Embedding 接口Spring AI 都支持。这里想提醒一个常被忽视的点Embedding 模型的版本和向量维度必须保持一致性。如果你的知识库是用旧模型生成的向量中途换了新模型旧向量在维度或语义空间上可能不兼容检索结果会稀碎。所以模型升级一定意味着知识库全量重新向量化。我在实际工作中维护过一个线上文档库每次切换 Embedding 模型之前都要先做全量向量迁移否则线上的检索准确率会瞬间崩塌。另外向量相似度的解读也跟模型有关系。text-embedding-3-small生成的向量在余弦相似度上通常集中在 0.7 到 0.9 之间不太会出现低于 0.5 的情况。所以如果你看到阈值设置在 0.7 仍然检索出大量无关内容不是阈值设低了而是你的文本切分质量有问题。4.3 Prompt 模板优化把控制力拿回来很多人调 RAG 的时候只顾着调检索参数忽略了 Prompt 模板本身。这其实是个很大的误区。RAG 的效果上限一半在检索质量另一半在 Prompt 对模型的约束力。一个好的 RAG Prompt 模板至少应该包含三层指令角色要求、回答边界、格式要求。角色要求告诉模型你是一个基于知识库回答问题的助手回答边界告诉模型如果知识库里没有相关内容不要自己编造格式要求告诉模型回答应该简洁精准可以直接引用资料原文。我测试过一个有意思的现象同样是检索到相同的内容把 Prompt 从请根据以下内容回答问题改成请仔细阅读以下资料并严格基于资料信息回答用户问题。重要要求当资料中不存在答案时必须明确告知用户不能自行推测模型在资料中没有答案的情况下的拒答率提升了两倍多。别看只是措辞上的变化对模型输出的影响非常大。4.4 上下文组装与多轮对话优化如果你的 RAG 服务需要支持多轮对话事情会稍微复杂一点。直接的做法是把历史对话拼到 Prompt 里但这会让上下文越来越长Token 成本和响应时间都会上涨。我的建议是做一个短期记忆窗口只保留最近两到三轮的对话内容并且把历史对话中的非关键信息做摘要压缩。这样既能保证对话的连贯性又不会让上下文无限膨胀。Spring AI 在 1.0 版里对 Chat Memory 的支持也逐渐完善了你可以通过ChatMemory接口把历史消息存储到内存或 Redis 中。我自己目前的方案是每轮对话结束后把用户问题、检索到的文档 ID、最终回答一并存起来下一轮如果用户追问先基于历史检索到的文档 ID 做二次检索补充再组装 Prompt。这样既避免重复检索带来的性能损耗又能在用户追问时保持较好的上下文一致性。5. 常见问题与排查技巧实录5.1 检索结果总是不理想的排查路径RAG 落地过程中最让人崩溃的就是检索出来一大堆内容但模型就是答不对。我总结了一套排查路径只要你按这个顺序查大概率能快速定位问题。第一层看切分结果。把切分后的文本块打印出来或者写入日志人工检查一下每个块是否语义完整、有没有被切断。如果块的内容残缺后面的检索说破天也不会有好效果。第二层看 Embedding 效果。用同一段文本的相似文本和无关文本分别做向量化计算一下余弦相似度进行对比。如果相似文本和无关文本的分数差距很小问题在 Embedding 模型上需要换模型或者换语义理解更强的版本。第三层看检索排序。把topK调大比如调到 10然后人工查看排名前 10 的文档块到底相关不相关。如果前 10 里混进了大量无关内容问题在向量库的索引参数或者元数据过滤条件上。第四层看 Prompt 组装。确认最终发给模型的上下文是否正确拼接了检索结果而不是因为代码逻辑问题导致上下文丢失。5.2 中文内容切分错误导致的答非所问中文和英文在 Token 切分逻辑上有本质区别。英文单词天然有空格分隔Token 切分基本不会把单词切碎但中文没有天然分隔符按 Token 切分时很容易在一个句子的中间硬切导致语义断裂。我遇到过最典型的一个案例某个知识库文档是在讲如何配置数据库连接池切分时把连接池的最大连接数从连接和池之间切开了导致检索数据库连接池配置时召回的内容里只剩下池的最大连接数是 20这种信息残缺的句子模型自然答不对。解决这个问题有几个思路一是用按句子级别切分的策略先根据中文标点符号句号、问号、感叹号拆分句子再把相邻句子组合成块尽量避免在句子中间切断二是给切分器配置自定义的分隔符让它优先在段落或者句号处切分三是增加chunkOverlap用重叠来补偿切断带来的信息丢失。5.3 topK 和 threshold 的跷跷板效应topK和similarityThreshold在调参时其实是互相制约的。topK控制的是数量上限similarityThreshold控制的是质量下限。你设了一个很高的topK但如果similarityThreshold也很高实际召回的数量可能远少于topK反过来如果similarityThreshold设得太低即使topK很小返回的内容也永远会被垃圾填满。我建议在初期调优时先固定一个相对宽松的similarityThreshold比如 0.5只调topK把召回数量先确定下来。然后再逐步提高similarityThreshold观察检索结果的相关性变化。等找到两者都比较合适的临界点后再微调一两个档位即可。5.4 向量库连接与一致性检查最后说一个比较隐蔽的坑向量库的数据一致性问题。Spring AI 在写入向量时是异步的如果你的代码里执行了vectorStore.add(documents)之后立刻查询有可能查不到刚写入的数据。这在 Pinecone 这类分布式向量库上尤其明显索引更新有延迟。解决方法是设置一个合理的等待时间或者在写入后主动做一次一致性检查。我自己在测试环境里遇到过一次自动化测试跑了一百多次偶尔会出现知识库明明传完了但检索为空的问题后来定位到是向量库索引传播延迟导致的。最终在测试用例里加了显式等待这个问题才消失。如果你在生产环境做知识库的增量更新也建议在写入完成后主动触发一次describeIndexStats或者类似接口来确认向量数量符合预期再切换线上流量避免用户检索到不完整的数据。6. 从小 Demo 到可用系统一些工程上的进一步建议如果你的 RAG 服务会被真实用户使用那性能和数据管理这块就要提前想好。RAG 的在线链路里最耗时的通常是两步向量检索和大模型生成。向量检索的耗时还能压一压大模型生成的速度基本取决于模型本身。为了降低大模型的并发压力我建议引入两级缓存。第一级是完全相同问题的缓存直接基于用户问题的哈希或者规范化文本做查询第二级是相似问题的缓存利用向量库本身的能力在回答之前先检索一下历史问答记录如果命中了高度相似的已有问答就可以直接复用。另外知识库的数据更新策略也值得重视。最省事的方案是定期全量重建每天凌晨跑一次离线任务把最新的文档重新切分、向量化后覆盖写入。但全量重建的成本会随着文档量的增长而线性上升所以后期可以改成增量更新只对新增或变更的文档做切分和向量化通过元数据中的版本号或时间戳来区分新旧数据。最后说一点关于日志和监控的体会。RAG 服务和传统 CRUD 接口不太一样它的问题往往不在有没有报错而在回答得好不好。所以建议从第一天就把检索到的 TopK 文档 ID、相似度分数、最终 Prompt 内容都记录下来。出问题的时候顺着日志一查基本都能找到是检索的问题还是 Prompt 的问题还是模型的问题。这个习惯帮我省掉了很多无效排查时间。结合我自己的实际体会Spring AI 目前已经具备承载真实业务 RAG 需求的成熟度。它不是万能的在复杂文档理解、长文本处理上还有不少需要自己补齐的活儿但对于绝大多数私有知识库问答类需求来说它能帮你把链路跑通、跑稳。关键是理解每一个参数背后的权衡而不是盲目照搬网上的调参模板。希望这篇文章能给你一些可复用的经验。
阅读完成 · 觉得有帮助?