向量嵌入(Vector Embedding)是用于比较文本、图像、音频、实体等数据的学习型数值表示。它支撑语义搜索、推荐、聚类和 RAG,但单个向量不是稳定接口。向量含义取决于生成它的模型、Revision、任务模式、预处理、维度、归一化和距离函数。

本文只负责工程生命周期:模型选型、检索评测、索引、安全和迁移。术语页承担简明定义;语义搜索指南承担搜索系统设计;向量数据库指南承担存储引擎选型。

直接回答

生产级 Embedding 系统是一份版本化的转换与检索契约:

flowchart LR A["源内容 + 访问策略"] --> B["分块 + 预处理"] B --> C["模型 + 任务模式"] C --> D["向量 + 版本契约"] D --> E["精确 / ANN 索引"] E --> F["权限感知召回"] F --> G["可选重排"] G --> H["业务结果 + 评测"] H --> I["监控 + 迁移"]

只有属于兼容向量空间的向量才能比较。相似度分数不是概率,不是全局通用相关性阈值,也不能证明源文档有权被当前用户访问。完整契约必须作为数据保存,不能隐藏在 SDK 默认值中。

向量嵌入如何工作

嵌入模型学习一个映射函数,把输入转换为固定长度向量,使训练目标关心的关系可以在该空间中度量。模型不会保留源数据的所有属性,而是压缩特定信号;因此相关性、语言覆盖、领域行为和失败模式都受训练数据与任务设计影响。

静态、上下文与检索嵌入

表示类型 表示单元 什么会改变向量 常见用途
静态词向量 单词或 Token 词表中的学习结果 语言分析与传统 NLP
上下文 Token 向量 上下文中的 Token 周围序列 下游模型特征
句子或文档向量 一段文本 完整输入及 Pooling / 训练目标 检索、聚类、分类
多模态向量 文本、图像、音频或视频 模态编码器和对齐目标 跨模态检索与匹配

BERT 的基础 Hidden State 不会自动成为高质量句子检索向量。Sentence-BERT 通过 Bi-encoder 训练结构生成可以独立计算、再做相似度比较的句向量。现代检索模型在此基础上加入任务指令、非对称 Query/Document 编码、多语言训练或多种表示。

对称任务与非对称任务

语义相似度与信息检索不是同一目标。对称任务中的两个输入角色相同,例如比较两句是否互为改写;非对称检索则要求一个短问题找到一段更长、真正回答问题的内容。

Google Embeddings 官方文档通过 RETRIEVAL_QUERY、RETRIEVAL_DOCUMENT 和 SEMANTIC_SIMILARITY 等 Task Type 明确区分这些目标。其他模型可能使用 Prompt、Prefix,或单独的 encode_query / encode_document 方法表达同一契约。建立索引和处理查询时,必须持续遵循目标模型的文档约定。

相似度函数也是契约的一部分

余弦相似度、点积和欧氏距离只有在归一化满足特定条件时才会产生等价排序。

对向量 x 和 y:

text
cosine_similarity(x, y) = (x · y) / (||x|| ||y||)
dot_product(x, y)       = x · y
euclidean_distance(x,y) = ||x - y||₂

两个非零向量都完成 L2 归一化后,余弦相似度等于点积,欧氏距离平方则等于 2 - 2 * cosine_similarity,所以在精确计算下排序一致。未经归一化时,向量模长会改变点积和欧氏距离排序。

应使用模型推荐的距离函数,并为索引配置匹配的 Operator。OpenAI 当前指南在文本搜索中使用余弦相似度,并要求手工截断维度后重新归一化;这只是该 Provider 的具体用法,不是所有模型的通用规则。即使 Provider 默认处理归一化,也应把它显式写入契约。

不要复制其他数据集的相似度阈值。模型、语言、Chunk、语料、任务和 Hard Negative 变化后,分数分布都会移动。阈值应从带标签样本校准,并为模糊分数保留拒答或升级路径。

定义可复现的 Embedding Contract

每条向量都应保存足够信息,使系统能够复现、比较、迁移和删除:

json
{
  "sourceId": "policy-42",
  "sourceVersion": "sha256:...",
  "chunkId": "policy-42#section-7",
  "tenantId": "tenant-acme",
  "embeddingContract": {
    "modelId": "provider/model-name",
    "modelRevision": "immutable-revision",
    "task": "retrieval_document",
    "preprocessingVersion": "chunk-v4",
    "dimensions": 768,
    "normalization": "l2",
    "distance": "cosine"
  },
  "indexedAt": "2026-01-15T09:30:00Z",
  "accessPolicyVersion": "acl-v12"
}

