Skip to content

预计时间

2 周

学习目标

  • 掌握 Chunking 策略
  • 理解 Embedding 原理
  • 能选 Vector DB
  • 实现完整 RAG 流程

一、为什么需要 RAG?

LLM 的两大硬伤:

text
1. 知识截止 — GPT-4 的知识截止到 2023 年
   问:"今天深圳天气怎么样?" → 不知道

2. 幻觉 — 不知道就编
   问:"你们公司的请假流程?" → 现编一个看起来很真的

RAG 的思路:

text
不靠模型"回忆",靠检索找答案:

用户问题 → 在知识库中检索相关文档 → 把文档内容拼进 Prompt → LLM 基于文档回答

类比:
  LLM  = 考试不带参考书的考生(靠记忆)
  RAG  = 给了你参考书的考生(查资料再答)
  RAG  = LLM + Google

二、RAG 全流程

text
【离线阶段:准备知识库】
  文档 (PDF/Markdown/Text)
    ↓ Chunking(文本分块)
  ["片段1", "片段2", ..., "片段N"]
    ↓ Embedding(向量化)
  [[0.1, -0.3, 0.8...], [0.2, 0.1, -0.5...], ...]
    ↓ Store(存入 Vector DB)
  Vector DB 就绪

【在线阶段:问答】
  用户提问:"公司请假流程是什么?"
    ↓ Embedding(问题向量化)
  [0.15, -0.25, 0.75...]
    ↓ Retrieve(向量相似度检索)
  Top-K 相关片段
    ↓ Augment(拼接到 Prompt)
  "根据以下资料回答问题:\n [片段1]\n [片段2]\n 问题:公司请假流程是什么?"
    ↓ Generate(LLM 生成回答)
  "根据公司规定,员工请假需提前 3 天..."

三、Chunking(文本分块)

3.1 分块策略对比

策略做法优点缺点
固定长度每 512 Token 切一刀简单可能在句子中间切断
按段落\n\n 分割语义完整段落长度不均
按标题### 标题分割结构清晰有些文档没标题
递归分割先按 \n\n\n平衡长度和语义稍复杂
语义分割用 Embedding 判断句子相似度最精确慢、贵

3.2 实战代码

javascript
// npm install langchain @langchain/textsplitters
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,        // 每个 chunk 最多 500 字符
  chunkOverlap: 50,      // 相邻 chunk 重叠 50 字符
  separators: ['\n\n', '\n', '。', '.', '!', '?', ',', ' '],
});

const chunks = await splitter.splitText(documentText);

// 结果示例:
// Chunk 0: "...请假流程如下..."
// Chunk 1: "...流程如下:1. 填写申请表..."  ← overlap 保证了上下文
// Chunk 2: "...写申请表。2. 主管审批..."

Overlap 很重要

如果没有 overlap,可能出现:

  • Chunk 1 结尾:"请假需要填写"
  • Chunk 2 开头:"申请表"

检索"请假申请表"时,两个 chunk 各匹配一半,都不够分。 有了 overlap → Chunk 1 结尾也有"申请表" → 命中。


四、Embedding

Embedding = 把文本变成向量(一串数字),语义相近的文本向量也相近。

text
"猫" → [0.2, -0.5, 0.8, 0.1, ...]
"狗" → [0.3, -0.4, 0.7, 0.2, ...]   ← 和"猫"很近
"汽车" → [-0.1, 0.6, -0.3, 0.9, ...] ← 和"猫"很远

4.1 生成 Embedding

javascript
import OpenAI from 'openai';

const openai = new OpenAI();

async function embed(text) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',  // 1536 维,便宜
    input: text,
  });
  return response.data[0].embedding;
  // → [0.0012, -0.034, 0.0089, ...] (1536 个数字)
}

// 批量处理(省 API 调用)
async function embedBatch(texts) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: texts,  // 最多 2048 条
  });
  return response.data.map(d => d.embedding);
}

4.2 Embedding 模型选型

