嵌入从原理讲起:小猫如何找猫,还跨语言找到Gato

从第一性原理拆解嵌入与余弦相似度,揭示多语言 RAG 静默失败的原因。
文章从关键词搜索为何失灵出发,用颜色类比解释嵌入:把文本含义转成固定长度向量,并通过余弦相似度用夹角衡量语义远近。作者用二十行代码演示「kitten」如何匹配「cat」并让西语「gato」排第二,展现多语言模型的跨语言能力。随后通过对比纯英文模型,指出拼写重叠会制造虚假高分——纯英文模型给西语句打高分只是因为共享 el、ga、##to 等分词片段,并非真正理解。文章最后提醒两个坑:不能跨模型比较向量,换模型必须重新嵌入全部文档;同时要注意最大 token 长度,超出会被静默截断。
计算机无法直接比较含义,只能比较数字。本文从第一性原理拆解嵌入模型:如何把一段文字变成向量,如何用余弦相似度衡量语义远近,以及为什么用错模型会让多语言 RAG 悄无声息地失败。

为什么关键词搜索会失灵
关键词搜索在两个场景下必然失效:同义词和翻译。查询「kitten」时,文档里写的是「cat」;查询「cat」时,文档里写的是西语「gato」。字母层面没有重叠,倒排索引就什么也找不到。
要解决这个问题,就需要一种把含义转成数字的方法。这就是嵌入(embedding)要干的事,也是 RAG 中「R」(retrieval,检索)的第一步。
嵌入是什么:用颜色类比理解向量
嵌入模型把任意文本转成一个固定长度的数字列表,也就是向量。屏幕上每一种颜色都只是 3 个数字(R、G、B):
Orange → [255, 165, 0]
Dark orange → [255, 140, 0] ← 数字与橙色接近
Blue → [ 0, 0, 255] ← 数字离两者都远
橙色和深橙色看起来相似,数字也接近;蓝色看起来不同,数字就远。3 个数字能描述 1600 万种颜色,但 3 个数字并不等于 3 种颜色。
嵌入做的是同一件事,只是数字更多。模型不同,维度也不同:
| 模型 | 每段文本的数字个数 |
|---|---|
| paraphrase-multilingual-MiniLM-L12-v2 | 384(本文使用) |
| BERT-base 系列 | 768 |
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
| Google gemini-embedding-001 | 3072 |
维度越高,能捕捉的细节越细,但存储成本更高、检索更慢。用 384 维模型的效果是这样的:
"The cat sat on the mat." → [ 0.43, -0.38, -0.23, ... ]
"El gato duerme en la alfombra." → [ ... 相似的数字 ... ] ← 含义相同,距离近
"Stock markets fell sharply." → [ ... 完全不同的数字 ... ] ← 含义无关,距离远
两个关键事实容易被忽略:384 是模型的输出维度,由模型设计决定,与文本的词数、字符数无关;单独一个数字没有意义,只有向量之间互相比较时,含义才会浮现。
余弦相似度:用夹角衡量语义距离
余弦相似度衡量两个向量之间的夹角:结果为 1 表示方向一致(含义相同),为 0 表示无关。
import numpy as np
def cosine(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
它忽略向量长度,所以一个短问题仍能匹配一段长文档。
二十行代码跑通语义检索
from sentence_transformers import SentenceTransformer
import numpy as np
docs = [
"The cat sat on the mat.",
"Dogs are loyal pets.",
"Stock markets fell sharply today.",
"El gato duerme en la alfombra.", # 西语:猫在毯子上睡觉
"The central bank raised interest rates.",
]
query = "Where is the kitten?"
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
doc_vecs = model.encode(docs)
q_vec = model.encode(query)
scores = [cosine(q_vec, d) for d in doc_vecs]
for score, doc in sorted(zip(scores, docs), reverse=True):
print(f"{score:.3f} {doc}")
运行结果:
0.560 The cat sat on the mat.
0.480 El gato duerme en la alfombra. ← 西语句排第二
0.138 Dogs are loyal pets.
0.049 Stock markets fell sharply today.
0.046 The central bank raised interest rates.
「kitten」找到了「cat」,两个句子没有任何共享单词。西语句拿到第二名。模型从不逐字母比较「gato」和「cat」,它在数百万翻译句对上训练,目标是让每一对句子落在向量空间中相同的位置——句子落在哪里取决于含义,而不是语言。
上述代码中的 paraphrase-multilingual-MiniLM-L12-v2 是 Sentence Transformers 库提供的多语言句子嵌入模型,它把 12 层 Transformer 编码器的输出通过平均池化压缩成 384 维向量,支持约 50 种语言。Sentence Transformers 是在 BERT、XLM-R 等预训练模型之上做对比学习微调的框架,训练目标是让语义相似的句子向量彼此靠近。模型首次使用时由 encode() 自动下载权重。真正让它具备跨语言能力的是多语言 XLM-R 主干与数百万翻译句对(如英语和西语互译)的训练数据:同义的跨语言句子对在训练中被拉近到向量空间的同一区域,因此模型能在不依赖共享单词的情况下比对语义。
多语言能力翻车:拼写重叠的假象
同样的测试换成只在英文上训练的 all-MiniLM-L6-v2,结果出现反转:
模型 查询 cat(英) gato(西)
多语言 "Where is the kitten?" 0.560 0.480
多语言 "¿Dónde está el gatito?" 0.569 0.467
纯英文 "Where is the kitten?" 0.418 0.096
纯英文 "¿Dónde está el gatito?" -0.069 0.616 ← 陷阱
最后一行是陷阱:纯英文模型给西语句打了最高分,但不是因为它看懂了。看它的分词器输出:
query: ['¿', 'don', '##de', 'est', '##a', 'el', 'ga', '##ti', '##to', '?']
doc: ['el', 'ga', '##to', 'due', '##rm', '##e', 'en', 'la', 'alf', '##om', '##bra', '.']
它们共享 el、ga、##to 这几个片段,所以这是拼写重叠,不是语义理解。同时,它把英文的猫句子排到了最后。
结论很直接:当查询和文档存在拼写重叠时,模型看起来像多语言。只有跨语言测试(A 语言提问、B 语言作答)才能证明它真的多语言。这也是 RAG 选错模型后「悄无声息失败」的典型方式。
这里的 ## 是 WordPiece 分词器的子词前缀标记。BERT 系列模型先把单词拆成词汇表里存在的子词:如果一个完整单词不在词表中,就退回到更短的片段,并用 ## 标记它是某个词的后半部分。例如 gatito 被切成 ga、##ti、##to,而 gato 也包含 ##to,于是两者在查询和文档之间出现共享 token。这个机制让模型能处理未登录词,但也带来副作用:正字法相近的语言(如英语和西语的拉丁字母词汇)会因碎片重叠而产生虚假相似度,而模型实际输出的只是对拼写碎片而非完整语义的编码。
两个容易踩的坑
不要跨模型比较向量。 每个模型都有自己的「语义地图」。用模型 A 的向量和模型 B 的向量做比较,相当于拿 GPS 坐标和 RGB 值比大小。一旦换模型,所有文档都必须重新嵌入。
注意最大长度。 这个多语言模型最多读取 128 个 token,超出部分会被静默截断。这正是 RAG 要切分文档块的原因。可以从下载模型的配置文件里查最大序列长度:
cd ~/.cache/huggingface/hub/models--sentence-transformers--paraphrase-multilingual-MiniLM-L12-v2/snapshots/*/
cat sentence_bert_config.json
# 输出:
# {
# "max_seq_length": 128,
# "do_lower_case": false
# }
分词器是模型的「眼睛」。如果它把一门语言切成语义上无意义的碎片,模型就只能匹配拼写,匹配不了含义。
背景补充
max_seq_length 是模型编码时允许的最大 token 数,这里的 128 个 token 对应大约 80 到 100 个英文单词或更少的非拉丁字母语言。Sentences Transformers 在编码时默认会把超长文本截断到该长度,且不发出警告,因此较长的文档块后半部分会被静默丢弃,检索时仿佛那些内容从未出现。RAG 中常见的 256 或 512 token 分块策略正是为了匹配不同嵌入模型的这一上限。查询同样受此限制,超长查询会被截断,导致关键信息丢失。在选模型时,除了维度,还应确认 max_seq_length 是否覆盖你的典型文档块长度。
相关推荐

前端转AI Agent开发:一线架构师解析职业路径与红利
一线AI架构师解析前端转AI Agent开发的行业变化与落地路径:纯前端岗位为何消失、Agent+全栈为何成为招聘主流,以及Agent开发工程师、FDE、远程接单三条转型路线和对应学习策略。

AI Agent开发课程怎么选?五条避坑指南
机械转行开发者踩坑两年总结的AI Agent开发课程筛选清单:完整交付链路、可写进简历的项目、真实开发视角、更新频率、陪跑反馈五个维度,附具体判断方法和避坑要点。

扣子(Coze)智能体搭建入门:零代码开发AI Agent实战指南
扣子(Coze)是字节跳动推出的一站式AI应用开发平台,支持零代码拖拽搭建AI智能体。本文基于B站教程梳理扣子的核心概念、三大优势、国内版与国际版差异,以及智能体与应用的区别,帮助初学者快速入门AI Agent开发。