RAG-Embedding

Embedding 模型本质是一个编码器神经网络(通常基于 Transformer),将文本压缩成固定长度的向量

        输入文本
           ↓
    ┌─────────────────┐
    │   Tokenizer     │  "苹果是水果" → [101, 679, 3221, 3717, 1963, 102]
    └────────┬────────┘
             ↓
    ┌─────────────────┐
    │  Transformer    │  多层 Self-Attention 提取语义特征
    │  Encoder 层     │
    │  (BERT结构)     │
    └────────┬────────┘
             ↓
    ┌─────────────────┐
    │   池化层         │  取 [CLS] token 或平均池化
    │  (Pooling)      │
    └────────┬────────┘
             ↓
    ┌─────────────────┐
    │   L2 归一化      │  向量长度归一化为 1
    └────────┬────────┘
             ↓
    [0.023, -0.156, 0.891, ..., 0.034]  ← 768/1024/1536 维向量

一、Tokenizer

RAG 中的 Tokenizer(分词/切块)策略主要决定如何将原始文档切分成适合检索的片段(chunk),直接影响召回质量。

1、Tokenizer的常见策略有一下几种:

  • 1、按固定 token 数或字符数切分,可设置重叠窗口(overlap)避免语义断裂。
[chunk1: 0-512 tokens]
[chunk2: 400-912 tokens]  ← overlap=112
[chunk3: 800-1312 tokens]
优点: 简单高效,易于实现
缺点: 可能在句子/段落中间截断,破坏语义完整性
  • 2、递归字符切分(Recursive Character Splitting)
按优先级依次尝试分隔符:\n\n → \n → 。 → → 字符,尽量保持语义完整。
代表: LangChain 的 RecursiveCharacterTextSplitter
优点: 比固定切分更自然,尊重段落/句子边界
缺点: chunk 大小不均匀
  • 3、语义感知切分(Semantic-aware Chunking)
3.1 基于句子嵌入的语义切分
将文本按句子分割,计算相邻句子的语义相似度
相似度骤降处作为切分点
代表: LlamaIndex SemanticSplitterNodeParser
3.2 基于 NLP 的句子边界切分
使用 NLP 工具(spaCy、jieba)识别句子/词语边界
按句子为单位组合,控制 chunk 大小
  • 4、结构化文档切分(Structure-aware Chunking)

根据文档本身结构切分,保留层级关系:

文档类型 切分依据
Markdown 标题层级(H1/H2/H3)
HTML <section>, <p>, <div>
PDF 段落、页面、表格
代码 函数、类、模块

优点: 语义最完整,chunk 有明确的业务含义

  • 5、父子切分(Parent-Child / Hierarchical Chunking)
    存储大 chunk(parent),索引小 chunk(child):
    检索时用小 chunk 精准匹配
    返回时扩展到父级大 chunk 保证上下文完整
[父 chunk: 整个段落 1000 tokens]
    ├── [子 chunk1: 128 tokens] ← 用于向量检索
    ├── [子 chunk2: 128 tokens]
    └── [子 chunk3: 128 tokens]
  • 6、命题切分(Proposition Chunking)
用 LLM 将段落分解为原子性的、自包含的陈述句(proposition),每个 proposition 独立可理解。
优点: 检索精度极高
缺点: 预处理成本高(需要调用 LLM)
  • 7、Late Chunking(延迟切分)
先对整个文档做 embedding,再切分,让每个 chunk 的向量带有全局上下文信息,解决"切分后语义孤立"问题。

2、策略选型建议

场景 推荐策略
快速原型 固定大小 + overlap
通用文档 递归字符切分
结构化文档(Markdown/HTML) 结构感知切分
高精度问答 父子切分 / 命题切分
长文档全局理解 Late Chunking
中文文档 结合 jieba 的句子边界切分

3、核心调参指标:

  • chunk_size:通常 256~1024 tokens,取决于 embedding 模型上下文窗口
  • chunk_overlap:通常 10%~20% 的 chunk_size
  • 切分后建议对每个 chunk 添加元数据(来源、标题、页码),增强检索后的可解释性

二、Transformer Encoder

一句话:把一段话变成一组数字,让电脑能"比较"两段话是不是在说同一件事。

你和朋友聊天时,你能马上听出"我饿了"和"想吃饭"是一个意思。但电脑只认数字,所以需要一个翻译器,把文字翻译成数字,且保证意思相近的话,翻译出来的数字也相近。

Transformer Encoder 就是这个翻译器。


整个过程只有 4 步

第 1 步:拆字

电脑不认识"苹果手机推荐"这个字符串,需要先拆成小块:

"苹果手机推荐"
      ↓ 拆开
