ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java RAG知识库实战:从选型、混合检索到避坑指南

Java RAG知识库实战:从选型、混合检索到避坑指南 简介这份资源是一套基于 Java 实现的 RAG增强检索生成实战项目定位于需要落地知识库与语义检索的开发者解决传统关键词匹配难以理解复杂查询的问题。项目将 Java 后端能力与向量检索、知识库组织相结合可用于企业知识管理、在线问答、客户服务等场景适合具备一定 Java 基础并希望掌握 RAG 实践路径的工程技术人员。包体整体为 zip 压缩包大小 14.32MB共 266 个文件其中以 231 个 Java 源码文件为主体配合 XML 配置、YAML 环境配置、SQL 初始化脚本、Dockerfile 等构成可部署、可调试的完整工程少量 PNG 图片和 Markdown 文档辅助说明界面与流程。目前已有 1651 人浏览学习。除完整工程外资源还附带流程教程从项目结构到数据入库、检索调用均有说明。通过阅读源码可深入理解 Embedding 存储、检索服务、LLM 调用等模块的协作方式对希望快速上手 RAG 或改造自身知识库系统的开发者具有实际参考价值。1. Java 做 RAG 不是硬凑知识库检索增强到底解决什么问题给老板演示知识库问答时翻车过一次问“公司报销制度里差旅费上限是多少”大模型答得头头是道数字却是网上抄来的通用值跟公司制度完全对不上。这就是典型的 RAG 瓶颈——模型自身知识再强没接上你的知识库回答永远停留在“听说过”的层面。这个标题里的 Java 实现的 RAG 项目本质上是把知识库、检索引擎和大模型生成串成一条流水线让问答结果有据可依核心落在“增强检索”四个字上。这套方案解决的是企业里最实际的诉求内部知识库问答、客服机器人、运维文档助手。很多团队觉得 RAG 是 Python 的专利但真实业务服务的后端往往是 Java 写的把 RAG 直接嵌进 Spring Boot 服务里比另起一套 Python 服务再互相调省太多事。这个实战项目适合有 Java 基础、想在现有系统里落地知识库问答的工程师。一句话RAG 不挑语言挑的是知识库构建质量和检索召回率这两点才是 rag 实战里真正要死磕的地方。2. 开工前的选型清单向量库、Embedding 模型和 LLM 接入各有各的取舍想跑通一条 Java 版 RAG 流水线先别急着写代码把三个选型定下来项目基调就稳了向量存哪、文本向量化用什么模型、大模型怎么接。做错一个后面返工都是伤筋动骨的。我见过不少人先拿 ChromaDB 起步数据量一上来就换库结果所有文档得重新切分重写索引。选型这事前面的时间花得越狠后面越省事。2.1 向量库选型Elasticsearch 8.x 是 Java 项目最省事的一条路市面上的向量存储方案不少但放到 Java 技术栈里筛一遍立刻窄了很多。真正值得比较的就几类方案检索能力Java 友好度适用场景Elasticsearch 8.xkNN 向量检索 BM25 关键词检索天然支持混合检索官方 Java ClientSpring Data 集成成熟已有 ES 集群的团队通用首选Milvus专业向量检索性能强支持标量过滤HTTP SDK 可用但运维组件多数据量千万级以上、检索性能要求极高pgvector向量检索够用部署最简单JDBC 直连无额外组件百万级以内数据、不想引入 ESChromaDB轻量Python 生态好用Java 只能走 HTTP能力有限本地原型验证不推荐上生产我一般优先推 Elasticsearch 8.x原因很直接ES 本身是 Java 写的Java 项目里接入它没有任何心智负担。ES 8.x 内置了 kNN 检索能力dense_vector 字段类型直接支持向量索引而且它同时具备 BM25 全文检索能力——这意味着混合检索向量 关键词在同一个引擎里就能做掉不需要再单独维护一套检索服务。如果你所在公司已经用了 ES那这条路几乎是白捡的。数据量不大、团队也没人愿意运维 ES 的话pgvector 是可以考虑的次选。它就是一个 PostgreSQL 插件CREATE EXTENSION vector 就能用JDBC 直接连省掉一个中间件。但它的短板也明显向量检索性能到几百万条之后开始吃力而且混合检索需要自己拼 SQL体验比 ES 差一截。至于 Milvus除非数据规模大到 ES 扛不住否则别一上来就上这个重武器运维成本会吃掉你的开发时间。2.2 Embedding 模型本地跑服务还是 HTTP 调用 API向量库定了之后第二个关键选择是 Embedding 模型——它负责把文本变成向量直接决定检索召回质量。中文场景下我常用的是 bge 系列bge-m3 和 bge-large-zh 的效果都经过大量验证。但在 Java 里跑这些模型有个现实问题模型推理本身可以用 ONNX Runtime 的 Java 绑定麻烦的是 tokenizer 和预处理流程HuggingFace 生态的 tokenizer 是 Python 原生的Java 移植版不全自己用正则去复刻分词逻辑效果总是差那么一点。所以我在 Java 项目里的常见做法是把 Embedding 推理单独包一个 Python 微服务用 FastAPI 暴露一个 HTTP 接口Java 端只需要发请求拿向量。这样做的好处是模型更新、tokenizer 升级都不影响 Java 主链路模型推理的资源消耗也被隔离出去。数据敏感、必须全部内网部署的场景这个 Python 服务就部署在内网数据不出网合规上也说得过去。还有一个路线是直接调用第三方 Embedding API比如各类大模型平台开放的 embedding 接口。这条路最省事但每百万 token 要花钱而且文本内容要发到外部服务企业的知识库内容往往不允许这么做。如果你做的项目是个人学习或公开知识库用 API 没毛病如果是企业内部文档老老实实本地部署模型。另外注意一点不管用哪个模型必须记住它输出的维度bge-m3 是 1024 维bge-large-zh 是 1024 维有些小模型是 768 维。向量维度写错ES 索引直接建不出来这个坑后面会细说。2.3 LLM 接入Spring AI / LangChain4j / 原生 HTTP 三选一最后一步是把大模型接进来。Java 生态里现在有三条路可以走。Spring AI 是 Spring 官方出的 AI 框架它的定位是把 AI 能力做成 Spring Boot 的 starter自动配置、依赖注入那一套都给你安排好了。但 Spring AI 目前对国内模型的适配不如对 OpenAI 那么顺滑如果你用的是国产模型有时候得自己写适配器。LangChain4j 是 Java 生态里公认的开源 rag 框架RAG 相关的组件很齐全EmbeddingStore、ContentRetriever、ChatMemory 都有抽象想快速搭一个带知识库的问答服务用它起步很快。它好的一点是抽象层做得干净向量库从 ES 换到 pgvector只需要改配置和依赖业务代码基本不动。原生 HTTP 是最可控的方案直接拿 Java 11 的 HttpClient 调模型接口自己拼请求、自己解析流式响应。大模型的接口基本都是 OpenAI 兼容格式国内主流模型也大多兼容这个协议所以写一个通用调用类并不难。代价是重排、上下文管理等逻辑全要自己实现适合不想被框架束缚、想完全掌控链路的场景。我的建议是先拿 LangChain4j 跑通全流程因为它上手快、踩坑资料多。跑通之后如果你发现框架的某些抽象限制了你做混合检索调优再往原生 HTTP 迁移也不迟反正核心链路无非就是检索 拼提示词 调用模型。3. 知识库构建流水线从 PDF、Word 到向量索引的全过程选型定了接下来进入正题知识库是怎么建的。这一步我把它拆成三段——文档解析、文本切分、向量化写入。很多 RAG 项目最后效果差七成问题出在这一步而不是模型选得不好。你要明白一个前提大模型不会替你理解你的文档它能看到的只是你切好并向量化的那些文本片段。片段切得烂检索再好也白搭。3.1 文档解析用 Apache Tika 抽文本别自己写解析器知识库的输入很杂PDF、Word、Markdown、HTML可能还有扫描图片。Java 生态里做文档解析Apache Tika 是绕不开的选择。它一个库就能处理几十种格式内部统一转成纯文本不用你为每种格式各写一套解析逻辑。值得警惕的是很多人图省事用 PDFBox 只处理 PDF结果 Word 文档进来就傻眼用 Tika格式兼容问题一次解决。import org.apache.tika.Tika; import java.io.File; import java.io.FileInputStream; // Tika 会自动识别文件类型并抽取文本内容 Tika tika new Tika(); String text tika.parseToString(new FileInputStream(/data/docs/报销制度.pdf)); // 常见的清洗压缩连续换行和空格避免切分时产生大量空白片段 text text.replaceAll(\\s, ).trim(); System.out.println(抽取文本长度: text.length());这段代码看着简单但 parseToString 背后做了很多事解析 PDF 的段落结构、剔除图片里的乱码、处理 Word 里的表格内容。Tika 对 PDF 里的选区和表格排版支持有限如果文档是复杂表格抽出来的文本顺序可能错乱。更关键的一个问题扫描版 PDF 没有文字层Tika 抽出来是空字符串。这种情况要么配 OCRTika 可以集成 Tesseract要么老老实实先把扫描件转成可复制文字的 PDF。3.2 切分参数chunk_size、overlap 怎么设依据是什么拿到纯文本之后下一步是切分。切分的目标不是把文本截断而是让每个片段都保持相对完整的语义。切太碎一句完整的话被拦腰折断检索时语义不连贯切太长单个片段里塞了太多无关信息向量化后的特征被稀释。中文场景下我常用的参数是 chunk_size500、overlap80单位是字符数。500 个汉字大约能表达一个完整主题80 个字符的重叠保证跨片段的信息不丢。public ListString splitText(String text, int chunkSize, int overlap) { ListString chunks new ArrayList(); int start 0; while (start text.length()) { int end Math.min(start chunkSize, text.length()); // 优先在句号、问号、换行处断开而不是直接从中间切开 if (end text.length()) { int boundary text.lastIndexOf(。, end); if (boundary start chunkSize / 2) { boundary text.lastIndexOf(\n, end); } if (boundary start) { end boundary 1; } } String chunk text.substring(start, end).trim(); if (chunk.length() 50) { // 过滤过短的噪声片段 chunks.add(chunk); } start end - overlap; } return chunks; }这个切分逻辑的关键在边界处理不要在句子中间硬切尽量等到一个句号或换行符再收刀。参数上 chunk_size 和 overlap 的搭配有讲究500/80 是通用起步值可以根据文档类型调整合同条款类文档可以缩到 300因为它们本身条目就短技术手册类文档可以用 800因为段落语义完整。overlap 的作用是给检索一个“缓冲带”避免一个问题恰好落在两个片段的接缝处导致召回失败。调参的时候看一个指标检索结果里有没有包含问题的核心关键词所在的原文片段如果没有多半是切分把关键词切丢了。3.3 Embedding 生成与向量写入 Elasticsearch切分完成接下来把每个片段转成向量再写入 ES。前面说过Embedding 推理放在 Python 微服务里Java 这边走 HTTP 调用。这里用 Java 11 的 HttpClient 写一个调用方法import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // 调用本地 Python embedding 服务返回 float[] 向量 public float[] getEmbedding(String text) throws Exception { HttpClient client HttpClient.newHttpClient(); String requestBody {\text\: \ text.replace(\, \\\) \}; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://localhost:8000/embed)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); // 解析响应 JSON常见的返回格式是 {embedding: [0.1, 0.2, ...]} return parseEmbedding(response.body()); }拿到向量之后批量写入 ES。写入前先建好索引dense_vector 字段的维度必须和模型输出对齐这里以 bge-m3 的 1024 维为例PUT /knowledge { mappings: { properties: { content: { type: text, analyzer: ik_max_word }, source: { type: keyword }, chunk_index: { type: integer }, embedding: { type: dense_vector, dims: 1024 } } } }Java 端批量写入的核心代码import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.BulkRequest; import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import java.util.ArrayList; import java.util.List; // 批量写入batchSize 建议 32-64过大容易导致 ES 拒绝请求 public void batchIndexChunks(ListKnowledgeChunk chunks) throws Exception { ListBulkOperation operations new ArrayList(); for (int i 0; i chunks.size(); i) { KnowledgeChunk chunk chunks.get(i); operations.add(BulkOperation.of(op - op .index(idx - idx .index(knowledge) .id(chunk.getId()) .document(chunk) ))); // 每 32 条提交一次避免内存里堆太多对象 if (operations.size() 32) { BulkRequest request BulkRequest.of(b - b.operations(operations)); client.bulk(request); operations.clear(); } } if (!operations.isEmpty()) { client.bulk(BulkRequest.of(b - b.operations(operations))); } }这里两个细节值得注意。第一个是幂等性同一个文档重复导入时chunk 的 id 要稳定生成比如 source chunkIndex 做哈希否则重复导入会产生重复片段检索时同一个答案出现两遍体验很差。第二个是 embedding 的批量推理性能每调一次 Python 服务只处理一条文本效率太低建议 Java 端攒一批文本、一次性发给 Python 服务做批量推理。批量大小调到 32 左右吞吐量能提好几倍。4. 检索与生成混合检索、重排和 Prompt 拼接的完整链路知识库建好之后真正的 rag 实战才刚开始。用户问一个问题链路是把问题向量化 → 去 ES 里召回相关片段 → 把片段拼进 Prompt → 交给大模型生成答案。看起来简单但每一步都有坑。最核心的一点千万别只做向量检索那会让你在精确关键词场景下全面翻车。4.1 混合检索向量相似度和 BM25 为什么不能二选一向量检索擅长语义匹配比如用户问“报销要几天到账”知识库里写的是“财务审核周期为三个工作日”两者没有共同关键词但语义接近向量检索能召回来。然而向量检索处理精确匹配很弱用户查“单号 REQ-2024-0018”向量检索很可能召回一堆“单号”相关但完全不是目标的那条记录。这种场景恰恰是 BM25 关键词检索的强项。所以正规做法是混合检索一次查询同时跑向量检索和关键词检索再把两路结果合并。ES 8.x 直接在 bool 查询里同时塞 kNN 和 match 子句import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; // 混合检索向量召回 关键词召回 同时进行 SearchRequest searchRequest SearchRequest.of(s - s .index(knowledge) .size(20) // 召回 20 条后续重排再压缩 .query(q - q .bool(b - b .should(sh - sh.knn(k - k .field(embedding) .queryVector(queryVector) .k(20) .numCandidates(100))) // 候选集调大召回更全 .should(sh - sh.match(m - m .field(content) .query(questionText) .boost(1.0f))) ))); SearchResponseKnowledgeChunk response client.search(searchRequest, KnowledgeChunk.class);kNN 子句里的 numCandidates 参数值得解释下它是向量索引在召回阶段扫描的候选数量调大能提升召回率但会增加查询延迟。数据量在十万级时numCandidates 设 100 就够了百万级数据可以往 200 以上调。BM25 那边的 boost 默认 1.0如果你的业务场景偏向精确匹配比如查编号、查政策条款可以把 boost 调到 2.0 甚至更高让关键词匹配的权重压过向量相似度。这套方案就是很多人常说的“语义检索 关键词检索双通道”也是 dify 知识库流水线背后的核心逻辑只是它把编排过程可视化我们这里用代码手工实现。4.2 重排不引入 rerank 模型时的低成本替代法混合检索只是把两路结果合并了但两路分数不是一个尺度向量相似度是余弦值BM25 分数则受文档长度影响极大直接相加等于瞎排。一个低成本的修法是 RRFReciprocal Rank Fusion不看具体分数只看排名位置。它的思想很简单每个文档在两路结果里的排名越靠前融合分越高。public MapString, Double rrfFusion(ListString vectorHits, ListString bm25Hits) { MapString, Double scoreMap new HashMap(); int k 60; // RRF 常数经验值 60 // 对向量召回结果按排名赋予权重 for (int i 0; i vectorHits.size(); i) { scoreMap.merge(vectorHits.get(i), 1.0 / (k i 1), Double::sum); } // 对关键词召回结果做同样处理分数累加 for (int i 0; i bm25Hits.size(); i) { scoreMap.merge(bm25Hits.get(i), 1.0 / (k i 1), Double::sum); } // 按融合分倒序排列取前 5 条 return scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(5) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) - e1, LinkedHashMap::new)); }RRF 的好处是不需要调权重参数对两路分数的尺度差异天然免疫。但它也有天花板它只知道排名不知道相似度差距两条都很差的召回和两条都很好的召回融合后排序可能一样。数据量小的时候够用如果追求更好的效果下一步就是引入 rerank 模型比如 bge-reranker把召回的 20 条逐对计算和问题的相关度重排后取前 5 条。rerank 模型的推理同样可以扔给 Python 服务去跑Java 只负责传问题 候选列表、拿重排结果。这是典型的“用机器算力换质量”离线场景多的时候效果提升非常明显。4.3 Prompt 拼接与答案生成让模型严格“只基于知识库回答”检索再好最后一步 Prompt 没写好答案照样翻车。我在 RAG 项目里最常见的一个问题是模型不肯老老实实引用知识库而是悄悄掺进去它自己的知识回答看着流畅但内容不可信。解决办法是把约束写死在 system prompt 里并在拼接上下文时标明来源。public String generateAnswer(String question, ListKnowledgeChunk contexts) { // 把召回的多个片段拼成一个上下文块并标注来源编号 StringBuilder contextBuilder new StringBuilder(); for (int i 0; i contexts.size(); i) { KnowledgeChunk chunk contexts.get(i); contextBuilder.append([).append(i 1).append(] ) .append(来源: ).append(chunk.getSource()) .append(\n内容: ).append(chunk.getContent()) .append(\n\n); } // system prompt 强制模型只依赖给定资料回答不确定就直接说不知道 String systemPrompt 你是企业内部知识库问答助手。 请只根据下面提供的参考资料回答用户问题。 参考资料中没有的信息一律回答知识库中没有找到相关信息。 禁止编造禁止使用资料之外的知识。 回答时请在末尾标注引用编号如 [1]、[2]。 ; String userPrompt 参考资料 %s 用户问题%s .formatted(contextBuilder.toString(), question); // 调用大模型接口参数按实际情况调整 return callLLM(systemPrompt, userPrompt); }这段代码里有两个细节直接影响答案质量。一是要明确告诉模型“资料里没有就回答不知道”这能极大减少幻觉——很多人觉得 RAG 就能防幻觉其实 RAG 只是把幻觉概率压低不加这句约束模型照样编。二是要求标注引用编号这样业务方可以反查答案对应的是知识库哪篇文档出了问题好追溯。上下文长度也要注意把 5 个片段全部塞进 Prompt如果每个片段 500 字就是 2500 字加上问题和历史对话可能超出小模型的上下文窗口。上下文太长还会稀释注意力模型更容易漏看关键信息必要时只取最相关的 3 条。5. RAG 避坑排查检索为空、答案跑偏、内存溢出的 5 个血泪经验这个章节的内容全是实操里踩过的坑。RAG 项目的坑和普通 Web 项目不一样它往往不是“报错”而是“结果不对”排查起来更隐蔽。我把最常遇到的 5 个问题按现象、原因、解决的思路写出来都是血泪经验。5.1 检索结果为空但知识库里明明有内容现象用户问的问题很普通但检索返回 0 条结果。原因最常见的两类。一是 embedding 服务挂了但没报错Java 端拿到了空向量写入 ES 的向量全是 0检索时自然什么都匹配不上。二是索引 mapping 里的 dense_vector 维度写错查询向量是 1024 维而索引字段是 768 维ES 直接拒绝执行。这两类问题都是“静默失败”日志里不一定有明显报错。解决在检索入口加一个前置校验每次拿到查询向量先检查向量长度是否为非零且等于索引维度不满足直接抛出业务异常。另外在写入阶段对空向量做拦截宁可写失败也不写空值。这些校验代码成本极低但能省掉后面排查的大把时间。5.2 答案通顺但内容不对回答根本没引用知识库现象模型回答得头头是道但内容跟检索回来的上下文对不上明显是模型用了自己的知识。原因system prompt 约束失效。排查下来往往是两个原因一是 prompt 里只说“参考以下资料”而没有说“如果资料里没有直接回答不知道”模型自主发挥的空间太大。二是检索回来的上下文本身质量差比如切分时把问题的关键信息切到了两个片段里模型拼不回去只能自己补。解决先看检索日志检查注入 Prompt 的上下文里到底有没有答案片段。没有问题在检索或切分有但模型不用问题在 prompt 约束。用上一章那段 system prompt把“禁止编造”写死能压掉大部分跑偏。如果还不行考虑换更强指令遵循能力的模型。5.3 处理大文件时内存溢出现象往知识库里导入一个 200 页的 PDFJava 进程直接 OOM。原因很多人在解析完文件后把全文一次性做成一个超大字符串切分时又一次性加载所有片段几百个片段同时持有完整文本和向量对象内存很快就爆了。200 页 PDF 的全文可能有几十万字符加上每个片段的向量1024 维 float 数组约 4KB几百个片段轻松吃掉几十 MB在一个 512MB 的容器里必挂。解决改成流式处理。解析一段、切分一段、批量写入一段不要让整个文件的片段同时驻留内存。写入批次控制在 32 条写入完成立刻释放引用。如果文档很大还可以按章节分页处理比如每 50 页提交一次。5.4 知识库扩大到 1000 篇文档后检索质量明显下降现象只有几十篇文档时回答很准扩到上千篇后答非所问明显增多。原因这是典型的“召回噪声”问题。文档变多之后向量检索召回的 20 条里混入了大量语义相似但实际无关的片段而 kNN 的 numCandidates 没跟着扩大导致真正相关的片段反而排不进去。重排阶段只取前 5 条如果里面 3 条都是噪声答案必然偏。解决几件套一起上。第一把 numCandidates 从 100 调到 200 以上扩大召回候选池。第二给文档打标签用 metadata 过滤比如按部门、按文档类型过滤缩小检索范围。第三引入 rerank 模型做二次排序这一步对千级以上知识库几乎是必需品。5.5 模型返回格式不稳定JSON 解析偶发失败现象要求模型返回 JSON 格式的答案大部分时候正常偶尔返回一段带解释的文字解析直接抛异常。原因很多模型对“只返回 JSON”的指令遵循不彻底追问或复杂问题时它会先给一段说明再给 JSON或者把 JSON 包在 Markdown 代码块里。这和 RAG 链路无关但会卡住整个问答流程。解决解析时先做容错处理——剥离 Markdown 代码块标记、截取第一个 { 到最后一个 } 之间的内容再解析。更稳妥的方式是要求模型用 JSON Schema 约束输出格式或者用支持强制 JSON 输出的模型服务。这个矛盾看着小但在生产环境里一天能遇到几十次必须在解析层兜底。6. 验证与进阶用一份评测集量化 RAG 效果再谈优化RAG 项目做完最怕的就是没有评测体系全凭感觉调参。我自己的习惯是项目上线前先建一个 30-50 条的评测集每条包含一个真实问题和它对应的标准答案片段。不需要多复杂但必须覆盖三类问题精确匹配型查编号、查金额、语义匹配型换一种说法问同一件事、跨片段型答案分散在多个片段里需要模型自己整合。评测集建好后用一段简单的代码自动跑分// 遍历评测集对每个问题做一次完整的 RAG 检索 for (EvalItem item : evalItems) { // 1. 用混合检索召回 top 10 ListKnowledgeChunk hits retrieve(item.getQuestion(), 10); // 2. 如果标准答案对应的片段在召回结果里记一次命中 boolean hit hits.stream().anyMatch(h - h.getId().equals(item.getExpectedChunkId())); hitCount hit ? 1 : 0; } // 输出 Top-5 召回率 double recallRate (double) hitCount / evalItems.size();评测只跑一轮还不够要交叉对比不同参数组合切分 chunk_size 分别用 300/500/800overlap 用 50/80/120记录每组参数下的召回率和答案质量挑最好的组合固定下来。召回率低于 60% 说明知识库构建环节有严重问题往切分和 embedding 服务方向排查召回率过了 80% 但答案质量还是差问题出在重排和 Prompt 上。进阶方向按投入产出比排第一优先加上 rerank 模型投入小、见效快检索精度能立刻上一个台阶。第二优先做父子 chunk 结构——检索粒度小、回答粒度大先用短片段精确命中再把它所在的父段落整段交给大模型答案完整性会好很多。第三优先做增量更新和去重知识库不能每次全量重建源文档变更时只更新对应片段这就需要在写入时为每个 chunk 记录文档指纹和版本号更新时先删旧后写新。上面这些每做一步都用评测集重新跑一遍用数据说话。我最早做 RAG 项目时只在内存里放了三十几条文档Demo 跑得欢一上生产就被检索质量打脸。后来老老实实把评测集、混合检索、rerank 一步步补上效果才真正稳定。希望你做完这个项目别急着堆功能先把评测这一课补上。希望帮到你。本文还有配套的精品资源点击获取
返回列表