Skip to content

五、文本嵌入

5.1 为什么要嵌入

切分之后,每个Chunk仍然是一段人类语言的文字。但后续检索的核心操作是"比较两段内容在语义上有多相似"——计算机无法直接对比两段中文的含义,它只能处理数字。

因此我们需要一个桥梁,把文字转换成计算机能够计算的形式。这个桥梁就是嵌入(Embedding):通过嵌入模型,将一段文本映射为一组高维浮点数向量。经过这一转换后,语义相近的文本在向量空间中距离更近,语义无关的文本距离更远。后续检索时,只需计算用户查询向量和文档向量之间的距离,就能找到最相关的内容。

5.2 嵌入模型简史

那么,嵌入模型是怎么做到"理解语义"的?

2018年谷歌推出的BERT能够将文本嵌入为向量表示,但BERT并未针对有效生成句子级别的嵌入进行优化,由此促使了Sentence-BERT的诞生。Sentence-BERT调整了BERT的架构以及预训练任务,专门用于生成包含语义的句子嵌入向量。这些向量可通过余弦相似度等指标轻松比较,大大降低了查找相似句子的计算开销。

此后,各机构陆续推出了更多针对不同语言和场景优化的嵌入模型。

5.3 常用嵌入模型

模型机构维度序列长度特点
bge-large-zhBAAI1024512开源,中文效果好
bge-base-zhBAAI768512开源,中文场景常用
bge-small-zhBAAI512512开源,轻量级
bge-m3BAAI10248192开源,多语言,支持稀疏向量
text-embedding-3-smallOpenAI15368192多语言,性价比高
text-embedding-3-largeOpenAI30728192多语言,精度更高

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]])

向量生成好了,接下来需要一个地方把它们存起来,并支持高效的相似度检索——这就是向量数据库的职责。