["苹果", "手机", "推荐"]
      ↓ 每个词给一个编号
[  678,   312,   891  ]

就像给每个词发了一张编号卡,编号是查字典得来的。


第 2 步:给每个词一个初始描述

光有编号没用,需要给每个词一个"画像"——用 768 个数字描述它。

"苹果" → [0.5, 0.1, 0.3, 0.8, 0.2, ... ] 共768个数字
"手机" → [0.3, 0.8, 0.6, 0.1, 0.4, ... ] 共768个数字
"推荐" → [0.7, 0.2, 0.5, 0.9, 0.3, ... ] 共768个数字

但此时有个大问题: 每个词的画像是"死"的。不管"苹果"出现在"苹果手机"还是"苹果好吃"里,画像都一样。它还不理解上下文。


第 3 步:让词和词之间"聊天"(Self-Attention)

这是最核心的一步。

打个比方:你新来一家公司,你只知道自己的岗位(初始画像)。你需要和周围同事聊天,了解部门整体在做什么,才能真正理解自己在这个团队里的角色。

Self-Attention 做的就是这件事:让每个词去问其他所有词——"你和我什么关系?"

场景: "苹果 手机 推荐"

"手机"开始问:
  → 问"苹果": 你和我什么关系?
    "苹果"回答: 我是你的品牌修饰词,关系很密切! (分数: 80分)

  → 问"推荐": 你和我什么关系?
    "推荐"回答: 我是动作,你是对象,有关系 (分数: 50分)

  → 问自己: 你和我什么关系?
    自己回答: 我就是我 (分数: 90分)

然后按分数高低,把其他词的信息"吸收"过来更新自己的画像:

新的"手机"画像 = 自身信息×40% + "苹果"信息×35% + "推荐"信息×25%

更新后,"手机"的画像里就融入了"苹果"的信息,电脑就知道这里说的是苹果品牌的手机了。

每个词都这样做一遍。一轮之后,所有词都"理解"了上下文。

为什么要做 12 轮?

一轮只能理解最直接的关系。多轮之后理解越来越深:

第 1-2 轮:  知道"苹果"和"手机"是挨在一起的
第 3-5 轮:  理解"苹果手机"是一个品牌产品
第 6-9 轮:  理解整句话是"有人在找苹果手机的推荐"
第 10-12轮: 形成高度抽象的语义理解,能和"iPhone哪款好"匹配上

类比:你在新公司第一天只认识隔壁同事。一个月后认识了整个部门。半年后理解了公司的战略方向。每一轮"聊天"都让理解更深一层。

多头注意力是什么?

每一轮聊天不只聊一个话题,同时聊 12 个话题:

话题1: 谁修饰谁?       → "苹果"修饰"手机"
话题2: 谁是动作对象?    → "推荐"的对象是"手机"
话题3: 谁和谁意思相近?  → "推荐" ≈ "建议"
话题4: 谁挨着谁?       → "苹果"紧挨"手机"
... 共12个话题同时进行

每个话题聊出来的结果拼在一起,信息更全面。


第 3.5 步:FFN(深度消化)

每次"聊天"之后,每个词需要单独"消化"吸收到的信息:

"手机"刚吸收了"苹果"和"推荐"的信息
    ↓ 经过FFN"消化"
"手机"把新信息和自己已有的知识融合
    → 形成更成熟的语义理解

类比:开完会(Self-Attention)后回到工位整理笔记(FFN),把讨论内容转化成自己的理解。


第 4 步:合并成一个句子向量

12 轮之后,每个词都有了充分理解上下文的向量。最后需要合并成一个向量代表整句话:

"苹果" → [0.31, 0.67, ...]
"手机" → [0.58, 0.44, ...]
"推荐" → [0.37, 0.71, ...]
              ↓
         池化层(Pooling)
              ↓
句子向量 → [0.42, 0.61, ...]  768个数字

这 768 个数字,就是"苹果手机推荐"这句话在电脑眼里的"身份证号"。


总结

步骤 做了什么 大白话
拆字 文本变Token 把句子拆成词
初始向量 Token变768维向量 给每个词一张初始画像
Self-Attention ×12轮 词与词交互 让每个词看看周围词,理解上下文
FFN 深度变换 开完会后整理笔记
Pooling 多个向量→1个 把所有词的理解合并成句子的理解

本质就是:拆词 → 给初始画像 → 反复交流更新画像 → 合并成一个代表整句话的数字

三、池化层

经过 Transformer Encoder 的 12 层处理后,每个 Token 都有了自己的 768 维向量。但做检索时,我们需要一个向量代表整段话

池化层的工作就是:把多个词向量,压缩成一个句子向量。

