RAG检索的致命盲区:段落找到了,标题却丢了

RAG的许多“幻觉”源于按token切分文档时标题与正文分离,导致语义坍缩。
文章用一个真实案例切入:AI助手把企业套餐的三十天退款期限答给了基础套餐用户,而后者实际只有十四天。作者指出问题不在模型,而在文档切分——机械按token切分把标题和正文拆到了不同块,使两段措辞几乎相同的退款说明失去了区分依据,检索器只能随机挑选。这被概括为“语义坍缩”:文本脱离上下文后信息丢失。作者给出四条修复原则:沿文档结构切分、在embedding前注入层级路径、把关键维度存为可过滤元数据、让模型看到标题并引用来源;并建议编写“同措辞不同章节不同答案”的测试用例来暴露这类接缝问题。核心结论是:一段文字不是事实,而是事实加上它被写下的位置。
一个用户问退款期限,AI助手自信地回答"三十天",还引用了政策原文。但这位用户用的是基础套餐——基础套餐只给十四天。引用的内容是真的,只不过来自企业账户的条款。
问题的根源不在模型,而在文档切分。这是一篇值得每个做RAG(检索增强生成)的人认真读的技术短文,作者 Serguey Asael Shinder 用诗歌体讲清了一个被普遍忽视的工程陷阱。

