在大模型应用里,Embeddings 向量嵌入是把文本变成机器可计算语义的第一步。它能把”苹果手机”和”iPhone”映射到相近的向量空间,让搜索、推荐与 RAG 不再依赖关键词精确匹配,而是真正度量语义相似度。本文从原理讲到代码,落地一套可复用的向量嵌入与语义检索方案。
一、Embeddings 到底是什么
Embedding 本质是把离散的符号(词、句子、文档)映射成一个稠密浮点数向量。传统one-hot 编码维度高、几乎全 0,无法表达”相似”;而 Embedding 把语义压缩进低维连续空间:含义接近的文本,向量夹角更小、余弦相似度更高。模型通常通过对比学习训练——让正例(语义相同)向量靠近、负例(无关)向量远离。最终我们得到的是一个”语义坐标系”,后续所有相似度计算都只是向量运算。最直观的例子是词向量里的”国王 − 男人 + 女人 ≈ 女王”,说明向量空间确实编码了语义关系而非字面字符。实际文本嵌入的维度通常在 384 到 3072 之间:维度越高表达能力越强,但存储与检索成本也近似线性上升,需结合任务与预算权衡,并非越大越好。
二、主流 Embedding 模型怎么选
选型先看语言覆盖、维度与部署成本。中文场景优先考虑在中文语料上训练过的模型,跨语言检索则需要 multilingual 模型。下面是常见选择:
| 模型 | 维度 | 语言 | 适用场景 |
|---|---|---|---|
| text-embedding-3-small/large | 1536 / 3072 | 多语 | 调用简单、效果稳,按 token 计费 |
| BAAI/bge-large-zh | 1024 | 中文 | 中文检索效果强,可本地部署 |
| bge-m3 | 1024 | 多语 | 稠密+稀疏混合检索,跨语言友好 |
| moka-ai/m3e | 768 | 中文 | 轻量本地推理,资源占用低 |
| multilingual-e5 | 1024 | 100+ 语言 | 跨语言检索、出海业务 |
经验法则:检索质量对”模型版本”极其敏感,一旦选定就要在代码与数据里锁定版本,否则旧向量与新向量不在一个空间,召回会整体崩坏。
三、用 Python 生成文本向量
本地推理推荐 sentence-transformers,一行就能拿到向量;云端则用 OpenAI 兼容接口。两者都返回 list[float],长度等于模型维度。
# 本地推理(sentence-transformers)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-large-zh")
texts = ["如何重置 MySQL 根密码", "MySQL root 密码忘了怎么办"]
vecs = model.encode(texts, normalize_embeddings=True) # 归一化到单位向量
print(vecs.shape) # (2, 1024)
# 云端调用(OpenAI 兼容)
from openai import OpenAI
client = OpenAI(base_url="https://api.your-proxy/v1", api_key="sk-xxx")
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
vecs = [d.embedding for d in resp.data]
四、余弦相似度:衡量语义距离
向量归一化后,余弦相似度等价于点积,取值 [-1, 1],越接近 1 越相似。下面用 numpy 实现一个批量相似度函数:
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
a, b = a / np.linalg.norm(a), b / np.linalg.norm(b)
return float(np.dot(a, b))
# 归一化后可直接用点积,速度更快
def top_k(query_vec: np.ndarray, corpus: np.ndarray, k: int = 3):
scores = corpus @ query_vec # 已归一化 => 即余弦
idx = np.argsort(-scores)[:k]
return [(int(i), float(scores[i])) for i in idx]
q = vecs[0]
print(cosine_similarity(vecs[0], vecs[1])) # 两个问句语义接近,分数偏高
五、实战:构建语义检索(RAG 基础)
语义检索把”文档切块→向量化→入库→查询召回”串成流水线,正是 RAG 评估 的前置环节。小规模可用 numpy 暴力检索,规模上来后换 FAISS / Milvus 等向量库:
chunks = [
"使用 mysqld --skip-grant-tables 可跳过权限表启动,用于重置 root 密码",
"Redis 大 key 会导致主从同步阻塞与超时",
"Kubernetes HPA 根据 CPU 或自定义指标自动扩缩容",
]
emb = model.encode(chunks, normalize_embeddings=True)
def search(query: str, k: int = 1):
qv = model.encode([query], normalize_embeddings=True)[0]
hits = top_k(qv, emb, k=k)
return [(chunks[i], s) for i, s in hits]
print(search("怎么改 MySQL 密码")) # 命中第一条
召回后的 chunk 拼进 Prompt 交给大模型,就组成了最小可用的 RAG。若想把整条链路产品化,可参考 Dify + Ollama 的低代码编排,或 从零手写 AI Agent 把检索封装成工具。
六、实战:近重复检测与去重
Embedding 同样适合去重:把候选文本向量化,两两比较,超过阈值就判定为重复。这比 MD5 精确匹配更鲁棒,能抓住”换说法但意思一样”的近重复。
def find_duplicates(items, threshold=0.92):
vecs = model.encode(items, normalize_embeddings=True)
pairs = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
if cosine_similarity(vecs[i], vecs[j]) >= threshold:
pairs.append((items[i], items[j]))
return pairs
dups = find_duplicates([
"MySQL 忘记 root 密码怎么重置",
"如何重置 MySQL 的 root 密码",
"Redis 大 key 优化方案",
])
print(dups) # 前两条被判为近重复
七、落地要点与常见坑
1. 维度与归一化要一致。检索库和查询必须用同一模型、同一维度;是否归一化也要前后统一,否则点积和余弦会得出不同排序。
2. 锁定模型版本。模型升级后向量空间会变,旧数据需全量重算,否则召回雪崩。
3. 批量与并发。云端接口按 token 计费且有 RPM 限制,长文本分批、加退避重试;本地推理用 FP16 或量化降低显存。
4. chunk 粒度。切块太大稀释语义、太小丢失上下文,建议 200–500 字并保留重叠。
5. 安全与成本。不要把涉密内容送到公网 Embedding 接口;自建模型可用 DeepSeek 本地部署 同款算力跑私有化推理。
6. 混合检索更稳。纯向量检索对生僻词、版本号、报错码不敏感,叠加 BM25 关键词召回再融合排序(RRF),能显著提升长尾查询的命中率。先向量、后混合,是多数生产系统的演进路径。
八、规模化的下一步:向量数据库选型
当语料从几百条涨到百万级,numpy 的 O(N) 暴力检索会明显变慢,这时需要近似最近邻(ANN)索引。选型不必一步到位,可随数据量平滑升级:
| 方案 | 适用规模 | 特点 |
|---|---|---|
| numpy 暴力检索 | < 1 万 | 零依赖、精度 100%,适合原型与中小库 |
| FAISS | 百万级 | 本地内存索引,支持 HNSW / IVF,召回快 |
| Milvus | 千万级+ | 分布式部署,支持标量过滤与多向量 |
| pgvector | 已有 PostgreSQL | 在 SQL 内直接检索,运维简单(见 PG 向量检索) |
实践中,先用语义向量 + 暴力检索跑通链路,确认效果后再引入 ANN 引擎;同时保留一条”精确匹配兜底”分支,应对专有名词、错误码等向量检索不擅长的场景。数据量、是否需要元数据过滤、团队运维能力,共同决定最终选型。
结语
Embeddings 是连接”自然语言”与”机器计算”的桥梁:一次向量化,检索、去重、聚类、推荐全都能复用。掌握它,你就握住了 RAG 与智能体检索能力的最底层钥匙。下一步可以把它接入 Function Calling 工具,让 Agent 在需要时主动检索知识库。




