跳转至

第4章 Embedding API

学习目标

  • 理解 Embedding(向量嵌入)的语义与在 AI 工程中的作用
  • 掌握 OpenAI text-embedding-3-small / text-embedding-3-large 的差异与选型
  • 能够使用 SDK 批量生成文本向量并控制维度
  • 了解 Qdrant、Milvus、PGVector 三类向量库的接入方式
  • 实现基于余弦相似度的语义搜索最小原型

问题导入

传统搜索依赖关键词倒排索引,搜"苹果手机"时包含"iPhone"的文档可能完全匹配不到。问题的根源在于:关键词无法表达"语义"。Embedding 把文本映射为高维向量,语义相近的文本在向量空间中距离也相近,从而让"按意思搜索"成为可能。

OpenAI 提供的 text-embedding-3 系列模型把任意文本编码为 1536 或 3072 维浮点向量,配合向量数据库可实现毫秒级语义检索。它是 RAG(检索增强生成)的基石——先把知识库向量化入库,再用同样的模型把用户问题向量化,检索出最相关的片段交给 LLM 作答。本章将从 Embedding 的生成、存储到相似度计算,带你完成语义搜索的完整链路。

核心概念

1. Embedding 是什么

Embedding 是把离散对象(词、句、图片)映射为稠密浮点向量的技术。OpenAI 的文本 Embedding 把语义信息压缩到向量空间的每一维中,使得:

  • 语义相近的文本向量距离小(余弦相似度大)。
  • 语义无关的文本向量距离大。
  • 向量运算有意义,如 国王 - 男人 + 女人 ≈ 女王。

2. 模型选型:3-small vs 3-large

模型 维度 价格(每百万 token) 适用场景
text-embedding-3-small 1532 低 大规模入库、成本敏感
text-embedding-3-large 3072 中 高精度检索、专业领域

两个模型都支持 dimensions 参数降维(如把 3072 降到 256),降维后向量仍保持较好的语义结构,可显著降低存储与检索成本。

3. 向量数据库选型

  • Qdrant:Rust 实现,高性能,支持过滤与负载,Docker 部署友好,适合中小团队。
  • Milvus:分布式架构,支撑十亿级向量,适合大规模生产。
  • PGVector:PostgreSQL 扩展,与现有业务库共用,适合向量规模不大、需要事务一致性的场景。

选型核心看向量规模、QPS、是否需要与关系数据联查。

4. 相似度计算

语义搜索常用 余弦相似度(Cosine Similarity),衡量两个向量方向的一致性,取值 [-1, 1],越接近 1 越相似。OpenAI 的 Embedding 已做归一化,余弦相似度等价于点积,可直接用 numpy.dot 加速。

5. 批量生成与限流

embeddings.create() 支持一次传入多个文本(input 为列表),单次最多 2048 个。批量生成能摊薄网络开销,但要注意 token 速率限制,建议在限流时退避重试或用队列削峰。

原理图

flowchart LR
    A[原始文本库] --> B[切分/分块]
    B --> C[Embedding API]
    C --> D[向量]
    D --> E[(向量数据库)]

    F[用户查询] --> G[Embedding API]
    G --> H[查询向量]
    H --> I[相似度检索]
    E --> I
    I --> J[Top-K 相关片段]
    J --> K[LLM 生成答案]

图:Embedding 在语义搜索与 RAG 中的数据流

示例代码

import os
import json
import numpy as np
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
EMBEDDING_MODEL = "text-embedding-3-small"


# 1. 生成单条文本向量
def embed_text(text: str, dim: int = 1536) -> list[float]:
    resp = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=text,
        dimensions=dim,
    )
    return resp.data[0].embedding


# 2. 批量生成
def embed_batch(texts: list[str], dim: int = 1536) -> list[list[float]]:
    resp = client.embeddings.create(
        model=EMBEDDING_MODEL,
        input=texts,
        dimensions=dim,
    )
    # 注意按 index 排序,保证顺序与输入一致
    sorted_data = sorted(resp.data, key=lambda x: x.index)
    return [d.embedding for d in sorted_data]


# 3. 余弦相似度(OpenAI 向量已归一化,点积即余弦)
def cosine_similarity(a: list[float], b: list[float]) -> float:
    return float(np.dot(a, b))


# 4. 内存版语义搜索(演示用,生产请用向量库)
class InMemoryVectorStore:
    def __init__(self):
        self.texts: list[str] = []
        self.vectors: list[list[float]] = []

    def add(self, texts: list[str]):
        vecs = embed_batch(texts)
        self.texts.extend(texts)
        self.vectors.extend(vecs)

    def search(self, query: str, top_k: int = 3):
        qv = embed_text(query)
        scores = [cosine_similarity(qv, v) for v in self.vectors]
        ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
        return [(self.texts[i], scores[i]) for i in ranked[:top_k]]


# 5. 接入 Qdrant(伪代码,需 pip install qdrant-client)
def store_to_qdrant(texts: list[str], dim: int = 1536):
    """
    from qdrant_client import QdrantClient
    from qdrant_client.models import Distance, VectorParams, PointStruct

    qc = QdrantClient(host="localhost", port=6333)
    qc.recreate_collection(
        collection_name="docs",
        vectors_config=VectorParams(size=dim, distance=Distance.COSINE),
    )
    vecs = embed_batch(texts, dim)
    qc.upsert(
        collection_name="docs",
        points=[
            PointStruct(id=i, vector=v, payload={"text": texts[i]})
            for i, v in enumerate(vecs)
        ],
    )
    """
    pass


