RAG通关系列:第3关|把文字变成数字:向量化到底是什么魔法

2 阅读11分钟

上一关我们搞清楚了 RAG 文本分块是个啥,这一关来解决 RAG 的第一个核心问题:你的文档和用户的问题,语言完全不一样,AI 怎么找到它们之间的关联?

答案就藏在四个字里:向量化(Embedding)

一、开篇:为什么 AI 能"看懂"你的问题?

先来一个灵魂拷问——

你的知识库里有份文档,写的是:"本产品自签收之日起7日内支持无理由退货"

用户问的是:"买完不想要了能退吗?"

从字面上看,这两句话没有一个字是一样的。传统关键词匹配(Elasticsearch)大概率要翻车。

但 Embedding 模型会说:小场面,看我的。

它能把"7日内支持无理由退货"和"买完不想要了能退吗"变成两个向量,然后在向量空间里一算——哎,相关度 0.92,匹配上了。

这就是向量的魔力:把人类能理解的文字,翻译成 AI 能计算的数字


二、核心概念:向量化到底是个什么原理?

2.1 从 One-Hot 到 Embedding:文字的数字漂流史

要让计算机处理文字,第一步永远是数字化

最早的方案是 One-Hot 编码:假设你有 10000 个词,那就建一个 10000 维的向量,每个词在属于自己的位置上标 1,其他全是 0。

比如"猫" = [0, 0, 0, ..., 1, ..., 0],"狗" = [0, 0, 0, ..., 0, 1]

问题来了:"猫"和"狗"都是动物,距离应该很近吧? 但 One-Hot 眼里它俩距离一样远,猫和汽车的距离 = 猫和狗的距离。

这显然不科学。

后来有了 Word2Vec,通过词共现统计学习词的向量表示。语义相近的词,向量也会靠近。"国王"的向量减去"男人"的向量,再加上"女人"的向量,结果和"女王"很接近——这个经典实验让整个 NLP 圈沸腾了。

但 Word2Vec 是静态的,同一个词不管在什么语境下,向量都一样。"苹果"在"吃苹果"和"买苹果手机"里含义完全不同,静态向量搞不定。

BERT 来了,问题解决。

BERT 基于 Transformer 的自注意力机制,能够根据上下文动态生成词向量。同样的词在不同的句子里,向量是不同的。

打个比方:

  • One-Hot 就像给每个中国人发一张印着身份证号的纸条,然后说"你们自己找老乡"——信息量约等于零
  • Word2Vec 像是给每个人发了个星座标签,同星座的自动归队——有点用了
  • BERT 则是给每个人装了 GPS+社交网络,能根据你实时在哪儿、跟谁在一起来判断你是不是"本地人"——这才是真正的智能

2.2 句子级 Embedding:从词到段的跨越

RAG 里用的可不是词向量,而是句子级或段落级 Embedding

主流做法有两种:

第一种:BERT 直接输出[CLS] 标记的向量,作为整个句子的表示。

第二种:BERT 输出所有词的向量,然后做 Mean Pooling(平均池化)或 Max Pooling,取最大/平均值作为句子向量。

第二种更常用,因为 [CLS] 位有时候学得不够充分,Mean Pooling 更稳定。

2.3 向量相似度:怎么判断两段文字"像不像"?

拿到向量之后,判断相似度的常用方法有两个:

方法公式特点
余弦相似度cos(θ) = A·B/(|A||B|)最常用,只看方向不看大小
点积A·B简单直接,对向量长度敏感
欧氏距离||A-B||几何距离,值越小越相似

RAG 场景下,余弦相似度是绝对主流,因为我们关心的是"语义方向"而不是"向量长度"。


三、主流模型对比:2025 年谁最强?

3.1 核心评测榜单:C-MTEB

C-MTEB(Chinese Massive Text Embedding Benchmark)是中文 Embedding 模型的权威评测榜单,涵盖检索、分类、聚类等 50+ 任务。

3.2 主流模型横向对比

模型参数量维度中文 MTEB英文 MTEB多语言延迟部署方式
BGE-M3567M102464.862.5✅ 100+50ms开源本地
BGE-large-zh335M102463.258.1⚠️ 部分40ms开源本地
m3e-base220M76860.552.1❌ 中文35ms开源本地
text-embedding-3-large未公开307262.565.2✅ 50+220msAPI调用
text-embedding-3-small109M153658.560.1✅ 50+80msAPI调用
jina-embeddings-v3520M102459.861.5✅ 80+130ms开源/API

数据来源:CSDN/知乎综合测试结果,2025-2026 年实测数据

3.3 真实场景测试:Top-5 准确率对比

