Skip to content

11.3 机器怎么读懂语义?—— 向量嵌入与多模态 ​

痛点场景:你把文档切好块了,但计算机根本不认识“退款”“退货”“售后”这些词——它只认 0 和 1。你问“退款多久到账?”,如果机器只能做字面匹配,它永远无法把这个问题和“退货后资金原路返还时效”这段资料联系起来。嵌入(Embedding)就是给文字装上“语义坐标”,让机器从“认字”升级到“懂意”。


痛点:字面匹配 vs 语义匹配 ​

用户问题字面匹配能命中吗语义匹配能命中吗
“退款多久到账?”❌ 库里的资料写的是“退货后资金原路返还”✅ 语义相近,命中
“食堂几点开饭?”❌ 库里的资料写的是“餐厅用餐时段”✅ 命中
“怎么连不上网?”⚠️ 可能命中“网络故障”但不精准✅ 更精准命中

一句话:字面匹配看“字长得像不像”,语义匹配看“意思挨得近不近”。Embedding 解决的就是后者。


思路:把文字变成高维坐标 ​

1. 相亲角比喻:多维打分雷达图 ​

想象相亲角有一张打分表,维度是「身高、年薪、幽默感、颜值」:

  • 张三:[180, 50, 85, 90]
  • 李四:[178, 48, 80, 88]
  • 王五:[150, 10, 20, 30]

计算机不需要“认识”张三李四,只要算坐标距离,就能立刻判断“张三和李四是一路人,王五差得远”。Embedding 就是把这句话映射到几百上千维的坐标空间,语义相近的句子坐标就靠近。

2. 三种相似度度量的“适用边界” ​

度量方式公式直觉生活比喻适用场景
余弦相似度 (Cosine)看两向量夹角,值域 [-1,1]不管个子高矮,方向一致就算同路文本语义匹配(工业界默认)
欧氏距离 (L2)看空间直线距离用尺子量地图上的物理距离图像、聚类
点积 (Dot)方向和长度一起看综合投影向量已归一化时的极速计算

💡 实践提示:绝大多数向量库对文本场景默认用 Cosine;如果你已把向量做过 L2 归一化,用点积(Dot)计算等价且更快。


代码实现一:Embedding 计算、三种度量与“手写最近邻检索” ​

下面的代码演示的不只是“算个相似度”,而是把“Embedding → 度量 → 最近邻检索”整条链路自己实现一遍,帮你真正看清向量检索的本质。

python
import numpy as np
from langchain_openai import OpenAIEmbeddings

# 1. 初始化嵌入模型(也可换成智谱/BGE/GTE 等任意 OpenAI 兼容端点)
embed = OpenAIEmbeddings(model="text-embedding-3-small")

# 2. 构造知识库句子与用户问题
docs = [
    "退货成功后,退款将在 48 小时内原路返回至支付账户",
    "年度体检由公司在每年 6 月统一组织,员工可自行预约",
    "猎鹰九号火箭于今日凌晨成功完成回收",
]
query = "我申请退货,钱多久能回来?"

# 3. 批量向量化(embed_documents 会走批量,比逐个调用更快更省)
doc_vectors = np.array(embed.embed_documents(docs))
q_vector = np.array(embed.embed_query(query))

# 4. 三种度量自己实现一遍
def cosine(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

def euclidean(v1, v2):
    return np.linalg.norm(v1 - v2)

def dot(v1, v2):
    return np.dot(v1, v2)

print("=== 三种度量对比(对第 0 篇文档)===")
print(f"余弦相似度 : {cosine(q_vector, doc_vectors[0]):.4f}  (越大越近)")
print(f"欧氏距离   : {euclidean(q_vector, doc_vectors[0]):.4f}  (越小越近)")
print(f"点积       : {dot(q_vector, doc_vectors[0]):.4f}  (越大越近)")

# 5. 手写“最近邻检索”:对每篇文档算余弦,取 Top-K
scores = [cosine(q_vector, v) for v in doc_vectors]
top_idx = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:2]
print("\n=== 最近邻 Top-2 ===")
for i in top_idx:
    print(f"[{scores[i]:.4f}] {docs[i]}")

