14.01-RAG基本概念
要点
- 第 13 章讲完了 Prompt 工程——但 Prompt 的上下文窗口是有限的,模型无法知道窗口之外的信息
- RAG(Retrieval-Augmented Generation)解决的是「让模型基于外部知识回答问题」
- 核心流程:索引阶段(文档 → 切分 → 向量化 → 入库)+ 查询阶段(问题 → 检索 → 拼接 → 生成)
- RAG 和微调是两种不同的知识注入方式,适用场景不同
- 向量相似度检索是 RAG 的核心——找到和用户问题语义最相关的文档片段
- 这一章从基本概念出发,逐层拆开每个环节的实现细节
1. 从一个真实问题说起
假设你在做一个「公司内部知识库问答」系统。公司有几万份文档——产品手册、技术规范、FAQ、会议纪要。员工想知道「报销流程是什么」「某个 API 的参数怎么填」,不想翻文档,希望直接问 AI 得到答案。
第一反应可能是:把所有文档塞进 Prompt 里,让模型回答。
// ❌ 把所有文档塞进 Prompt
const allDocs = await loadAllCompanyDocuments() // 可能几百万字
const prompt = `
根据以下文档回答用户的问题。
${allDocs}
用户问题:${userQuestion}
`这条路径走不通,原因有三个:
- 上下文窗口有限——GPT-4o 的上下文窗口是 128K token,大约相当于 10 万字。几万份文档根本塞不下
- 成本爆炸——即使塞得下,每次请求都发送几百万 token 的文档,成本会是天文数字
- 信噪比太低——模型需要在海量无关内容中找到相关信息,效果远不如只给它看相关的几段
更好的思路:先找到和用户问题最相关的那几段文档,只把这几段塞进 Prompt。
这就是 RAG 的核心思想。
2. RAG 是什么
RAG 全称 Retrieval-Augmented Generation,检索增强生成。名字已经把流程说清楚了:
- Retrieval——从外部知识库中检索相关内容
- Augmented——用检索到的内容增强 Prompt
- Generation——让模型基于增强后的 Prompt 生成回答
和直接把所有文档塞进 Prompt 的区别:RAG 在中间加了一个「检索」步骤,每次只给模型看最相关的内容。
┌──────────────────────────────┐
│ 知识库(几万份文档) │
└──────────────┬───────────────┘
│
│ 1. 问题向量化
│ 2. 相似度检索
▼
┌──────────────────────────────┐
│ 检索到 top-K 个相关文档片段 │
└──────────────┬───────────────┘
│
│ 拼进 Prompt
▼
┌──────────────────────────────┐
│ 增强的 Prompt = │
│ 系统指令 + 检索到的文档 │
│ + 用户问题 │
└──────────────┬───────────────┘
│
│ 3. 模型生成回答
▼
┌──────────────────────────────┐
│ 最终回答 │
└──────────────────────────────┘
这个流程分两个阶段:
索引阶段(离线):把知识库里的文档预处理成可检索的形式——切分、向量化、存入向量数据库。这一步只在文档新增或更新时执行。
查询阶段(在线):把用户问题向量化,在向量数据库中检索最相关的文档片段,拼进 Prompt 让模型生成回答。这一步每次用户提问都要执行。
3. 索引阶段:从文档到向量
索引阶段把一个文档变成「可被检索的向量」。流程分四步:
步骤一:文档解析
把各种格式的文档(PDF、Word、HTML、Markdown)转成纯文本。
// 这一步的输入输出
type ParsedDocument = {
id: string
title: string
content: string // 纯文本内容
metadata: {
source: string // 来源:文件路径、URL
format: 'pdf' | 'docx' | 'html' | 'markdown'
createdAt: string
author?: string
}
}不同格式的解析难度不同。纯文本和 Markdown 最简单;HTML 需要去掉标签保留内容;PDF 最复杂——扫描件要先 OCR,表格和图表的提取也需要专门处理。
步骤二:文本切分(Chunking)
一篇文档几千上万字,直接算一个向量效果不好——语义会被「平均化」。需要把文档切成小段(chunk),每段独立算向量。
// 切分策略示意
const chunks = chunkText(document.content, {
chunkSize: 1000, // 每段 1000 字符
overlap: 200, // 段与段之间重叠 200 字符
})
// 一篇 5000 字的文档可能切成 6-8 段
// 每段独立成为一个检索单元切分策略直接影响检索质量。切太大,语义稀释;切太小,丢失上下文。第 14.05 节会展开讲各种切分策略。
步骤三:向量化(Embedding)
把每个 chunk 转成一个高维向量。这个向量捕捉了文本的语义信息——语义相近的文本,向量距离也近。
// 调用 embedding 模型
const embedding = await callEmbeddingAPI(chunk.text)
// 返回类似 [0.12, -0.34, 0.88, ..., 0.05] 的浮点数数组
// 维度通常是 768、1024 或 1536Embedding 模型的选择很关键——它决定了向量空间里「什么算相似」。第 14.06 节会讲模型选型。
步骤四:存入向量数据库
把 chunk 的向量和元数据存入向量数据库。
// 写入向量数据库
await vectorDB.upsert({
id: `${document.id}-chunk-${index}`,
vector: embedding,
metadata: {
text: chunk.text,
documentId: document.id,
documentTitle: document.title,
chunkIndex: index,
source: document.metadata.source,
},
})向量数据库专门优化了向量相似度检索的性能。普通的 PostgreSQL 也能做(通过 pgvector 扩展),但数据量大时性能不如专用向量数据库。第 14.07-14.10 节会讲几种主流方案。
4. 查询阶段:从问题到回答
查询阶段是索引阶段的逆过程:把问题变成向量,检索最相似的 chunk,拼进 Prompt 生成回答。
步骤一:问题向量化
和索引阶段用同一个 embedding 模型,把用户问题转成向量。
const queryVector = await callEmbeddingAPI(userQuestion)步骤二:相似度检索
在向量数据库中找和 queryVector 最相似的 top-K 个 chunk。
const results = await vectorDB.query(queryVector, {
topK: 5,
returnMetadata: 'all',
})
// results.matches 是 5 个最相似的 chunk
// 每个 match 包含 id、score(相似度分数)、metadata(原文、来源等)相似度算法通常是余弦相似度(cosine similarity)——两个向量的夹角余弦值,范围 -1 到 1,越大越相似。
步骤三:拼接上下文
把检索到的 chunk 拼进 Prompt。
const context = results.matches
.map((m, i) => `[${i + 1}] ${m.metadata.text}`)
.join('\n\n')
const systemPrompt = `你是一个根据提供的上下文回答问题的助手。
只使用下面的上下文回答。如果上下文里没有答案,明确告知用户。
上下文:
${context}`
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userQuestion },
]拼接方式会影响模型的回答质量。第 14.14 节会讲上下文拼接的优化策略。
步骤四:模型生成
把拼好的 Prompt 发给模型,生成回答。
const response = await callModelAPI({
model: 'gpt-4o',
messages,
temperature: 0, // 基于事实的回答,降低随机性
})
const answer = response.choices[0].message.content同时把检索到的来源也返回给前端,方便用户验证:
return c.json({
answer,
sources: results.matches.map((m) => ({
documentId: m.metadata.documentId,
documentTitle: m.metadata.documentTitle,
chunkIndex: m.metadata.chunkIndex,
score: m.score,
})),
})5. RAG vs 微调
知识注入有两种主要方式:RAG 和微调(Fine-tuning)。两者的区别:
| 维度 | RAG | 微调 |
|---|---|---|
| 知识来源 | 外部知识库,运行时检索 | 训练进模型权重 |
| 知识更新 | 更新知识库即可,不需要重新训练 | 需要重新训练,成本高 |
| 知识溯源 | 可以追溯到具体文档片段 | 无法溯源,模型「凭记忆」回答 |
| 适用知识类型 | 事实性知识、最新信息、私有数据 | 风格、语气、特定任务格式 |
| 成本 | 每次查询需要检索 + 调用 embedding | 训练成本高,推理成本和普通模型一样 |
| 幻觉风险 | 较低——基于检索到的事实回答 | 较高——模型可能混淆训练数据 |
什么时候用 RAG
- 知识会频繁更新(产品文档、技术规范)
- 需要溯源——用户想知道「这个答案来自哪个文档」
- 知识量大,无法全部放进上下文窗口
- 多租户场景——不同用户看到不同知识库
什么时候用微调
- 需要模型学习特定的输出风格或格式
- 知识相对稳定,不需要频繁更新
- 不需要溯源,只要回答对就行
- 任务非常专业,通用模型即使有上下文也做不好
什么时候两者都用
很多实际场景会同时用 RAG 和微调:
- 微调让模型学会「怎么回答」(风格、格式、领域术语)
- RAG 提供「回答什么」(具体事实、最新数据)
// 同时使用 RAG 和微调
const messages = [
{ role: 'system', content: ragContext }, // RAG:提供事实
// 模型本身经过微调,知道怎么组织回答 // 微调:提供风格
{ role: 'user', content: userQuestion },
]
const response = await callModelAPI({
model: 'my-fine-tuned-model', // 微调过的模型
messages,
})6. 一个最小可运行的 RAG 示例
把上面的流程串起来,写一个完整的 RAG 服务:
// src/services/rag/index.ts
import { Hono } from 'hono'
import { callEmbeddingAPI } from '@/services/embedding'
import { callModelAPI } from '@/services/llm'
import { vectorDB } from '@/services/vector-db'
const ragApp = new Hono()
// 索引接口:上传文档并建立索引
ragApp.post('/index', async (c) => {
const { documents } = await c.req.json<{
documents: Array<{ id: string; title: string; content: string; source: string }>
}>()
const indexed = []
for (const doc of documents) {
// 1. 切分
const chunks = chunkText(doc.content, { chunkSize: 1000, overlap: 200 })
// 2. 向量化(批量)
const embeddings = await callEmbeddingAPI(chunks.map((c) => c.text))
// 3. 写入向量数据库
const vectors = chunks.map((chunk, i) => ({
id: `${doc.id}-chunk-${i}`,
vector: embeddings[i],
metadata: {
text: chunk.text,
documentId: doc.id,
documentTitle: doc.title,
chunkIndex: i,
source: doc.source,
},
}))
await vectorDB.upsert(vectors)
indexed.push({ documentId: doc.id, chunks: chunks.length })
}
return c.json({ indexed })
})
// 查询接口:检索 + 生成
ragApp.post('/query', async (c) => {
const { question, topK = 5 } = await c.req.json<{
question: string
topK?: number
}>()
// 1. 问题向量化
const [queryVector] = await callEmbeddingAPI([question])
// 2. 检索
const results = await vectorDB.query(queryVector, {
topK,
returnMetadata: 'all',
})
// 3. 拼接上下文
const context = results.matches
.map((m, i) => `[${i + 1}] ${m.metadata.text}`)
.join('\n\n')
const systemPrompt = `你是一个根据提供的上下文回答问题的助手。
## 规则
- 只使用下面的上下文回答
- 如果上下文里没有答案,回答「根据现有资料无法回答」
- 回答时引用来源编号,如「根据文档 [1]」
## 上下文
${context}`
// 4. 生成回答
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question },
],
temperature: 0,
})
return c.json({
answer: response.choices[0].message.content,
sources: results.matches.map((m) => ({
documentId: m.metadata.documentId,
documentTitle: m.metadata.documentTitle,
chunkIndex: m.metadata.chunkIndex,
score: m.score,
})),
})
})
// 切分函数(简化版,第 14.05 节会展开)
function chunkText(text: string, options: { chunkSize: number; overlap: number }) {
const chunks: Array<{ text: string; start: number }> = []
let start = 0
while (start < text.length) {
const end = Math.min(start + options.chunkSize, text.length)
chunks.push({ text: text.slice(start, end), start })
start += options.chunkSize - options.overlap
}
return chunks
}
export default ragApp这个示例覆盖了 RAG 的核心流程:
- 索引:切分 → 向量化 → 写入
- 查询:问题向量化 → 检索 → 拼接 → 生成
后续章节会逐层拆开每个环节,讨论优化策略。
7. RAG 的挑战
RAG 看起来简单,但做好不容易。几个常见的挑战:
挑战一:检索质量
如果检索到的文档和问题不相关,模型的回答就会偏。检索质量直接决定 RAG 的上限。
可能的原因:
- 切分策略不好——关键信息被切断
- Embedding 模型不合适——语义空间没有捕捉到领域知识
- 用户问题的表述和文档的表述差异太大——语义匹配失败
第 14.11-14.13 节会讲怎么提升检索质量。
挑战二:上下文利用
检索到了相关文档,但模型没有用好。可能原因:
- 拼接方式不好——模型找不到重点
- 上下文太长——模型注意力分散
- Prompt 设计不好——模型没有理解「只基于上下文回答」
第 14.14 节会讲上下文拼接的优化。
挑战三:幻觉问题
即使有上下文,模型仍然可能编造信息。可能的原因:
- 上下文里没有答案,但模型硬凑了一个
- 上下文里有矛盾信息,模型选了错的
第 14.15 节会讲怎么通过引用溯源降低幻觉风险。
挑战四:评估困难
RAG 的质量怎么评估?检索准不准、回答对不对,都需要量化指标。没有评估就没有优化方向。
第 14.17 节会讲 RAG 的评估指标。
8. 本章在整体中的位置
第 13 章讲 Prompt 工程——怎么让模型在单次调用中更好地完成任务。但模型的知识范围受限于上下文窗口和训练数据。
第 14 章的 RAG 扩展了模型的知识来源——让它能基于外部知识库回答问题。
两者配合:RAG 提供「外部知识」,Prompt 工程提供「怎么用好这些知识」。
本章的展开顺序:
01 基本概念(本篇) → RAG 是什么、流程、与微调的区别
02 适用场景 → 什么时候该用 RAG
03-06 索引链路 → 文档上传 → 解析 → 切分 → 向量化
07-10 向量数据库 → 选型 + 三种主流方案实践
11-13 检索优化 → 相似度检索 → 混合检索 → Rerank
14-16 查询链路 → 上下文拼接 → 引用返回 → 权限控制
17 评估 → 怎么度量 RAG 质量
18 接口设计 → 完整 API 设计
19 实战 → Cloudflare Vectorize 实现(已有内容)
从基本概念到工程实践,覆盖 RAG 的完整链路。
总结
回顾这一节的要点:
- RAG 解决的是「让模型基于外部知识回答问题」
- 核心流程:索引阶段(文档 → 切分 → 向量化 → 入库)+ 查询阶段(问题 → 检索 → 拼接 → 生成)
- RAG 和微调是两种互补的知识注入方式
- 向量相似度检索是 RAG 的核心——找到和用户问题语义最相关的文档片段
- 主要挑战:检索质量、上下文利用、幻觉问题、评估困难
- 第 13 章的 Prompt 工程 + 第 14 章的 RAG = 让模型既有知识又能用好知识
下一篇讲 RAG 的适用场景——什么情况下该用 RAG,什么情况下不该用。