有人用 200 条中文技术文档 + 50 条真实用户 query 做了系统性测试:

模型Top-5 准确率平均延迟上下文长度
gte-Qwen2-7B81%1800ms4096
BGE-large-zh78%95ms512
jina-embeddings-v376%130ms8192
text-embedding-3-large74%220ms8192
m3e-base71%45ms512
text-embedding-3-small67%80ms8192

划重点:

  1. 中文场景首选 BGE-large-zh:78% 准确率 + 95ms 延迟,性价比绝了
  2. BGE-M3 综合能力最强:多语言 + 长文本 + 混合检索,生产环境首选
  3. OpenAI 的中文能力被高估了:text-embedding-3-large 中文只有 74%,和 BGE-large-zh 差不多,但贵很多
  4. m3e-base 是小钢炮:百 M 级别做到 71%,延迟只有 35ms,对延迟敏感场景很香

3.4 为什么中文模型比 OpenAI 强?

核心原因就一个:训练数据

BGE 和 m3e 用的是中文互联网大规模文本,而 OpenAI 的模型中文数据比例相对低。Embedding 模型的能力上限,基本由训练数据决定。

这就像让一个老外做中文高考题 vs 让一个土生土长的中国人做——题目再难,母语者的优势是碾压性的。


四、避坑指南:选型与实战的血泪教训

4.1 坑一:维度不是越高越好

text-embedding-3-large 有 3072 维,听起来很厉害。但实测中文场景下,1024 维的 BGE-large-zh 反而效果更好。

原因:维度大意味着信息密度高,但也意味着存储和计算成本指数上升。实践中 1024-2048 维对大多数中文场景已经够用。

OpenAI 的 Matryoshka 技巧:可以主动截断向量维度,比如把 3072 维截到 1024 维,精度损失只有 1-2%,但存储和计算成本直接降 2/3。

4.2 坑二:向量维度必须和数据库匹配

ChromaDB 默认维度是 1536,如果你用 BGE(1024 维)的向量存进去,查询时会报错或者返回错误结果。

解决方案:创建 collection 时必须指定维度:

collection = client.create_collection(
    name="my_collection",
    metadata={"hnsw:space": "cosine"},
    dimension=1024  # 必须和你的 Embedding 模型维度一致!
)

4.3 坑三:中文分词不是你想的那样

很多 Embedding 模型在中文上表现不好,有时候不是因为模型本身差,而是分词策略不对

比如 BERT 的中文 tokenizer 基于字符,对于"深度学习"这种词,会分成"深/度/学/习"四个 token,而不是一个完整的词。

解决方案

  1. 使用专门针对中文优化的模型(BGE-M3、m3e-base)
  2. 或者在预处理阶段做中文分词(jieba),把"深度学习"作为一个整体传给模型

4.4 坑四:多语言场景的模型选择

如果你的知识库是纯中文,选 BGE 或者 m3e 就行。

但如果你的知识库是中英混合,或者要做跨境电商的多语言检索,推荐:

  • BGE-M3:原生支持 100+ 语言,多语言检索效果好
  • text-embedding-3-large:多语言泛化能力强,但中文一般

4.5 坑五:batch_size 和 max_length 的平衡

Embedding 模型有个参数叫 max_length,决定单次能处理的最大 token 数。

  • 太短:长文本被截断,信息丢失
  • 太长:内存占用高,推理速度慢

实践建议

  • 知识库文档:max_length=512 够用(大多数文档块都在 500 token 以内)
  • 用户 query:通常很短,max_length=128 就够了
  • batch_size:16-32 比较通用,太大容易 OOM

五、代码实战:LangChain 加载不同 Embedding 模型

5.1 环境准备

pip install langchain langchain-community langchain-huggingface
pip install sentence-transformers torch
pip install chromadb

5.2 方案一:加载开源 BGE 模型

from langchain_huggingface import HuggingFaceEmbeddings

# BGE-large-zh(推荐中文场景)
bge_embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-zh-v1.5",
    model_kwargs={'device': 'cuda'},  # 有 GPU 用 GPU,没 GPU 自动用 CPU
    encode_kwargs={'normalize_embeddings': True}  # 归一化,方便后续余弦相似度计算
)

# BGE-M3(推荐多语言场景)
bge_m3_embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={'device': 'cuda'},
    encode_kwargs={'normalize_embeddings': True}
)

# 测试一下
text = "如何申请产品退货退款?"
query_vector = bge_embeddings.embed_query(text)
print(f"向量维度: {len(query_vector)}")
print(f"向量前5个值: {query_vector[:5]}")

5.3 方案二:加载 m3e-base(轻量级选择)

