Java版RAG项目实战:从架构设计到检索调优的完整指南 简介基于Java实现的RAG增强检索生成项目整合知识库、语义检索与大模型生成能力面向需要构建企业知识管理、智能问答、客户服务等系统的Java开发者和AI工程人员。压缩包共266个文件整体仅14.32MB核心为231个Java源文件覆盖知识库管理、向量嵌入存储、语义检索服务、大模型调用抽象、用户与会话管理等关键模块同时提供XML/YAML配置、SQL初始化脚本、Dockerfile及env环境文件便于一键化部署和二次开发。附带的流程教程从数据入库、文本向量化、索引构建到检索生成逐步拆解配合可直接阅读的源码层次帮助开发者从零掌握RAG系统的完整落地路径。已有1651人学习下载无论是高校项目、企业级检索应用还是个人学习进阶都是一份高质量的实战参考。1. 拿到一个Java版RAG项目先想清楚它帮你解决了哪一半问题做企业级知识库问答的人多半都经历过这种尴尬模型能聊但不敢信内部文档一堆但没人去翻最后写出来的AI助手只会复读百科。RAG做的事情很简单把“让模型背知识”变成“先检索再生成”模型不需要记住你的业务文档它只负责把你检索回来的那几段内容组织成像样的答案。市面上开源项目不少但大多是Python生态想嵌进Spring Boot技术栈反而费劲。这个基于Java实现的RAG项目价值在于它给出了知识库、检索、生成这一整条链路在JVM生态里的完整落地方案附带源码和流程教程适合已经会用Spring写接口、但没碰过向量检索和嵌入模型的中高级后端工程师。这也是少有的能让你把RAG跑进生产环境而不是只停留在Demo阶段的项目。2. Java RAG项目的整体设计与选型从知识库到生成链路先画一张图2.1 RAG在Java技术栈里的几种常见拼法本地模型与云端接口怎么选先搞清楚一件事RAG不是一个算法是一条流水线。这条流水线通常由四段组成——文档解析、文本切分、向量化入库、检索生成。用Java做的团队很少会像Python项目那样把每个环节都用PyTorch重写一遍更常见的做法是Java负责调度和业务逻辑嵌入模型和生成模型放在外部接口向量数据库单独部署Java通过客户端去读写。我拆过不少类似的Java RAG项目它们的架构基本逃不出下面这个骨架Spring Boot 服务 ├── ingest 模块读取 PDF/Word/Markdown切分文本 ├── embed 模块调用嵌入接口把切分后的文本变成向量 ├── store 模块把向量和原文写入向量数据库 ├── retrieve 模块对用户问题做同样嵌入召回 TopK 片段 └── generate 模块把召回片段和问题拼成 Prompt调用大模型选型上第一个要定的事情是嵌入模型用本地的还是接口的。本地的好处是数据不出内网适合政企客户坏处是Java生态里跑嵌入模型不像Python那么顺手通常还是要起一个独立的推理服务Java过去调HTTP接口。接口调用省心但你要接受两个现实一是每次文档入库都要花嵌入费用二是如果文档里含敏感内容数据要出得去才行。我一般建议先按接口方式把链路跑通因为这个环节并不是RAG项目的瓶颈。真正的瓶颈在于后面要讲的切分策略和检索质量。等全流程跑明白之后如果信息安全要求高再把嵌入接口换成内网的服务代码改动量其实很小只需要替换一个HTTP调用的实现类。2.2 向量库选型三套能直接用的组合与你该盯住的指标Java项目里向量存储的选型比想象中更重要。很多人会套用Python社区的习惯直接上Chroma但在Java服务里Chroma的官方客户端支持一般反而多一些手工维护成本。我拆过项目源码后发现大家最常用的组合大概有三种拿张表说清楚比较合适组合部署方式Java接入难度适合场景Elasticsearch dense_vector已有ES集群加一个向量字段低RestHighLevelClient直接用公司本来就用ES存业务数据Milvus 独立部署单独的向量库服务中需要引入Milvus SDK文档量大千万级以上向量SQLite 内存计算 / 本地文件索引嵌在Java进程里低但召回要自己做个人项目、内网小规模知识库这三个组合里Elasticsearch是最稳妥的生产选择。原因很简单公司里用Java写后端的团队十有八九已经有ES在跑日志或做搜索。复用ES意味着你不需要额外运维一套Milvus集群只需要在索引里加一个dense_vector字段再写一个script_score查询做余弦相似度计算。Milvus适合数据量大到ES撑不住的时候。它的Java SDK做得不差但部署多一个组件运维多一份压力。SQLite方案更像是用来跑通流程做验证的向量存进表里查询时按段读出来自己算相似度数据量上了百万之后查询耗时明显变大。选向量库时不要把注意力全放在“相似度算得准不准”上真正要盯的是三件事写入吞吐、查询延迟和索引是否需要重建。ES在写入向量时需要把全部向量都加载到内存里才能保证低延迟数据量大之后对堆内存要求很高这往往是Java项目里最先爆掉的地方。2.3 源码包通常长什么样Maven工程里的五个核心模块拿到一份带源码和流程教程的Java RAG项目先别急着跑。花十分钟把目录结构过一遍基本就能判断这个项目是正经工程还是个玩具。大多数这类项目都是以Maven多模块工程组织的pom.xml里会有几个子模块名字可能叫ingest、retrieve、common之类但职责划分大体一致。一个合格的Java RAG项目通常会包含这五块内容第一块是common模块放统一的返回结构、异常定义、配置项。第二块是ingest模块负责读取各类文档把PDF、Word的内容提取成纯文本。第三块是embed模块封装嵌入模型的调用入参是文本出参是浮点数组。第四块是store模块封装向量库操作提供新增、删除、查询向量三个核心方法。第五块是retrieve模块做召回和重排对外暴露一个检索接口。如果你拿到手的项目源码里这几个模块顺序清晰、依赖方向一致那这个项目是值得往下跟的。反过来如果所有代码都堆在几个Controller类里那它就是一个流程演示项目适合学思路不适合直接接到生产环境。操作上把项目导入IDEA后第一步是看application.yml里的配置项。重点关注四个配置嵌入接口地址、向量库连接信息、切分参数chunkSize和overlap、生成模型接口地址。这四项配置弄明白整个项目就通了七成。第二步是找到项目里的README或流程教程文档里面一般会给出从空白环境到跑通全流程的步骤。照着做之前先确认JDK版本和Spring Boot版本是否合适很多这类开源项目是在JDK 8上写的如果你本机装的是JDK 17第一件事就是先改pom.xml里的编译版本免得一上来就被构建错误劝退。3. 知识库入库的第一步文本解析、切分与向量化写入3.1 文本切分参数是个要反复试的活不要一开始就上大段落入库这件事看起来不就是读文件、存起来吗等你自己做一遍就知道最影响RAG最终效果的并不是模型而是切分。切分是把长文档切成若干个小片段每个片段单独向量化检索时以片段为单位召回。这个环节决定了“检索能不能命中、命中的内容是不是完整”。切分有两个核心参数chunkSize和overlap。chunkSize是每个片段的长度overlap是相邻片段之间重叠的长度。这两个参数的设计目的完全不同。chunkSize决定了一次召回能给大模型多少上下文设太小一个完整的知识点被拦腰截断设太大向量化之后语义被稀释而且超过模型上下文窗口的部分会被截掉。overlap则是为了抵消切分造成的语义断层让跨越切分边界的信息至少在前一个片段和后一个片段里各出现一次。常见的起步值是chunkSize500、overlap50。这个组合对中文技术文档表现比较稳。但如果你处理的是操作手册这类信息密集的文档每条操作步骤本身就短500还是会偏大处理的是政策法规这类长条款文本500又会把好几条条款混在一起。所以源码包里通常会把这两个参数开放成配置项你要做的就是准备好一批真实文档多跑几组参数对比效果。文本清洗也常常被忽略。从PDF里抽出来的文本经常带着页眉页脚、目录页码、换行符这些噪声不清理向量化后会拉低检索命中率。我见过不少项目第一版跑出来的语义检索结果很怪排查到最后发现是PDF里每页底部的“第几页共几页”被当成了正文字段检索“故障排查”时反而把页脚里的“第3页共12页”召回上来了。3.2 用Java写一个带重叠窗口的切分器核心代码与参数解释下面这段代码是一个带重叠窗口的文本切分器是我在项目里最常写的工具类。它做的事情很直接输入一段长文本输出一个字符串列表列表里每个元素就是一个独立的文档片段片段之间携带重叠内容。public class TextSplitter { /** * 按固定窗口大小切分文本并保留重叠区域。 * * param text 原始文本 * param chunkSize 每个片段的字符数 * param overlap 与前一片段重叠的字符数 * return 切分后的片段列表 */ public static ListString split(String text, int chunkSize, int overlap) { ListString chunks new ArrayList(); if (text null || text.isEmpty()) { return chunks; } // 去掉连续的空白字符避免把缩进和空行也算进有效内容里 String normalized text.replaceAll(\\s, ).trim(); int length normalized.length(); int start 0; while (start length) { int end Math.min(start chunkSize, length); // 尽量不要在句子中间切断这里找了个就近的句号做回退 if (end length) { int lastPeriod normalized.lastIndexOf(。, end); if (lastPeriod start chunkSize / 2) { end lastPeriod 1; } } chunks.add(normalized.substring(start, end)); // 每次前进的步长 chunkSize - overlap这样下一段才会带上上一段的尾部 start end - overlap; if (start end) { break; } } return chunks; } }这段代码的逻辑说明入口参数只有三个文本内容和两个切分参数。先做空白字符归一化这是因为PDF解析出来的文本里经常有大量换行和连续空格不清理会导致切分出来的片段一半是空白。然后按固定步长向右滑动start的增量是chunkSize减overlap所以每段都会和上一段重叠。中间那句lastIndexOf是为了避免把文本从句子中间切断尽量让每个片段结束在一个句号后面。参数怎么调我给一套可以直接用的判断逻辑。如果切出来的片段在检索召回时经常出现“内容对但结论半截”的情况说明chunkSize太小了。如果召回结果里经常出现多个片段内容高度重复说明overlap太大了。表现到检索结果上前者是答案不完整后者是上下文冗余。一般先固定overlap为chunkSize的十分之一再单独调chunkSize效果不对再动overlap。3.3 嵌入调用与向量库批量写入几个常见的接入姿势切分完成后下一步是把每个片段转成向量再写入向量库。这一步在Java项目里的实现没有太多神秘感封装一个EmbeddingClient里面放一个方法入参是文本字符串出参是float数组。底层实现可以是调用在线接口也可以是请求内网部署的本地嵌入服务。public class EmbeddingClient { private final RestTemplate restTemplate; private final String embedUrl; private final String apiKey; public EmbeddingClient(String embedUrl, String apiKey) { this.restTemplate new RestTemplate(); this.embedUrl embedUrl; this.apiKey apiKey; } /** * 把单个文本片段转成向量。 * 返回的是一个长度为 dim 的 float 数组dim 取决于嵌入模型。 */ public float[] embed(String text) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); MapString, Object body Map.of(input, text); HttpEntityMapString, Object request new HttpEntity(body, headers); ResponseEntityEmbeddingResponse response restTemplate.postForEntity(embedUrl, request, EmbeddingResponse.class); return response.getBody().getData().get(0).getEmbedding(); } }这段代码背后的逻辑说明与参数说明RestTemplate是Spring自带的HTTP客户端足够用。embedUrl是嵌入服务的地址不同服务商路径不一样但请求体结构大同小异一般就是一个input字段。apiKey放在请求头里做鉴权。返回结构里取第一个结果即可因为一次只嵌入一个片段。重点在EmbeddingResponse的字段映射它里面嵌套了一个data数组每个元素有embedding字段这是当前主流的响应格式。如果你的服务返回结构不一样改成对应的JSON字段路径就行。批量写入向量库时我强烈建议控制并发度。不要写一个for循环然后每个片段都等结果回来再写下一个太慢。也不要一口气开几十个线程嵌入服务会被打爆。常见的做法是每批10到20个片段固定线程池并行调用全部完成后批量写一次向量库。写入ES时使用bulk接口写入Milvus时用insert接口都支持批量。这里还有一个容易踩的坑向量库里的字段维度要和嵌入模型输出的维度一致。比如模型输出768维你在ES里建的索引字段是1024维写入时ES会直接拒绝。后面避坑章节还会专门讲这个问题。4. 检索的实现向量召回、混合检索和Prompt拼接4.1 一个能用的语义检索方法算相似度、取TopK检索是RAG项目的核心环节。这一段做得好不好直接决定用户问一句“上个月的报表为什么对不上”系统到底是返回了三段账单明细还是返回了一份考勤制度。只做向量检索是入门方案。思路是把用户的问题也嵌入成向量然后在向量库里找出距离最近的K个片段。ES里可以用script_score实现思路是把每个doc的向量字段取出来和查询向量做余弦相似度计算再按分数排序。public class VectorRetriever { /** * 基于余弦相似度的向量召回。 * 分数越高表示片段与问题语义越接近。 */ public static double cosineSimilarity(float[] vectorA, float[] vectorB) { if (vectorA.length ! vectorB.length) { throw new IllegalArgumentException(向量维度不一致: vectorA.length vs vectorB.length); } double dotProduct 0.0; double normA 0.0; double normB 0.0; for (int i 0; i vectorA.length; i) { dotProduct vectorA[i] * vectorB[i]; normA vectorA[i] * vectorA[i]; normB vectorB[i] * vectorB[i]; } if (normA 0.0 || normB 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } }这个方法的逻辑说明余弦相似度衡量的是两个向量方向上的接近程度和向量长度无关。放在检索场景里它的语义就是“这个问题和这段文档在语意方向上是否一致”。代码本身没有难点真正值得注意的是阈值。相似度0.75以上通常意味着强相关0.6到0.75之间算弱相关0.6以下的召回结果送到大模型手里反而会干扰回答。TopK的选择也有讲究。K太小召回不全K太大噪声太多。起步设K5意思是检索结果取5个片段拼进上下文。如果回答里经常缺关键信息就把K往8到10调整如果回答里出现了多个来源互相矛盾的内容就把K往回收一点。4.2 混合检索与分数归一化关键词得分和向量得分怎么合并只用向量检索有一个隐藏问题向量检索擅长找“意思相近”的内容但不擅长找“字面一致”的内容。比如用户问的是一个精确的订单编号或者异常码向量检索很可能召回一堆语义相关但编号不是用户要的那个的片段。反过来BM25这种关键词检索能精准匹配编号但对“报表为什么对不上”这种口语化问题就束手无策。生产环境里我一般会做混合检索向量检索和关键词检索各跑一遍把两边的分数归一化后加权合并。这样精确匹配和语义匹配都能覆盖。源码包里如果只提供了向量检索你自己加一个BM25召回并不难。public class HybridRetriever { /** * 混合检索向量分和关键词分各自归一化到 [0,1]再按权重合并。 * * param vectorScore 向量召回得分 * param keywordScore BM25关键词召回得分 * param vectorWeight 向量结果权重默认0.7 * param keywordWeight 关键词结果权重默认0.3 */ public static double mergeScore(double vectorScore, double keywordScore, double vectorWeight, double keywordWeight) { double normalizedVector normalize(vectorScore); double normalizedKeyword normalize(keywordScore); return normalizedVector * vectorWeight normalizedKeyword * keywordWeight; } private static double normalize(double score) { // 按经验做了一次sigmoid缩放让分数分布更集中便于设阈值 return 1.0 / (1.0 Math.exp(-score)); } }这段代码的逻辑说明mergeScore把两个不同量纲的分数合并成一个。向量分数的取值范围通常是某个模型相关的区间有的在0.7到0.9之间波动有的在0.5到0.8之间BM25的分数更是可以到十几甚至几十。不归一化直接相加BM25会统治结果向量语义检索等于白做。这里用sigmoid把分数压缩到0到1之间本质上是在帮两种检索方式对齐量纲。需要注意的是归一化方式要和你实际分数分布匹配有的项目用的是min-max归一化效果也差不多。4.3 把检索结果拼成可用的上下文Prompt边界与流式输出检索到片段不是终点这些片段要组织成给大模型的上下文。这里有一个经常被忽略的细节给大模型的上下文质量比数量重要。宁可给三段高度相关的片段也不要给十段勉强相关的片段。拼接Prompt时我习惯做三件事。第一件是把片段按分数从高到低排序。第二件是加上来源编号像“[1] 见文档第三章”这种方便大模型在回答里引用。第三件是在Prompt里明确告诉模型“只根据以下内容回答内容里没有的信息不要编造”。最后这一点是抑制幻觉的有效手段。public class PromptBuilder { /** * 构建注入式Prompt。 * 把检索到的片段按序号拼进上下文并约定回答规则。 */ public static String build(ListRetrievedChunk chunks, String question) { StringBuilder context new StringBuilder(); for (int i 0; i chunks.size(); i) { context.append([).append(i 1).append(] ) .append(chunks.get(i).getContent()) .append(\n\n); } return You are an assistant that answers questions based on the provided context.\n If the answer is not in the context, say 根据已有资料无法回答.\n\n Context:\n context Question: question; } }代码逻辑很简单但有一个细节要留意上下文的长度要控制在大模型的上下文窗口之内。如果切分参数设的是chunkSize500、TopK10那上下文里光片段就有5000个字符再加上问题和指令很容易超过小模型的窗口。我一般建议上下文总量控制在窗口的一半以内留出一半给模型“思考”和生成答案。到这里一个完整的Java RAG流程已经跑通了文档入库、切分、向量化、检索、Prompt拼接、模型生成。下一章聊一聊从“Demo跑通”到“生产可用”之间最劝退的那些坑。5. 从源码到能跑通的排错现场五个最常见的坑5.1 中文乱码与标题被截断现象、原因、解决第一个跳出来的坑基本都出在编码上。现象是入库时预览切分结果中文全变成了乱码或者标题显示了一半就断掉。第一次遇到十个有九个以为是代码bug查半天发现是文件解析环节的编码设置错了。原因分两层。底层的是PDF解析库本身对中文支持不完善尤其是一些扫描版PDF提取出来的文本用的编码不是标准的UTF-8。上层的是Java代码读文件时用默认编码在Windows上默认是GBK在Linux上是UTF-8同一个项目换台机器运行结果就不一样。解决方法是在文本解析入口强制指定UTF-8并且对PDF解析结果做一次编码检测。Java读文件用new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))不要依赖系统默认。对于扫描版PDF不要强行做OCR接入这类文本解析出来本身就是乱码可以直接在流程上跳过或者用专门OCR服务转一遍再进知识库。5.2 向量维度不一致导致写入失败查看两个地方的版本现象是入库时一切正常但向量写入向量库时报错提示维度错误或者ES返回“mapper_parsing_exception”。原因几乎可以锁定在嵌入模型和向量库索引的配置上不一致。常见的情况是嵌入模型换了老代码里存的向量是768维新模型吐出来的是1024维ES索引却还按768维建着写进去就报错。另一个可能的原因是本地嵌入服务和在线嵌入接口返回的维度不一样代码里写死了某一个。解决方法是在入库流程里先取一条文本跑一次嵌入把返回向量的长度打出来和向量库索引里的维度配置对比。排查时重点看两个文件一个是application.yml里的索引定义一个是EmbeddingClient里的模型版本。修改时要把索引删掉重建ES不会允许你改已有索引的向量字段维度。这是一个血泪教训千万不要只改代码不重建索引。5.3 检索结果TopK全是无关内容阈值与切分大小的联动关系现象是用户问了一个明确的问题检索回来的TopK片段看起来和问题没有任何关系或者相关度极低。这个问题最容易被归咎到“向量检索不行”上但实际多数情况下是切分参数和阈值的配合出了问题。原因一chunkSize设得过大一个片段里包含了多个知识点向量化后语义被平均导致任何问题都能和它“沾点边”。原因二检索时没有设置最低阈值低到0.4的片段也进了TopK模型被一堆弱相关内容干扰。解决方法分两步。先把切分大小降下来比如从500改成300观察召回片段的主题是否更聚焦。然后在检索入口设置一个最低接受分数低于0.6的片段直接丢弃宁缺毋滥。这一步做完检索质量问题会改善一大半。5.4 并发入库时内存溢出流式处理与批量提交现象是文档有几万条入库跑到一半Java进程直接OutOfMemoryError然后整个服务挂掉。原因是入库代码把所有文档一次性读进了内存或者把几万个片段全部积压在一个ArrayList里等批量写入。文本解析本身是内存大户一个100页的PDF读进内存可能就要几百MB再叠加片段集合还没开始向量化就爆了。解决方法是按“读一份、切一份、写一份”的节奏来做不要先全量读取再统一处理。每个文档解析完后立刻切分、立刻提交入库然后释放引用。批量入库时设置合理批次大小比如每200条依赖提交一次。ES的bulk接口和Milvus的批量插入都支持这种按批提交的方式。内存问题在Java RAG项目里非常常见但解决方案通常都不复杂核心是不要一次性把所有东西都装进内存。5.5 相似度结果恒为0黑匣子里最常被忽略的问题现象是所有向量检索的相似度返回全是0或者接近0无论怎么调参数都一样。这是最让人摸不着头脑的一个坑因为代码看起来完全没毛病。原因通常有两类。一类是嵌入接口返回的数据解析出了问题比如ResponseBody里拿到的字段名和实际JSON字段不一致导致拿到的全是空数组余弦计算时除以0返回了0。另一类是向量库查询时查询向量的维度虽然一样但值域没做归一化比如模型吐出的向量需要L2归一化后才计算余弦相似度没有归一化时相似度会被压缩到非常小的区间看起来像0。解决方法是写一个单元测试把同一个文本嵌入两次计算相似度正常情况下应该是1.0。如果不是1.0去查嵌入接口的解析代码。再拿两段明显不相关的文本嵌入后算相似度正常情况下应该是负数或接近0。用这两个极端值去确认整个相似度计算链路是通的。这个“先自检再查数据”的思路能帮你少走很多弯路。6. 跑通之后怎么验证与调优建一个小规模评测集测命中率6.1 一个简单的评测集结构问题、期望标题、期望答案片段RAG项目跑通之后最尴尬的问题来了怎么证明它好用肉眼抽几条问题试一下看着还行但你不敢上线。因为一旦上线用户问的问题千奇百怪谁也不知道系统会在哪个问题上翻车。我一般会从项目里抽人力建一个小规模评测集规模不用大50到100条问题就够了但结构一定要固定。每条评测数据包含三个字段问题、期望命中的文档标题或片段ID、期望答案里包含的一个关键信息。比如问题是“设备温度超过多少度必须停机”期望文档标题是《设备安全操作规程》期望答案里必须包含“85度”这个关键词。建这个评测集的时候要和业务方一起做因为只有他们知道真正的高频问题是什么。评测集的目的是把“感觉回答还行”变成“命中率从68%提升到了83%”。没有这个数字你后面调切分参数、调TopK都是在黑匣子里碰运气。6.2 跑分方法命中率与完整率的统计口径跑分分两个维度检索命中率和答案完整率。检索命中率看的是TopK里是否包含期望命中的文档答案完整率看的是生成答案里是否包含期望关键信息。两个维度合起来才能判断问题到底出在检索环节还是生成环节。一条最简单的评测逻辑可以按下面这段Java代码来组织public class Rageval { /** * 简单评测判断TopK结果和最终答案是否满足预期。 * 只统计两个指标命中率、完整率。 */ public static void evaluate(ListTestCase cases, RetrievalService retrieval) { int hitCount 0; int fullCount 0; for (TestCase testCase : cases) { ListRetrievedChunk topK retrieval.retrieve(testCase.getQuestion()); boolean hit topK.stream() .anyMatch(chunk - chunk.getTitle().contains(testCase.getExpectedTitle())); if (hit) { hitCount; } } System.out.println(检索命中率: (hitCount * 100.0 / cases.size()) %); } }这段代码的逻辑说明它把每条评测数据的问题拿去真实检索一遍检查TopK里有没有期望的文档标题。如果你的向量库里没有存标题字段可以把期望标题改成期望片段ID或关键词。实际使用时你需要按自己的RetrievalService接口调整。完整率的判断逻辑更简单把最终生成的答案拿出来检查里面的字符串是否包含期望关键信息。跑分结果出来以后按两组数据排查定位问题命中率高但完整率低问题出在生成环节比如Prompt里没有强调“必须引用上下文里的内容”或答案被模型截断了命中率低问题出在检索环节需要调整切分参数或TopK数量。6.3 根据评测结果调三类参数切分大小、TopK、重排轮数有了评测分数调参就不再是玄学。按照我自己的习惯调参顺序固定为先调切分参数再调TopK最后才考虑重排。切分参数调整的观察点如果同一个问题在评测集里反复无法命中正确片段先把chunkSize从500降到300跑一遍观察命中率变化。往往只改这一个参数命中率就提升五到十个百分点。原因是长片段向量化后语义被稀释问题向量难以准确匹配。如果降低之后答案经常不完整说明切得太碎略微回调一点。TopK调整的观察点命中率低而TopK结果里有正确答案但排位靠后说明K太小了。从5调到8再看命中率。如果K到了10命中率没变化就不要继续往上加了加多了只会给生成环节注入噪声。重排是最后的加分项。所谓重排就是检索返回Top20再用一个更精细的模型对Top20重新排序取前5进Prompt目的是让TopK的排序更符合真实相关性。这个环节不是必做的在评测集命中率低于70%时先不要碰它因为问题大概率出在切分而不是排序。命中率到85%以上还想继续压再考虑加重排。我自己回看折腾RAG项目的这段经历最值钱的一条经验是一切以评测集的数据为准。肉眼看着不错的回答放进评测集里跑一遍分数可能只有六成你以为没调好的检索链路跑完分反而发现已经稳定在八成。把评测集做起来后面所有优化都会变得有方向、有反馈不再靠感觉改参数。希望这个习惯对你也能派上用场希望帮到你。本文还有配套的精品资源点击获取