上一节我们学习了RAG四大链路的核心概念这一届我们依旧从四大链路入手但是我们这一节更多偏向于实战优化优化--文档收集和切割文档的质量决定了 AI 回答能力的上限其他优化策略只是让 AI 回答能力不断接近上限。因此文档处理是 RAG 系统中最基础也最重要的环节。1、优化原始文档(最重要!!!)知识完备性是文档质量的首要条件。如果知识库缺失相关内容大模型将无法准确回答对应问题。我们需要通过收集用户反馈或统计知识库检索命中率不断完善和优化知识库内容。在知识完整的前提下我们要注意 3 个方面1内容结构化原始文档应保持排版清晰、结构合理如案例编号、项目概述、设计要点等文档的各级标题层次分明各标题下的内容表达清晰列表中间的某一条之下尽量不要再分级减少层级嵌套2内容规范化语言统一确保文档语言与用户提示词一致比如英语场景采用英文文档专业术语可进行多语言标注表述统一同一概念应使用统一表达方式比如 ML、Machine Learning 规范为 “机器学习”可通过大模型分段处理长文档辅助完成减少噪音尽量避免水印、表格和图片等可能影响解析的元素3格式标准化优先使用 Markdown、DOC/DOCX 等文本格式PDF 解析效果可能不佳可以通过百炼 DashScopeParse 工具将 PDF 转为 Markdown再借助大模型整理格式如果文档包含图片需链接化处理确保回答中能正常展示文档中的插图可以通过在文档中插入可公网访问的 URL 链接实现这里提出了 “AI 原生文档” 的概念也就是专门为 AI 知识库创作的文档。我们可以将上述规则输入给 AI 大模型让它对已有文档进行优化。2、文档切片合适的文档切片大小和方式对检索效果至关重要。文档切片尺寸需要根据具体情况灵活调整避免两个极端切片过短导致语义缺失切片过长引入无关信息。具体需结合以下因素文档类型对于专业类文献增加长度通常有助于保留更多上下文信息而对于社交类帖子缩短长度则能更准确地捕捉语义提示词复杂度如果用户的提示词较复杂且具体则可能需要增加切片长度反之缩短长度会更为合适不当的切片方式可能导致以下问题1文本切片过短出现语义缺失导致检索时无法匹配。2文本切片过长包含不相关主题导致召回时返回无关信息。3明显的语义截断文本切片出现了强制性的语义截断导致召回时缺失内容。最佳文档切片策略是结合智能分块算法和人工二次校验。智能分块算法基于分句标识符先划分为段落再根据语义相关性动态选择切片点避免固定长度切分导致的语义断裂。在实际应用中应尽量让文本切片包含完整信息同时避免包含过多干扰信息。3、元数据标注可以为文档添加丰富的结构化信息俗称元信息形成多维索引便于后续向量化处理和精准检索。若读到这里不是很清楚“元信息”是什么的读者请移步到我上一篇文章5.1知识库概念进阶-CSDN博客有详细介绍~在编程实现中可以通过多种方式为文档添加元数据1手动添加元信息单个文档documents.add(new Document( 案例编号LR-2023-001\n 项目概述180平米大平层现代简约风格客厅改造\n 设计要点\n 1. 采用5.2米挑高的落地窗最大化自然采光\n 2. 主色调云雾白(哑光NCS S0500-N)配合莫兰迪灰\n 3. 家具选择意大利BB品牌真皮沙发北欧白橡木茶几\n 空间效果通透大气适合商务接待和家庭日常起居, Map.of( type, interior, year, 2025, month, 05, style, modern, )));2利用 DocumentReader 批量添加元信息比如我们可以在 loadMarkdown 时为每篇文章添加特定标签例如 恋爱状态String status fileName.substring(fileName.length() - 6, fileName.length() - 4); MarkdownDocumentReaderConfig config MarkdownDocumentReaderConfig.builder() .withHorizontalRuleCreateDocument(true) .withIncludeCodeBlock(false) .withIncludeBlockquote(false) .withAdditionalMetadata(filename, fileName) .withAdditionalMetadata(status, status) .build();3自动添加元信息Spring AI 提供了生成元信息的 Transformer 组件可以基于 AI 自动解析关键词并添加到元信息中。这个组件我们上一篇文章有所提及--MetadataEnricher 元数据增强器如有不懂也可以移步我上一篇文章5.1知识库概念进阶-CSDN博客代码如下Component class MyKeywordEnricher { Resource private ChatModel dashscopeChatModel; ListDocument enrichDocuments(ListDocument documents) { KeywordMetadataEnricher enricher new KeywordMetadataEnricher(this.dashscopeChatModel, 5); return enricher.apply(documents); } } Bean VectorStore loveAppVectorStore(EmbeddingModel dashscopeEmbeddingModel) { SimpleVectorStore simpleVectorStore SimpleVectorStore.builder(dashscopeEmbeddingModel) .build(); ListDocument documents loveAppDocumentLoader.loadMarkdowns(); ListDocument enrichedDocuments myKeywordEnricher.enrichDocuments(documents); simpleVectorStore.add(enrichedDocuments); return simpleVectorStore; }清楚了以上知识接下来我们对我们自己的项目来做优化本地知识库优化回顾我们的项目之前有关“切分”的工作只是用了之前介绍的Reader组件读取知识然后简单按照符号去切分内容。可以回顾这一期看看最初我们是怎么做的4.1RAG知识库基础-CSDN博客所以在“文档切分”这一方面能够优化的点就是我们上一篇文章所讲的ETL中的T部分了因此我们的优化思路为在文档入库前增加一条 Document 处理流水线 Reader 负责读文档 TextSplitter 负责把大 Document 切成小 Document MetadataEnricher 负责和 手动添加元数据的方法 给 Document 补充标签 最后 VectorStore 负责向量化并写入 PGVector首先我们先添加TextSplitter和MetadataEnricher的链路接着在原来使用reader组件的方法进行修改定义手动添加的元数据内容完整代码如下public class LoveAppDocumentLoader { private static final String STATUS_METADATA_KEY status; private final ResourcePatternResolver resourcePatternResolver; private final ListDocumentTransformer documentTransformers; public LoveAppDocumentLoader( ResourcePatternResolver resourcePatternResolver, ChatModel chatModel) { this.resourcePatternResolver resourcePatternResolver; /* * Transformer 链 * 1. TokenTextSplitter把较大的 Document 切成适合检索的小块。 * 2. KeywordMetadataEnricher调用 ChatModel为每个切片生成关键词 * 保存到 excerpt_keywords 元数据中。 * * 先切片再提取关键词是为了让关键词对应“具体切片” * 而不是对应整篇过大的 Markdown 文档。 */ this.documentTransformers List.of( TokenTextSplitter.builder() .withChunkSize(400) .withMinChunkSizeChars(120) .withMinChunkLengthToEmbed(20) .withKeepSeparator(true) .build(), KeywordMetadataEnricher.builder(chatModel) .keywordCount(5) .build() ); } /** * 读取全部 Markdown 文档并完成入库前的转换处理。 */ public ListDocument loadMarkdowns() { ListDocument allDocuments new ArrayList(); try { // 从 resources/documents 目录读取所有 Markdown 文件。 Resource[] resources resourcePatternResolver.getResources(classpath*:documents/*.md); for (Resource resource : resources) { String fileName resource.getFilename(); MarkdownDocumentReaderConfig config MarkdownDocumentReaderConfig.builder() // Markdown 水平分割线可以先进行一次结构化拆分。 .withHorizontalRuleCreateDocument(true) // 知识库主要保存正文不把代码块作为知识内容。 .withIncludeCodeBlock(false) // 不把引用块作为主要知识内容。 .withIncludeBlockquote(false) // 手动保存源文件名方便后续追踪检索结果来源。 .withAdditionalMetadata(filename, fileName) .build(); // EReader 负责把 Markdown 解析成 Document。 MarkdownDocumentReader reader new MarkdownDocumentReader(resource, config); ListDocument documents reader.get(); // 手动补充业务元数据每个文档只能属于三种恋爱状态之一。 String status resolveStatus(fileName); documents.forEach(document - document.getMetadata().put(STATUS_METADATA_KEY, status)); // T依次执行切分器和 MetadataEnricher。 for (DocumentTransformer transformer : documentTransformers) { documents transformer.transform(documents); } allDocuments.addAll(documents); } } catch (IOException e) { log.error(Markdown 文档加载失败, e); } log.info(Markdown 文档处理完成共得到 {} 个 Document 切片, allDocuments.size()); return allDocuments; } /** * 根据文件名推断业务状态。 * * p这里是手动元数据的来源。后续如果文件命名规则发生变化 * 只需要修改这个方法不影响 Reader、Splitter 和向量库。/p */ private String resolveStatus(String fileName) { if (fileName null) { throw new IllegalArgumentException(Markdown 文件名不能为空); } // 只匹配文件名最后的状态后缀避免把“恋爱常见问题和回答-已婚篇.md” // 中公共标题里的“恋爱”误判成 status。 if (fileName.endsWith(-单身篇.md)) { return 单身; } if (fileName.endsWith(-恋爱篇.md)) { return 恋爱; } if (fileName.endsWith(-已婚篇.md)) { return 已婚; } // 防止未知文件被错误写入知识库。 throw new IllegalArgumentException( 无法从文件名识别恋爱状态只允许单身、恋爱、已婚。文件名 fileName); } }测试结果如下至此本地知识库在“切分”这一流程的优化就做完了云知识库优化思路和本地知识库优化也是一样的只不过到了云知识库我们就不需要写这些繁杂的代码手动在云知识库官网调整参数即可切分在创建云知识库的时候一定要选择智能切分采用智能切分策略时知识库会首先利用系统内置的分句标识符将文档划分为若干段落基于划分的段落根据语义相关性自适应地选择切片点进行切分而非根据固定长度切分这种方法能更好地保障文档语义完整性避免不必要的断裂。这一策略将应用于知识库中的所有文档包括后续导入的文档。此外建议在文档导入知识库后进行一次人工检查确认文本切片内容的语义完整性和正确性。如果发现切分不当或解析错误可以直接编辑文本切片进行修正我这里导入的时候忘记人工修正了大家在创建导入的时候记得要去检查修正元数据设置要想让切片带有元数据我们就得先开启Metadata抽取功能注意云知识库只有在创建的时候才能开启这个Metadata抽取因此在创建的时候一定要勾选勾选完成后我们在设置Metadata信息这里设置我们要抽取的Metadata创建完成后大模型会自动抽取我们设置好的metadata并且标注上同时创建完成后我们也可以自己给数据打上自定义标签优化--向量转换和存储这一部分没有什么好说的其实就是优化向量存储的位置(用什么数据库好)和优化向量转换所用到的模型向量存储配置需要根据费用成本、数据规模、性能、开发成本来选择向量存储方案比如内存 / Redis / MongoDB。比如我们自己学习的项目的本地知识库向量就是存储在阿里云的云Postgre数据库中用PGvector接口去实现操作在云平台中通常提供多种存储选项比如内置的向量存储或者云数据库选择合适的嵌入模型嵌入模型负责将文本转换为向量其质量直接影响相似度计算和检索准确性。可以在代码中修改public VectorStore loveAppVectorStore( Qualifier(pgVectorJdbcTemplate) JdbcTemplate pgVectorJdbcTemplate, EmbeddingModel dashscopeEmbeddingModel)云平台通常提供多种嵌入模型选项优化--文档过滤和检索这个环节是我们开发者最能大显身手的地方在技术已经确定的情况下优化这个环节可以显著提升系统整体效果。如果对以下要介绍的概念了解不清楚或者过少可以参照我的上一篇文章5.1知识库概念进阶-CSDN博客1.多查询扩展在多轮会话场景中用户输入的提示词有时可能不够完整或者存在歧义。多查询扩展技术可以扩大检索范围提高相关文档的召回率。使用多查询扩展时要注意设置合适的查询数量建议 3 - 5 个过多会影响性能、增大成本保留原始查询的核心语义在编程实现中可以通过以下代码实现多查询扩展Configuration public class LoveAppQueryExpansionConfig { /** * 构造 LoveApp 专用的多查询扩展器。 * * pnumberOfQueries(3)只额外生成 3 个查询避免扩展太多导致检索噪音变大。/p * pincludeOriginal(false)返回结果里不包含原始问题只观察模型扩展出的 3 个问题。/p */ Bean(name loveAppMultiQueryExpander) public QueryExpander loveAppMultiQueryExpander(ChatModel chatModel) { return MultiQueryExpander.builder() .chatClientBuilder(ChatClient.builder(chatModel)) .numberOfQueries(3) .includeOriginal(false) .build(); } }编写测试方法查看结果看原问题是否有扩展为我们设定好的3个扩展问题多查询扩展的完整使用流程可以包括三个步骤使用扩展后的查询召回文档遍历扩展后的查询列表对每个查询使用DocumentRetriever来召回相关文档。整合召回的文档将每个查询召回的文档进行整合形成一个包含所有相关信息的文档集合。也可以使用 文档合并器 去重使用召回的文档改写 Prompt将整合后的文档内容添加到原始 Prompt 中为大语言模型提供更丰富的上下文信息。 需要注意多查询扩展会增加查询次数和计算成本效果也不易量化评估所以个人建议慎用这种优化方式。(所以在这次项目优化中我没有用多扩展查询优化用户输入prompt而是使用了下面的“查询重写”的方式)2.查询重写和翻译查询重写和翻译可以使查询更加精确和专业但是要注意保持查询的语义完整性。主要应用包括使用RewriteQueryTransformer优化查询结构配置TranslationQueryTransformer支持多语言(个人认为没啥用因此不会在我的项目实现它)接下来我们使用“查询重写”的方式来优化我们项目中用户所询问的问题编写查询重写的配置类完成配置后先编写测试方法查看原来的问题是否被重写可以看到用户prompt成功被重写确认好查询重写配置类编写的无问题后那就运用到项目中优化用于与AI交互时用户所写的prompt对于云知识库我们也可以开启“多轮对话改写”来实现查询重写的效果3.检索器Retriever配置检索器配置是影响检索质量的关键因素主要包括三个方面相似度阈值、返回文档数量和过滤规则。1设置合理的相似度阈值相似度阈值控制文档被召回的标准需根据具体问题调整问题解决方案知识库的召回结果不完整没有包含全部相关的文本切片建议降低 相似度阈值提高 召回片段数以召回一些原本应被检索到的信息知识库的召回结果中包含大量无关的文本切片建议提高相似度阈值以排除与用户提示词相似度低的信息在编程实现中可以通过文档检索器配置DocumentRetriever documentRetriever VectorStoreDocumentRetriever.builder() .vectorStore(loveAppVectorStore) .similarityThreshold(0.5) .build();云平台提供了更便捷的配置界面参考文档2控制返回文档数量召回片段数topK控制返回给模型的文档数量平衡信息完整性和噪音水平。在编程实现中可以通过文档检索器配置DocumentRetriever documentRetriever VectorStoreDocumentRetriever.builder() .vectorStore(loveAppVectorStore) .similarityThreshold(0.5) .topK(3) .build();使用云平台可以在编辑百炼应用时调整召回片段数参考文档的 提高召回片段数 部分召回片段数即多路召回策略中的 K 值。系统最终会选取相似度分数最高的 K 个文本切片。不合适的 K 值可能导致 RAG 漏掉正确的文本切片影响回答质量。在多路召回场景下如果应用关联了多个知识库系统会从这些库中检索相关文本切片然后通过重排序选出最相关的前 K 条提供给大模型参考。3配置文档过滤规则通过文档过滤规则可以控制查询范围提高检索精度和效率。主要应用场景场景解决方案知识库中包含多个类别的文档希望限定检索范围建议为文档 添加标签知识库检索时会先根据标签筛选相关文档知识库中有多篇结构相似的文档希望精确定位提取元数据知识库会先使用元数据进行结构化搜索再进行向量检索在编程实现中运用 Spring 内置的文档检索器提供的 filterExpression 配置过滤规则。优化实战(本地)写一个工厂类 LoveAppRagCustomAdvisorFactory根据用户查询需求生成对应的 advisorComponent Slf4j public class LoveAppRagCustomAdvisorFactory { private static final SetString ALLOWED_STATUSES Set.of(单身, 恋爱, 已婚); /** * 当前项目知识库数据量不大先保持较低阈值避免过早过滤导致召回为空。 * 后续如果噪音太多再逐步提高到 0.3、0.5 做对比测试。 */ private static final double SIMILARITY_THRESHOLD 0.0; /** * topK 表示最多召回多少个文档切片。 * 这里沿用项目之前的 8保证课程链接等信息不容易漏召回。 */ private static final int TOP_K 8; private final VectorStore loveAppVectorStore; private final QueryTransformer loveAppRewriteQueryTransformer; public LoveAppRagCustomAdvisorFactory( Qualifier(loveAppVectorStore) VectorStore loveAppVectorStore, Qualifier(loveAppRewriteQueryTransformer) QueryTransformer loveAppRewriteQueryTransformer) { this.loveAppVectorStore loveAppVectorStore; this.loveAppRewriteQueryTransformer loveAppRewriteQueryTransformer; } /** * 创建不带状态过滤的 RAG Advisor。 * * p适合用户没有明确状态或者你暂时只想走普通本地 RAG 的场景。/p */ public Advisor createLoveAppRagCustomAdvisor() { return createLoveAppRagCustomAdvisor(null); } /** * 创建带 status 元数据过滤的 RAG Advisor。 * * pstatus 只能是“单身、恋爱、已婚”。当传入状态时Retriever * 只会从对应状态的知识切片中检索减少跨类别知识干扰。/p */ public Advisor createLoveAppRagCustomAdvisor(String status) { DocumentRetriever documentRetriever createDocumentRetriever(status); return RetrievalAugmentationAdvisor.builder() // 先重写用户问题再拿重写后的问题去向量库检索。 .queryTransformers(loveAppRewriteQueryTransformer) // Retriever 负责真正去 PGVector 中找相关 Document。 .documentRetriever(documentRetriever) // QueryAugmenter 负责把检索到的 Document 和用户问题拼成最终 Prompt。 .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(false) .promptTemplate(new PromptTemplate(LoveAppPrompts.RAG_PROMPT_TEMPLATE)) .build()) .build(); } /** * 创建文档检索器并按需添加 metadata 过滤条件。 */ private DocumentRetriever createDocumentRetriever(String status) { VectorStoreDocumentRetriever.Builder retrieverBuilder VectorStoreDocumentRetriever.builder() .vectorStore(loveAppVectorStore) .similarityThreshold(SIMILARITY_THRESHOLD) .topK(TOP_K); if (hasText(status)) { Filter.Expression expression createStatusFilterExpression(status); log.info(创建带 status 过滤的本地 RAG Retrieverstatus{}, status); retrieverBuilder.filterExpression(expression); } else { log.info(创建不带 status 过滤的本地 RAG Retriever); } return retrieverBuilder.build(); } /** * 构造 metadata 过滤表达式metadata.status status。 */ private Filter.Expression createStatusFilterExpression(String status) { if (!ALLOWED_STATUSES.contains(status)) { throw new IllegalArgumentException(status 只能是单身、恋爱、已婚当前值 status); } return new FilterExpressionBuilder() .eq(status, status) .build(); } private boolean hasText(String value) { return value ! null !value.isBlank(); } }构造完成后我们就可以在我们的项目中调用它了编写测试方法查看优化效果优化云知识库使用云平台目前百炼支持以下两种方式使用标签来实现过滤通过 API 调用百炼应用 时可以在请求参数tags中指定标签。在控制台编辑应用时设置标签但本方式仅适用于 智能体应用。请注意此处的设置将应用于该智能体应用后续的所有用户问答。如图云百炼还支持元数据过滤开启后知识库会在向量检索前增加一层结构化搜索完整过程如下从提示词中提取元数据 {key: name, value: 程序员鱼皮}根据提取的元数据找到所有包含该元数据的文本切片再进行向量语义检索找到最相关的文本切片通过 API 调用应用时可以在请求参数metadata_filter中指定 metadata。应用在检索知识库时会先根据 metadata 筛选相关文档实现精准过滤参考官方文档。最后无论采用何种配置都应多进行命中测试验证检索效果优化--查询增强和关联经过前面的文档检索系统已经获取了与用户查询相关的文档。此时大模型需要根据用户提示词和检索内容生成最终回答。然而返回结果可能仍未达到预期效果需要进一步优化。就是优化“大模型把知识检索结果和用户问题作为上下文交给回答问题大模型”这一步骤这一部分更像是做一个“兜底机制”如果检索时没有检索到相关文章或者用户提问的问题和我们的主题毫不相关那就让AI回答类似于联系人工的回答错误处理机制在实际应用中可能出现多种异常情况如找不到相关文档、相似度过低、查询超时等。良好的错误处理机制可以提升用户体验。异常处理主要包括允许空上下文查询即处理边界情况提供友好的错误提示引导用户提供必要信息边界情况处理可以使用 Spring AI 的 ContextualQueryAugmenter 上下文查询增强器RetrievalAugmentationAdvisor.builder() .queryAugmenter( ContextualQueryAugmenter.builder() .allowEmptyContext(false) .build() )如果不使用自定义处理器或者未启用 “允许空上下文” 选项系统在找不到相关文档时会默认改写用户查询 userText如果启用 “允许空上下文”系统会自动处理空 Prompt 情况不会改写用户输入而是使用原本的查询。我们也可以自定义错误处理逻辑来运用工厂模式创建一个自定义的 ContextualQueryAugmenterpublic class LoveAppContextualQueryAugmenterFactory { public static ContextualQueryAugmenter createInstance() { PromptTemplate emptyContextPromptTemplate new PromptTemplate( 你应该输出下面的内容 抱歉我只能回答恋爱相关的问题别的没办法帮到您哦 有问题可以联系客服 ); return ContextualQueryAugmenter.builder() .allowEmptyContext(false) .emptyContextPromptTemplate(emptyContextPromptTemplate) .build(); } }其他建议除了上述优化策略外还可以考虑以下方面的改进问题类型改进策略大模型并未理解知识和用户提示词之间的关系答案生硬拼凑建议 选择合适的大模型提升语义理解能力返回的结果没有按照要求或者不够全面建议 优化提示词模板引导模型生成更符合要求的回答返回结果不够准确混入了模型自身的通用知识建议 开启拒识 功能限制模型只基于知识库回答相似提示词希望控制回答的一致性或多样性 建议 调整大模型参数如温度值等如果有必要的话还可以考虑更高级的优化方向比如分离检索阶段和生成阶段的知识块针对不同阶段使用不同粒度的文档进一步提升系统性能和回答质量针对查询重写、关键词元信息增强等用到 AI 大模型的场景可以选择相对轻量的大模型不一定整个项目只引入一种大模型