RAG从入门到实战(四):文档侧优化——让知识片段更容易被找到
RAG从入门到实战四文档侧优化——让知识片段更容易被找到文章目录RAG从入门到实战四文档侧优化——让知识片段更容易被找到前言一、什么是文档侧优化二、为什么原始片段有时不容易检索2.1 片段缺少标题2.2 片段存在指代2.3 用户提问与原文用词不同三、策略一标题和章节路径增强3.1 基本做法与上篇的标题切分结果衔接3.2 标题增强的注意事项四、策略二上下文增强4.1 什么是上下文增强4.2 一种生成上下文的提示词4.3 生产中要考虑什么五、策略三生成可能的问题用于检索5.1 问题索引的思路5.2 数据结构示例5.3 问题生成的质量控制5.4 代码示例构建问题索引并去重六、如何判断优化是否有效七、常见误区八、总结前言上一篇介绍了文档解析与文本切分。切分后每个片段不一定能独立表达完整含义。假设员工手册中的原文是# 员工手册 ## 年假申请 申请需提前三个工作日提交并经直属主管审批。如果只把最后一句作为一个片段用户问“年假怎么申请”检索系统就需要从没有出现“年假”的文本中找到对应内容。即使这句话本身没有错误脱离标题后也很难判断它属于哪一种申请。本文讨论三种常见的文档侧优化为片段补充文档标题和章节路径。为片段补充简短上下文。为片段生成可能的用户问题作为额外的检索入口。本文的制度、文档和问题均为虚构示例。目标不是证明某一种方式永远最好而是理解它们解决的问题、代价和评估方式。一、什么是文档侧优化RAG 的优化可以发生在不同环节文档侧解析、切分、补充标题和上下文、建立索引 查询侧理解问题、改写、多查询等 检索侧向量检索、关键词检索、混合检索 检索后重排序、上下文组织、去重与引用文档侧优化主要发生在知识入库时通过更好地组织和表示片段让后续查询更容易找到它们。与之相对查询优化是在用户提出问题后调整问题的表达方式。两者可以配合使用但职责不同文档侧优化关注“这个片段如何被描述和索引”。查询优化关注“这个问题如何表达更利于检索”。修改文档表示可能要求重新向量化和重建索引修改查询方式通常不必重新处理全部文档。二、为什么原始片段有时不容易检索2.1 片段缺少标题原文文档员工手册 章节年假申请 正文申请需提前三个工作日提交并经直属主管审批。切分后如果只保留正文“申请”指的是什么就不清楚了。补充标题后检索文本变成文档员工手册 章节年假申请 正文申请需提前三个工作日提交并经直属主管审批。这样用户问题和片段之间有了更明确的主题关联。标题可以参与向量化或关键词索引也可以作为元数据单独保存供过滤和展示。具体策略需要依据数据和评估结果决定。2.2 片段存在指代例如原文的上下文是产品 A 的等待期为 30 天。 等待期内发生的特定情况按照产品条款处理。如果第二句话被单独切出“等待期”对应哪个产品可能不明确。这时可以通过保留邻近段落、补充文档标题或构建上下文说明来改善但不能擅自推断条款未说明的细节。2.3 用户提问与原文用词不同文档可能写“需提前三个工作日提交申请”用户却问“请年假要提前多久报备”语义向量有时能够匹配不同表述但不保证一定成功。为原始片段构造可能的问题可以提供额外检索入口。不过生成的问题需要经过质量控制它们是索引辅助不是新的事实依据。三、策略一标题和章节路径增强3.1 基本做法在建立索引时将文档标题、章节路径与片段组合标题员工手册 章节休假制度 / 年假申请 片段申请需提前三个工作日提交并经直属主管审批。一种简单的数据结构chunk{chunk_id:policy-2026-001-03,document_id:policy-2026-001,title:员工手册,section:休假制度 / 年假申请,text:申请需提前三个工作日提交并经直属主管审批。,}retrieval_text(f文档{chunk[title]}\nf章节{chunk[section]}\nf正文{chunk[text]})其中retrieval_text用于建立检索表示。text仍保留原始片段用于展示和追溯。chunk_id等标识用于回查原始资料。也可以分别索引标题、正文或为不同字段设置不同权重实际做法取决于向量库、全文检索引擎及评估结果。与上篇的标题切分结果衔接第三篇的MarkdownHeaderTextSplitter会把标题层级放进每个Document的metadata。索引时可以在保留原文不变的前提下额外构造检索文本defmake_retrieval_text(document):接收标题切分得到的 Document返回检索文本和原文。headings[document.metadata.get(key,)forkeyin(一级标题,二级标题,三级标题)]path / .join(str(item)foriteminheadingsifitem)originaldocument.page_content retrieval_textf章节{path}\n正文{original}ifpathelseoriginalreturnretrieval_text,original这是接入片段document来自第三篇的标题切分结果示例不会修改原文或自动入库。注意一个衔接细节第三篇示例使用strip_headersFalsepage_content中已经保留了标题行沿用该结果时original会与path重复包含标题实际拼接时应先去重或改用strip_headersTrue后再组合。检索表示可能包含模型生成的说明或附加标题但最终引用应回查经过授权的原始片段而不是把增强文本直接当作独立事实。标题是否会提升效果要在同一问题集上对照验证。3.2 标题增强的注意事项标题本身必须来自正确文档及章节解析时要防止层级错配。对所有片段重复加入很长的标题路径可能增加索引成本和无关信息。标题可能过于宽泛例如所有片段都叫“常见问题”对区分片段帮助有限。如文档更新或移动章节需要同步更新相应的增强表示与索引。四、策略二上下文增强4.1 什么是上下文增强上下文增强是在片段旁补充必要的背景信息使片段脱离原文后仍容易理解。例如原始片段申请需提前三个工作日提交并经直属主管审批。 上下文说明该片段来自员工手册的年假申请章节描述年假申请的提交时间和审批要求。可用的上下文来源包括已识别的文档标题和章节路径。当前片段的相邻段落或所属父块。基于原始资料生成的简短上下文说明。这些方法各有取舍直接附加相邻文本可能更容易追溯但会引入重复或噪声模型生成的说明更灵活却可能遗漏条件或编造内容。4.2 一种生成上下文的提示词以下是示意模板不是完整生产方案你将收到文档背景和其中一个片段。 请用一到两句话说明该片段位于文档中的什么位置、讨论什么主题 只使用给定文档中明确出现的信息不要补充外部事实。 如果信息不足请明确说明。 文档标题{title} 章节路径{section} 文档背景{document_context} 片段{chunk}生成后可以将说明与原文一起用于检索但需要明确区分检索表示 标题 章节 生成的上下文说明 原始片段 事实依据 原始片段及可追溯的原始文档生成说明可能出错不能因为它提高了召回就把它当作已验证事实。对高风险资料应增加人工抽检、自动校验或只使用确定性的元数据增强。4.3 生产中要考虑什么上下文生成需要模型调用成本随文档数量、片段数量和输入长度增加。如果每个片段都附带整篇文档可能超出模型输入限制且成本很高。文档内容可能包含恶意指令应将其视为不可信数据提示词不能单独保证安全。文档可能包含敏感信息发送给外部模型前需要确认权限、合规要求和数据处理边界。原始文档变更后上下文说明也可能过期应建立版本关联与更新机制。五、策略三生成可能的问题用于检索5.1 问题索引的思路针对一个原始片段生成几个它可能回答的用户问题原始片段 员工入职满一年后每年可申请 5 天带薪年假。 可能的问题 - 入职满一年后有几天年假 - 年假需要满足什么入职时间条件然后把这些问题作为额外的检索表示并记录它们对应的原始片段 ID用户问题 → 检索生成的问题 → 找到对应的原始片段 → 读取原文 → 回答也可以同时检索原文索引和问题索引再合并候选结果。它与用户问题改写不同生成可能的问题是入库阶段围绕文档构造检索入口查询改写是在提问阶段调整当前用户问题。5.2 数据结构示例records[{record_id:policy-001-chunk-0:question:1,parent_chunk_id:policy-001-chunk-0,kind:question,index_text:入职满一年后有几天年假,},{record_id:policy-001-chunk-0:original,parent_chunk_id:policy-001-chunk-0,kind:original,index_text:员工入职满一年后每年可申请5天带薪年假。,},]命中后通过parent_chunk_id获取原始片段、文档来源和版本而不是直接把生成问题当作事实证据。对多个命中同一原文的结果应考虑去重删除或更新原始片段时也需要同步维护关联记录。5.3 问题生成的质量控制一个候选问题至少应满足能从关联的原始片段或明确给出的上下文中得到支持。没有引入原文不存在的人物、日期、条件或结论。与已有问题不过度重复。用词符合目标用户但不改变原意。一份片段生成的问题数量有上限避免索引膨胀。例如原文只说“入职满一年可申请 5 天年假”不能据此生成“入职半年有几天年假”并假定原文提供了答案。若片段需要其他章节才能回答也应标注这种依赖不能只靠单片段假装完整。5.4 代码示例构建问题索引并去重下面只演示确定性的标题增强和候选结果去重不调用大模型或向量数据库生成问题应由其他组件产生并经过校验。defbuild_index_records(chunk,questions):为原文和已审核问题分别构造检索记录。chunk_idchunk[chunk_id]source_text(f标题{chunk[title]}\nf章节{chunk[section]}\nf正文{chunk[text]})records[{record_id:f{chunk_id}:original,parent_chunk_id:chunk_id,kind:original,index_text:source_text,}]seenset()forquestioninquestions:questionquestion.strip()ifnotquestionorquestioninseen:continueseen.add(question)records.append({record_id:f{chunk_id}:question:{len(seen)},parent_chunk_id:chunk_id,kind:question,index_text:question,})returnrecordsdefdeduplicate_by_chunk(hits):假设 hits 已按最终排序顺序排列保留每个原始片段的首次命中。seenset()result[]forhitinhits:keyhit[parent_chunk_id]ifkeynotinseen:seen.add(key)result.append(hit)returnresultif__name____main__:chunk{chunk_id:policy-001-chunk-0,title:员工手册,section:休假制度 / 年假申请,text:员工入职满一年后每年可申请5天带薪年假。,}recordsbuild_index_records(chunk,[入职满一年后有几天年假,年假申请有什么入职时间要求,入职满一年后有几天年假,])print(索引记录数,len(records))# 原文 1 条、去重后的问题 2 条print(命中原始片段数,len(deduplicate_by_chunk(records)))# 1在实际系统中多个索引各自返回的相似度不一定能直接比较不能简单拼接后按原始分数排序。可以先通过合适的融合方法得到统一排序再做去重RRF 等方法将在检索策略一篇介绍。六、如何判断优化是否有效建议先固定文档集、切分策略和测试问题只改变文档表示方式实验索引内容基线原始片段实验 A标题 章节 原始片段实验 B标题 章节 上下文说明 原始片段实验 C原文索引 关联问题索引比较时要同时观察检索效果前 K 个结果是否包含支持答案的原始片段。证据质量片段是否包含完整条件引用是否可定位。最终回答答案是否准确、是否遗漏条件、是否引用了真正支持结论的原文。成本与延迟额外的模型调用、向量数量、索引体积和检索时间。失败案例生成的问题或上下文是否引入错误、噪声或过时信息。最好使用未参与生成和调参的独立测试问题避免只对模型自己生成的问题表现良好。结果不应只看平均值还要抽查高风险和多条件问题。七、常见误区误区一把生成的上下文当成事实。上下文说明可能出现幻觉应回到原文核查。引用存在也不等于结论一定被引用内容支持。误区二生成的问题越多越好。过多问题会造成冗余和索引膨胀可能挤占其他候选片段的位置。应限制数量并去重。误区三文档增强可以代替文档治理。原始文件错误、过期或未经授权时增强索引不能修复这些问题需要源头更新、权限控制和版本管理。误区四一个片段适合所有问题。复杂问题可能需要多个片段或跨文档证据不能仅靠某一条生成问题保证完整回答。八、总结文档侧优化是在知识入库时改善片段的检索表示标题和章节路径可以补充片段主题。上下文说明可以缓解片段脱离原文后的信息缺失但生成内容需核查。可能的问题可以作为额外检索入口命中后仍应回查原始片段。建议从可追溯、成本较低的标题增强开始建立基线后再测试上下文生成和问题索引。下一篇将进一步介绍 Embedding、相似度和向量检索。