LLMNotes

第 10 章:向量库

zjc 于 2026-01-10 发布

这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 向量库负责存储向量、元数据和索引,并支持相似度查询、过滤、更新和分布式扩展。选型时要同时看召回、延迟、规模、事务、运维和生态。

10.1 核心能力

能力 说明
向量索引 ANN 近似最近邻
标量过滤 权限、租户、时间、类型
全文检索 BM25 或稀疏向量
混合查询 向量 + 关键词 + 过滤
upsert 按 ID 更新
删除 物理或逻辑删除
持久化 快照、日志、备份
监控 QPS、延迟、召回率

10.2 常见方案

方案 特点
pgvector PostgreSQL 扩展,事务和运维友好
Elasticsearch 搜索生态强,支持混合检索
Milvus 面向大规模向量,组件较多
Qdrant 向量优先,过滤能力强
Weaviate 内置模块和混合检索
Chroma 轻量,适合原型和中小场景
OpenSearch 生态接近 Elasticsearch
FAISS 库而非数据库,需要自建服务

没有绝对最优。小规模知识库可从 pgvector 开始;强搜索需求可看 Elasticsearch/OpenSearch;大规模高并发向量场景再评估专用库。

10.3 索引类型

类型 特点
Flat 精确检索,小数据集适用
IVF 聚类分桶,需训练
HNSW 图索引,召回和延迟平衡好
DiskANN 面向磁盘大规模数据
量化 压缩内存,可能损失精度

HNSW 示例参数:

参数 影响
M 图连接数,越大召回越高、内存越多
ef_construction 建索引搜索宽度,影响构建时间
ef_search 查询搜索宽度,影响召回和延迟

ANN 是近似检索,需要用业务查询集测召回。

10.4 表设计

pgvector 示例:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE doc_chunk (
  chunk_id BIGINT PRIMARY KEY,
  doc_id TEXT NOT NULL,
  tenant_id TEXT NOT NULL,
  acl TEXT[] NOT NULL DEFAULT '{}',
  title_path TEXT NOT NULL DEFAULT '',
  content TEXT NOT NULL,
  embedding vector(1536) NOT NULL,
  effective_time TIMESTAMPTZ,
  active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX doc_chunk_embedding_idx
ON doc_chunk
USING hnsw (embedding vector_cosine_ops);

过滤索引:

CREATE INDEX doc_chunk_tenant_idx
ON doc_chunk(tenant_id, active);

10.5 查询示例

SELECT chunk_id, doc_id, title_path, content,
       1 - (embedding <=> $1::vector) AS score
FROM doc_chunk
WHERE tenant_id = $2
  AND active = TRUE
  AND $3 = ANY(acl)
  AND effective_time <= NOW()
ORDER BY embedding <=> $1::vector
LIMIT 30;

查询要点:

  1. 先做权限和租户过滤;
  2. 限制返回字段;
  3. 设置 top K;
  4. 设置分数阈值需谨慎;
  5. 保存 chunk_id 供引用;
  6. 超时后可降级关键词检索。

10.6 标量过滤

常见过滤:

tenant_id = current_tenant
acl @> current_roles
doc_type IN (policy, faq, manual)
effective_time <= now()
expires_time > now()
language = zh-CN
active = true

注意:

  1. 权限过滤必须在数据库或检索引擎内完成;
  2. 不能先取 top K 再由应用补权限;
  3. 高选择性过滤可能影响 ANN 性能;
  4. 大租户可分索引或分区;
  5. 过滤字段变更要有同步机制。

10.7 写入与更新

上传文档
  -> 解析
     -> 切块
        -> embedding
           -> 批量 upsert
              -> 状态置为 indexed

一致性建议:

  1. 使用稳定 chunk ID;
  2. 文档级别版本号;
  3. 先写新版本,再原子切换;
  4. 删除文档时标记 inactive 并异步清理;
  5. 定期对账源存储和向量库;
  6. 记录失败队列并重试。

10.8 混合检索

Query
  |-- dense vector search
  |-- sparse / BM25 search
  |-- metadata filter
  -> candidate union
     -> fuse
        -> rerank

融合示例:

def rrf_fusion(result_lists, k=60, top_n=20):
    scores = {}
    for results in result_lists:
        for rank, item in enumerate(results, start=1):
            scores[item] = scores.get(item, 0) + 1 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)[:top_n]

混合检索适合错误码、产品型号、专有名词、中英混合和长尾术语。

10.9 容量与性能

指标 说明
vectors 向量总量
dimensions 维度
QPS 查询吞吐
p50 / p95 / p99 latency 延迟
recall@K 召回质量
filter ratio 过滤后比例
write throughput 写入吞吐
memory usage 内存占用

压测要使用真实查询分布和真实过滤条件,不能用随机向量代表业务负载。

10.10 运维

必备工作:

  1. 备份向量数据或可重建管道;
  2. 监控磁盘、内存和查询延迟;
  3. 记录索引版本和模型版本;
  4. 支持重建索引;
  5. 设置慢查询告警;
  6. 控制批量写入速率;
  7. 灰度切换索引;
  8. 定期做召回回归。

推荐保留原始文档、解析结果和 chunk,向量丢失时可以重建。

本章小结

向量库是 RAG 的检索基础设施。设计时要先明确规模、过滤条件、混合检索和一致性要求,再选择方案。权限过滤必须发生在检索阶段,索引和模型版本要可追踪,原始数据要可重建。

思考题

  1. 为什么不能先向量检索 top K 再做权限过滤?
  2. HNSW 的 ef_search 如何影响召回和延迟?
  3. 混合检索解决什么问题?
  4. 文档更新时如何设计 chunk ID 和版本切换?
  5. 如何为向量库设计容量评估?