RAG 的语义表示层:Tokenization、Embedding 与对比学习
在 RAG 系统里,检索质量很大程度上取决于“文本是怎么被表示出来的”。表面上看,我们只是把一段问题和一批文档送进向量模型,再用相似度做召回;但往下拆,会发现这里至少有三层不同的问题:文本如何被切成模型可处理的单元,单元如何变成向量,向量空间又是如何被训练成“相似的靠近,不相似的分开”的。
这篇文章想把这条链路接起来:从 Tokenization 开始,经过 Embedding 层,再到检索模型常见的双塔结构和对比学习目标。这样看 RAG 的语义表示层,会更容易理解后面常见的检索误差到底出在哪一层。
文本先被切开,但这一步还没有语义
进入模型之前,文本首先会经过 Tokenization。它做的事情比较朴素:把原始字符串切成一组离散单元,再映射成对应的 ID。这个过程决定了模型“看到”的最小输入颗粒度。
现代模型通常不会直接按完整单词切分,而是使用子词级别的切分方式,例如 BPE 或 WordPiece。这样做的目的不是让切分更“符合人类直觉”,而是控制词表规模,同时保留对未登录词、复合词和长尾表达的覆盖能力。像 embedding 这样的词,在某些分词方案下可能会被拆成多个片段,而不是一个整体。
这里有两个边界需要先划清:
第一,Tokenizer 负责的是切分和编号,不负责理解语义。某个 token 被映射成什么 ID,本身没有语义上的邻近关系。ID 100 和 ID 101 只是编号相近,不代表对应的文本也更相似。
第二,Token 数和自然语言里的“词数”不是一一对应的。草稿里提到“100 tokens 约等于 75 个英文单词”,这类经验值可以帮助估算上下文长度,但它只适合粗略估算,不能当成稳定规则。不同文本类型、不同语言和不同分词器下,这个比例都会变化。
Token 数只适合拿来估算成本和上下文长度,不适合直接推断语义密度。
同样是 100 个 token,可能是一段结构清晰的英文说明,也可能是一段包含代码、符号和专有名词的混合文本。对检索模型来说,这两者的表示难度并不相同。
真正承载语义的是 Embedding 层
文本被切成 token 并编号之后,模型才开始进入“表示学习”的部分。最直接的一步,是根据 token ID 去查一个可训练的权重矩阵,拿到对应的向量表示。
可以把这一步理解成查表,但这个“表”不是人工写死的词典,而是在训练过程中通过梯度更新学出来的参数空间。每个 token 对应的向量并不是一个单一标签,而是一组分布式特征:某些维度可能更偏向语法模式,某些维度可能更容易响应领域词、上下文共现关系或局部语义模式。单看某一维通常没有直观解释,语义主要来自多维组合。
这也是为什么“Token ID 不带语义”不等于“Token 表示没有语义”。前者只是编号,后者是经过训练得到的向量参数,两者不在一个层面上。
不过,到了这里还不能直接把它等同于 RAG 里的文本向量。因为 LLM 内部的 token embedding 和检索系统里使用的 text embedding,虽然都叫 embedding,但训练目标并不一样,最终服务的任务也不同。
LLM 的词向量和检索向量,不是在解决同一个问题
这一点在 RAG 场景里很容易混淆。
LLM 内部的 token embedding,是语言建模链路的一部分。它服务的是“给定上下文,预测后续内容”这一类目标。模型当然会在这个过程中学到语义,但这种语义是否适合直接拿来做检索,不是同一个问题。
检索模型关心的是另一件事:给定一个 query,能不能把相关文档映射到更近的位置,把无关文档推远。这里需要的是跨文本片段、跨 query-doc 配对的可比较表示空间。也就是说,RAG 里的 embedding 模型不是单纯把文本“编码一下”,而是在学习一种适合相似度计算的空间结构。
把这两个概念拆开之后,很多工程问题会更清楚:为什么某个底座模型很强,不代表直接拿它的中间层输出做召回就一定好;为什么同样是“向量化”,检索模型的训练数据和训练目标会直接影响召回表现。
双塔结构把“可离线”和“可在线”分开了
RAG 之所以能在大规模文档上做高效召回,一个常见前提就是双塔结构,也就是 Bi-Encoder。
它通常由两部分组成:
- Query 塔:把用户问题编码成向量
v_q - Doc 塔:把文档块编码成向量
v_d
两边各自独立编码,最后在同一个向量空间里比较相似度,常见做法是余弦相似度或点积。
这个结构最大的工程价值,不是“表达能力最强”,而是它把离线计算和在线计算拆开了。文档向量可以提前算好,存进向量库;真正在线请求到来时,只需要把 query 编码一次,再去做 ANN 或相似度检索。这种解耦决定了它很适合 RAG 的高吞吐召回场景。
但双塔的局限也正来自这种解耦。因为 query 和 doc 在编码阶段彼此不可见,模型学到的是一种相对稳定的全局表示,而不是针对某个具体 query 动态建模细粒度交互。遇到下面这些情况时,它通常更容易吃亏:
- 否定关系很关键,例如“支持”和“不支持”只差一个词
- 多跳条件组合,例如既要满足时间条件又要满足角色条件
- 语义相关但表述非常绕,必须看局部交互才能判断是否匹配
双塔的局限不是“效果差”,而是它有明确的精度上限。
它换来了离线预编码和大规模召回能力,但代价是编码阶段缺少 query-doc 之间的细粒度交互。后续很多“召回后再重排”的设计,本质上都是在补这部分能力。
对比学习决定了这个空间怎么长出来
双塔只是结构,真正让这个结构学会“谁该近、谁该远”的,是训练目标。检索向量模型里最常见的一类方法是对比学习。
它的核心思路可以概括成一句话:把正样本对拉近,把负样本对推远。
放到 RAG 语境里,正样本通常是语义上应该匹配的一对文本,例如问题和对应答案片段,或者查询与被标注为相关的文档块。负样本则是那些看起来相近、但实际上不该命中的内容。它可以是随机采样的无关文本,也可以是更难分辨的 hard negatives。
这里最值得注意的,不是公式,而是样本定义。因为模型最后学到的“语义空间”并不是抽象意义上的语言理解,而是被训练数据具体塑造出来的检索偏好:
- 如果正样本主要来自 FAQ 问答,模型会更擅长问答式匹配
- 如果负样本过于随机,模型可能只学会区分明显不相关,遇到近义干扰时就不够稳
- 如果 chunk 切分方式改变,正负样本边界也会跟着变,最终影响向量空间里的聚类方式
换句话说,对比学习学到的不是“通用真理”,而是“在给定标注方式和样本分布下,什么样的相似性对检索最有用”。
一开始容易把问题想成“向量化”,其实核心是“空间约束”
很多人在刚接触 RAG 表示层时,会把注意力放在“文本怎么变成数字”。这当然没错,但只停在这里,通常解释不了实际检索中的问题。
真正影响召回效果的,不是“有没有变成向量”,而是这个向量空间被什么目标函数约束过。Tokenization 决定输入颗粒度,Embedding 层提供可训练表示,双塔结构决定编码方式是否可离线扩展,而对比学习则决定这个空间里的几何关系。
把这几层拆开之后,很多现象就更容易解释:
- 召回不准,不一定是向量维度不够,可能是训练样本定义有偏
- 文档切块一改,效果波动很大,不一定是索引参数问题,可能是表示单元本身变了
- 某些语义非常细的匹配做不好,不一定是向量库问题,而是双塔模型天然缺少交互
这条链路解释了 RAG 表示层的边界
回过头看,RAG 的语义表示层并不是一层单独的“Embedding 黑盒”。它至少包含这样一条连续链路:
文本先经过 Tokenization,被切成模型可处理的离散单元;这些单元再通过 Embedding 层进入可训练表示空间;而检索模型通过双塔结构和对比学习,把 query 与 doc 映射到一个可比较的向量空间里。
这条链路解释了 RAG 为什么能做高效召回,也解释了它为什么会在某些精细匹配上碰到上限。到了这里,后面“多阶段检索”“重排模型”“向量维度与成本权衡”这些工程问题,才有了比较稳的讨论基础。
不过,这个表示层本身也有明显的不完美之处:它依赖训练样本定义,受 chunk 粒度影响明显,而且在 query-doc 需要强交互时会暴露双塔结构的限制。后续如果要继续往上提精度,通常就得进入召回后的重排阶段,而不是只在向量空间里继续做微调。
延伸阅读:
- 下一步要处理的,是如何弥补双塔召回的精度上限:从召回到重排:构建高精度 RAG 的多阶段检索链
- 如果关注工程侧约束,可以继续看向量维度、索引规模和成本之间的取舍:企业级 RAG 落地指南:成本、规模与精度权衡
- 如果是本地部署场景,重点会落在模型选型、评测方法和常见误区上:本地检索模型选型与避坑指南(2026 版)
参考资料:
- Sentence Transformers: Bi-Encoder vs Cross-Encoder
- Hugging Face: Conceptual Guide on SetFit and Contrastive Learning