Encoder 输出:
[CLS]  → [0.31, 0.67, 0.45, ...]  768维
苹果    → [0.45, 0.52, 0.38, ...]  768维
手机    → [0.58, 0.44, 0.61, ...]  768维
推荐    → [0.37, 0.71, 0.29, ...]  768维
[SEP]  → [0.12, 0.33, 0.55, ...]  768维
                  │
            池化层(Pooling)
                  │
                  ▼
句子向量 → [?, ?, ?, ...]  768维   ← 怎么算这个?

不同的池化策略,"怎么算"的方式不同,效果也不同。


四种主流池化策略

    1. CLS Pooling(取 [CLS] 向量)

做法: 直接取 [CLS] 这个特殊 Token 的向量作为句子向量,其他词全部丢弃。

[CLS]  → [0.31, 0.67, 0.45, ...]  ← 直接用这个
苹果    → [0.45, 0.52, 0.38, ...]  ← 丢弃
手机    → [0.58, 0.44, 0.61, ...]  ← 丢弃
推荐    → [0.37, 0.71, 0.29, ...]  ← 丢弃
[SEP]  → [0.12, 0.33, 0.55, ...]  ← 丢弃

句子向量 = [0.31, 0.67, 0.45, ...]

原理: BERT 在预训练时,专门训练 [CLS] Token 去汇聚整句话的语义。经过 12 层 Self-Attention,[CLS] 已经"看过"所有词,理论上包含了全句信息。

优点:

  • 计算最快,只取一个向量
  • BERT 原生设计就是用 [CLS] 做句子级任务

缺点:

  • [CLS] 的预训练目标是 NSP(下一句预测),不是语义相似度
  • 实测在语义检索任务上效果不如 Mean Pooling
  • 信息过度集中在一个 Token 上,容易丢失细节

适用: 分类任务(情感分析、文本分类)


    1. Mean Pooling(平均池化)

做法: 把所有 Token 的向量逐维取平均值。

[CLS]  → [0.31, 0.67, 0.45, ...]
苹果    → [0.45, 0.52, 0.38, ...]
手机    → [0.58, 0.44, 0.61, ...]
推荐    → [0.37, 0.71, 0.29, ...]
[SEP]  → [0.12, 0.33, 0.55, ...]

第1维平均: (0.31+0.45+0.58+0.37+0.12) / 5 = 0.366
第2维平均: (0.67+0.52+0.44+0.71+0.33) / 5 = 0.534
第3维平均: (0.45+0.38+0.61+0.29+0.55) / 5 = 0.456
...

句子向量 = [0.366, 0.534, 0.456, ...]

带 Attention Mask 的版本(实际使用)

实际文本长度不一样,短文本会用 [PAD] 填充到统一长度。Padding 位置不应该参与计算:

Token:  [CLS] 苹果  手机  推荐  [SEP] [PAD] [PAD] [PAD]
Mask:   [  1,   1,   1,    1,    1,    0,    0,    0  ]

只对 Mask=1 的位置求平均:
句子向量 = (CLS + 苹果 + 手机 + 推荐 + SEP) / 5

优点:

  • 每个词都参与,信息保留最完整
  • 不依赖某个特殊 Token 的训练质量
  • 在语义检索任务上效果最好

缺点:

  • 所有词权重相同,"的""了"这类虚词也参与,会稀释关键词信息

适用: RAG 语义检索(业界主流方案)


    1. Max Pooling(最大池化)

做法: 在每一维上,取所有 Token 中的最大值。

             第1维   第2维   第3维
[CLS]  →     0.31   0.67   0.45
苹果    →     0.45   0.52   0.38
手机    →     0.58   0.44   0.61
推荐    →     0.37   0.71   0.29
[SEP]  →     0.12   0.33   0.55

取每列最大值:
第1维: max(0.31,0.45,0.58,0.37,0.12) = 0.58  ← 来自"手机"
第2维: max(0.67,0.52,0.44,0.71,0.33) = 0.71  ← 来自"推荐"
第3维: max(0.45,0.38,0.61,0.29,0.55) = 0.61  ← 来自"手机"

句子向量 = [0.58, 0.71, 0.61, ...]

原理: 每一维取最大值,相当于保留了"最显著的特征"。如果某个词在某个语义维度上激活很强,就保留它。

优点:

  • 对关键词敏感,突出特征明显的词
  • 不容易被虚词和 Padding 稀释

缺点:

  • 丢失了词序和比例信息
  • 只取极端值,对细腻语义差异不敏感
  • 一个"噪声"词的异常激活可能影响整体

适用: 关键词匹配、短文本检索


    1. Weighted Mean Pooling(加权平均池化)

做法: 给不同层/位置的 Token 分配不同权重,再加权平均。