最小契约包括:

  • 不可变模型 Revision,而不是可漂移的 Alias
  • Query / Document 的任务模式或指令
  • Tokenization、清洗、分块和截断版本
  • 输出维度与 Dtype
  • 归一化方式与距离函数
  • 源内容哈希和生命周期状态
  • 租户、Owner 和访问策略引用
  • 索引代次和摄取时间

这不是可选的运维元数据。缺少契约时,重建任务无法判断哪些向量已经过期,查询服务可能悄悄使用错误模型,数据删除也无法证明所有派生记录已经清理。

如何选择 Embedding 模型

不存在对所有任务都最优的向量嵌入模型。MTEB 原论文横跨检索、重排、语义文本相似度、分类、聚类、Pair Classification、Bitext Mining 和 Summarization,结论之一就是没有单一方法统治全部任务。

公开 Benchmark 只用于筛选候选模型,最终选择必须来自真实负载。

决策维度 需要回答的问题 忽略后的失败
任务目标 检索、相似度、聚类、分类、代码还是多模态? STS 强模型可能不擅长检索
Query/Document 非对称性 是否需要 Prefix、Instruction 或 Task Type? 查询和文档落入不匹配区域
语言与领域 哪些语言、术语、代码和实体最重要? 中文、混合语言或专业语料静默退化
输入行为 Token 上限、截断、Pooling 和长文档策略? 关键证据被截断或稀释
输出契约 维度、Dtype、归一化和原生缩短能力? 索引不兼容或排序漂移
部署方式 API、自托管、硬件、Batch 和吞吐? 成本、延迟或可靠性不达标
治理要求 数据驻留、许可证、留存和 Provider 日志? 合规或隐私失败
生命周期 Revision、可用性和迁移机制? 被迫重建且无法回滚

不要把某一时点的模型排行榜复制进常青文章。排名、模型可用性、价格和硬件行为都会变化。可审计的选型记录应保存模型 Revision、数据集版本、评测指标、运行测量和最终决策。

先确定内容决策,再选择 Embedding 任务

Embedding 模型提供的是表示层,不是一个通用的内容功能。应先确定下游任务,再定义数据、标签、指标和自动化权限:

目标 输出 必须评测什么 不能默认什么
回答在线查询 排序后的证据候选 Recall、排序质量、权限、无答案行为 最近向量就是充分证据
整理无标注语料 Cluster 和候选 Topic Cohesion、Separation、离群比例、人工可用性 Cluster 天然拥有正确名称
路由到已知 Taxonomy 带置信度的 Class Label 各类别 Precision、Recall、拒答、Label Drift 相似度就是校准后的类别概率
发现可能重复内容 成对记录的复核队列 Pair Precision、错误合并成本、来源关系 高相似度可以直接合并
重排检索候选集 Query-Document 排序 nDCG 或 MRR Uplift、p95 延迟、成本 Reranker 能找回漏召回内容

MTEB 将 Retrieval、Clustering、Classification、Pair Classification 和 Reranking 分开评测,因为某个任务表现好,不能证明其余任务也有效。表格中间三类离线内容决策可继续阅读Embedding 内容分析:文本聚类、分类与语义去重:该文补充了索引本身没有解决的内容身份、人工复核、删除传播和决策策略契约。

构建检索评测集

Embedding 评测必须覆盖真实检索任务,而不是只比较句子相似度。先收集真实查询或经过授权的合成查询,再标注哪些文档与查询相关。

至少包含:

  1. 直接关键词匹配和语义改写
  2. 缩写、别名、拼写错误和低频实体
  3. 答案不会重复问题措辞的问答
  4. 关键词相似但实际回答不同问题的 Hard Negative
  5. 业务涉及的中文、多语言和 Code-switching 查询
  6. 权限受限文档及其预期排除结果
  7. 语料中没有答案、应返回空结果的情况
  8. 更新、删除和不同 Source Version 的内容

使用开发集调模型与索引参数,使用独立测试集完成最终比较。近重复文档不能跨集合泄漏。

检索指标

没有一个指标可以单独证明系统有效:

  • Recall@k: 相关内容是否至少有一条进入候选集,适合评估重排之前的召回。
  • MRR: 第一条相关结果出现得有多靠前。
  • nDCG@k: 当相关性存在多个等级时,评估整体排序。
  • Precision@k: 返回结果中有多少真正相关。
  • 无答案准确性: 语料没有答案时,系统是否停止返回相似但错误的内容。
  • 业务结果: 搜索、推荐或 RAG 回答是否基于有效证据完成任务。