问题是怎么发生的
政策文档的结构很清晰:每个套餐配一个标题,标题下有一段以相同句式开头的说明——"退款可在……内申请"。基础套餐和企业套餐的段落措辞几乎一模一样,唯一的区别藏在标题里。
文档被索引之前,一个脚本按token数量从上往下切分,每块几百token,完全不管章节从哪里开始。结果就是:标题落在了一块文本的末尾,段落落在了下一块的开头。
索引里于是出现了两段关于退款的文本,措辞几乎相同,但谁都没说明自己属于哪个套餐。检索器按语义相似度排序,两段和问题的匹配度评分一样,模型收到两个没有标签的"自信答案",随机挑了一个。
模型没错,错的是喂给它的东西
作者强调了一个容易被情绪化归因的点:模型完全无辜。它忠实地基于收到的上下文作答,没有任何幻觉。问题出在文本脱离了上下文之后,语义发生了坍缩。
作者用三个类比来描述这种信息丢失:
- 一行数据丢了列名
- 一个步骤丢了所属流程
- 一个限制条件丢了它约束的产品
这就是分块(chunking)的本质风险:机械按token数切分,等于主动放弃作者在文档结构里已经搭建好的语义层级。标题、章节、列表项、表头——这些结构信息不是装饰,它们是消歧义的必需品。
语义坍缩这个概念借用了物理学里“引力坍缩”的隐喻:一段文本的完整语义由“说什么”和“在哪说”两个维度共同支撑,一旦抽掉后者,语义空间里的坐标就会向最近的公共吸引子塌陷,多个原本可区分的点收缩成同一个点。在嵌入空间里,这表现为两段本应相距较远的文本获得了几乎相同的向量——因为嵌入模型只看到“退款可在……内申请”这样的句式,无法感知它头顶的标题。需要强调的是,这并不是嵌入模型的缺陷:任何把文本压成定长向量的方法,都必须丢弃一部分信息,问题在于我们主动丢掉了作者已经免费提供的结构线索。理解这一点,就能明白为什么“换一个更强的 embedding 模型”往往修不好这类 bug——坍缩发生在编码之前,模型再强也无力回天。
怎么修:给每块文本一个"地址"
作者给出的方案很具体,可以归纳为四条操作原则。
1. 沿结构切分,别按数量切分
按照作者已经构建好的结构来分块——标题、章节、列表项、保持表头的数据行。文档结构本身就是最自然的分块边界。
2. 在embedding前注入层级路径
在嵌入一个chunk之前,把文档标题和它上面所有标题构成的路径放在最前面。比如"定价政策 > 基础套餐 > 退款"。这样每块文本自带上下文坐标。
层级路径注入(有时也叫 contextual chunk header 或 breadcrumb prefix)之所以有效,是因为它把消歧信息放进了和正文同一个向量里,而不是留在外部元数据中等待检索阶段被动使用。做法上通常是在 chunk 正文前拼接一行路径,如“定价政策 > 基础套餐 > 退款\n退款可在十四天内申请……”,再整体送入 embedding。代价是前缀会稀释正文的权重、略微拉长序列,因此路径不宜过深,一般保留到最近的两三层即可。另一种等价的变体是“contextual retrieval”:先用一次 LLM 调用为每个 chunk 生成一句它“所属情境”的说明再嵌入,效果类似但成本更高。无论哪种做法,核心都是让每个块自带一个可被检索到的上下文坐标,而不仅仅依赖检索器去猜。
3. 元数据存为可过滤字段
把"套餐类型"这类关键维度存成独立字段,支持过滤。当你知道提问者是谁时,直接过滤掉不相关的分支,而不是靠向量相似度去赌。
元数据过滤属于混合检索(hybrid retrieval)的一环:先用结构化条件缩小候选集,再在缩小后的范围内做向量相似度排序,也就是常说的 pre-filtering。它和前一条原则互补——层级路径解决“文档内部怎么区分”,元数据过滤解决“文档之间、用户之间怎么区分”。实践中要注意两点:一是过滤字段必须在索引阶段就抽取好,事后再补往往要重建整个索引;二是过度过滤会带来召回率下降,当过滤条件过窄、候选集为空时,系统需要优雅降级(例如放宽条件或提示无法回答),而不是硬着头皮返回一条不相关的片段。把“套餐类型”这类高区分度的维度做成可过滤字段,本质上是在用结构化知识替检索器做它不擅长的排除法。
4. 让模型看到标题并要求引用
把标题和正文一起给模型,并让它同时引用两者。如果模型必须输出"来源:定价政策 > 基础套餐 > 退款",那么标题丢失的问题会在引用环节暴露出来。
测试用例:同一句话,不同章节,不同答案
作者还补了一个关键建议:写一个能抓住这类bug的测试。
测试的设计思路是:问题答案因章节而异,但表述相同——同样的措辞,不同的套餐,不同的数字。如果检索系统在这些用例上答错,说明结构信息在切分阶段已经丢了。
这种测试比"问一个能被正确回答的问题"有用得多,因为它专门攻击分块逻辑最脆弱的接缝处。
这类测试在工程上常被称为差分测试或对照测试:构造一对输入,只在某一个维度(这里是所属章节)上不同,其余完全相同,然后断言系统必须给出不同答案。它和普通的“问答准确率测试”思路相反——后者追求“答对”,前者专门追求“在极易混淆处不能答错”,因而更能暴露检索层面的系统性缺陷。设计时建议覆盖三个方向:正例(问基础套餐应得十四天)、干扰例(问企业套餐应得三十天,验证系统没有把两段混为一谈)、可引用性(要求模型输出来源路径,验证标题确实进入了上下文)。把这类用例固化成回归测试,任何切分策略的改动都会立刻在 CI 里暴露问题,比上线后靠用户投诉发现要便宜得多。
一个常被忽视的工程常识
文章结尾那句话值得贴在每个RAG项目的README里:
A paragraph is not a fact. It is a fact plus where it was written.
一段文字不是事实,它是事实加上它在原文中被写下的位置。
RAG系统里大量"幻觉"、"答非所问"、"引用错误"的问题,追根溯源,很多根本不是模型能力问题,而是索引构建阶段的信息丢失。切分策略、元数据设计、层级注入——这些看起来无聊的工程细节,决定的恰恰是系统在边界case上的可信度。
模型负责推理,但前提是你没有在它看到问题之前,就把答案的上下文剪掉了。
相关推荐

主流世界之外:被忽视地区的安全与隐私困境
人本安全研究长期集中于西方,导致安全方案在主流世界失效。研究者Collins Minando基于肯尼亚移动贷款应用、网吧认证困境及九国本地专家研究,揭示社会经济约束如何重塑隐私实践,并提出移动货币隐私重设计与PETs部署路径。

Claude被曝可在正常图片中隐藏秘密信息
Reddit帖子称Claude Fable 5.1能在正常图片中隐藏秘密信息,本文梳理事件背景、图片隐写技术原理及潜在安全风险,并给出从传闻走向验证的测试路径建议。

AI助手何时该主动建议?时机决定一切
AI助手何时该主动建议?Reddit讨论揭示:同样的建议在不同时机感受迥异。本文分析主动建议的双面性、用户期待的分界线及设计原则,探讨如何平衡自动化与用户控制。