理解 RAG、搜索、推荐共同的数学基础
为什么要把文字变成向量
到目前为止我们和 LLM 的交互都是"文本进文本出"。但有一类更基础、看不见的能力支撑着所有"让 AI 在你的数据里找东西"的产品——把任意文字编码成一组固定长度的数字(向量),然后用数学运算比较两段文字的"相似度"。
举几个直观的应用场景:
- 语义搜索:用户搜"如何重置密码",应该能匹配到标题为"忘记密码怎么办"的文档,而不只是字面一致
- 推荐系统:用户读过文章 A,找出和 A "相似"的 B、C 推给他
- 去重 / 聚类:把语义重复的客服工单合并到一起
- RAG:让 LLM 在回答之前先从你的私有文档里找到相关段落
这些场景的共同核心就是 Embedding——把文本映射到一个高维向量空间,让"语义相似"这件事可以用"向量距离近"来表达。理解这一篇,你就理解了 RAG、搜索、推荐三大类应用的共同基础。
几何直觉:向量空间里的语义
想象一个三维空间(实际是几百到几千维,但三维便于脑补)。每段文字被映射成空间里的一个点:
- "猫"和"狗"的点会很近,因为它们都属于"宠物 / 哺乳动物"
- "猫"和"轮胎"的点会很远,因为它们语义不相干
- 神奇的是,向量之间的算术运算也有意义:
vec("国王") - vec("男") + vec("女") ≈ vec("女王"),这是早期 word2vec 时代就观察到的现象
衡量两段文字的相似度,最常用的是 余弦相似度(cosine similarity)——把两个向量看作从原点出发的箭头,比较它们的夹角。完全同向是 1,垂直是 0,反向是 -1。文本 embedding 中实际值通常在 0~1 之间。
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
很多 embedding 模型在输出时已经做了归一化(向量长度为 1),这种情况下余弦相似度就等价于点积,可以省掉除法:
# 归一化向量的余弦相似度 = 点积
similarity = float(np.dot(a, b))调用 Embedding API
主流厂商都提供 embedding API,协议同样兼容 OpenAI。我们继续用 OpenAI SDK 指向不同后端。
注意 DeepSeek 目前不提供 embedding 接口,需要用其他服务。常用国内方案是 智谱 AI 的 embedding-3 或 硅基流动 转发的开源模型,国外用 OpenAI 的 text-embedding-3-small:
# embedding_api.py
import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
# 这里以智谱为例(注册后拿 key)
client = OpenAI(
api_key=os.getenv("ZHIPU_API_KEY"),
base_url="https://open.bigmodel.cn/api/paas/v4/",
)
def embed(text: str) -> list[float]:
resp = client.embeddings.create(
model="embedding-3",
input=text,
)
return resp.data[0].embedding
def embed_batch(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(
model="embedding-3",
input=texts,
)
return [item.embedding for item in resp.data]
if __name__ == "__main__":
vec = embed("你好世界")
print(f"维度:{len(vec)}")
print(f"前 5 维:{vec[:5]}")
返回的向量维度由模型决定:智谱 embedding-3 是 2048 维,OpenAI text-embedding-3-small 是 1536 维(且支持手动降到更小),开源的 BGE-large 是 1024 维。维度高一般效果好,但向量库存储和检索成本也线性上升。
批量调用的提醒:API 通常允许一次传一批文本(list of string),共用一次 HTTP 开销,吞吐量比逐条调高得多。处理大量文档时一定要走批量。
本地 Embedding:sentence-transformers
API 调用简单但有两个问题:每次调用都有费用、网络延迟(一两百毫秒),以及涉及私有数据时的合规顾虑。这些场景下用本地模型更合适。
sentence-transformers 是最成熟的本地 embedding 库,第一次运行时会自动下载模型权重到本地(通常几百 MB),之后纯本地推理:
pip install sentence-transformers# embedding_local.py
from sentence_transformers import SentenceTransformer
# 第一次运行会下载模型,之后从缓存加载
# bge-m3 是当前中文 + 多语言场景综合最强的开源 embedding 模型
model = SentenceTransformer("BAAI/bge-m3")
texts = [
"今天天气很好",
"外面阳光明媚",
"Python 是一门编程语言",
]
# 一次调用编码全部文本,返回 (n, dim) 的 numpy 数组
vectors = model.encode(texts, normalize_embeddings=True)
print(vectors.shape) # (3, 1024)
几个关键点:
normalize_embeddings=True让输出已经归一化,后续比较相似度直接用点积即可- 第一次运行时下载几百兆模型权重到
~/.cache/huggingface/,之后离线可用 - CPU 推理对短文本足够(几十毫秒一条),长文本或批量大建议有 GPU 或用 Apple Silicon 的 MPS
- 中文场景目前推荐
BAAI/bge-m3或BAAI/bge-large-zh-v1.5,多语言场景同样首选bge-m3
第一个语义搜索 demo
把上面的全部内容合并,做一个最小可用的语义搜索——给一堆候选文本,输入查询返回最相关的几条:
# semantic_search.py
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3")
# 候选文档
docs = [
"Python 的列表推导式让代码更简洁",
"如何在 macOS 上安装 Homebrew",
"FastAPI 是一个现代的 Python Web 框架",
"煮意大利面要在水里加盐",
"用 Pandas 处理 CSV 文件的最佳实践",
"今天去爬山看到了一只松鼠",
"asyncio 提供了 Python 的异步编程模型",
]
# 提前把所有候选编码好(实际系统里这步会持久化)
doc_vecs = model.encode(docs, normalize_embeddings=True)
def search(query: str, top_k: int = 3) -> list[tuple[str, float]]:
q_vec = model.encode([query], normalize_embeddings=True)[0]
# 归一化向量的点积 = 余弦相似度
sims = doc_vecs @ q_vec
idx = np.argsort(-sims)[:top_k]
return [(docs[i], float(sims[i])) for i in idx]
for q in ["Python 异步怎么写", "做饭技巧"]:
print(f"\n查询:{q}")
for text, score in search(q):
print(f" [{score:.3f}] {text}")
跑一下,你会看到:
查询:Python 异步怎么写
[0.682] asyncio 提供了 Python 的异步编程模型
[0.512] FastAPI 是一个现代的 Python Web 框架
[0.481] Python 的列表推导式让代码更简洁
查询:做饭技巧
[0.624] 煮意大利面要在水里加盐
...
注意"Python 异步"匹配到了 asyncio 那条,即使候选文本里压根没有"异步"这个词,也没有"怎么写"。这就是 embedding 相对传统全文检索(基于词频的 BM25)的核心优势——它理解语义,而不是字面。
模型选型与维度的权衡
embedding 模型选型主要看三个维度:
1. 语种支持——纯中文用 bge-large-zh-v1.5,多语言或中英混合用 bge-m3。OpenAI 的 text-embedding-3 系列对中文也不错但要付费。
2. 维度——常见的有 384、768、1024、1536、2048。维度大的通常质量好但向量存储和搜索都更耗资源。bge-m3 是 1024 维,对绝大多数场景都够用。OpenAI text-embedding-3-large 支持手动降维,可以在质量和成本之间平衡。
3. 上下文长度——每段文本能编码的最大 token 数。bge-m3 支持 8192 tokens,对长文档友好。短上下文模型(512 tokens)需要在使用前先做切分,否则超长部分被截断,效果断崖式下降。
实战经验:先用 bge-m3 起步,验证业务效果之后如果吞吐不够,再考虑换更小的模型或加 GPU。不要在没有 baseline 之前就过早优化。
向量数据库:为什么要它
上面的 demo 把所有向量保存在内存的 numpy 数组里,文档少(<10 万条)这样足够。但当你的文档量到百万、千万级,问题来了:
- 全量计算相似度太慢——每次查询要算 N 次点积,N 千万就要几秒
- 内存放不下——千万 × 1024 维 × 4 字节 ≈ 40GB
- 增删改查不方便——你想加几条新文档要重建整个数组吗?
向量数据库就是为这个场景而生的。它做两件事:用近似最近邻(ANN)算法把搜索复杂度从 O(N) 降到 O(log N) 级别;提供持久化和增量更新的工程接口。常见选项:
- Chroma——纯 Python,本地嵌入式,最适合个人项目和原型。第 06 篇 RAG 实战会用它
- Qdrant——Rust 实现的高性能开源数据库,可以本地或集群部署
- Milvus——大规模生产场景的事实标准,但部署相对重
- PostgreSQL + pgvector——已经在用 PG,加这个扩展就能存向量,运维最简单
- Pinecone——纯托管 SaaS,零运维但要付费
下一篇 RAG 实战我们用 Chroma,它的体验和 SQLite 一样轻——一个 Python 库 + 一个文件,没有任何服务要起。
本篇要点
- Embedding 把任意文本编码成固定长度向量,让"语义相似"可以用"向量距离近"度量
- 余弦相似度是最常用的相似度指标,归一化向量的点积就是余弦相似度
- API 路线和本地路线各有优势:API 省心、本地省钱并保护隐私
- 中文场景目前推荐
BAAI/bge-m3,多语言通用,1024 维,8K 上下文 - 文档量大要上向量数据库,原型阶段用 Chroma 即可
- 永远批量调用,不要逐条 embed,吞吐量差几十倍
下一篇
第 06 篇是真正的实战——把这些零件拼成一个能跑的 RAG 知识库:你给它一堆 PDF 或 markdown 文档,它能回答关于这些文档的问题,并且每个回答都标出引用了哪些文档片段。这是目前应用最广泛、商业价值最直接的 LLM 应用形态之一。
参考资料
- Sentence Transformers 官方文档
- BGE 模型仓库 (BAAI) — 中文最好的开源 embedding 系列
- OpenAI Embedding 指南
- MTEB 排行榜 — embedding 模型在多任务上的权威评测
- Chroma 文档
- Pinecone Learn — 向量检索领域的优质技术文章合集
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:Embedding 与向量:把文字变成数字
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/05-Embedding与向量/
本文最后一次更新为 天前,文章中的某些内容可能已过时!