编码延迟、检索延迟、索引构建时间、内存、存储和成本应与相关性分开测量。更快但无法召回必要证据的模型,不是优化。

可运行的本地评测

下面的零依赖 Go 程序评测一份版本化向量 Fixture,而不是只打印没有解释的坐标。先从经过批准的 Embedding 服务导出候选文档与 Query 向量,并把模型契约与 Fixture 一起保存,发布复核时无需再次依赖该服务即可运行评测器。

go
package main

import (
	"fmt"
	"math"
	"sort"
)

type Document struct {
	ID     string
	Vector []float64
}

type Query struct {
	ID          string
	Vector      []float64
	RelevantIDs []string
}

type Fixture struct {
	ModelRevision string
	Documents     []Document
	Queries       []Query
}

func normalize(vector []float64) ([]float64, error) {
	if len(vector) == 0 {
		return nil, fmt.Errorf("vector must not be empty")
	}
	var squaredNorm float64
	for _, value := range vector {
		if math.IsNaN(value) || math.IsInf(value, 0) {
			return nil, fmt.Errorf("vector contains a non-finite value")
		}
		squaredNorm += value * value
	}
	if squaredNorm == 0 {
		return nil, fmt.Errorf("zero vector cannot be normalized")
	}
	norm := math.Sqrt(squaredNorm)
	result := make([]float64, len(vector))
	for index, value := range vector {
		result[index] = value / norm
	}
	return result, nil
}

func dot(left, right []float64) (float64, error) {
	if len(left) != len(right) {
		return 0, fmt.Errorf("vector dimension mismatch")
	}
	var score float64
	for index := range left {
		score += left[index] * right[index]
	}
	return score, nil
}

