用Vector Database + LLM构建RAG应用:独立开发者的AI产品实战
用Vector Database + LLM构建RAG应用:独立开发者的AI产品实战
RAG是什么?为什么它是"AI产品的核心架构"
RAG(Retrieval-Augmented Generation,检索增强生成)是解决LLM(大语言模型)"幻觉"和"知识过时"问题的核心技术。
问题一:LLM的幻觉(Hallucination)
LLM会"编造"它不知道的信息。如果你问"你们产品的退款政策是什么?",LLM可能生成一个"听起来合理但实际不存在"的退款政策——这会导致用户投诉。
问题二:LLM的知识截止日期
GPT-4的知识截止到2023年12月,Claude 3的截止到2024年某月。如果你的产品有"最新功能更新",LLM不知道。
RAG的解决方案:给LLM提供"外部知识库"
RAG的流程:
- 检索(Retrieve):根据用户问题,从"知识库"里找到最相关的文档
- 增强(Augment):把检索到的文档,作为"上下文"提供给LLM
- 生成(Generate):LLM基于"上下文"回答问题——不再编造
技术栈选型:Vector DB + Embedding Model + LLM
Vector Database(向量数据库):存储和检索"文档的向量表示"
主流选择:
- Pinecone:托管服务,免费计划(5万向量),简单易用
- Weaviate:开源,可自托管,也可托管
- Qdrant:开源,Rust编写,性能极高,适合自托管
- Supabase pgvector:如果你已经用Supabase,可以直接用它的pgvector扩展(免费)
Embedding Model(嵌入模型):把文本转换成向量
- OpenAI text-embedding-3-small:性能好,价格低($0.02/1M tokens)
- Cohere embed-english-v3.0:多语言支持好
- 开源方案:all-MiniLM-L6-v2(本地运行,无API成本)
LLM:回答生成
- GPT-4o/GPT-4-turbo:质量最高,价格中等
- Claude 3.5 Sonnet:长上下文(200K tokens),适合"大量文档"场景
- 开源方案:Llama 3(用Groq或自托管,成本更低)
实战:用Supabase pgvector + OpenAI构建"产品文档问答机器人"
第一步:准备知识库文档
把你的产品文档、FAQ、帮助文档整理成"Markdown文件"。每个文件应该是一个"独立的主题"(如"如何退款.md"、"如何更改定价计划.md")。
第二步:文档分块(Chunking)
LLM的上下文窗口有限(即使200K,也不能把整个知识库都塞进去)。需要把文档"分块"。
分块策略:
- 按标题分块:每个H2/H3章节作为一个块
- 固定大小分块:每块500-1000个字符,重叠100-200字符(避免切断语义)
- 按语义分块:用LLM判断"这段是否在讲同一个主题"
我的实现(固定大小分块):
// lib/chunkDocuments.ts
export function chunkDocument(text: string, chunkSize = 800, overlap = 200): string[] {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = start + chunkSize;
const chunk = text.slice(start, end);
chunks.push(chunk);
start += chunkSize - overlap; // 重叠,避免切断语义
}
return chunks;
}
第三步:生成向量并存储到pgvector
// scripts/embedDocuments.ts
import { OpenAI } from 'openai';
import { createClient } from '@supabase/supabase-js';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);
// 启用pgvector扩展(在Supabase SQL编辑器里执行)
// CREATE EXTENSION IF NOT EXISTS vector;
// 创建表(如果还没创建)
// CREATE TABLE documents (
// id BIGSERIAL PRIMARY KEY,
// content TEXT,
// embedding VECTOR(1536), -- OpenAI embedding维度是1536
// metadata JSONB -- 存储标题、来源URL等
// );
async function embedAndStore() {
// 1. 读取所有文档(假设在docs/目录)
const docs = await readAllDocuments('./docs');
for (const doc of docs) {
const chunks = chunkDocument(doc.content);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
// 2. 生成embedding
const embeddingResp = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunk,
});
const embedding = embeddingResp.data[0].embedding;
// 3. 存储到pgvector
await supabase.from('documents').insert({
content: chunk,
embedding: `[${embedding.join(',')}]`, // pgvector格式
metadata: {
title: doc.title,
source: doc.url,
chunk_index: i,
},
});
console.log(`Stored chunk ${i} of ${doc.title}`);
}
}
}
第四步:实现检索(相似度搜索)
// lib/retrieve.ts
import { OpenAI } from 'openai';
import { createClient } from '@supabase/supabase-js';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);
export async function retrieveRelevantDocs(query: string, topK = 5) {
// 1. 把查询转换成向量
const embeddingResp = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const queryEmbedding = embeddingResp.data[0].embedding;
// 2. 在pgvector里做相似度搜索(余弦距离)
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: `[${queryEmbedding.join(',')}]`,
match_threshold: 0.78, // 相似度阈值(0-1,越大越严格)
match_count: topK,
});
if (error) {
console.error('Search error:', error);
return [];
}
return data; // 返回相关的文档块
}
在Supabase里创建match_documents函数:
CREATE OR REPLACE FUNCTION match_documents (
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id BIGINT,
content TEXT,
metadata JSONB,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
documents.metadata,
1 - (documents.embedding <=> query_embedding) AS similarity -- 余弦相似度
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY similarity DESC
LIMIT match_count;
END;
$$;
第五步:构建Prompt并调用LLM
// lib/rag.ts
import { OpenAI } from 'openai';
import { retrieveRelevantDocs } from './retrieve';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
export async function answerWithRAG(question: string): Promise<string> {
// 1. 检索相关文档
const relevantDocs = await retrieveRelevantDocs(question);
if (relevantDocs.length === 0) {
return "抱歉,我在知识库里没有找到相关信息。请联系support@yourproduct.com。";
}
// 2. 构建上下文
const context = relevantDocs.map(doc => doc.content).join('\n\n---\n\n');
// 3. 构建Prompt
const prompt = `你是${process.env.PRODUCT_NAME}的帮助助手。基于以下上下文回答问题。如果上下文里没有答案,说"我不知道",不要编造。
上下文:
${context}
问题:${question}
回答:`;
// 4. 调用LLM
const resp = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: '你是 helpful 的产品帮助助手。' },
{ role: 'user', content: prompt },
],
temperature: 0.3, // 降低温度,减少幻觉
max_tokens: 500,
});
return resp.choices[0].message.content!;
}
评估与优化:让RAG应用更准确
常见问题一:检索到的文档不相关
原因: Embedding模型没有理解"领域专业术语"。
解决方案: 用"领域微调的Embedding模型",或在检索前"改写用户问题"(用LLM把口语化问题转换成"包含关键词"的问题)。
async function rewriteQuery(originalQuery: string): Promise<string> {
const resp = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: '你是查询优化助手。把用户的问题改写成包含关键词的查询,用于向量数据库检索。' },
{ role: 'user', content: originalQuery },
],
temperature: 0.3,
});
return resp.choices[0].message.content!;
}
常见问题二:LLM仍然编造答案
原因: 上下文不够明确,或LLM"太自信"。
解决方案: 让LLM"引用来源"。
// 在Prompt里要求"引用来源"
const prompt = `基于以下上下文回答问题。每个回答后面,用[来源: 文档标题]格式引用来源。如果上下文里没有,说"我不知道"。
上下文:
${context}
问题:${question}
回答:`;
评估指标:
- 检索准确率(Retrieval Precision):检索到的Top-5文档里,有多少是"真的相关"?
- 回答准确率(Answer Accuracy):LLM的回答是否正确?(人工评估,或用"有标准答案的测试集"自动评估)
- 拒答率(Abstain Rate):当知识库没有答案时,LLM是否说"我不知道"?(越高越好,避免幻觉)
部署与监控:让RAG应用稳定运行
缓存常见查询:
用Redis缓存"常见问题"的回答。
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL! });
export async function answerWithCache(question: string) {
// 尝试从缓存读取
const cached = await redis.get(`rag:${question}`);
if (cached) return cached;
// 缓存未命中,执行RAG
const answer = await answerWithRAG(question);
// 写入缓存(过期时间1小时)
await redis.set(`rag:${question}`, answer, { EX: 3600 });
return answer;
}
监控:
用LangSmith(LangChain的监控平台)或Helicone监控:
- 每次检索的"相似度分数"分布
- LLM的"Token消耗"和"响应时间"
- 用户反馈(点赞/点踩)
结论:RAG是独立开发者的"AI产品入场券"
你不需要训练自己的LLM(成本高、技术难度大)。用RAG架构,你可以用"少量文档 + 开源/商业化向量数据库 + API调用的LLM",构建出"高质量、低成本"的AI功能。
最小可行RAG(MVP):
- 用Supabase pgvector存储文档向量(免费)
- 用OpenAI text-embedding-3-small生成向量($0.02/1M tokens,很便宜)
- 用GPT-4o生成回答($0.005/1K input tokens)
这套方案,处理1万次查询的成本约$20-50——对独立开发者完全可承受。
下一步: 把RAG应用到"客户支持自动化"、"产品推荐"、"个性化学习路径"等场景——这些是"AI时代独立开发者的核心竞争优势"。
更多推荐


所有评论(0)