模型维度价格适合
text-embedding-3-small512/1536$0.02/1M tokens通用,性价比高
text-embedding-3-large256/1024/3072$0.13/1M tokens精度要求高
Cohere Embed v31024~$0.10/1M tokens多语言好
BGE (BAAI)1024免费(自部署)中文场景
Jina Embeddings768有免费层长文本支持好

五、Vector DB

5.1 选型对比

PineconepgvectorMilvusChromaWeaviate
部署云服务PG 扩展自部署自部署自部署
规模十亿级百万级十亿级百万级十亿级
运维零运维跟着 PG中等中等
过滤元数据WHERE 很强元数据元数据GraphQL
价格$70/月起免费免费免费免费
适合不想管运维已有 PG大规模原型验证多模态

推荐

起步用 pgvector — 不用引入新数据库,SQL 就能查。等向量超百万再考虑 Milvus/Pinecone。

5.2 pgvector 使用

sql
-- 安装扩展
CREATE EXTENSION vector;

-- 创建带向量字段的表
CREATE TABLE knowledge_chunks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content TEXT NOT NULL,
  embedding vector(1536),  -- 向量字段
  metadata JSONB
);

-- 创建索引(加速检索)
CREATE INDEX ON knowledge_chunks
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

-- 检索:找与查询向量最相似的 Top 5
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM knowledge_chunks
ORDER BY embedding <=> $1
LIMIT 5;
-- <=> 是余弦距离,值越小越相似
-- 1 - 余弦距离 = 余弦相似度,越大越相似

5.3 Prisma + pgvector

prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["postgresqlExtensions"]
}

datasource db {
  provider   = "postgresql"
  url        = env("DATABASE_URL")
  extensions = [pgvector(map: "vector", schema: "public")]
}

model KnowledgeChunk {
  id         String   @id @default(uuid())
  content    String
  embedding  Unsupported("vector(1536)")
  documentId String   @map("document_id")

  @@map("knowledge_chunks")
}

六、完整 RAG Pipeline

javascript
// 1. 离线:处理文档
async function indexDocument(docText, docId) {
  const chunks = await splitText(docText);  // Chunking
  const embeddings = await embedBatch(chunks);  // Embedding

  for (let i = 0; i < chunks.length; i++) {
    await prisma.knowledgeChunk.create({
      data: {
        content: chunks[i],
        embedding: pgvector.toSql(embeddings[i]),
        documentId: docId,
      },
    });
  }
}

// 2. 在线:RAG 问答
async function ragQuery(question) {
  // 2a. 把问题向量化
  const queryEmbedding = await embed(question);

  // 2b. 检索 Top 5
  const chunks = await prisma.$queryRaw`
    SELECT content, 1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
    FROM knowledge_chunks
    ORDER BY embedding <=> ${queryEmbedding}::vector
    LIMIT 5
  `;

  // 2c. 拼 Prompt
  const context = chunks.map(c => c.content).join('\n\n');
  const prompt = `根据以下参考资料回答问题。如果资料中没有相关信息,请如实告知。

参考资料:
${context}

问题:${question}`;

  // 2d. 调 LLM 生成回答
  const response = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: prompt }],
  });

  return response.choices[0].message.content;
}

七、检索优化

7.1 Hybrid Search(混合检索)

text
只用向量检索的问题:
  搜索"苹果公司的股票代码" → 可能返回水果相关文档
  因为 Embedding 不知道"苹果"是公司还是水果

Hybrid Search = 向量检索 + 关键词检索

  向量检索:语义相近 → "Apple Inc.", "AAPL", "Tim Cook"
  关键词检索:精确匹配 → "股票代码"
  融合排序 (RRF) → 排名更准

7.2 Rerank(精排)

text
初检取的 20 条不一定最相关,加一步精排:

  Top 20(粗排,快)→ Reranker Model → Top 5(精排,准)

Cohere Rerank API 示例:
  const results = await cohere.rerank({
    query: question,
    documents: top20Chunks,
    topN: 5,
  });

实践

  1. 找一篇技术文章 → 写代码分块 + 存 pgvector
  2. 测试:同样的问题,直接问 LLM vs RAG 问答,看差异
  3. 试着问知识库中没有的内容,检验"不知道就说不知道"的效果