4.1 按层加权(越深层权重越大)

BERT 有12层,每层都有输出:

Layer 1 的"手机"向量  × 权重 0.02
Layer 2 的"手机"向量  × 权重 0.03
...
Layer 11 的"手机"向量 × 权重 0.15
Layer 12 的"手机"向量 × 权重 0.20  ← 最后一层权重最大

原理: 深层语义更抽象更适合检索,给更大权重

4.2 按位置加权(越靠后权重递增/递减)

Token:  [CLS] 苹果  手机  推荐  [SEP]
权重:    0.1   0.2   0.3   0.3   0.1

句子向量 = 0.1×CLS + 0.2×苹果 + 0.3×手机 + 0.3×推荐 + 0.1×SEP

4.3 按注意力分数加权(Attention-weighted)

利用最后一层 Self-Attention 中 [CLS] 对各词的注意力权重:

[CLS] 对各词的注意力:
  苹果: 0.30  ← 模型认为"苹果"很重要
  手机: 0.35  ← "手机"最重要
  推荐: 0.25  ← 其次
  [SEP]: 0.10

句子向量 = 0.30×苹果 + 0.35×手机 + 0.25×推荐 + 0.10×SEP

优点:

  • 重要的词贡献更大,更精准
  • 灵活性强,可以根据任务调整权重

缺点:

  • 权重需要额外学习或手工设计
  • 实现复杂度高

适用: 对精度有极高要求的场景


四种策略对比

策略 计算方式 信息保留 检索效果 计算成本
CLS Pooling 取[CLS]向量 低(只取一个Token) 一般 最低
Mean Pooling 所有Token平均 高(每个词都参与)
Max Pooling 每维取最大值 中(只保留极端值) 中等
Weighted Mean 加权平均 最高(按重要性分配) 最好 较高

主流 Embedding 模型都用什么池化?

模型 池化策略 备注
Sentence-BERT Mean Pooling 开创性工作,验证了Mean优于CLS
OpenAI text-embedding-ada-002 未公开(推测Mean) 闭源模型
BGE 系列(智源) CLS Pooling 但经过专门的对比学习训练CLS
GTE 系列(阿里) Mean Pooling 中文效果好
E5 系列(微软) Mean Pooling 多语言支持好
Jina Embeddings Mean Pooling 长文本支持好

实际建议

大多数 RAG 场景 → 直接用 Mean Pooling,简单且效果最好
关键词检索场景  → 可以尝试 Max Pooling
追求极致精度   → 用 Weighted Mean 或选择已经训练好的模型(如 BGE)

最重要的一点: 池化策略的选择远不如 Encoder 本身的训练质量重要。一个经过对比学习精心训练的模型,用 CLS 也能超过未经训练的 Mean Pooling。选好模型比选池化策略更关键。

四、归一化

每个 Transformer Block 中出现两次:

输入
  │
  ▼
┌──────────────────────────┐
│  Self-Attention          │
└────────────┬─────────────┘
             │
     Add & LayerNorm  ← 第1次
             │
             ▼
┌──────────────────────────┐
│  Feed-Forward (FFN)      │
└────────────┬─────────────┘
             │
     Add & LayerNorm  ← 第2次
             │
             ▼
传入下一层

这里的 "Add & LayerNorm" 其实是两步操作:残差连接(Add) + 层归一化(LayerNorm)。

残差连接(Add)
输出 = 子层输出 + 原始输入
用大白话说:

你参加了一个培训班(Self-Attention / FFN)

不用残差: 你把以前学的全忘了,只记得培训班的内容
用残差:   你保留了以前学的,再叠加培训班的新知识

原始输入:  [0.5, 0.3, 0.8]  ← 你已有的知识
子层输出:  [0.1, 0.4, -0.2] ← 培训班学到的新东西
残差相加:  [0.6, 0.7, 0.6]  ← 新旧结合

作用:

  • 保证信息不会在深层网络中完全丢失
  • 解决深层网络"梯度消失"问题(训练时梯度可以通过残差通道直接回传)
  • 没有残差连接,12 层 Encoder 根本训练不起来
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 5,434评论 0 6
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 1,107评论 1 2
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 1,635评论 0 0
  • 跟随樊老师和伙伴们一起学习心理知识提升自已,已经有三个月有余了,这一段时间因为天气的原因休课,顺便整理一下之前学习...
    学习思考行动阅读 1,048评论 0 2
  • 一脸愤怒的她躺在了床上,好几次甩开了他抱过来的双手,到最后还坚决的翻了个身,只留给他一个冷漠的背影。 多次尝试抱她...
    海边的蓝兔子阅读 1,030评论 1 4

友情链接更多精彩内容