
Spring AI 2.0 的 RAG 工程优化说到底是把“能跑”的 demo 打磨成“能上线”的系统。今年我在两个生产级项目里把完整的 RAG 链路重新过了一遍从文本切分、向量检索、关键词召回一路做到重排序过程里踩了不少文档里没写清楚的坑。这篇就把我这套工程实践的完整思路、参数选型和代码实现都摊开讲清楚给正在做 Java 技术栈 RAG 项目的朋友一份可以直接抄的作业。先说结论Chunking 决定 RAG 效果的下限混合检索把召回到位Rerank 把精度拉满。三个环节各管一段缺一个都会在真实业务里露馅。Spring AI 2.0 在这个链路上的最大价值是它把向量数据库接入、Prompt 模板、模型调用这些基础设施标准化了让我们能把主要精力放在策略调优上而不是奔波在各种 SDK 的边缘 case 里。1. 内容整体设计与思路拆解1.1 为什么 RAG 工程需要分阶段优化如果你做过几版 RAG demo大概率会遇到这种尴尬知识库问答在测试集上表现不错一上生产就各种答非所问。原因通常不在大模型本身而在 RAG 的链路设计。信息的流转路径是这样的——文档进来先切块、做向量化用户提问后走检索找出最相关的片段再送给大模型作为上下文生成答案。任何一环的信息损耗都会在下游被放大。这就像做菜。食材处理得好不好Chunking决定了能不能入味菜市场的采购网络检索决定了能不能买到新鲜食材出锅前的调味Rerank决定了最终口感。光买好食材不处理不行处理和采购都做好了不调味也不行。RAG 的链路优化逻辑一模一样而且是强耦合的——前面环节的失误后面环节再怎么补救都有上限。所以我把整个优化拆成三大块来思考Chunking 优化解决“信息以什么粒度入库”的问题这是所有后续工作的地基混合检索解决“怎么能把相关片段都找到”的问题通过多路召回保证不遗漏Rerank 精排解决“找回来的片段里哪些真正有用”的问题把最相关的内容送到 LLM 面前这个顺序不能乱。检索策略要跟着 Chunking 方式调Rerank 的输入质量又取决于检索结果。先定切分再调召回最后上精排是成本最低的推进路径。1.2 Spring AI 2.0 在 RAG 链路上的定位Spring AI 2.0 不是来替代 LangChain 的它的核心价值在于把 Java 生态里做 AI 应用的基础设施标准化了。如果你用 Spring Boot 做后端之前接大模型是各种 SDK 混着写今天调 OpenAI 的 HTTP 接口明天换通义千问又得改一套代码。Spring AI 2.0 用统一抽象把这层问题解决了不同的 Model、Embedding、VectorStore 都能通过配置切换业务代码基本不用动。在 RAG 场景里Spring AI 2.0 的几个核心抽象是这样的EmbeddingModel统一文本向量化接口可以对接本地部署的 bge-m3也能切到智谱的 embedding 接口VectorStore统一向量存储接口支持 Milvus、PGVector、Redis 等主流实现Advisor提供了一种类似 Spring AOP 的机制可以在问答链路上做检索增强、日志记录等横切操作ChatClient统一对话客户端底层对接 DeepSeek、通义千问、智谱等模型都有对应的 Starter这套抽象带来的直接好处是你在本地调试用的可能是 PostgreSQL 的 pgvector到生产换 Milvus代码层面几乎不用动。Embedding 模型从开源切到商用 API也只是改配置的事。1.3 方案选型的整体考量在动手之前有几个关键选型需要先厘清思路。第一向量数据库选型。生产环境我建议优先考虑 Milvus 或 Elasticsearch。Milvus 在纯向量检索的性能上更极致ES 的优势是如果你本来就有 ES 集群它可以同时承担全文检索和向量检索少维护一套系统。开发环境用 PGVector 最省事Spring AI 对它的支持很完善docker 起一个 postgres 镜像就全搞定了。第二重排序模型的部署方式。Rerank 模型现在主流选择是 bge-reranker 系列大一点的用 v2-m3轻量的用 base。如果你手头只有 Windows 机器也能通过 Ollama 或者 FastAPI 封装的方式跑起来后面我会详细讲部署细节。第三混合检索的实现路径。Spring AI 2.0 的 VectorStore 接口本身只做向量检索关键词召回需要自己实现常见的方案是用 ES 的 BM25 或者直接查数据库做全文索引。两条路都行取决于你的基础设施。2. Chunking 优化决定 RAG 效果下限的环节2.1 Chunking 的底层逻辑与常见误区很多教程把 Chunking 讲得很玄乎实际上它的核心矛盾就一个块太大信息密度低检索精度差块太小上下文碎片化语义不完整。块太大的问题很好理解。你把一篇五千字的文档切成一个块用户问其中某个细节向量检索返回的是整篇文档的向量。这个向量被五千字的内容“稀释”了和问题的相关度被大量无关内容拉低召回精度自然上不去。块太小的问题发生在召回之后。假设你把文档切成每句一个块用户问“这个项目的预算是多少”相关的那句话可能依赖上下文里的业务背景才能回答。LLM 只拿到了孤零零的一句话缺少必要的上下文回答质量就崩了。我见过很多团队在 Chunking 上的通病是只调 chunk_size 和 chunk_overlap 这两个参数完全忽略了文档结构。结构化信息被强行切成碎片Markdown 的标题层级、表格的行列关系、代码块的完整性全都被破坏掉了。之前我做过一个运维知识库项目里面有大量操作手册用的是 Markdown 格式。一开始用固定 500 字切块结果调查问题的答案经常在三个不同块里各讲一半检索怎么调都找不齐。后来针对 Markdown 文档做结构感知切分按标题层级来分块同一个二级标题下的内容尽量留在一个块里效果立竿见影。2.2 分块策略选型固定窗口与结构感知的取舍目前主流的 Chunking 策略可以归成三类固定大小切分按字符数或 token 数硬切加上 overlap 保证上下文衔接。优点是实现简单通用性强缺点是会破坏语义完整性和文档结构。结构感知切分解析文档结构标题、段落、列表、表格按结构边界切分。优点是语义完整信息密度高缺点是解析逻辑复杂对文档格式的规范性有要求。语义切分用 embedding 计算句子间的语义相似度在语义断裂处切分。优点是最符合语义边界缺点是需要 embedding 计算开销而且效果直接取决于 embedding 模型的质量。实际生产项目中我用的最多的是“结构感知为主固定窗口兜底”的组合策略。具体来说对于 Markdown 和 HTML 文档走结构感知切分按标题层级控制块大小对于 PDF 和 Word 这类二进制格式先抽取文本再用固定窗口切分对于代码和日志按代码块边界切分保证代码语义完整Spring AI 自带了一个TokenTextSplitter可以用。但说实话它的能力比较基础就是按 token 数硬切。如果文档结构复杂我更建议自己写一个切分器基于常见 Markdown 解析库去实现。切分逻辑原则上不难难的是处理各种边界情况。2.3 参数选择chunk_size、overlap 与 embedding 模型的匹配chunk_size和chunk_overlap不是拍脑袋定的它们和 embedding 模型的最大输入长度强相关。以 bge-m3 为例它的最大输入长度是 8192 个 token但这不代表你应该把块切到 8000 多 token。你还要考虑 LLM 的上下文窗口、检索精度、存储成本之间的平衡。块越大embedding 的语义越模糊检索精度下降块越大召回后送进 LLM 的上下文占的 token 越多成本越高。我实践下来比较稳妥的几组参数场景chunk_sizechunk_overlap说明通用文档问答500 token50 token平衡精度与语义完整代码知识库300 token30 token代码块语义相对独立长文档摘要/分析1000 token100 token需要更完整的上下文细粒度事实问答250 token25 token精度优先适合查证类场景这里 token 数不是字符数。一个中文字符大概占 1 到 2 个 token英文一个词大概 1 到 2 个 token。如果你用 tiktoken 或者其他 tokenizer 来切要搞清楚底层统计的是字符还是 token。另外要注意的是chunk_overlap 不能省。没有 overlap紧挨着切分边界的信息就会被割裂。用户问的问题如果刚好落在边界上两边的块都只有一半的上下文效果就很差。我一般用 10% 的比例作为 overlap 的起始值再根据实际情况微调。2.4 实操用 Spring AI 实现结构感知切分器Spring AI 的TextSplitter抽象给了扩展点实现的接口就一个方法。不过实际项目中我通常是在文档预处理阶段做切分而不是在运行时切。离线把文档切好存到数据库里运行时只做检索这样能省很多计算资源。下面给一个相对完整的 Markdown 结构感知切分思路你可以根据自己的文档格式调整public class MarkdownStructureSplitter { public ListDocument split(MarkdownDocument mdDoc) { ListDocument result new ArrayList(); // 1. 解析 Markdown 标题层级按 H1/H2 划分逻辑区块 ListSection sections parseSections(mdDoc.getContent()); for (Section section : sections) { // 2. 检查区块长度小于阈值直接作为一个块 if (section.tokenCount() MAX_CHUNK_SIZE) { result.add(toDocument(section)); continue; } // 3. 超长区块用段落边界做二次切分 ListSection subSections splitByParagraph(section, MAX_CHUNK_SIZE); for (Section sub : subSections) { // 4. 保留标题作为前缀上下文保证 LLM 能理解片段来源 result.add(toDocument(sub.withPrefix(section.getHeading()))); } } return result; } }实现时有一个细节值得注意给每个块保留一个元数据字段记录来源和标题路径。比如source哪个文件、heading_pathH1 H2 H3 的路径。这两个字段在后续做引用溯源和结果展示时非常有用没有它们RAG 的回答就少了可信度支撑。2.5 实操心得不同文档类型的切分调优实际项目里的文档类型往往五花八门一个统一的切分策略很难打天下。根据我的经验产品文档/操作手册结构感强适合结构感知切分。按章节切标题带上信息密度高。FAQ 类数据一条 FAQ 是一个天然的最小语义单元。直接按条目切每条一个块overlap 设 0 就行。合同/公告有固定的条款结构按条款切比按字数切靠谱得多。如果是 PDF 表格还要先考虑表格解析的准确性。代码仓库按文件和函数切注释和代码不要拆散。代码和自然语言混合的场景函数级切分效果最好。还有一个很多人忽视的坑PDF 解析质量会直接决定切分质量。PDF 解析出来是乱序文本切分得再花哨也没用。这块建议前期多花点时间调研解析方案常见的开源库比如 PDFBox、pdfplumber 各有优缺点要针对自己的 PDF 类型做测试。如果是扫描版 PDF还要先做 OCR这些成本要在项目排期里预留出来。3. 混合检索从单路召回走向多路融合3.1 为什么纯向量检索不够用纯向量检索的思路是把文档和问题都转成向量然后计算余弦相似度或内积来找最相关的块。这条路在语义搜索场景效果不错但对精确匹配的场景就力不从心了。典型的例子是查订单号、身份证号、错误码这类包含精确 token 的场景。比如用户问“错误码 E10023 怎么解决”向量检索很可能把注意力放在“错误码”“解决”这些语义词上对“E10023”这个精确串不敏感。如果把文档里的E10023和E10032搞混了向量上几乎区分不出来。还有代码变量名、专业缩写这类词汇embedding 模型的词表里可能根本没有对应的好表示。向量化之后这些关键信息被平均稀释了检索结果自然不满意。这个问题的根源在于embedding 模型擅长的是语义相关性不擅长精确匹配和稀有 token 的命中。而全文检索比如 BM25恰恰擅长这个——它基于词频和逆文档频率来计算相关性对精确词项的命中非常敏感。所以混合检索的思路很直接向量检索负责语义层面的召回全文检索负责词项层面的召回两路结果合并后再统一排序。3.2 混合检索的两种主流实现路径在 Java 技术栈里混合检索的落地路径主要有两条路径一Elasticsearch 一站式方案如果你的系统已经用了 ES这条路成本最低。ES 同时支持 BM25 全文检索和 dense_vector 向量检索可以在一个查询里同时跑两路召回{ query: { bool: { should: [ { match: { content: 错误码 E10023 } }, { knn: { embedding: { vector: [...], k: 10 } } } ] } } }这个方案的好处是架构简单一套系统全搞定缺点是 ES 的向量检索性能在超大数据量下不如专业向量数据库。路径二向量数据库 关键词检索组合方案向量检索走 Milvus/PGVector关键词召回走数据库的全文索引或独立的 ES 集群。两路结果在应用层合并。这个方案灵活度高每一路都可以选最适合的引擎但要多一次网络调用和一次结果融合的逻辑。我个人在中小型项目里推荐路径二因为改造灵活可以先只做向量检索后续再把关键词路加进来不影响现有架构。如果你的项目已经重度依赖 ES那就直接上路径一不要为了“专业”而多维护一套系统。3.3 检索融合策略RRF 还是加权评分两路召回的结果怎么合并成一路这一步叫“结果融合”常用的有两种策略。RRFReciprocal Rank Fusion倒数排名融合的原理很优雅不关心每路的原始得分只看排名位置。score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档 d 在第 i 路召回结果中的排名k 是平滑常数通常取 60。这个公式的含义是名次越靠前贡献的分数越高两路都排前面的文档总分自然最高。RRF 的好处是不需要调权重而且对每路召回的打分尺度不敏感——因为只看排名。坏处是它没有利用每路的原始相关度信息理论上信息利用率低一些。加权评分融合则是把每路的分数归一化后加权求和score(d) α * norm(vector_score(d)) β * norm(bm25_score(d))这里的α和β是权重需要根据业务调优。一般从α 0.7, β 0.3起步语义为主、词项为辅的场景这个比例比较稳。如果业务里精确匹配多就加大 β。我的实际建议是先用 RRF 起步因为它零调参上线后通过观察失败案例再决定要不要换成加权评分。大多数场景 RRF 已经够用盲调权重反而容易过拟合到测试集上。3.4 实操Spring AI 2.0 中实现混合检索Spring AI 的VectorStore接口只负责向量召回。要做混合检索需要自己在 Service 层把两路检索编排起来。下面是我在项目里用的简化版实现Service public class HybridSearchService { private final VectorStore vectorStore; private final KeywordSearchService keywordSearchService; public ListDocument hybridSearch(String query, int topK) { // 1. 向量召回 ListDocument vectorResults vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(topK) .build() ); // 2. 关键词召回通过 ES 或数据库全文索引实现 ListDocument keywordResults keywordSearchService.search(query, topK); // 3. RRF 融合 return RrfFusion.merge(vectorResults, keywordResults, topK); } }关键词召回的部分如果用了 ES 就很直接构造一个 BM25 查询即可。如果没用 ESPostgreSQL 自带全文检索也能顶一顶。SQL 大概是这样的SELECT id, content, ts_rank(to_tsvector(chinese, content), query) AS rank FROM documents, plainto_tsquery(chinese, ?) AS query WHERE to_tsvector(chinese, content) query ORDER BY rank DESC LIMIT ?;中文全文检索在 PG 里的关键是分词。默认的中文分词效果一般建议装一个zhparser或者pg_jieba扩展否则“中华人民共和国”会被拆成“中华”“人民”“共和国”精确匹配的效果会打折扣。3.5 实测效果混合检索到底能提升多少之前把一个技术工单知识库从纯向量检索切成混合检索后做了一轮评测。评测集是 200 个真实工单问题指标是 Recall10前 10 条结果中包含正确答案的比例检索方式Recall10说明纯向量检索68.5%bge-m3 embedding纯 BM25 检索61.0%词项召回能力不错语义泛化不行混合检索RRF81.5%两路互补融合后显著提升召回率从不到 70% 拉到 80% 以上这 13 个百分点的差距对最终回答质量的影响非常大。因为如果正确答案根本没被召回后面 Rerank 和大模型再强也白搭。混合检索的核心收益在于语义路能兜住“换一种说法也能找到”的场景词项路能兜住“精确匹配不遗漏”的场景两条腿走路比单腿稳得多。4. Rerank 精排把最相关的片段送到 LLM 面前4.1 Rerank 解决的问题与工作原理先理解一个前提向量检索和混合检索的目标是“别漏掉正确答案”而 Rerank 的目标是“把最正确答案排到最前面”。检索阶段的结果是按向量相似度或者词项相关度排的这个排序和“真正能回答用户问题”之间还有一段距离。原因在于检索阶段的相似度计算和 LLM 理解上下文的能力之间存在模式差异。bge-m3 的向量相似度算的是语义接近程度但某个块是不是真的能支撑回答这个问题需要更精细的语义匹配判断。Rerank 模型做的事情就是打这个补丁。它把“用户问题”和“候选文档块”拼在一起输入模型输出一个相关度分数。这个分数是模型基于深度语义交互算出来的比单纯向量相似度精确得多。打个比方向量检索是初选面试官简历方向大致对口就放进候选池Rerank 是终面面试官面对面聊一轮才知道这个人到底是真合适还是只是简历好看。4.2 Rerank 模型选型与部署方案目前用的最多的是 BGE 系列的 Rerank 模型。选型上主要看数据规模、机器算力和延迟要求。模型特点推荐场景bge-reranker-v2-m3多语言能力强效果最好生产环境首选机器够好就上bge-reranker-base轻量CPU 也能跑机器资源紧张或对延迟敏感bge-reranker-large效果和速度的中间档单语言场景性价比较高部署方式上生产环境建议用专门的推理服务封装模型。用 FastAPI 包一层 HTTP 接口Spring AI 侧通过 RestClient 来调用是比较常见的做法。如果你用 Ollama从 0.4 版本开始也支持 rerank 模型了Windows 本机调试最省事。很多人在 Windows 上部署 Rerank 模型时会遇到坑主要集中在这几个地方Python 版本和 PyTorch 版本不匹配torch 的 CPU 版本装错会导致无法加载模型。建议直接用官方提供的 Docker 镜像方案部署一次性解决环境问题。模型下载慢或失败可以从 ModelScope 的镜像站下载模型权重然后改成从本地路径加载。内存不足bge-reranker-v2-m3 的模型文件大约 2GB 左右加载起来内存峰值可能到 8GB 以上机器配置要留意。4.3 实操在 Windows 环境部署 Rerank 模型在 Windows 本机调试时我是用 FastAPI 包了一层模型服务。代码不复杂几百行搞定。核心部分大概是这样的from fastapi import FastAPI, Request from sentence_transformers import CrossEncoder import torch app FastAPI() model CrossEncoder(BAAI/bge-reranker-v2-m3, devicecuda if torch.cuda.is_available() else cpu) app.post(/rerank) async def rerank(request: Request): data await request.json() query data[query] documents data[documents] # 构造 query-document 对输入模型计算相关度 pairs [[query, doc] for doc in documents] scores model.predict(pairs) # 返回按分数降序的索引和分数 results sorted(zip(range(len(documents)), scores.tolist()), keylambda x: x[1], reverseTrue) return {results: [{index: idx, score: score} for idx, score in results]}启动之后Spring AI 侧通过 WebClient 或者 RestClient 调用这个 HTTP 接口即可。需要注意的是Rerank 接口每次请求的候选文档数量要控制在 20 到 50 条以内。如果混合检索返回了 100 条候选别一股脑全丢给 Rerank先截断到 top 30 再精排性能和效果都更平衡。4.4 Spring AI 2.0 集成 Rerank 的工程实现Spring AI 2.0 没有现成的 Rerank 抽象所以我在项目里直接用 WebClient 调用 Rerank 服务逻辑封装在 Service 层。这样依赖最小后面要换模型服务也方便。Service public class RerankService { private final WebClient webClient; public ListDocument rerank(String query, ListDocument candidates, int topK) { // 1. 构造请求体只传文档内容控制传输量 MapString, Object body Map.of( query, query, documents, candidates.stream().map(Document::getContent).toList() ); // 2. 调用 rerank 服务 RerankResponse response webClient.post() .uri(/rerank) .bodyValue(body) .retrieve() .bodyToMono(RerankResponse.class) .block(); // 3. 按分数排序返回 topK 个文档 return response.results().stream() .sorted(Comparator.comparing(RerankItem::score).reversed()) .limit(topK) .map(item - candidates.get(item.index())) .toList(); } }这个实现的注意点是响应里的 index 是原候选列表的下标排序后要记得按原下标映射回 Document别直接按 Rerank 返回的顺序取否则文档就错乱了。4.5 Rerank 对最终回答效果的提升实测还是拿工单知识库来说加了 Rerank 之后回答质量的提升很直观。评测方式是让大模型基于 RAG 上下文回答 200 个问题由人工判断答案是否正确链路准确率纯向量检索 LLM62.5%混合检索 LLM71.0%混合检索 Rerank LLM78.5%Rerank 在这条链路上贡献了 7.5 个百分点的提升。原理也不难理解之前排在第一位但不相关的内容经过 Rerank 后被挤出 topK相关的内容被顶上来。LLM 拿到的上下文更精准回答质量自然水涨船高。5. 工程化落地的关键细节与避坑指南5.1 大模型接入的工程实践以 DeepSeek 和智谱为例RAG 链路里的最后一步是调用大模型生成答案。Spring AI 2.0 对接国内大模型的生态已经比较成熟了DeepSeek 和智谱都有对应的 Starter 可以用。对接智谱的一个标准 POM 配置dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-zhipuai/artifactId version2.0.0/version /dependency配置文件里把 key 和模型名配上spring: ai: zhipu: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-plus对接本地部署的 DeepSeek 也是一样的套路重点在配置 Base URLspring: ai: openai: base-url: http://localhost:11434/v1 api-key: dummy chat: options: model: deepseek-r1这里有个容易踩的坑本地模型服务如果没有兼容 OpenAI 的 API 格式Spring AI 的 OpenAI Starter 是连不上的。好在大多数本地推理框架比如 Ollama、vLLM都做了 OpenAI 兼容层配置里指对 base-url 就行。5.2 缓存策略别让 RAG 链路的每一环都重复计算线上 RAG 服务如果每次都走完整的“检索 → 精排 → 生成”链路成本和延迟都是不小的压力。工程上必须做分层缓存。Embedding 缓存文档内容不变向量就不变。向量入库时以内容 hash 为 key 做幂等避免重复向量化。检索结果缓存相同或高度相似的问题在短时间内返回相同的检索结果。可以用 Redis 做缓存TTL 设 15 到 30 分钟。Prompt 级缓存有语义缓存方案可以直接缓存 LLM 生成的答案命中了就跳过整个链路。但要注意业务对实时性的要求知识库内容频繁更新时缓存 TTL 要相应调短。5.3 可观测性RAG 链路的日志与指标监控RAG 链路比普通接口多了好几个环节每环都可能出问题必须把可观测性做起来。我的团队在实践中沉淀了一套最小可观测方案日志埋点每个环节记一条结构化日志包含 query_id、环节名、耗时、返回条数、top1 文档 ID。有 traceId 贯穿全链路排查问题特别方便。埋点指标Rerank 前后的 top1 命中率变化、平均检索耗时、平均生成耗时、token 消耗量。失败样本收集用户在问答后面的“赞同/反对”反馈是优化 RAG 链路最宝贵的信号。有一个成本很低但收益很大的做法把线上 badcase 定时回流到评测集。每次用户点了“反对”或者超时把这个问题连同当时的检索结果和模型回答存下来。每周跑一次回归评测链路优化的进度就能量化。我在项目里做了个简单的 Badcase 回放工具用一个脚本把近七天的失败样本重新过一遍新链路对比回答质量。这个工具帮我们发现了不少隐藏问题比人工翻日志高效得多。5.4 常见问题速查我踩过的坑和排查思路最后把我在 RAG 工程化过程中遇到的高频问题整理成一张表方便大家对照排查问题现象可能原因排查与解决办法召回结果总是不相关Chunking 粒度不合适先用知识库抽 20 个典型问题做评测检查切块语义完整性调 chunk_size精确匹配场景答错纯向量检索漏召回加 BM25 关键词路做混合检索Rerank 服务响应慢候选数太多或模型太大限制候选数量在 30 条内模型换成 base 版本答案总是“不知道”上下文送入太少信息不足增加 topK、调大 chunk_size、检查 Rerank 阈值是否过滤太狠中文效果差分词或 embedding 不支持中文换支持中文的分词插件embedding 换 bge-m3 这类中文友好模型文档更新后回答没变化缓存没失效检查检索结果缓存和向量入库的幂等逻辑这里特别提醒一个容易忽略的点向量数据库的数据更新策略。很多团队在做知识库更新时只删旧数据、插新数据忽略了还在缓存里的旧检索结果。如果业务对数据实时性要求高建议缓存时间设短一些或者在数据更新时主动清理相关缓存。5.5 从 demo 到生产的完整落地清单写到这里把整个 RAG 工程化落地过程中我认为最关键的检查项整理一个清单每一个都是我踩过坑之后才意识到的文档切分有没有针对文档类型做策略选择还是无脑固定大小向量化的 embedding 模型是否支持你的业务语言和领域词汇检索链路有没有关键词召回兜底还是纯向量一条路走到黑混合检索的结果融合用的是 RRF 还是加权上线后有没有对比数据Rerank 服务的容量和延迟有没有压测过并发上来会不会挂LLM 接入的 Base URL、API Key、模型名、超时参数是否都配置正确缓存 TTL 和数据更新策略是否匹配业务实时性要求链路的日志和指标埋点是否完整Badcase 是否能闭环回流这套清单看起来简单但每一条背后都有真实的线上事故。RAG 项目别急着往上叠功能先把基础链路打扎实后续加东西才稳。我个人做完几个 RAG 项目的体会是系统工程最大的成本从来不在模型选型而在这些看起来不起眼的细节里。把 Chunking、混合检索、Rerank 这三个核心环节吃透Spring AI 2.0 就能真正变成你手里的趁手工具而不是又一个跑通就吃灰的 demo。最后再分享一个建议生产环境上线前至少准备一两百条真实业务问题做回归集每次改完链路跑一遍你会发现这个习惯能帮你省掉太多线上排查的时间。