🐸 青蛙编程网
首页 / AI / 检索增强 RAG / 4. 向量与嵌入

4. 向量与嵌入:文字怎么变成数字

这是什么:嵌入(Embedding) = 把任意一段文字变成一串数字,让「意思相近」变成「数字相近」。

为什么用:上一章切好了块,可怎么判断「报销」和「费用」说的是一件事?两个字面一个字不重合,检索却必须能找到。嵌入就是那把比较语义的尺子。

如何用:本章手算一次余弦相似度,弄懂维度与归一化,最后记住「换模型必须重建索引」这条铁律。

4.1 嵌入是什么

一段文字 → 一个固定长度的数字列表(向量):

"报销" → [0.021, -0.113, 0.045, ..., 0.087]   # 一共 1536 个数

模型在训练时见过海量「意思相近的句子」,于是学会把相近的意思放在向量空间里相近的位置。方向相近 = 语义相近。

概念 说明
维度 向量里有多少个数。text-embedding-3-small 是 1536 维
向量空间 高维空间;每维本身没有含义,是整体表达语义
相似度 两个向量有多接近,用余弦相似度衡量

4.2 手算余弦相似度

余弦相似度衡量两个向量夹角的大小:完全同向 = 1,无关 = 0,方向相反 = −1。

import numpy as np

def cosine(a, b):
    """余弦相似度:1 = 同向,0 = 无关,-1 = 反向"""
    a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

拿三句话实测一下:

from openai import OpenAI
import os

client = OpenAI(api_key=os.environ["LLM_API_KEY"],
                base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"))

sentences = [
    "怎么申请报销?",
    "差旅住宿的报销标准是多少?",
    "公司年会在哪里举办?",
]
resp = client.embeddings.create(model="text-embedding-3-small", input=sentences)
vecs = [d.embedding for d in sorted(resp.data, key=lambda d: d.index)]

print("维度:", len(vecs[0]))        # 1536
for i in range(len(sentences)):
    for j in range(i + 1, len(sentences)):
        print(f"{cosine(vecs[i], vecs[j]):.3f}  {sentences[i]}  ×  {sentences[j]}")

你会看到(示例):

维度: 1536
0.652  怎么申请报销?  ×  差旅住宿的报销标准是多少?
0.318  怎么申请报销?  ×  公司年会在哪里举办?
0.297  差旅住宿的报销标准是多少?  ×  公司年会在哪里举办?

前两句字面几乎不重合,向量却明显更近——这就是嵌入在做的事。

相似度 一般可理解为
> 0.7 高度相关
0.4 ~ 0.7 相关
< 0.3 基本无关

这个阈值因模型而异,别照抄。看到陌生模型时,先拿几组「该匹配 / 不该匹配」的样本量一量,定出自己的阈值。

4.3 维度:越多越好吗

不是。维度越高,存储和计算越贵,而收益递减:

模型 维度 特点
text-embedding-3-small 1536 快、便宜,日常够用
text-embedding-3-large 3072 略准,贵一些

OpenAI 的 v3 系列支持降维(Matryoshka 技术):指定更小的维度,质量略降但速度和体积明显变好。

resp = client.embeddings.create(model="text-embedding-3-small",
                                input=["测试"], dimensions=512)
print(len(resp.data[0].embedding))     # 512

降维必须在建索引时就定好,并且查询时用同样的维度——中途改会算错。

4.4 归一化:一个立刻能用的公式

把向量除以自己的长度,就得到单位向量:

def l2_normalize(v):
    v = np.asarray(v, dtype=float)
    return v / np.linalg.norm(v)

单位向量之间有个漂亮的恒等式:

‖a − b‖² = 2 − 2·cos(a, b)      ⟹      cos(a, b) = 1 − ‖a − b‖² / 2

知道欧氏距离,就能算出余弦相似度。 第 5 章讲向量库时会直接用上这条。

4.5 铁律:换了嵌入模型,索引必须重建

不同模型的向量空间互不兼容——拿模型 A 建的索引,配模型 B 查询,算出来的相似度全是噪声。

你做了什么 必须做什么
换了嵌入模型 删掉索引,全量重建
只是调了 chunk_size 重建(块变了,向量也得重算)
只是加了元数据 不用重建(可以单独 update)
# 重建的笨办法但最可靠:删库重来
import shutil
shutil.rmtree("./chroma_data", ignore_errors=True)

记住这句话:向量和模型是一套的。 生产环境里把「嵌入模型名 + 版本」一起记进索引的元数据里,一眼就能查清。

4.6 本地嵌入:不要 API Key 也能玩

有些服务不支持 /v1/embeddings,那就用本地模型:

pip install sentence-transformers
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")   # 支持中文
vecs = model.encode(sentences, normalize_embeddings=True)
print(vecs.shape)     # (3, 384)

本地模型的好处是免费、离线、数据不出本机;代价是首次要下载几百 MB 模型,且准确率通常略低于顶级的云端嵌入。

版本说明:sentence-transformers 6.1.0 依赖 PyTorch,而 PyTorch 目前只发布到 Python 3.14 的 wheel——需要本地嵌入时,请用 Python 3.12 ~ 3.14;用 3.15 就走 API 嵌入那条路。

4.7 常见翻车现场

报错 / 现象 原因 修法
相似度全在 0.5 上下,毫无区分度 查询和索引用了不同模型 重建索引(第 4.5 节)
向量长度对不上 建索引用 1536 维、查询用 512 维 两边保持一致
中文检索效果远差于英文 用了纯英文训练的嵌入模型 换多语言模型,如上面的 MiniLM
dimensions 参数报错 老模型不支持降维 去掉该参数

4.8 动手改

  1. 把第 4.2 节的三句话换成你自己的业务问句,量一组「该匹配 / 不该匹配」的相似度,定出你自己的阈值
  2. 用 dimensions=512 再跑一遍,比较维度、相似度数值的变化
  3. 用 l2_normalize 处理向量后,验证一下 1 - d²/2 算出的余弦和手写 cosine() 是否一致

4.9 小结

  • 嵌入把文字变成高维向量,方向相近 = 语义相近
  • 相似度用余弦衡量;不同模型的阈值不能照抄
  • 维度越高越贵,v3 系列支持降维,但建索引和查询必须一致
  • 单位向量满足 cos = 1 − d²/2,能从距离反推相似度
  • 换嵌入模型必须重建索引——这是 RAG 最贵的坑之一

下一章:向量数据库,把向量存下来并高效检索 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。