这段代码揭示的本质:任何“向量检索”底层都在干这件事 —— 把查询转成向量,和库里的向量比距离,取最近的 K 个。所谓“向量数据库”,只是把这个过程加速并做成了可以落盘、过滤、并发的服务(11.4 详述)。


代码实现二:Matryoshka 降维(省钱利器) ​

很多商业嵌入模型(如 OpenAI text-embedding-3)支持 Matryoshka 表征学习(MRL):允许你把 3072 维向量直接“掐头”截短到 512 维、256 维,精度损失很小,但存储和检索成本大幅下降。

python
from langchain_openai import OpenAIEmbeddings

# 老办法:完整 3072 维(text-embedding-3-large),占内存大
emb_full = OpenAIEmbeddings(model="text-embedding-3-large")
v_full = emb_full.embed_query("如何申请年假")
print(f"完整向量维度: {len(v_full)}")

# 降维办法:只取前 256 维(MRL 特性,需模型支持)
v_small = v_full[:256]
print(f"截断后维度: {len(v_small)}  内存降为 1/12")

# 工程建议:先在完整维度上离线评测,找到“精度可接受的最小维度”再定配置

⚠️ 注意:并非所有模型都支持“直接截断维度”,只有训练时采用 MRL 的模型才支持。使用前务必查阅模型说明,或先做小规模离线评测。


多模态嵌入:图文混杂场景怎么办? ​

企业文档里除了纯文本,还有大量架构图、报表截图、UI 设计稿、扫描合同。传统做法是“先 OCR 再文本嵌入”,但 OCR 会丢失排版、颜色、箭头指向等空间信息。

  • CLIP(OpenAI):把图片和文字放进同一个语义空间,输入“红色跑车”的图片与文字都能互相检索;
  • ColPali(基于 PaliGemma):把 PDF 页面以图像形式输入视觉模型,生成基于图像补丁的多向量表征,从而绕过 OCR。用户输入“查看 2025 年各季度净利润柱状图”时,可以直接命中对应页面截图。

🖼️ 多模态嵌入:图文混杂场景怎么办?


主流 Embedding 模型怎么选?(MTEB / C-MTEB) ​

权威基准是 MTEB(英文) 与 C-MTEB(中文)。选型时参考这张速查表:

模型特点适合场景
BGE-M3(智源)Dense + Sparse 三合一、8192 长上下文、100+ 语言中文私有化部署首选
GTE-Qwen2(阿里)基于 Qwen2 底座,在中文检索基准上表现较强中文强语义理解
text-embedding-3(OpenAI)MRL 可降维、生态完善海外/多云场景、快速接入
Jina Embeddings v3支持 LoRA 任务适配需要按领域微调适配

排行榜只能海选,真实语料才是面试 ​

公共排行榜像高考成绩:能筛掉明显不合格者,却不能保证候选人适合你公司的岗位。模型可能在百科问答上很强,却分不清你们内部的相似产品型号;也可能支持上百种语言,但中英混问时把数字和否定词处理错。

Embedding 选型至少做四组检查:

检查为什么最低验证方式
训练时要求的前缀/指令有些模型要求 Query 和 Document 使用不同前缀严格按模型卡编码,再与“不加前缀”对照
最大长度与截断位置关键条款可能在文末被截掉构造长文,把答案分别放开头、中间、末尾
归一化与距离度量模型、库、度量配错会让排序悄悄变化明确是否 L2 归一化,核对 Cosine/Dot 配置
领域与语言公共语料不包含内部缩写、错误码和制度用真实问题测 Recall@K、MRR、nDCG

换模型不是“改一行配置” ​

不同 Embedding 模型产生的是不同坐标系。把查询换成新模型、文档仍保留旧向量,就像拿上海地铁图上的坐标去纽约找站点,数值能算,结果没有意义。正确升级流程是:

  1. 新建独立索引版本,全量重算文档向量;
  2. 用同一评测集对比旧索引和新索引;
  3. 同时记录索引大小、写入时间、查询 P95 与单次成本;
  4. 通过门禁后切换别名,保留旧索引供回滚;
  5. 让缓存键包含 Embedding 与索引版本。

