主题
14.01-RAG基本概念
要点
- Prompt 的上下文窗口有限,模型无法知道窗口之外的信息——RAG 解决的就是这个问题
- 核心流程分两个阶段:索引(文档 → 切分 → 向量化 → 入库)和查询(问题 → 检索 → 拼接 → 生成)
- 向量相似度检索是 RAG 的引擎:找到和用户问题语义最相关的文档片段
- RAG 和微调是两种互补的知识注入方式,适用场景不同
- 检索质量决定 RAG 的上限,上下文拼接决定模型能用到多少
1. 把所有文档塞进 Prompt,为什么行不通
假设你在做一个公司内部知识库问答系统。公司有几万份文档——产品手册、技术规范、FAQ、会议纪要。员工想知道「报销流程是什么」「某个 API 的参数怎么填」,不想翻文档,希望直接问 AI 得到答案。
最直接的想法:把所有文档塞进 Prompt,让模型回答。
typescript
// ❌ 把所有文档塞进 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. 索引阶段:把文档变成可检索的向量
索引阶段把一个文档变成「可被检索的向量」,分四步完成。
3.1 文档解析
把各种格式的文档(PDF、Word、HTML、Markdown)转成纯文本。
typescript
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,表格和图表也需要专门处理。
3.2 文本切分(Chunking)
一篇文档几千上万字,直接算一个向量效果不好——语义会被「平均化」。需要把文档切成小段(chunk),每段独立算向量。
typescript
const chunks = chunkText(document.content, {
chunkSize: 1000, // 每段 1000 字符
overlap: 200, // 段与段之间重叠 200 字符
})
// 一篇 5000 字的文档可能切成 6-8 段切分策略直接影响检索质量。 切太大,语义稀释;切太小,丢失上下文。第 14.05 节会展开讲各种切分策略和选型建议。
3.3 向量化(Embedding)
把每个 chunk 转成一个高维向量。这个向量捕捉了文本的语义信息——语义相近的文本,向量距离也近。
typescript
const embedding = await callEmbeddingAPI(chunk.text)
// 返回 [0.12, -0.34, 0.88, ..., 0.05] 的浮点数数组
// 维度通常是 768、1024 或 1536Embedding 模型的选择很关键——它决定了向量空间里「什么算相似」。通用模型未必能捕捉你所在领域的语义差异。第 14.06 节讲模型选型。
3.4 存入向量数据库
把 chunk 的向量和元数据存入向量数据库。
typescript
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 生成回答。
4.1 问题向量化 + 相似度检索
和索引阶段用同一个 embedding 模型,把用户问题转成向量,然后在向量数据库中找最相似的 top-K 个 chunk。
typescript
const [queryVector] = await callEmbeddingAPI(userQuestion)
const results = await vectorDB.query(queryVector, {
topK: 5,
returnMetadata: 'all',
})
// results.matches 是 5 个最相似的 chunk
// 每个 match 包含 id、score(相似度分数)、metadata(原文、来源等)相似度算法通常是余弦相似度(cosine similarity)——两个向量的夹角余弦值,范围 -1 到 1,越大越相似。
4.2 上下文拼接 + 模型生成
把检索到的 chunk 拼进 Prompt,发给模型。
typescript
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 },
]
const response = await callModelAPI({
model: 'gpt-4o',
messages,
temperature: 0, // 基于事实的回答,降低随机性
})拼接方式会影响模型的回答质量——上下文太长会分散注意力,排列不当会让模型找不到重点。第 14.14 节讲上下文拼接的优化策略。
同时把检索到的来源返回给前端,方便用户验证:
typescript
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,
})),
})5. RAG vs 微调:两种知识注入方式的取舍
知识注入有两种主要方式。两者的核心区别不在技术实现,而在知识放在哪里、怎么更新、能不能溯源。
| 维度 | RAG | 微调 |
|---|---|---|
| 知识来源 | 外部知识库,运行时检索 | 训练进模型权重 |
| 知识更新 | 更新知识库即可 | 需要重新训练,成本高 |
| 知识溯源 | 可以追溯到具体文档片段 | 无法溯源,模型「凭记忆」回答 |
| 适用知识类型 | 事实性知识、最新信息、私有数据 | 风格、语气、特定任务格式 |
| 成本 | 每次查询需检索 + embedding | 训练成本高,推理成本不变 |
| 幻觉风险 | 较低——基于检索到的事实回答 | 较高——模型可能混淆训练数据 |
5.1 什么时候选 RAG
满足以下任一条件,RAG 通常是更好的选择:
- 知识会频繁更新(产品文档、技术规范、政策法规)
- 需要溯源——用户想知道「这个答案来自哪个文档」
- 知识量大,无法全部放进上下文窗口
- 多租户场景——不同用户看到不同知识库
5.2 什么时候选微调
- 需要模型学习特定的输出风格或格式
- 知识相对稳定,不需要频繁更新
- 不需要溯源,只要回答对就行
- 任务非常专业,通用模型即使有上下文也做不好(比如特定领域的代码生成或医学诊断)
5.3 什么时候两者都用
很多实际场景会同时使用 RAG 和微调,各司其职:
- 微调让模型学会「怎么回答」——风格、格式、领域术语
- RAG 提供「回答什么」——具体事实、最新数据
typescript
const messages = [
{ role: 'system', content: ragContext }, // RAG:提供事实
// 模型本身经过微调,知道怎么组织回答 // 微调:提供风格
{ role: 'user', content: userQuestion },
]
const response = await callModelAPI({
model: 'my-fine-tuned-model', // 微调过的模型
messages,
})6. 最小可运行 RAG 服务
把上面的流程串起来,写一个完整的 RAG 服务。
typescript
// 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) {
const chunks = chunkText(doc.content, { chunkSize: 1000, overlap: 200 })
const embeddings = await callEmbeddingAPI(chunks.map((c) => c.text))
await vectorDB.upsert(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,
},
})))
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
}>()
const [queryVector] = await callEmbeddingAPI([question])
const results = await vectorDB.query(queryVector, {
topK,
returnMetadata: 'all',
})
const context = results.matches
.map((m, i) => `[${i + 1}] ${m.metadata.text}`)
.join('\n\n')
const systemPrompt = `你是一个根据提供的上下文回答问题的助手。
## 规则
- 只使用下面的上下文回答
- 如果上下文里没有答案,回答「根据现有资料无法回答」
- 回答时引用来源编号,如「根据文档 [1]」
## 上下文
${context}`
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 系统的实际表现。
7.1 检索质量:RAG 的上限由检索决定
检索到的文档和问题不相关,模型回答就一定偏。检索质量直接决定 RAG 的上限,后面的所有优化都是在逼近这个上限。
常见的失败原因:
- 切分策略不好——关键信息被切断,比如一个结论和它的前提分属不同 chunk
- Embedding 模型不合适——通用模型没有捕捉到领域语义差异
- 用户问题和文档的表述差异太大——「报销流程」vs「差旅费用审批步骤」,语义相近但表述不同
第 14.11-14.13 节讲怎么提升检索质量。
7.2 上下文利用:检索到了但模型没用好
检索到了相关文档,模型却没有用好。问题通常出在拼接环节:
- 上下文太长,模型注意力分散,找不到重点
- 排列顺序不当,最相关的内容没有放在最前面
- Prompt 没有让模型理解「只基于上下文回答」的约束
第 14.14 节讲上下文拼接的优化。
7.3 幻觉:有上下文也可能编
即使给了上下文,模型仍然可能编造信息:
- 上下文里没有答案,但模型硬凑了一个——这是最常见的幻觉来源
- 上下文里有矛盾信息,模型选了错的那条
引用溯源是对抗幻觉的重要手段:让模型在回答中标注来源编号,回答完检查引用是否真实存在。第 14.15 节展开讲。
7.4 评估:没有指标就没有优化方向
RAG 的质量需要量化评估,否则优化靠感觉:
- 检索准不准——Recall@K、MRR、NDCG
- 回答对不对——忠实度、相关性、完整性
第 14.17 节讲 RAG 的评估指标。
7.5 本章在系列中的位置
第 13 章讲 Prompt 工程——怎么让模型在单次调用中更好地完成任务。但模型的知识范围受限于上下文窗口和训练数据。RAG 扩展了模型的知识来源——让它能基于外部知识库回答问题。两者配合:RAG 提供外部知识,Prompt 工程提供怎么用好这些知识。
本章建立了 RAG 的整体心智模型。后续的展开路线:
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,什么情况下不该用。