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']}\n"f"章节:{chunk['section']}\n"f"正文:{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)original=document.page_content retrieval_text=f"章节:{path}\n正文:{original}"ifpathelseoriginalreturnretrieval_text,original这是接入片段,document来自第三篇的标题切分结果;示例不会修改原文或自动入库。注意一个衔接细节:第三篇示例使用strip_headers=False,page_content中已经保留了标题行;沿用该结果时,original会与path重复包含标题,实际拼接时应先去重,或改用strip_headers=True后再组合。检索表示可能包含模型生成的说明或附加标题,但最终引用应回查经过授权的原始片段,而不是把增强文本直接当作独立事实。标题是否会提升效果,要在同一问题集上对照验证。
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_id=chunk["chunk_id"]source_text=(f"标题:{chunk['title']}\n"f"章节:{chunk['section']}\n"f"正文:{chunk['text']}")records=[{"record_id":f"{chunk_id}:original","parent_chunk_id":chunk_id,"kind":"original","index_text":source_text,}]seen=set()forquestioninquestions:question=question.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 已按最终排序顺序排列;保留每个原始片段的首次命中。"""seen=set()result=[]forhitinhits:key=hit["parent_chunk_id"]ifkeynotinseen:seen.add(key)result.append(hit)returnresultif__name__=="__main__":chunk={"chunk_id":"policy-001-chunk-0","title":"员工手册","section":"休假制度 / 年假申请","text":"员工入职满一年后,每年可申请5天带薪年假。",}records=build_index_records(chunk,["入职满一年后有几天年假?","年假申请有什么入职时间要求?","入职满一年后有几天年假?",])print("索引记录数:",len(records))# 原文 1 条、去重后的问题 2 条print("命中原始片段数:",len(deduplicate_by_chunk(records)))# 1在实际系统中,多个索引各自返回的相似度不一定能直接比较,不能简单拼接后按原始分数排序。可以先通过合适的融合方法得到统一排序,再做去重;RRF 等方法将在检索策略一篇介绍。
六、如何判断优化是否有效?
建议先固定文档集、切分策略和测试问题,只改变文档表示方式:
| 实验 | 索引内容 |
|---|---|
| 基线 | 原始片段 |
| 实验 A | 标题 + 章节 + 原始片段 |
| 实验 B | 标题 + 章节 + 上下文说明 + 原始片段 |
| 实验 C | 原文索引 + 关联问题索引 |
比较时要同时观察:
- 检索效果:前 K 个结果是否包含支持答案的原始片段。
- 证据质量:片段是否包含完整条件,引用是否可定位。
- 最终回答:答案是否准确、是否遗漏条件、是否引用了真正支持结论的原文。
- 成本与延迟:额外的模型调用、向量数量、索引体积和检索时间。
- 失败案例:生成的问题或上下文是否引入错误、噪声或过时信息。
最好使用未参与生成和调参的独立测试问题,避免只对模型自己生成的问题表现良好。结果不应只看平均值,还要抽查高风险和多条件问题。
七、常见误区
误区一:把生成的上下文当成事实。
上下文说明可能出现幻觉,应回到原文核查。引用存在也不等于结论一定被引用内容支持。
误区二:生成的问题越多越好。
过多问题会造成冗余和索引膨胀,可能挤占其他候选片段的位置。应限制数量并去重。
误区三:文档增强可以代替文档治理。
原始文件错误、过期或未经授权时,增强索引不能修复这些问题;需要源头更新、权限控制和版本管理。
误区四:一个片段适合所有问题。
复杂问题可能需要多个片段或跨文档证据,不能仅靠某一条生成问题保证完整回答。
八、总结
文档侧优化是在知识入库时,改善片段的检索表示:
- 标题和章节路径可以补充片段主题。
- 上下文说明可以缓解片段脱离原文后的信息缺失,但生成内容需核查。
- 可能的问题可以作为额外检索入口,命中后仍应回查原始片段。
建议从可追溯、成本较低的标题增强开始,建立基线后再测试上下文生成和问题索引。下一篇将进一步介绍 Embedding、相似度和向量检索。