为什么你需要自己的RAG
过去半年,我试了各种RAG方案。Pinecone方便但贵,Weaviate功能全但运维重,Chroma轻量但生产环境差点意思。最后选了PostgreSQL pgvector——不是因为它最炫,是因为我的数据本来就在PostgreSQL里,多一个扩展就能做向量搜索,少维护一套系统。
RAG(Retrieval-Augmented Generation)说白了就是:用户问一个问题,你先去数据库搜相关文档,然后把文档+问题一起扔给LLM,让它基于搜到的内容回答。好处很明显——回答有依据、能引用来源、还能控制LLM不胡扯。
pgvector 是什么
pgvector 是 PostgreSQL 的向量相似度搜索扩展。你可以在普通的 PostgreSQL 表里加一个向量类型的列,然后做余弦相似度、L2距离、内积等计算。最关键的是——它支持IVFFlat和HNSW索引,几百万条数据也能秒级响应。
据 PostgreSQL 官方社区调查,pgvector 已是 PostgreSQL 生态中增长最快的扩展之一,GitHub 上超过 1.2 万星。
第一步:安装和配置
安装 pgvector
-- 如果你的 PostgreSQL 是 14+
CREATE EXTENSION vector;
在 Ubuntu 上:
sudo apt install postgresql-16-pgvector
Mac:
brew install pgvector
Docker:
# docker-compose.yml
services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
创建表和向量字段
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536) -- OpenAI ada-002 维度
);
-- 创建 HNSW 索引(推荐,性能比 IVFFlat 好)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
第二步:生成向量
// embed.ts
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function getEmbedding(text: string): Promise<number[]> {
const resp = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
dimensions: 1536,
});
return resp.data[0].embedding;
}
// 批量处理文档
async function indexDocuments(docs: { title: string; content: string }[]) {
for (const doc of docs) {
const embedding = await getEmbedding(doc.title + '\n' + doc.content);
await sql`
INSERT INTO documents (title, content, embedding)
VALUES (${doc.title}, ${doc.content}, ${embedding}::vector)
`;
}
}
用 text-embedding-3-small 而不是 ada-002 的原因:价格便宜5倍,性能相当。据 OpenAI 官方数据,text-embedding-3-small 在 MTEB benchmark 上得分 62.3%,略高于 ada-002 的 61.0%。
第三步:语义搜索
// search.ts
export async function searchDocuments(query: string, limit = 5) {
const queryEmbedding = await getEmbedding(query);
const results = await sql`
SELECT
id, title, content,
1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
FROM documents
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${limit}
`;
return results;
}
结合关键词搜索
SELECT id, title, content,
(1 - (embedding <=> ${queryEmbedding}::vector)) * 0.7
+ (ts_rank(to_tsvector('english', content), plainto_tsquery('english', ${query}))) * 0.3
AS combined_score
FROM documents
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', ${query})
OR 1 - (embedding <=> ${queryEmbedding}::vector) > 0.7
ORDER BY combined_score DESC
LIMIT 10;
第四步:接入LLM做RAG
// rag.ts
export async function askWithRAG(question: string) {
// 1. 检索相关文档
const docs = await searchDocuments(question, 3);
// 2. 构建上下文
const context = docs.map(d =>
`【${d.title}】\n${d.content.slice(0, 1000)}`
).join('\n\n');
// 3. 调用LLM
const resp = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: `基于以下资料回答用户问题。如果资料不足,直接说不知道。\n\n资料:\n${context}` },
{ role: 'user', content: question }
],
});
return {
answer: resp.choices[0].message.content,
sources: docs.map(d => ({ title: d.title, similarity: d.similarity })),
};
}
这里用 gpt-4o-mini 而不是 gpt-4o 的原因:RAG场景下,LLM的主要工作是总结和引用,不需要太强的推理能力。gpt-4o-mini 价格是 gpt-4o 的 1/30,效果在这个场景下足够了。
性能调优经验
踩过几个坑,直接说结论:
1. HNSW 的 ef_search 参数:默认值太小。搜索时设大一点:
SET hnsw.ef_search = 100; -- 默认40,设到100召回率明显提升
2. 分批生成嵌入向量:OpenAI API 有 rate limit。用 p-limit 控制并发:
import pLimit from 'p-limit';
const limit = pLimit(10);
const embeddings = await Promise.all(docs.map(d => limit(() => getEmbedding(d))));
3. 别把全文存进向量:content 截取前 1000 字生成嵌入就行。太长的文本会让向量表达”平均化”,反而搜不准。
4. 定期 reindex:HNSW 虽然不需要训练,但大量写入后查询性能会下降。每周跑一次:
REINDEX INDEX documents_embedding_idx;
总结
这套方案我在三个项目里用了,数据量从几万到上百万条,查询都在 100ms 以内。成本方面,pgvector 是免费的,你只需要付 PostgreSQL 的服务器钱。对比 Pinecone 的起步价 70 刀/月,自建 pgvector 在 10 刀/月的 VPS 上就能跑得很舒服。
如果你的项目数据量在 500 万条以下,别折腾专门的向量数据库,pgvector 足够了。
下一步可以做的:加上文档分块策略(chunking strategy),用 LangChain 或者直接自己写分块逻辑。分块大小对搜索质量影响很大——这个下次再聊。