# 6. 接入 PGVector(伪代码,需 pip install pgvector psycopg)
def store_to_pgvector(texts: list[str]):
    """
    import psycopg
    from pgvector.psycopg import register_vector

    conn = psycopg.connect("postgresql://user:pass@localhost/db")
    register_vector(conn)
    with conn.cursor() as cur:
        cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
        cur.execute("CREATE TABLE IF NOT EXISTS docs (id serial, content text, emb vector(1536))")
        vecs = embed_batch(texts)
        for text, vec in zip(texts, vecs):
            cur.execute("INSERT INTO docs (content, emb) VALUES (%s, %s)", (text, vec))
    conn.commit()
    """
    pass


if __name__ == "__main__":
    store = InMemoryVectorStore()
    docs = [
        "Python 是一种解释型、面向对象的高级编程语言。",
        "JavaScript 主要用于浏览器端交互开发。",
        "向量数据库用于存储和检索高维向量。",
        "OpenAI 的 GPT 模型可以生成自然语言文本。",
        "Kubernetes 用于容器编排与集群管理。",
    ]
    store.add(docs)

    query = "用什么技术做容器编排?"
    for text, score in store.search(query, top_k=2):
        print(f"[{score:.3f}] {text}")

企业案例

案例 1:企业知识库语义搜索

某公司将内部 Wiki、Confluence 文档切片后用 text-embedding-3-small 向量化,存入 Qdrant。员工搜索"出差报销流程"能命中标题为"差旅费用管理办法"的文档,召回率较关键词搜索提升 40%。后续接入 LLM 实现 RAG 问答。

案例 2:电商商品推荐

电商平台把商品标题与描述拼接后生成 Embedding,用户浏览商品时实时检索相似向量,实现"看了又看"。用 dimensions=512 降维后,单商品存储成本降低 60%,检索延迟稳定在 10ms 内。

案例 3:客服工单去重与聚类

客服中心对新工单生成 Embedding,与历史工单做相似度匹配,相似度高于 0.92 的自动关联到同一解决方案,减少重复处理。每周用 K-Means 对全量工单聚类,发现长尾问题主题。

常见问题

Q1:Embedding 模型选 small 还是 large? 优先 small 跑通链路并评估召回率,若专业领域(法律、医学)召回不足再升级到 large。注意查询与入库必须用同一模型与同一 dimensions。

Q2:向量该存多少维? 3-large 默认 3072 维,存储与检索成本高。可先用全维度评测,再逐步降维(1536 → 768 → 512)观察召回率下降幅度,在精度与成本间权衡。

Q3:文本多长适合做 Embedding? 建议单条不超过 512 token。过长文本语义被稀释,过短缺乏上下文。RAG 场景常用 200-500 字的分块,配合 chunk overlap 防止切断语义。

Q4:如何处理多语言? text-embedding-3 系列原生支持多语言,中英文混合文档可直接入库。但跨语言检索精度略低于同语言,必要时可先统一翻译。

Q5:向量库更新频繁怎么办? 高频更新场景建议选支持流式写入的 Qdrant / Milvus;PGVector 在百万级以下、更新不频繁时表现良好。重建索引时用双写切换避免停服。

面试题

1. 什么是 Embedding?它在 RAG 中的作用? 答:Embedding 是把文本映射为高维稠密向量,使语义相近的文本向量距离相近。在 RAG 中用于把知识库与用户查询向量化,通过相似度检索出最相关的片段交给 LLM,是检索阶段的核心。

2. 余弦相似度与欧氏距离的区别?为什么语义搜索常用余弦? 答:欧氏距离衡量向量绝对距离,受向量长度影响;余弦相似度只衡量方向,对长度不敏感。文本 Embedding 关注语义方向而非绝对长度,故常用余弦。OpenAI 向量已归一化,二者等价。

3. text-embedding-3-small 与 3-large 的差异? 答:维度(1532 vs 3072)、价格、精度不同。large 在专业领域精度更高但成本与存储更大,均支持 dimensions 参数降维。

4. Qdrant、Milvus、PGVector 各自适用场景? 答:Qdrant 适合中小团队与高性能单机/小集群;Milvus 适合十亿级向量的大规模分布式场景;PGVector 适合与现有 PostgreSQL 业务库共用、需要事务一致性的中小规模场景。

5. 如何评估 Embedding 检索质量? 答:构建标注好的查询-文档对,计算 Recall@K、MRR、NDCG 等指标;对比不同模型与维度组合;用 badcase 分析做针对性优化(如调分块策略、加同义词扩展)。

练习题

  1. 用 text-embedding-3-small 把 20 条 FAQ 向量化入库,实现一个命令行问答检索工具,输出 Top-3 相关问题及相似度。
  2. 对比 dimensions=1536 与 dimensions=256 在同一组查询上的 Recall@5,分析精度损失是否可接受。
  3. 把示例中的内存版换成 Qdrant 或 PGVector,重新实现 add 与 search 方法,对比性能差异。

小结

Embedding 把"语义"量化为可计算的向量,是语义搜索与 RAG 的基础设施。本章覆盖了模型选型、批量生成、相似度计算与三类向量库接入。关键要点:查询与入库必须用同一模型与维度;分块策略影响召回质量;选型要权衡规模、QPS 与成本。下一章进入文件上传与微调,看如何让模型"更懂你的领域"。