func contains(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func evaluateRecallAtK(fixture Fixture, k int) (float64, error) {
	if fixture.ModelRevision == "" {
		return 0, fmt.Errorf("model revision is required")
	}
	if k < 1 || k > len(fixture.Documents) {
		return 0, fmt.Errorf("k must be between 1 and the document count")
	}
	if len(fixture.Queries) == 0 {
		return 0, fmt.Errorf("at least one query is required")
	}

	documents := make([]Document, len(fixture.Documents))
	for index, document := range fixture.Documents {
		vector, err := normalize(document.Vector)
		if err != nil {
			return 0, fmt.Errorf("document %s: %w", document.ID, err)
		}
		documents[index] = Document{ID: document.ID, Vector: vector}
	}

	hits := 0
	for _, query := range fixture.Queries {
		queryVector, err := normalize(query.Vector)
		if err != nil {
			return 0, fmt.Errorf("query %s: %w", query.ID, err)
		}
		type scoredDocument struct {
			id    string
			score float64
		}
		scores := make([]scoredDocument, 0, len(documents))
		for _, document := range documents {
			score, err := dot(document.Vector, queryVector)
			if err != nil {
				return 0, fmt.Errorf("query %s: %w", query.ID, err)
			}
			scores = append(scores, scoredDocument{document.ID, score})
		}
		sort.Slice(scores, func(i, j int) bool {
			if scores[i].score == scores[j].score {
				return scores[i].id < scores[j].id
			}
			return scores[i].score > scores[j].score
		})

		retrieved := make([]string, 0, k)
		hit := false
		for _, result := range scores[:k] {
			retrieved = append(retrieved, result.id)
			hit = hit || contains(query.RelevantIDs, result.id)
		}
		if hit {
			hits++
		}
		fmt.Printf("%s: top%d=%v, hit=%t\n", query.ID, k, retrieved, hit)
	}
	return float64(hits) / float64(len(fixture.Queries)), nil
}

func main() {
	fixture := Fixture{
		ModelRevision: "fixture/example-v1",
		Documents: []Document{
			{"refund", []float64{1, 0, 0, 0}},
			{"password", []float64{0, 1, 0, 0}},
			{"shipping", []float64{0, 0, 1, 0}},
			{"invoice", []float64{0, 0, 0, 1}},
		},
		Queries: []Query{
			{"q1", []float64{0.95, 0.10, 0.05, 0}, []string{"refund"}},
			{"q2", []float64{0.05, 0.10, 0, 0.95}, []string{"invoice"}},
			{"q3", []float64{0.05, 0.95, 0.10, 0}, []string{"password"}},
		},
	}
	recall, err := evaluateRecallAtK(fixture, 2)
	if err != nil {
		panic(err)
	}
	fmt.Printf("model=%s Recall@2=%.3f\n", fixture.ModelRevision, recall)
}

这个 Fixture 只用于展示最小闭环,其中坐标只是示意数据,不能作为模型质量证据。生产评测应从各候选模型及其精确契约导出向量,读取版本化 Query-Document 标注,并使用置信区间或逐查询回归分析比较系统。不能用同一组样本同时调参和批准发布。

检索架构:精确、ANN、混合与重排

Embedding 只是检索信号之一,不等于完整搜索系统。

阶段 作用 优势 主要权衡
精确向量检索 计算全部已授权向量 可作为 Recall 参考基线 成本随语料和查询量增长
近似最近邻(ANN) 搜索 HNSW、IVF 等索引 大规模下延迟更低 用 Recall 换速度和内存
关键词检索 匹配术语、字段、ID 和低频实体 精确词汇命中 难以理解改写
混合检索 合并关键词与向量候选或分数 同时覆盖语义和精确匹配 需要融合策略与评测
Cross-encoder 重排 联合计算 Query-Candidate 相关性 提升最终排序 无法承受全库逐对计算

Sentence Transformers 官方文档给出的常用结构是 Retrieve-and-Rerank:Bi-encoder 或关键词系统先召回候选,再由 CrossEncoder 联合计算 Query 与候选。候选阶段必须保证足够的 Recall@k,因为重排器无法找回从未进入候选集的相关文档。

Google BigQuery 和 pgvector 都同时提供精确与近似搜索路径,这也说明专用向量数据库并非前置条件。优先选择能满足过滤、一致性、规模、恢复和运维要求的最简单存储。

权限感知的 pgvector Schema

下面的结构把 Embedding Contract 与向量一起保存。固定的 vector(384) 只属于当前索引代次;维度不同的模型应建立独立代次或表。

sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_embeddings_v1 (
  tenant_id text NOT NULL,
  document_id text NOT NULL,
  chunk_id text NOT NULL,
  source_version text NOT NULL,
  model_id text NOT NULL,
  model_revision text NOT NULL,
  preprocessing_version text NOT NULL,
  access_groups text[] NOT NULL,
  embedding vector(384) NOT NULL,
  PRIMARY KEY (
    tenant_id,
    document_id,
    chunk_id,
    model_id,
    model_revision,
    preprocessing_version
  )
);

CREATE INDEX document_embeddings_v1_tenant
  ON document_embeddings_v1 (tenant_id);

CREATE INDEX document_embeddings_v1_access_groups
  ON document_embeddings_v1 USING gin (access_groups);

CREATE INDEX document_embeddings_v1_embedding_hnsw
  ON document_embeddings_v1
  USING hnsw (embedding vector_cosine_ops);

SELECT document_id, chunk_id, source_version,
       1 - (embedding <=> $3::vector) AS cosine_similarity
FROM document_embeddings_v1
WHERE tenant_id = $1
  AND access_groups && $2::text[]
ORDER BY embedding <=> $3::vector
LIMIT $4;

该 Predicate 只是示例,不是完整授权边界。必须由可信服务确定租户身份,使用数据库 Row-Level Security 或等价服务策略,并禁止客户端直接声明可访问组。

近似索引可能只扫描部分候选后再应用过滤,导致符合租户或元数据条件的结果不足。必须用真实 Filter 评测 Recall;再按基数选择分区、Partial Index、Iterative Scan,或扩大重排前候选集。

维度与存储规划

维度更高不等于效果更好。维度属于模型训练与输出契约,不能通过比较无关模型的数字大小推断质量。

稠密浮点向量的基础容量估算为:

text
vector_bytes = item_count * dimensions * bytes_per_element

完整预算还要加上 ID、Metadata、索引图或质心、Allocator 开销、副本、快照、WAL,以及迁移期间并存的旧索引代次。

只有模型经过相应训练并明确支持时,才能使用原生缩短能力。例如 OpenAI 当前第三代 Embedding 模型提供基于 Matryoshka 类训练的 dimensions 参数。这不意味着其他模型也可以任意截断。

主成分分析(PCA)是另一套需要学习的后处理变换。它需要代表性拟合数据、持久化 Projection Version、归一化规则、评测和全量重建索引。旧版文章曾用 6 条样本拟合 256 个 PCA Component,这段代码无法运行,因为 PCA Component 数不能超过样本可用秩。

量化可以减少存储和索引内存,也可能改变邻居排序。应把量化候选召回与全精度重排作为一套系统共同评测。

安全迁移 Embedding 模型

不能把不同 Embedding Contract 的向量混入同一个搜索空间。安全迁移使用并行索引代次:

  1. 冻结旧契约: 记录模型 Revision、任务模式、预处理、维度、归一化和距离函数。
  2. 定义新契约: 写明迁移假设和批准指标。
  3. 回填独立索引: 保留 Source ID、Version、访问策略和删除状态。
  4. 双模型编码查询: 每个查询分别使用与各索引匹配的模型。
  5. Shadow 对比: 不影响用户地比较质量、延迟、失败和 Top Result 变化。
  6. Canary 流量: 给有限用户放量,并保留回滚。
  7. 切换读写: 在批准的回滚窗口内保持旧索引只读。
  8. 安全退役: 传播删除,清理旧向量与快照,并记录完成证据。

原地覆盖会失去结果对比和回滚能力。后台任务若使用可漂移的模型 Alias,也可能把多个向量空间写入所谓“新索引”。

安全与数据生命周期

Embedding 是派生数据,不是匿名数据。OWASP 把未授权访问、跨上下文泄漏、Embedding Inversion 和数据投毒列为 RAG 向量与嵌入风险。

向量化前后都要实施控制:

  • 对数据源做身份校验,拒绝隐藏或不可信摄取内容。
  • 每个 Chunk 保留 Tenant、Owner、分类和 Provenance。
  • 在检索阶段执行授权,不能等文本返回后再过滤。
  • 共享 ANN 行为可能造成不可接受的泄漏或 Recall 干扰时,隔离不兼容租户。
  • 加密传输和存储,限制原始向量导出和批量近邻查询。
  • 把源数据更新、Legal Hold、过期和删除传播到向量、索引、副本、缓存和评测 Fixture。
  • 记录检索 ID 和策略决策,但不要把敏感正文复制到遥测中。
  • 测试数据投毒、跨租户查询、过期 ACL、已删除内容和检索段落中的 Prompt Injection。

相关性不能授予权限。即使私有文档与查询高度相关,也不能进入未授权候选集。

常见失败模式

失败 根因 工程处理
全站共用一个相似度阈值 分数分布随模型、语言、Query 和语料变化 按任务校准并保留拒答
一个索引混入多个模型 Revision Alias 漂移或回填不完整 版本化契约并隔离索引代次
Query 与 Document 模式用反 忽略非对称指令 分别测试并保存任务模式
ANN 很快但丢失证据 只调速度,没有精确 Recall 基线 在带标签查询上对比精确搜索
Metadata Filter 后没有候选 近似扫描后才过滤 调大候选深度、分区或 Iterative Scan
Dense Search 找不到 ID 和专名 语义模型弱化精确词项 增加关键词检索并融合候选
相关候选排序过低 Bi-encoder 压缩了 Query-Document 交互 添加 Reranker 并测量 Uplift
返回过期或已删除内容 摄取链路没有 Source Version 与删除传播 建立可审计生命周期 Journal
跨租户召回 向量检索后才做授权 在暴露候选前过滤或分区
公开 Benchmark 胜出但线上失败 评测任务与真实负载不一致 用版本化领域测试集做发布门禁

生产监控

质量、运行与生命周期指标应分别监控:

  • Recall@k、MRR、nDCG@k、无答案准确性和 Reranker Uplift
  • 下游回答有据性、点击、转化或问题解决结果
  • 按模型与索引代次拆分的编码和检索 P50/P95 延迟
  • 授权过滤前后的候选数量
  • 索引覆盖率、Stale Vector 延迟、Embedding 失败和删除积压
  • Query 与 Document 的语言和领域漂移
  • ANN 相对抽样精确搜索的 Recall
  • 存储、内存、吞吐、Provider Usage 与重建成本

除服务故障外,还要对契约不匹配告警。HTTP 请求成功但使用了错误模型 Revision,属于数据损坏事件。

参考资料

总结

向量嵌入只有进入受控且经过评测的检索系统后才有价值。模型选择应来自真实任务证据;系统必须保存完整向量空间契约,评测精确与近似 Recall,按负载组合关键词检索和重排,并把授权与删除纳入索引生命周期。

长期稳定的工程单元不是一组浮点数,而是源数据、模型行为、检索策略、评测证据和生命周期之间的版本化关系。

相关资源