from langchain_huggingface import HuggingFaceEmbeddings

m3e_embeddings = HuggingFaceEmbeddings(
    model_name="moka-ai/m3e-base",
    model_kwargs={'device': 'cuda'},
    encode_kwargs={'normalize_embeddings': True}
)

# 批量文档向量化
docs = [
    "本产品自签收之日起7日内支持无理由退货",
    "退货时请保持商品完好,附带发票",
    "运费由消费者承担"
]

doc_vectors = bge_embeddings.embed_documents(docs)
print(f"处理了 {len(doc_vectors)} 个文档")

5.4 方案三:使用 OpenAI API

from langchain_openai import OpenAIEmbeddings

# text-embedding-3-small(便宜快速)
small_embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key="your-api-key"  # 建议用环境变量
)

# text-embedding-3-large(精度更高)
large_embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large",
    api_key="your-api-key"
)

5.5 完整 RAG 流程:Embedding + ChromaDB

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 1. 加载 Embedding 模型
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-zh-v1.5",
    model_kwargs={'device': 'cuda'},
    encode_kwargs={'normalize_embeddings': True}
)

# 2. 准备文档
texts = [
    "本产品自签收之日起7日内支持无理由退货",
    "退货时请保持商品完好,附带发票",
    "运费由消费者承担"
]

# 3. 创建向量数据库
db = Chroma.from_texts(
    texts=texts,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_name="product_policy"
)

# 4. 查询
query = "买完不想要了能退吗?"
results = db.similarity_search(query, k=2)

print("检索结果:")
for i, doc in enumerate(results):
    print(f"{i+1}. {doc.page_content} (相似度: {1 - doc.metadata.get('distance', 0):.3f})")

# 5. 带相似度分数的查询
results_with_score = db.similarity_search_with_score(query, k=2)
for doc, score in results_with_score:
    print(f"{doc.page_content} (余弦相似度: {1-score:.3f})")

5.6 模型横向对比:哪个最适合你?

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_openai import OpenAIEmbeddings
import time

# 测试不同模型的推理速度
models = {
    "BGE-large-zh": HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5"),
    "m3e-base": HuggingFaceEmbeddings(model_name="moka-ai/m3e-base"),
    "text-embedding-3-small": OpenAIEmbeddings(model="text-embedding-3-small"),
}

test_queries = ["如何申请退货?"] * 10

for name, embed_model in models.items():
    start = time.time()
    for q in test_queries:
        embed_model.embed_query(q)
    elapsed = time.time() - start
    print(f"{name}: {elapsed:.3f}s (平均 {elapsed/10*1000:.1f}ms/条)")

六、最佳实践总结

6.1 模型选择决策树

你的场景是什么?
├── 纯中文 + 生产环境
│   └── 推荐:BGE-M3 或 BGE-large-zh
├── 中文 + 多语言
│   └── 推荐:BGE-M3
├── 延迟敏感 + 精度要求不高
│   └── 推荐:m3e-base
├── 英文为主 + 不差钱
│   └── 推荐:text-embedding-3-large
└── 对隐私有要求(数据不能出境)
    └── 推荐:BGE-M3 / m3e-base(开源可本地部署)

6.2 性能优化 Tips

  1. GPU 加速:HuggingFace Embedding 模型用 GPU 推理,速度比 CPU 快 10-50 倍
  2. 批量处理:用 embed_documents 而不是循环调用 embed_query,速度提升 5-10 倍
  3. 向量压缩:用 OpenAI 的 Matryoshka 技巧截断维度,存储成本降 60%+
  4. 缓存向量:对于不常变化的文档,只索引一次,后续只做查询

6.3 常见问题自查清单

问题可能原因解决方案
检索结果全是错的向量维度不匹配检查 collection 维度设置
速度太慢用的是 CPU换成 GPU 或者选更小的模型
相似度都是 0.9+没有归一化设置 normalize_embeddings=True
长文档效果差max_length 太小调大 max_length 或改用 BGE-M3

七、思考题

  1. 如果你的知识库 99% 是中文,但有 1% 是英文文档,用 BGE-M3 还是 BGE-large-zh?为什么?

  2. 两个向量的余弦相似度是 0.95,但欧氏距离却很大,这可能是什么原因?

  3. 在什么场景下,你会选择用更"轻"的 Embedding 模型(比如 m3e-base)而不是更强的模型(比如 BGE-M3)?


下集预告

第 4 关 我们将深入 RAG 的检索环节——如何从海量向量中快速找到最相关的那几个?

预告:「向量搜索哪家强?ANN 算法详解 + 实战对比」


往期回顾: