Appearance
RAG 全景:先搞清楚你到底该不该用 RAG
1. 本节产出
你能画出一条完整的 RAG 链路并说出每一段的作用,能用一个四问决策表判断某个业务需求该不该上 RAG,并且能一眼看出别人方案里缺了哪一段。
2. 前置依赖
- 01-02 第一个 ChatClient
- 01-08 结构化输出
- 本地 Docker 可用
3. 为什么先讲「该不该用」
这是全篇最容易被跳过、也最值钱的一节。三个真实翻车:
翻车一:用 RAG 做格式转换。 需求是「把客户发来的非标 Excel 转成标准格式」。团队灌了几百份样例文档做 RAG,效果很差。正确做法:这是一个转换规则问题,写解析器 + 少量 few-shot 即可,跟检索毫无关系。
翻车二:把 RAG 当数据库用。 需求是「统计上季度各区域销售额」。RAG 检索出的片段是抽样,模型据此「估算」出的数字必然不准。正确做法:这是结构化查询,应该走 Text-to-SQL 或直接查库。
翻车三:文档没整理就上 RAG。 公司 Wiki 里同一个制度有 5 个版本、3 份互相矛盾。灌进去之后,模型检索到矛盾内容,给出随机答案,用户投诉「AI 胡说」。根因不在 RAG,在源文档治理。
共同点:都是把 RAG 当万能药。RAG 解决的是一个特定问题——「让模型基于它没见过的、且以文本形式存在的资料来回答」。
4. 核心原理
4.1 完整链路
【离线·灌库】 【在线·问答】
文档源 用户提问
│ │
▼ 解析(PDF/Word/HTML/MD) ▼ 查询前处理(改写/扩展)
纯文本 增强后的查询
│ │
▼ 切分(chunk) ▼ 检索(向量 + 关键词)
片段 + 元数据 候选片段
│ │
▼ 向量化(Embedding) ▼ 重排(Rerank)
向量 + 文本 Top-K 片段
│ │
▼ 写入向量库 ▼ 组装 Prompt
索引 上下文 + 问题
│
▼ 生成
答案 + 引用 [1][2]离线段决定上限,在线段决定体验。 这是全篇最重要的判断:
| 段 | 出问题时的表现 | 优化收益 |
|---|---|---|
| 离线(解析/切分/元数据) | 根本搜不到,怎么调都没用 | 最高 |
| 在线(检索/重排) | 能搜到但排不准 | 中等 |
| 生成(prompt/模型) | 搜到了但答得不好 | 较低 |
绝大多数「RAG 效果差」的项目,问题都在离线段。因为离线段做错是不可逆的——切碎了的语义拼不回来,丢了元数据的片段无法过滤。
4.2 四问决策表
| 问题 | 若答「否」说明什么 |
|---|---|
| 1. 答案是否存在于某批文档里? | 不存在 → 模型再怎么检索也答不出,这是知识缺失不是检索问题 |
| 2. 这些知识会变化吗? | 不变且量小 → 直接写进 prompt 或微调更省事 |
| 3. 需要溯源吗? | 不需要 → 可以简化掉引用机制,工程量大减 |
| 4. 文档量是否超过上下文窗口? | 没超 → 全量塞进去最简单,别做检索 |
四个都是「是」才值得上完整 RAG。第 4 问尤其重要:很多场景文档总量只有几千字,全量塞进上下文的效果比切分检索更好(避免切分损失),成本也更低。
4.3 RAG vs 微调 vs 长上下文
| 维度 | RAG | 微调 | 长上下文全塞 |
|---|---|---|---|
| 知识更新 | 灌库即可,分钟级 | 需重新训练,天级 | 无需处理 |
| 溯源 | 天然支持 | 不支持 | 天然支持 |
| 成本 | 每次检索 + 长 prompt | 训练贵、推理便宜 | 输入 Token 线性增长 |
| 适合 | 知识频繁变、要溯源 | 固定风格/格式、领域术语 | 文档少且固定 |
| 不适合 | 需要模型改变「思维方式」 | 知识频繁更新 | 文档量大 |
判断口诀:
- 要知道什么 → RAG
- 要怎么说话 → 微调
- 文档又少又固定 → 长上下文
提示:这三者可以组合。常见的高质量方案是「微调负责格式与风格 + RAG 负责知识」。但作为起步,只做 RAG 就够了,别一上来就三个都上。
5. 代码走查
5.1 用 Spring AI 的最小 RAG
java
// src/main/java/com/example/rag/config/RagConfig.java
@Configuration
public class RagConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel, JdbcTemplate jdbc) {
return PgVectorStore.builder(jdbc, embeddingModel)
.dimensions(1536) // 必须与 Embedding 模型维度一致
.distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.initializeSchema(true)
.build();
}
@Bean
public ChatClient ragClient(ChatClient.Builder builder, VectorStore store) {
return builder
.defaultSystem("只能依据提供的资料回答。资料中没有的,明确说不知道。")
.defaultAdvisors(QuestionAnswerAdvisor.builder()
.vectorStore(store)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.6) // 低于阈值不塞上下文
.build())
.build())
.build();
}
}QuestionAnswerAdvisor 是官方提供的「够用」实现:检索 → 塞进 prompt → 生成。它适合验证想法,不适合直接上生产,因为缺少:混合检索、重排、引用、拒答、多租户过滤。这些在 02B 逐章补上。
5.2 灌库:ETL 管道骨架
java
// src/main/java/com/example/rag/ingest/DocumentIngestService.java
@Service
public class DocumentIngestService {
private final VectorStore store;
public int ingest(Path file, String sourceId, String tenantId) {
// 1. 解析成文本
List<Document> docs = parser.parse(file);
// 2. 切分(见 02-04)
List<Document> chunks = splitter.split(docs);
// 3. 补元数据(见 02-07)—— 这一步决定后面所有过滤能力
chunks.forEach(d -> {
d.getMetadata().put("sourceId", sourceId);
d.getMetadata().put("tenantId", tenantId);
d.getMetadata().put("version", currentVersion(sourceId));
});
// 4. 写入(框架自动做向量化)
store.accept(chunks);
return chunks.size();
}
}注意第 3 步:元数据是手工补的,框架不会自动加。忘加 tenantId 的后果是后面根本无法做租户隔离,只能重灌全库——这个代价在 02-07 会详细算。
5.3 一键起依赖
yaml
# docker-compose.yml
services:
pgvector:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: rag
POSTGRES_USER: rag
POSTGRES_PASSWORD: rag
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]6. 跑起来
bash
git checkout ch02-01-rag-overview
cd demos/rag-enterprise-starter
docker compose up -d
mvn spring-boot:runbash
# 灌入一篇测试文档
curl -X POST http://localhost:8080/api/ingest \
-F "file=@docs/员工手册.pdf" -F "sourceId=handbook-2026"
# 提问
curl -X POST http://localhost:8080/api/ask \
-H "Content-Type: application/json" \
-d '{"question":"年假怎么计算?"}'期望:返回基于文档内容的答案,且带有来源标记。
| 检查项 | 通过标准 |
|---|---|
| 灌库 | 返回切分后的片段数,日志显示写入成功 |
| 命中 | 提问能答出文档里的内容 |
| 未命中 | 问一个文档里没有的问题,应回答「资料中没有」而不是编造 |
| 拒答测试 | 这一条必须测——它验证你的 system prompt 是否真的生效 |
7. 生产避坑
- 先问「该不该用 RAG」,再动手。这节不是走过场:不适合的场景硬上 RAG,后面所有优化都是在错误方向上加速。用四问决策表过一遍,通常能砍掉三成伪需求。
- 离线段做错是不可逆的。切分策略和元数据设计一旦定了并灌了库,改一次就要全量重灌(重灌要重新 Embedding,是要花钱的)。所以 02-04 和 02-07 必须想清楚再动手,不要「先跑起来再说」。
similarityThreshold不要设太高也不要不设。不设会把大量无关片段塞进上下文(费钱且干扰),设太高(>0.8)会导致大量正常问题被拒答。经验起点 0.6,然后用评测集调(见 02-17)。
8. 延伸与锚点
- 思考题:你的业务里有「统计类」问题吗?如果有,RAG 答不准,该怎么处理?(提示:结构化查询走 Text-to-SQL,或做混合路由——答案在 03C 实战)
- 代码锚点:
git checkout ch02-01-rag-overview - 下一课时:02-02 选型决策树
- 对应课件:L02-01 RAG 全景