用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的流程:

  1. 检索(Retrieve):根据用户问题,从"知识库"里找到最相关的文档
  2. 增强(Augment):把检索到的文档,作为"上下文"提供给LLM
  3. 生成(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}

回答:`;

评估指标:

  1. 检索准确率(Retrieval Precision):检索到的Top-5文档里,有多少是"真的相关"?
  2. 回答准确率(Answer Accuracy):LLM的回答是否正确?(人工评估,或用"有标准答案的测试集"自动评估)
  3. 拒答率(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):

  1. 用Supabase pgvector存储文档向量(免费)
  2. 用OpenAI text-embedding-3-small生成向量($0.02/1M tokens,很便宜)
  3. 用GPT-4o生成回答($0.005/1K input tokens)

这套方案,处理1万次查询的成本约$20-50——对独立开发者完全可承受。

下一步: 把RAG应用到"客户支持自动化"、"产品推荐"、"个性化学习路径"等场景——这些是"AI时代独立开发者的核心竞争优势"。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