预计时间
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-small | 512/1536 | $0.02/1M tokens | 通用,性价比高 |
text-embedding-3-large | 256/1024/3072 | $0.13/1M tokens | 精度要求高 |
| Cohere Embed v3 | 1024 | ~$0.10/1M tokens | 多语言好 |
| BGE (BAAI) | 1024 | 免费(自部署) | 中文场景 |
| Jina Embeddings | 768 | 有免费层 | 长文本支持好 |
五、Vector DB
5.1 选型对比
| Pinecone | pgvector | Milvus | Chroma | Weaviate | |
|---|---|---|---|---|---|
| 部署 | 云服务 | 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,
});实践
- 找一篇技术文章 → 写代码分块 + 存 pgvector
- 测试:同样的问题,直接问 LLM vs RAG 问答,看差异
- 试着问知识库中没有的内容,检验"不知道就说不知道"的效果