降维要测近邻保持率 ​

“维度从 3072 降到 256,体积变成十二分之一”只说明省空间,不说明还找得准。应把完整维度的精确 Top-K 或人工相关标注作为参照,逐档测试 1536、1024、512、256 维的 Recall@K 与延迟曲线。配套脚本的 evaluate_dimension_recall() 就是这个最小门禁。

⚠️ MTEB 是持续更新的排行榜,正文不应长期写“某模型霸榜”。更稳妥的写法是说明模型能力、发布日期与评测日期,再让读者复查当前榜单。


更换嵌入模型意味着重建索引 ​

向量只在同一模型、同一处理方式下具有可比性。更换嵌入模型、向量维度或文本预处理后,旧文档向量与新查询向量不能直接混用,应建立新索引并完成回归测试后再切换。

模型选择可先用业务语料构造近义表达、型号数字和中英混合问题,比较 Recall@k。排行榜适合缩小候选范围,最终仍要看自己的文档语言、部署条件、维度成本和许可证要求。


进阶:检索器也能“进修” —— 领域 Embedding 微调 ​

痛点场景:MTEB 排行榜只能帮你“海选”,通用模型未必懂你们行业的黑话。用户问“开票抬头怎么填”,你的库里写的是“发票购买方名称”;用户说“走个签”,文档里写的是“发起会签流程”。通用模型觉得这俩八竿子打不着,召回自然失败。

思路:与其继续换更大的通用模型,不如用自己的语料把模型“进修”一遍。做法是构造 3k–10k 条 (query, 正例, 难负例) 三元组:

元素含义怎么来
query用户的真实问法线上日志、客服记录、内部提问
正例(正样本)真正含答案的块人工标注,或从点击/引用反推
难负例(难负样本)被向量检索捞上来、却不含答案的块用当前模型跑一遍,专挑“像但不对”的

然后用对比学习微调:拉近 query 与正例、推远 query 与难负例。其中“难负例”是关键 —— 随机负例太容易区分,模型学不到细微差别。

收益与代价:

  • 常见收益大于“换一个更大的通用模型”,因为它学的是你语料的分布;
  • 还能和本文前面讲的 MRL 降维叠加:先微调出好向量,再截断降维省成本;
  • 代价是要标注/挖掘数据、要重跑训练,属于一次性投入。

⚠️ 红线:微调后向量空间已经变了,必须重建整个索引(呼应本文“更换嵌入模型意味着重建索引”一节),并跑一遍回归集确认没有整体退化 —— 别为了几个黑话把原有能力练废了。

路线成本适用场景
换模型低(改配置 + 重建索引)通用能力不足、语料无明显行业特性
微调模型高(标注 + 训练 + 重建索引)行业黑话多、query 分布稳定、语料量大
只做重排中(加一个重排模型)召回基本 OK,只是排序不理想

下面是 15 行左右的微调骨架,用 sentence-transformers 的损失函数:

python
from sentence_transformers import SentenceTransformer, losses
from torch.utils.data import DataLoader

model = SentenceTransformer("BAAI/bge-base-zh-v1.5")   # 中文检索常用底座

# 训练样本:每条是 (query, 正例, 难负例)
train_samples = [
    ("开票抬头怎么填", "发票购买方名称需填写公司全称", "发票开具时间不得超过当月"),
    # ... 数百到数千条,来自真实日志 + 人工标注
]

train_dataloader = DataLoader(train_samples, shuffle=True, batch_size=16)
train_loss = losses.TripletLoss(model)          # 也可换 MultipleNegativesRankingLoss

model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=1,                                   # 领域适配通常 1~3 个 epoch 就够
    warmup_steps=100,
)
model.save("bge-domain-ft")                     # 微调后:全量重算索引 + 跑回归集

边界:微调是“把通用模型调教成懂你行业的人”,不是“万能提分药”。如果召回本来就差在排序而非语义,优先上重排模型即可,不必一上来就动训练。参考:sentence-transformers 官方仓库、FlagEmbedding(BGE 系列)。


权威官方参考 ​