Skip to content

RAG 全景:先搞清楚你到底该不该用 RAG ​

1. 本节产出 ​

你能画出一条完整的 RAG 链路并说出每一段的作用,能用一个四问决策表判断某个业务需求该不该上 RAG,并且能一眼看出别人方案里缺了哪一段。

2. 前置依赖 ​

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:run
bash
# 灌入一篇测试文档
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. 生产避坑 ​

  1. 先问「该不该用 RAG」,再动手。这节不是走过场:不适合的场景硬上 RAG,后面所有优化都是在错误方向上加速。用四问决策表过一遍,通常能砍掉三成伪需求。
  2. 离线段做错是不可逆的。切分策略和元数据设计一旦定了并灌了库,改一次就要全量重灌(重灌要重新 Embedding,是要花钱的)。所以 02-04 和 02-07 必须想清楚再动手,不要「先跑起来再说」。
  3. similarityThreshold 不要设太高也不要不设。不设会把大量无关片段塞进上下文(费钱且干扰),设太高(>0.8)会导致大量正常问题被拒答。经验起点 0.6,然后用评测集调(见 02-17)。

8. 延伸与锚点 ​

  • 思考题:你的业务里有「统计类」问题吗?如果有,RAG 答不准,该怎么处理?(提示:结构化查询走 Text-to-SQL,或做混合路由——答案在 03C 实战)
  • 代码锚点:git checkout ch02-01-rag-overview
  • 下一课时:02-02 选型决策树
  • 对应课件:L02-01 RAG 全景