Appearance
五、文本嵌入
5.1 为什么要嵌入
切分之后,每个Chunk仍然是一段人类语言的文字。但后续检索的核心操作是"比较两段内容在语义上有多相似"——计算机无法直接对比两段中文的含义,它只能处理数字。
因此我们需要一个桥梁,把文字转换成计算机能够计算的形式。这个桥梁就是嵌入(Embedding):通过嵌入模型,将一段文本映射为一组高维浮点数向量。经过这一转换后,语义相近的文本在向量空间中距离更近,语义无关的文本距离更远。后续检索时,只需计算用户查询向量和文档向量之间的距离,就能找到最相关的内容。

5.2 嵌入模型简史
那么,嵌入模型是怎么做到"理解语义"的?
2018年谷歌推出的BERT能够将文本嵌入为向量表示,但BERT并未针对有效生成句子级别的嵌入进行优化,由此促使了Sentence-BERT的诞生。Sentence-BERT调整了BERT的架构以及预训练任务,专门用于生成包含语义的句子嵌入向量。这些向量可通过余弦相似度等指标轻松比较,大大降低了查找相似句子的计算开销。
此后,各机构陆续推出了更多针对不同语言和场景优化的嵌入模型。
5.3 常用嵌入模型
| 模型 | 机构 | 维度 | 序列长度 | 特点 |
|---|---|---|---|---|
| bge-large-zh | BAAI | 1024 | 512 | 开源,中文效果好 |
| bge-base-zh | BAAI | 768 | 512 | 开源,中文场景常用 |
| bge-small-zh | BAAI | 512 | 512 | 开源,轻量级 |
| bge-m3 | BAAI | 1024 | 8192 | 开源,多语言,支持稀疏向量 |
| text-embedding-3-small | OpenAI | 1536 | 8192 | 多语言,性价比高 |
| text-embedding-3-large | OpenAI | 3072 | 8192 | 多语言,精度更高 |
5.4 Embedding抽象接口
了解了嵌入模型之后,如何在代码中使用它们?LangChain设计了一个统一的Embedding抽象类,不同的模型(HuggingFace、OpenAI等)都实现这套接口,切换模型时只需更换实现类,业务代码无需修改:
python
@abstractmethod
def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""将多段文档文本嵌入为向量列表"""
@abstractmethod
def embed_query(self, text: str) -> list[float]:
"""将单条查询文本嵌入为向量"""5.5 HuggingFace嵌入
使用HuggingFace开源模型在本地完成嵌入,无需调用外部API,适合对数据隐私有要求的场景:
python
# uv install sentence-transformers langchain_huggingface
def embedding_demo():
from langchain_huggingface import HuggingFaceEmbeddings
embed_model = HuggingFaceEmbeddings(
model_name=r'D:\ai_models\huggingface_cache\bge-base-zh-v1.5'
)
# 单文本嵌入
query = "你好,世界"
query_result = embed_model.embed_query(query)
print(len(query_result)) # 768
print(query_result[0:10])
# 多文本嵌入
docs = ["你好,世界", "你好,世界"]
res = embed_model.embed_documents(docs)
print(type(res)) # list[list[float]]
embedding_demo()5.6 OpenAI嵌入
使用OpenAI云端模型完成嵌入,无需本地GPU,适合快速原型开发:
python
from langchain_openai import OpenAIEmbeddings
from dotenv import load_dotenv
load_dotenv()
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)
query = "什么是人工智能?"
docs = [
"人工智能是计算机科学的一个分支",
"机器学习是人工智能的子领域",
]
# 向量化
query_vector = embeddings.embed_query(query)
doc_vectors = embeddings.embed_documents(docs)
# 打印查询向量信息
print("=== 查询向量 ===")
print(f"查询文本: {query}")
print(f"向量维度: {len(query_vector)}")
print(f"向量前 5 维: {query_vector[:5]}")
print("=" * 40)
# 打印文档向量信息
print("=== 文档向量 ===")
for i, (text, vec) in enumerate(zip(docs, doc_vectors)):
print(f"文档 {i}: {text}")
print(f"向量维度: {len(vec)}")
print(f"向量前 5 维: {vec[:5]}")
print("-" * 40)5.7 稠密向量的局限
上面介绍的HuggingFace和OpenAI嵌入,生成的都是稠密向量——每一维都有值,捕捉的是文本的整体语义。稠密向量擅长理解"意思相近"的文本,但有一个短板:对关键词的精确匹配能力较弱。
举个例子:用户搜索"《民法典》第236条",稠密向量可能会返回各种民法相关的内容,但不一定精确命中包含"第236条"这个关键词的文档。
为了弥补这个不足,一些模型(如BGE-M3)除了生成稠密向量之外,还能同时生成稀疏向量——只记录关键词及其权重,天然擅长关键词匹配。将两者结合,就是后面检索章节会讲到的"混合检索"。
5.8 BGE-M3:同时生成稠密与稀疏向量
BGE-M3是BAAI推出的多语言嵌入模型,最大的特点是一次编码,同时输出稠密向量和稀疏向量:
- 稠密向量:由Transformer Encoder生成,捕捉整体语义,输出固定维度向量,适合语义相似度匹配
- 稀疏向量:同样由Transformer Encoder生成,但方式不同——对每个token分别计算重要性权重,只保留权重较高的token及其分数,输出"关键词→权重"的字典。同时补充了稀疏向量从早期TF-IDF统计方法到如今深度学习方法的演进脉络。

对比分析:
| 特性 | 稠密向量 | 稀疏向量 |
|---|---|---|
| 维度 | 固定 1024 维 | 动态(非零元素) |
| 存储 | 全量存储 | 只存非零元素 |
| 语义理解 | 强(同义词、近义词) | 弱(精确匹配) |
| 关键词匹配 | 弱 | 强(型号、品牌) |
| 适用场景 | 语义检索 | 关键词检索 |
python
# uv add FlagEmbedding==1.3.5
# uv add transformers==4.44.2
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel(model_name_or_path=r"D:\ai_models\huggingface_cache\bge-m3")
text = "标量字段通常用来存储一些元数据,并可以在搜索时通过元数据进行过滤"
res = model.encode(
[text],
return_sparse=True,
return_dense=True
)
print("=== 原始文本 ===")
print(text)
print("=" * 50)
# 1. 稀疏向量(关键词及其权重,原始 id 形式)
print("=== 稀疏向量(id → 权重)===")
lexical_weights = res["lexical_weights"][0] # batch 中第一条
# 只看前若干个,避免太长
for i, (token_id, weight) in enumerate(list(lexical_weights.items())[:20]):
print(f"[{i:02d}] id={token_id:<5} weight={weight:.4f}")
print("...(仅展示前 20 个)")
print("=" * 50)
# 2. 把稀疏向量的 id 转成可读 token
print("=== 稀疏向量(token → 权重)===")
sparse_tokens = model.convert_id_to_token(lexical_weights)
for i, (token, weight) in enumerate(list(sparse_tokens.items())[:20]):
print(f"[{i:02d}] token='{token}' weight={weight:.4f}")
# 3. 稠密向量(1024 维)
dense_vec = res["dense_vecs"][0]
print("=== 稠密向量信息 ===")
print(f"维度: {len(dense_vec)}")
print("前 10 维示例:")
print([round(x, 4) for x in dense_vec[:10]])向量生成好了,接下来需要一个地方把它们存起来,并支持高效的相似度检索——这就是向量数据库的职责。