RAG 知识库设计:pgvector 方案
RAG 知识库的存储层设计:为什么 MVP 阶段用 pgvector 而不是专用向量数据库、知识 schema 怎么分层、向量检索的索引选择、embedding 生成时机。
#type / concept
#status / growing
#tech / ai
#resource / postgresql
[!info] related notes
- 前置概念: Embedding 模型, Python RAG Agent
- 检索优化: 意图感知 RAG 重排
- 质量校验: RAG Faithfulness 校验
- 降级方案: Hashing Embedding 降级
RAG 知识库设计:pgvector 方案
这篇解决什么问题
RAG 系统需要一个地方存储知识文档的向量,以便语义检索。专用向量数据库(Milvus、Pinecone、Qdrant)功能强大,但引入新组件意味着额外的运维成本。对于 MVP 和中小规模场景,PostgreSQL + pgvector 可能是更务实的选择。
选型决策:pgvector vs 专用向量数据库
| 维度 | pgvector | 专用向量数据库 |
|---|---|---|
| 运维 | 已有 PostgreSQL,零额外组件 | 需要额外部署 |
| 事务 | ACID,与业务数据同库 | 独立系统 |
| 查询 | 可以 JOIN 业务表 | 需要应用层拼接 |
| 性能(百万级) | 够用 | 更优 |
| 性能(亿级) | 吃力 | 专为此设计 |
| 混合查询 | 天然支持(向量 + SQL 条件) | 需要特殊语法 |
决策原则:知识库规模 < 100 万条、需要 JOIN 业务数据、不想引入新组件 → pgvector。
知识 Schema 设计
知识库不应该是一个扁平的”文档+向量”表。按知识的来源和粒度分层:
knowledge_sources # 来源(一个视频、一篇文章、一份指南)
└─ knowledge_units # 知识单元(一个完整的知识点)
└─ knowledge_clips # 片段(单元内的子片段,可定位到原文位置)
knowledge_sources
CREATE TABLE knowledge_sources (
id SERIAL PRIMARY KEY,
source_key VARCHAR(255) UNIQUE NOT NULL, -- 唯一标识(用于去重)
source_type VARCHAR(50) NOT NULL, -- 'video' | 'article' | 'curated'
title TEXT NOT NULL,
author VARCHAR(255),
problem_slug VARCHAR(100), -- 问题分类标签
language VARCHAR(10),
ingest_status VARCHAR(50) DEFAULT 'pending',
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
knowledge_units(核心表)
CREATE TABLE knowledge_units (
id SERIAL PRIMARY KEY,
source_id INTEGER REFERENCES knowledge_sources(id),
unit_type VARCHAR(50) NOT NULL, -- 知识类型
title TEXT NOT NULL,
summary TEXT,
body_markdown TEXT NOT NULL,
embedding vector(1536), -- pgvector 向量列
tags TEXT[],
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX ON knowledge_units USING ivfflat (embedding vector_cosine_ops);
unit_type 的价值:不只是存储,它让后续的意图感知重排成为可能。
| unit_type | 内容 | 用户问的时候 |
|---|---|---|
| definition | 概念解释 | ”什么是颈椎前伸” |
| self_check | 自测方法 | ”怎么判断有没有” |
| exercise | 训练动作 | ”怎么改善/练什么” |
| cause | 原因分析 | ”为什么会这样” |
| warning | 风险禁忌 | ”有什么要注意的” |
向量检索查询
SELECT
ku.id, ku.title, ku.summary, ku.body_markdown,
ku.unit_type, ku.tags,
ks.title as source_title,
1 - (ku.embedding <=> $1::vector) as similarity
FROM knowledge_units ku
JOIN knowledge_sources ks ON ku.source_id = ks.id
ORDER BY ku.embedding <=> $1::vector
LIMIT $2
<=>是 pgvector 的 cosine distance 运算符1 - distance= similarity(0~1)- 可以同时 JOIN 来源表获取元信息
索引选择
| 索引类型 | 适用场景 | 特点 |
|---|---|---|
| ivfflat | < 100 万条 | 快速,需要先有数据后建索引 |
| hnsw | > 100 万条 | 更精确,内存占用更大 |
CREATE INDEX ON knowledge_units
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
注意:ivfflat 索引需要在有数据后重建才生效。空表建索引没用。
Embedding 生成时机
入库时生成,不是检索时:
# 入库:为每个 unit 生成 embedding
embedding = await embedding_generator.generate(
unit.title + "\n" + unit.summary + "\n" + unit.body_markdown
)
检索时只为查询文本生成一次 embedding:
# 检索:只为查询生成 embedding
query_embedding = await embedding_generator.generate(user_query)
# 然后用向量相似度搜索
常见错误
把所有内容塞进一个 unit
# ❌ 一个 10 分钟的视频就是一个 unit
# 检索出来的结果太长,LLM 无法聚焦
# ✅ 按知识点拆分
# "颈椎前伸的定义" 是一个 unit
# "颈椎前伸的自测方法" 是一个 unit
# "颈椎前伸的矫正动作" 是一个 unit
忽略 unit_type
# ❌ 所有 unit 都是同一个 type
# 无法做意图感知重排
# ✅ 标记 unit_type
# 用户问"怎么练"时,boost exercise 类型的结果
检索时才生成 embedding
# ❌ 检索时为库中所有文档生成 embedding
for doc in all_docs:
doc.embedding = await generate(doc.text) # 极慢
# ✅ 入库时生成,检索时只为查询生成
query_embedding = await generate(query)