乐于分享
好东西不私藏

#6、Spring AI RAG 深度技术解析 — 从原理到生产实践

#6、Spring AI RAG 深度技术解析 — 从原理到生产实践

Spring AI RAG 深度技术解析 — 从原理到生产实践

摘要: 检索增强生成(Retrieval-Augmented Generation, RAG)是当前大语言模型应用中最核心的架构范式之一。本文从 Spring AI 框架的视角出发,系统性地拆解 RAG 的技术原理、架构设计、核心组件、进阶策略及生产化落地要点,帮助读者建立起从理论到实践的完整知识体系。


1. RAG 的本质:为什么需要检索增强生成

1.1 大模型的核心痛点

大语言模型(LLM)虽然能力强大,但在实际应用中面临三个根本性局限:

  • 知识截止日期(Knowledge Cutoff)
    :模型训练完成后,无法自动获取最新信息。一个在 2025 年训练的模型不知道 2026 年发生了什么事。
  • 幻觉(Hallucination)
    :模型在不确定时会"编造"答案,尤其在处理长尾知识或专业领域问题时。这不是 bug,而是语言模型基于概率生成的天性。
  • 缺乏内部知识访问能力
    :模型无法"查阅"企业内部的数据库、文档库、知识库。它的知识完全来自于训练语料。

RAG 的出现正是为了解决这三个问题——它不试图让模型记住更多,而是让模型学会"查资料"

1.2 RAG 的核心思想

RAG 的核心逻辑极其简洁,可以用一句话概括:

在回答之前,先检索与问题相关的信息,然后将这些信息作为上下文提供给 LLM,让 LLM 基于检索到的内容生成答案。

这个简单的思路带来了三个关键转变:

传统 LLM 调用:  用户问题 → LLM → 答案(依赖模型内部知识)RAG 模式:       用户问题 → 检索器 → 相关知识 + 问题 → LLM → 基于事实的答案

1.3 RAG Pipeline 的抽象视角

从最抽象的层面看,任何 RAG 系统都可以分解为两个阶段、四个步骤:

graph LR    A[用户查询] --> B[检索阶段]    B --> C[检索结果]    C --> D[生成阶段]    D --> E[最终答案]    subgraph 索引管线(离线)        F[原始文档] --> G[文档分割]        G --> H[Embedding]        H --> I[(向量数据库)]    end    I -.-> B

索引管线(Indexing Pipeline) — 离线执行,将原始文档转化为可检索的向量索引。查询时管线(Query-time Pipeline) — 在线执行,处理用户查询并生成答案。


2. RAG 与 Fine-tuning 的博弈与协同

在讨论 RAG 的架构之前,必须澄清一个常被混淆的问题:RAG 和 Fine-tuning 到底是什么关系?

2.1 本质差异

维度
RAG
Fine-tuning
知识来源
外部检索 + 模型能力
模型内部参数
更新成本
更新索引库即可,无需重新训练
需要重新训练或微调
幻觉控制
强(基于检索事实)
弱(依赖模型记忆)
推理成本
增加检索延迟和 Token 消耗
基本不变
适合场景
知识密集型、频繁更新、高准确性要求
风格适配、指令遵循、稳定性要求

2.2 何时用 RAG

  • 需要引用具体数据源
    (法律条文、财报数据、产品文档)
  • 知识频繁变化
    (实时新闻、内部政策更新)
  • 需要精确追溯信息来源
  • 长尾知识
    (罕见问题、小众领域)

2.3 何时用 Fine-tuning

  • 改变模型的输出风格或语调
    (让模型更像客服或专家)
  • 让模型遵循特定指令格式
    (始终输出 JSON)
  • 少量高频任务
    (分类、实体抽取)
  • 减少推理时的 Prompt 长度
    (将行为编码进参数)

2.4 RAG + Fine-tuning 的最佳协同

在实践中,RAG 和 Fine-tuning 不是互斥的,而是互补的。理想的架构是:

Fine-tuning 负责"如何回答",RAG 负责"回答什么"。

举个例子:一个法律咨询 AI

  • Fine-tuning
     让模型学会法律文书的严谨语气和引用格式
  • RAG
     从法律数据库中检索最新的法条和判例

在 Spring AI 中,这意味着你可以同时使用 ChatClient 和 VectorStore —— 微调后的模型回答风格更专业,RAG 提供的事实更准确。


3. Spring AI RAG 整体架构

3.1 Spring AI 的定位

Spring AI 是 Spring 生态中面向 AI 应用的抽象层。它的设计哲学沿袭了 Spring 一贯的风格:

提供抽象接口,让开发者用最少的代码切换不同实现。

在 RAG 领域,Spring AI 提供了完整的构建块:

Spring AI RAG 组件层次┌─────────────────────────────────────────────┐│                 应用层                         ││   ChatClient / StreamingChatClient           │├─────────────────────────────────────────────┤│                 增强层                         ││   QuestionAnswerAdvisor                      ││   RetrievalAugmentationAdvisor               │├─────────────────────────────────────────────┤│                 检索层                         ││   VectorStore (PGVector / Pinecone / Redis)  ││   DocumentRetriever                          │├─────────────────────────────────────────────┤│                 处理层                         ││   DocumentReader / DocumentTransformer       ││   TokenTextSplitter / ContentFormatter       │├─────────────────────────────────────────────┤│                 基础设施层                      ││   EmbeddingModel (OpenAI / Ollama / BAAI)    ││   ChatModel / StreamingChatModel             │└─────────────────────────────────────────────┘

3.2 核心接口与抽象

Spring AI 为 RAG 定义了几个关键接口,理解这些接口是掌握整个框架的基础:

// 文档表示public class Document {    private String id;    private String content;          // 文档文本内容    private Metadata metadata;       // 元数据(来源、时间、类型等)    private List<Double> embedding;  // 向量表示}// 向量存储抽象public interface VectorStore {    void add(List<Document> documents);    void delete(List<String> idList);    List<DocumentsimilaritySearch(SearchRequest request);}// 检索增强建议器(核心 RAG 组件)public interface RetrievalAugmentationAdvisor {    ChatResponse advise(ChatRequest request);}// 文档检索器public interface DocumentRetriever {    List<Documentretrieve(RetrievalRequest request);}

3.3 最小化 RAG 实现

在 Spring AI 中,实现一个完整的 RAG 流程只需要几行代码:

@Beanpublic VectorStore vectorStore(EmbeddingModel embeddingModel) {    return new PgVectorStore(jdbcTemplate, embeddingModel);}@Servicepublic class RagService {    private final ChatClient chatClient;    public RagService(ChatClient.Builder builder, VectorStore vectorStore) {        this.chatClient = builder            .defaultSystem("""                你是一个专业的AI助手。请基于提供的上下文信息回答问题。                如果上下文信息不足以回答问题,请明确说明,不要编造答案。                回答时请引用信息来源。                """)            .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))            .build();    }    public String ask(String question) {        return chatClient.prompt()            .user(question)            .call()            .content();    }}

这段代码背后发生的事情:

  1. QuestionAnswerAdvisor
     拦截用户请求
  2. 调用 VectorStore.similaritySearch() 执行向量检索
  3. 将检索到的文档格式化为上下文,注入 Prompt
  4. 调用 LLM 生成基于上下文的回答

4. 文档加载与解析:RAG 的入口

RAG 的质量上限在很大程度取决于输入数据的质量。垃圾进,垃圾出 在 RAG 中体现得尤为明显。

4.1 Spring AI 的 DocumentReader 体系

Spring AI 提供了丰富的文档读取器,覆盖了大部分常见格式:

// 读取 PDFDocumentReader pdfReader = new PdfDocumentReader(    new FileSystemResource("document.pdf"));// 读取 JSONDocumentReader jsonReader = new JsonDocumentReader(    new ClassPathResource("data.json"));// 读取 MarkdownDocumentReader mdReader = new MarkdownDocumentReader(    new FileSystemResource("README.md"));// 读取 CSVDocumentReader csvReader = new CsvDocumentReader(    new FileSystemResource("data.csv"));// 读取 HTMLDocumentReader htmlReader = new HtmlDocumentReader(    new UrlResource("https://example.com"));

4.2 文档解析的深度策略

PDF 解析的陷阱

PDF 是 RAG 中最常见的输入格式,也是最容易出问题的格式。PDF 本质上是一个排版格式,不是内容格式,解析质量参差不齐。

Spring AI 的 PdfDocumentReader 基于 Apache PDFBox。对于扫描版 PDF(图片格式),需要使用 OCR 进行预处理:

// 原生 PDF(文本可选中)DocumentReader reader = new PdfDocumentReader(resource);// 扫描版 PDF(需先 OCR 处理)// Spring AI 本身不内置 OCR,可以结合 Tesseract 预处理// 或者使用 AWS Textract、Azure Document Intelligence 等云服务

实际经验:对于 PDF 中的表格,原生解析几乎一定会丢失结构信息。建议使用专门的文档解析服务或自定义表格提取逻辑。

元数据的价值

文档加载时保留的元数据(Metadata)在后续的检索和过滤中至关重要:

Document doc = new Document("content");doc.getMetadata().put("source""financial-report-2026-q1.pdf");doc.getMetadata().put("page"42);doc.getMetadata().put("category""quarterly");doc.getMetadata().put("created_at""2026-04-15");doc.getMetadata().put("author""finance-team");

为什么元数据重要? 元数据允许你在检索时进行预过滤——只检索特定分类、特定时间范围、特定来源的文档,大大提升检索精准度。


5. 文档分割:决定检索质量的隐形之手

5.1 为什么需要分割?

  • LLM 上下文窗口有限
    :即使 GPT-4 有 128K 上下文,检索几百个文档也不现实
  • 检索精度要求
    :一个 100 页的文档,不可能作为一个向量整体检索——相关性会稀释
  • 成本控制
    :Token 就是成本,塞入过多无关上下文既浪费又降低质量

5.2 分割粒度的权衡

太细(句子级)        ←→        太粗(章节级)  检索精度高                         上下文完整  上下文碎片化                       检索噪声大  丢失跨句关系                       可能含无关内容

经验法则:检索的"原子单位"应该是一个自包含的思想单元——能够独立被理解且包含足够上下文的最小段落。

5.3 Spring AI 的分割器策略

Spring AI 提供了 DocumentTransformer 接口以及开箱即用的 TokenTextSplitter

// 基于 Token 的分割(推荐)DocumentTransformer splitter = TokenTextSplitter.builder()    .defaultMaxTokenSize(500)        // 每个分割块最多 500 token    .minChunkSizeChars(350)          // 最小字符数    .minOverlap(50)                  // 块间重叠(保持上下文连续性)    .build();List<Document> chunks = splitter.apply(originalDocuments);

分割参数调优指南:

场景
推荐 chunk size
overlap
理由
问答型 FAQ
200-300 tokens
20-50
答案本身较短,精确匹配更重要
文档分析
500-1000 tokens
50-100
需要更多上下文来理解
代码检索
300-500 tokens
30-50
函数/类通常在此长度
法律条文
400-600 tokens
50-80
条款间有交叉引用
长文档摘要
800-1500 tokens
100-200
需要完整段落理解

5.4 高级分割策略

语义分割——不基于固定的 Token 数,而是基于内容的语义边界(段落、标题、列表):

// 自定义语义分割器示例(概念)List<Document> semanticChunks = documents.stream()    .flatMap(doc -> splitByMarkdownHeaders(doc).stream())    .flatMap(doc -> splitByParagraphs(doc).stream())    .collect(Collectors.toList());

递归字符分割——从粗粒度到细粒度递归分割,直到块大小满足要求:

// 递归分割:先按 ## 分割,再按段落分割,最后按句子分割// Spring AI 的 TokenTextSplitter 内置了类似递归逻辑

实际建议:在实践中,最好根据你的文档特性定制分割策略。比如 Markdown 文档可以按标题层级分割,代码可以按函数或类分割,PDF 可以按页面或段落分割。没有万能的分割策略。


6. Embedding:将语义转化为向量

6.1 Embedding 的核心作用

Embedding 模型将文本映射到高维向量空间,使得语义相近的文本在向量空间中距离较近。

"苹果发布了新款手机"      [0.23, -0.45, 0.78, ...]   ←→"iPhone 16 正式发布"      [0.21, -0.42, 0.80, ...]   ← 语义相近,向量距离小"天气预报说明天下雨"      [0.67, 0.12, -0.34, ...]   ← 语义无关,向量距离大

6.2 Spring AI 的 Embedding 抽象

public interface EmbeddingModel {    // 单条文本 embedding    EmbeddingResponse embed(EmbeddingRequest request);    // 批量 embedding(通常有优化)    List<EmbeddingResponseembed(List<EmbeddingRequest> requests);    // 获取向量维度    int dimensions();}

6.3 Embedding 模型选型

Spring AI 支持多种 Embedding 实现:

Embedding 模型
维度
适用场景
Spring AI 支持
OpenAI text-embedding-3-small
1536
通用、英文为主
OpenAI text-embedding-3-large
3072
高精度需求
BAAI/bge-large-zh-v1.5
1024
中文场景
✅ (ONNX)
Ollama (nomic-embed-text)
768
本地部署
Alibaba Qwen Embeddings
1024
中文/多语言

中文场景的现实考量:

// 中文推荐:使用 BAAI/bge 系列或 Qwen Embeddings@Beanpublic EmbeddingModel embeddingModel() {    // 方案1:本地部署(Ollama)    return new OllamaEmbeddingModel(ollamaApi)        .withModel("bge-m3");  // 支持中英文    // 方案2:阿里通义千问    return new TongyiEmbeddingModel(tongyiApi);    // 方案3:OpenAI(英文为主场景)    return new OpenAiEmbeddingModel(openAiApi)        .withModel("text-embedding-3-small");}

选择 Embedding 模型的关键维度:

  1. 语义理解能力
    :模型是否真正理解你所在领域的语义
  2. 维度大小
    :高维度更精确但计算和存储成本更高
  3. 语言支持
    :中文场景需要专门的中文 embedding 模型
  4. 延迟和吞吐
    :在线服务 vs 本地推理

6.4 Embedding 的最佳实践

// 批量处理优于逐条处理(大多数模型支持 batch)List<Document> batch = chunkedDocuments;List<List<Double>> embeddings = embeddingModel.embed(batch); // 一次调用// 缓存 embedding 结果(避免重复计算)// 向量数据库通常会自动处理缓存

7. 向量数据库:RAG 的记忆存储

7.1 向量数据库 vs 传统数据库

向量数据库专门为向量相似性搜索优化,但不存在"完美"的向量数据库——只有适合你场景的。

7.2 Spring AI 的 VectorStore 实现

Spring AI 通过 VectorStore 接口抽象了所有向量数据库的操作:

public interface VectorStore {    void add(List<Document> documents);    void delete(List<String> idList);    List<DocumentsimilaritySearch(SearchRequest request);}

7.3 主流实现对比

解决方案
部署方式
适合规模
Spring AI 支持度
特点
PGVector
自托管
百万级
原生支持
PostgreSQL 插件,善用已有数据库
Redis Stack
自托管
百万级
原生支持
缓存+向量,低延迟
Pinecone
SaaS
亿级
原生支持
全托管,无需运维
Chroma
嵌入式
十万级
原生支持
开发环境首选
Milvus
自托管
十亿级
原生支持
大规模分布场景
Qdrant
自托管 / SaaS
亿级
原生支持
Rust 实现,性能优秀
Elasticsearch
自托管
亿级
原生支持
全文检索+向量混合
Weaviate
自托管 / SaaS
亿级
原生支持
自带 schema 管理

7.4 生产选型建议

// 开发环境:Chroma(零配置嵌入式)@Beanpublic VectorStore chromaVectorStore(EmbeddingModel embeddingModel) {    return new ChromaVectorStore(chromaApi, embeddingModel, "collection_name");}// 中小规模生产:PGVector(复用 PostgreSQL 基础设施)@Beanpublic VectorStore pgVectorStore(EmbeddingModel embeddingModel,                                   JdbcTemplate jdbcTemplate) {    return new PgVectorStore(jdbcTemplate, embeddingModel,         PgVectorStore.PgDistanceType.COSINE_DISTANCE,         PgIndexType.HNSW,  // HNSW 索引,支持 ANN 搜索        false);             // 不移除现有文档}// 大规模生产:Pinecone(全托管)@Beanpublic VectorStore pineconeVectorStore(PineconeApi api,                                        EmbeddingModel embeddingModel) {    return new PineconeVectorStore(api, embeddingModel,         "my-index"PineconeVectorStore.MetadataFields.NONE);}

7.5 元数据过滤:提升检索精度的关键

纯向量搜索找到的是"语义相似"的文档。结合元数据过滤,可以找到"语义相似且符合条件"的文档。

// Spring AI 中实现带过滤的检索SearchRequest request = SearchRequest.builder()    .query("2026年第一季度财报数据")     // 查询文本    .topK(10)                                // 返回前10条    .similarityThreshold(0.75)               // 相似度阈值    .filterExpression("category == 'quarterly' && created_at > '2026-01-01'")    .build();List<Document> results = vectorStore.similaritySearch(request);

元数据过滤的核心价值

  • 权限控制
    :只检索用户有权访问的文档
  • 时效性控制
    :只检索特定时间范围内的信息
  • 分类过滤
    :只检索特定类别的文档
  • 混合搜索前置条件
    :缩小搜索范围后再执行向量搜索

8. 检索策略:如何找到最相关的信息

检索是 RAG 的核心环节。检索质量直接决定了生成质量。

8.1 基础检索:向量相似性搜索

最简单的检索方式是余弦相似度搜索:

// 余弦距离:1 - cos(A, B)// 值域 [0, 2],越小越相似PgVectorStore.PgDistanceType.COSINE_DISTANCE// 内积距离:常用于归一化向量// 值域 [-1, 1],越大越相似PgVectorStore.PgDistanceType.NEGATIVE_INNER_PRODUCT// 欧几里得距离:适合低维向量PgVectorStore.PgDistanceType.EUCLIDEAN_DISTANCE

经验结论:对于文本 Embedding,余弦相似度通常是默认选择。如果使用 OpenAI Embedding(已经 L2 归一化),内积和余弦等价。

8.2 Top-K 策略

// K 值的影响K = 1-3:   高精度,低召回(严谨问答场景)K = 5-10:  适中(通用知识问答)K = 10-20: 高召回,低精度(摘要、综合分析场景)

动态 Top-K:根据问题的复杂度动态调整 K 值。

// 概念:问题越复杂,需要的上下文越多int dynamicTopK(String question) {    int words = question.split(" ").length;    int tokens = estimateTokens(question);    if (tokens < 10return 3;    // 简单问题    if (tokens < 50return 5;    // 一般问题    return 10;                     // 复杂问题}

8.3 相似度阈值

只保留超过相似度阈值的文档,避免引入噪音:

SearchRequest request = SearchRequest.builder()    .query(question)    .topK(20)    .similarityThreshold(0.7)  // 低于 0.7 的结果将被丢弃    .build();

合理的阈值通常通过实验确定。先用 0.7 作为起点,根据实际效果上浮或下调。

8.4 Spring AI 的 Advisor 机制

Spring AI 通过 Advisor 模式实现灵活的检索增强控制:

// 1. QuestionAnswerAdvisor:最简单的 RAG// 自动执行检索并将结果注入 Prompt.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))// 2. RetrievalAugmentationAdvisor:更灵活的配置.defaultAdvisors(RetrievalAugmentationAdvisor.builder()    .documentRetriever(DefaultDocumentRetriever.builder()        .vectorStore(vectorStore)        .similarityThreshold(0.75)        .topK(5)        .build())    .contentFormatter(DefaultContentFormatter.builder()        .withTemplate("""            上下文信息:            ------------            {documents}            ------------            请基于上述上下文回答以下问题。            """)        .build())    .build())

8.5 高级检索策略

查询重写(Query Rewriting)——用户的问题往往不适合直接用于检索。比如"能告诉我这个怎么用?"中的"这个"是代词,需要解析。通过 LLM 将用户问题改写成更适合检索的形式。

用户问题: "去年的营收怎么样?"重写后: "2025年公司年度营收数据"

查询分解(Query Decomposition)——将复杂问题分解为多个子问题分别检索:

用户问题: "苹果和微软2025年的营收对比"子问题1: "苹果公司2025年营收是多少"子问题2: "微软公司2025年营收是多少"

HyDE(Hypothetical Document Embedding)——先生成一个假设的理想文档,再基于这个假设文档检索:

用户问题: "什么是量子计算?"假设文档: "量子计算是一种利用量子力学原理进行信息处理的计算方式..."检索: 用假设文档的 embedding 去匹配真实文档

这些高级策略在 Spring AI 中可以通过自定义 Advisor 或 Processor 实现。框架提供了扩展点,策略本身需要开发者根据场景实现。


9. 增强生成:让 LLM 基于上下文作答

9.1 Prompt 模板设计

检索到的文档如何呈现给 LLM 是影响生成质量的关键因素。

// 自定义上下文模板ContentFormatter formatter = DefaultContentFormatter.builder()    .withTemplate("""        你是一个专业的AI助手。        ## 上下文信息        以下是与你需要回答的问题相关的参考信息。请仔细阅读这些信息,        它们来自我们公司的知识库:        {% for document in documents %}        [来源:{{ document.metadata.source }}]        {{ document.content }}        {% endfor %}        ## 回答要求        1. 如果上下文信息充分,请基于上下文给出准确、详细的回答        2. 如果上下文信息不足,请明确说明"根据现有信息无法回答"        3. 回答时请在句末标注信息来源:[[来源:xxx]]        4. 不要编造上下文之外的信息        5. 使用专业、清晰的语言        ## 用户问题        {{ userInput }}        """)    .build();

9.2 Prompt 注入的工作流程

Spring AI 的 QuestionAnswerAdvisor 内部执行以下步骤:

1. 接收用户请求2. 调用 VectorStore.similaritySearch() 检索相关文档3. 将检索结果注入 Prompt(通过 ContentFormatter)4. 构造增强后的 Prompt:   System: [原始 System Message + 检索到的上下文]   User: [用户问题]5. 调用 ChatModel6. 返回基于上下文的回答

9.3 流式输出与 RAG

// 流式 RAG 响应@Beanpublic ChatClient streamingChatClient(ChatClient.Builder builder) {    return builder        .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))        .build();}// Controller@GetMapping("/chat/stream")public Flux<StringstreamChat(@RequestParam String question) {    return streamingChatClient.prompt()        .user(question)        .stream()        .content();}

9.4 引用溯源

在生产系统中,告诉用户答案的来源是建立信任的关键:

// 在 Prompt 中要求标注来源// 同时在后处理中解析引用@Servicepublic class CitedRagService {    public AnswerWithSources answer(String question) {        String response = chatClient.prompt()            .user(question)            .call()            .content();        // 解析引用来源(从元数据中提取)        List<String> sources = extractSources(response);        return new AnswerWithSources(response, sources);    }    private List<StringextractSources(String response) {        // 从 LLM 输出中解析 [[来源: xxx]] 格式的引用        Pattern pattern = Pattern.compile("\\[\\[来源:(.+?)\\]\\]");        Matcher matcher = pattern.matcher(response);        return matcher.results()            .map(r -> r.group(1))            .distinct()            .collect(Collectors.toList());    }}public record AnswerWithSources(    String answer,    List<String> sources) {}

10. 高级 RAG 模式

10.1 多轮对话中的 RAG

单轮 RAG 相对简单,多轮对话中的 RAG 则面临更多挑战:

挑战 1:上下文消歧

用户: "什么是量子计算?"助手: "量子计算是..."用户: "它和传统计算有什么区别?"      ↑ 这里的"它"指的是量子计算,需要从对话历史推断

解决方案:上下文压缩(Contextual Compression)

// 在检索前,将对话历史压缩为最新的上下文// 或者让 LLM 将用户问题补全为独立问题@Componentpublic class ContextualQueryEnricher {    public String enrichQuery(String currentQuestion,                                List<Message> history) {        if (history.isEmpty()) {            return currentQuestion;        }        // 使用 LLM 将问题补全        String enriched = chatClient.prompt()            .system("""                根据对话历史,将用户的最后一句话改写为可以独立检索的查询。                只需要返回改写后的查询,不要有其他内容。                """)            .user("""                历史对话:                %s                用户最新问题:%s                """.formatted(formatHistory(history), currentQuestion))            .call()            .content();        return enriched != null ? enriched : currentQuestion;    }}

挑战 2:历史信息的累积

随着对话进行,历史消息越来越多。如果不加控制:会超出上下文窗口、检索噪音增加、成本线性增长。

解决方案:滑动窗口和摘要化

10.2 多路检索(Multi-Route Retrieval)

结合多种检索策略,综合利用各自优势:

用户问题    │    ├──→ 向量检索(语义匹配)    ├──→ 关键词检索(精确匹配,如 Elasticsearch)    ├──→ 图检索(实体关系,如 Neo4j)    └──→ SQL 查询(结构化数据)            │            └──→ 融合排序 → 最终上下文

Spring AI 实现思路

@Componentpublic class HybridRetriever {    private final VectorStore vectorStore;    private final ElasticsearchRestClient esClient;    public List<Documentretrieve(String query, int topK) {        // 1. 向量检索        List<Document> vectorResults = vectorStore.similaritySearch(            SearchRequest.builder().query(query).topK(topK).build());        // 2. 关键词检索        List<Document> keywordResults = keywordSearch(query, topK);        // 3. 融合排序(Reciprocal Rank Fusion)        return reciprocalRankFusion(vectorResults, keywordResults, topK);    }    private List<DocumentreciprocalRankFusion(            List<Document>... rankings) {        // RRF: score = Σ 1/(k + rank)        // 融合多路检索结果,被多路都命中的文档排名更高        // ...    }}

10.3 RAPTOR:层次化摘要检索

RAPTOR(Recursive Abstractive Processing for Tree-Organized Retrieval)是一种将文档构建成层次化摘要树的方法。

            ┌─────────────┐            │ 全局摘要      │  ← Level 2(最抽象)            └──────┬──────┘                   │         ┌─────────┼─────────┐         │         │         │   ┌────┴───┐ ┌───┴────┐ ┌──┴────┐   │摘要集群A │ │摘要集群B │ │摘要集群C │  ← Level 1   └────┬───┘ └───┬────┘ └───┬───┘        │         │          │   ┌────┴───┐    ...         ...   │原始块A1 │   │原始块A2 │   └────────┘                   ← Level 0(原始文档块)

检索时,从顶层开始逐层匹配,找到最合适的粒度。这种模式在处理超长文档(如一本书或一份研究报告)时特别有效。

10.4 Self-RAG:让 LLM 自我评估

Self-RAG 是一种让 LLM 在生成过程中自我反思的范式:

1. 检索:根据问题检索相关文档2. 评估:判断检索到的文档是否与问题相关   - 相关 → 基于文档生成答案   - 不相关 → 拒绝回答或请求补充信息3. 事实核查:检查生成的答案是否基于检索到的文档4. 最终输出:如果有冲突,修正或标注不确定性

Spring AI 中可以通过自定义 Advisor 实现 Self-RAG 的核心逻辑。


11. Spring AI 的 RAG 生产化实践

11.1 完整的 RAG 管线

一个生产级的 RAG 系统通常包含以下步骤:

// 索引管线(IndexingPipeline)@Configurationpublic class IndexingPipelineConfig {    private final EmbeddingModel embeddingModel;    private final VectorStore vectorStore;    @Bean    public CommandLineRunner indexDocuments(            @Value("${documents.path}"String docPath) {        return args -> {            // 1. 扫描文档            List<File> files = scanDocuments(docPath);            for (File file : files) {                // 2. 文档加载                DocumentReader reader = createReader(file);                List<Document> documents = reader.read();                // 3. 文档分割                DocumentTransformer splitter = TokenTextSplitter.builder()                    .defaultMaxTokenSize(500)                    .minOverlap(50)                    .build();                List<Document> chunks = splitter.apply(documents);                // 4. 向量化并存储                vectorStore.add(chunks);                log.info("Indexed: {} ({} chunks)", file.getName(), chunks.size());            }        };    }    private DocumentReader createReader(File file) {        String name = file.getName().toLowerCase();        Resource resource = new FileSystemResource(file);        if (name.endsWith(".pdf")) return new PdfDocumentReader(resource);        if (name.endsWith(".md"))  return new MarkdownDocumentReader(resource);        if (name.endsWith(".csv")) return new CsvDocumentReader(resource);        // 扩展更多格式        throw new UnsupportedOperationException("Unsupported format: " + name);    }}

11.2 增量索引

生产环境中,文档会持续更新。全量重建索引成本太高,需要增量索引策略:

@Servicepublic class IncrementalIndexingService {    private final VectorStore vectorStore;    private final DocumentIdStrategy idStrategy;    // 文档更新时调用    public void updateDocument(String docId, String newContent) {        // 1. 删除旧索引        vectorStore.delete(List.of(docId));        // 2. 重新分割和索引        Document doc = Document.builder()            .id(docId)            .content(newContent)            .metadata(Map.of("updated_at"Instant.now().toString()))            .build();        List<Document> chunks = splitter.apply(List.of(doc));        vectorStore.add(chunks);    }    // 监听文件变化(适用于本地文件)    @EventListener    public void onFileChange(FileChangeEvent event) {        updateDocument(event.getFileId(), event.getContent());    }}

11.3 缓存策略

RAG 系统中,相同或相似的问题往往会被反复问到。缓存可以有效降低延迟和成本:

@Configurationpublic class RagCacheConfig {    @Bean    public CacheManager ragCacheManager() {        // 使用 Caffeine 作为本地缓存        CaffeineCacheManager cacheManager = new CaffeineCacheManager("rag-cache");        cacheManager.setCaffeine(Caffeine.newBuilder()            .maximumSize(1000)            .expireAfterWrite(1TimeUnit.HOURS)            .recordStats());        return cacheManager;    }    @Bean    public ChatClient cachedChatClient(ChatClient.Builder builder) {        return builder            .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))            .build();    }}@Servicepublic class CachedRagService {    @Cacheable(value = "rag-cache", key = "#question")    public String ask(String question) {        return chatClient.prompt()            .user(question)            .call()            .content();    }}

11.4 异步索引和批量处理

@Servicepublic class AsyncIndexingService {    private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();    @Async    public CompletableFuture<VoidindexDocumentAsync(MultipartFile file) {        return CompletableFuture.runAsync(() -> {            // 异步处理文档索引            indexDocument(file);        }, executor);    }    // 批量索引    public void batchIndex(List<File> files) {        List<CompletableFuture<Void>> futures = files.stream()            .map(file -> CompletableFuture.runAsync(() -> indexDocument(file), executor))            .toList();        // 等待所有完成        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))            .join();    }}

11.5 API 设计

@RestController@RequestMapping("/api/rag")public class RagController {    private final RagService ragService;    // 标准问答    @PostMapping("/ask")    public ResponseEntity<RagResponseask(@RequestBody @Valid RagRequest request) {        RagResponse response = ragService.ask(request);        return ResponseEntity.ok(response);    }    // 流式问答    @PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)    public Flux<ServerSentEvent<String>> askStream(@RequestBody RagRequest request) {        return ragService.askStream(request)            .map(content -> ServerSentEvent.<String>builder()                .data(content)                .build());    }    // 带引用来源的问答    @PostMapping("/ask/cited")    public ResponseEntity<CitedResponseaskWithCitations(            @RequestBody @Valid RagRequest request) {        CitedResponse response = ragService.askWithCitations(request);        return ResponseEntity.ok(response);    }    // 文档上传和索引    @PostMapping("/documents/upload")    public ResponseEntity<VoiduploadDocument(            @RequestParam("file"MultipartFile file) {        ragService.indexDocument(file);        return ResponseEntity.accepted().build();    }}public record RagRequest(    @NotBlank String question,    @Min(1) @Max(20Integer topK,    Double similarityThreshold,    String filterExpression) {}public record RagResponse(    String answer,    long processingTimeMs) {}public record CitedResponse(    String answer,    List<Source> sources,    long processingTimeMs) {    public record Source(String title, String content, double score) {}}

12. 评估与持续优化

12.1 RAG 评估框架

评估 RAG 系统需要从两个维度进行:

检索质量

  • 命中率(Hit Rate)
    : 检索结果中是否包含正确答案
  • 平均倒数排名(MRR)
    : 正确答案在结果中的位置
  • NDCG(归一化折损累计增益)
    : 排序质量的综合指标

生成质量

  • 忠实度(Faithfulness)
    : 答案是否基于检索到的上下文,而非幻觉
  • 答案相关性(Answer Relevance)
    : 答案是否回答了问题
  • 上下文精度(Context Precision)
    : 检索结果中的噪音比例

Spring AI 本身不内置评估工具,但可以集成 Spring Boot Actuator 和应用监控:

@Componentpublic class RagMetricsCollector {    private final MeterRegistry meterRegistry;    public void recordQuery(String question, RagResponse response) {        // 记录查询延迟        meterRegistry.timer("rag.query.latency")            .record(response.processingTimeMs(), TimeUnit.MILLISECONDS);        // 记录检索结果数量        meterRegistry.gauge("rag.retrieval.topk", response.sources().size());        // 记录平均相似度分数        double avgScore = response.sources().stream()            .mapToDouble(Source::score)            .average()            .orElse(0.0);        meterRegistry.gauge("rag.retrieval.avg_score", avgScore);    }}

12.2 RAGAS 评估框架集成

RAGAS(Retrieval Augmented Generation Assessment)是专门评估 RAG 系统的框架。虽然它不是 Spring AI 原生组件,但可以作为独立服务集成。

12.3 常见的 RAG 失败模式

失败模式              表现形式                    解决方案───────              ────────                    ────────检索失败              返回了完全不相关的文档        调整分割策略、embedding模型、检索参数上下文窗口溢出          Token 使用超过限制          减小 Top-K、压缩上下文幻觉                  答案包含检索结果中没有的信息   优化 Prompt、增加 faithfulness 检查引用错误               标注了错误的来源              加强引用解析逻辑更新延迟               文档已修改但索引未更新         实现增量索引机制冷启动问题              新知识库没有高质量检索结果     预置种子数据,逐步优化嵌入

13. 总结与展望

13.1 RAG 的核心原则

回顾全文,RAG 成功的关键可以归结为几点:

  1. 数据质量决定了 RAG 的天花板
     — 花最多的精力在数据清洗、分割和元数据标注上
  2. 检索策略比模型选择更重要
     — 一个中等水平的 LLM + 优秀的检索机制,远好于一个顶级 LLM + 糟糕的检索
  3. 评估驱动优化
     — 不测量就无法改进。建立 RAG 评估指标(RAGAS)是上线前最重要的工作
  4. RAG + Fine-tuning 是最佳组合
     — 两者解决不同层次的问题,合力才能达到最佳效果
  5. 监控和反馈闭环
     — 生产环境中的 RAG 需要持续的监控、日志分析和用户反馈循环来不断提升

13.2 Spring AI 的优势

Spring AI 在 RAG 领域的独特价值在于:

  • 完整的抽象层
     — 切换向量数据库、Embedding 模型、LLM 提供商时只需修改配置
  • 与 Spring 生态深度融合
     — 事务管理、缓存、异步处理、监控、安全等开箱即用
  • 模块化设计
     — Advisor 机制允许灵活组合 RAG 策略
  • 企业级特性
     — 生产化部署所需的一切都已经在 Spring 生态中

13.3 未来趋势

RAG 技术仍在快速演进,值得关注的方向包括:

  • Agentic RAG
     — RAG 系统从被动检索转变为主动推理,如自动判断是否需要补充检索
  • Graph RAG
     — 利用知识图谱的结构化关系增强检索的相关性和可解释性
  • 多模态 RAG
     — 不仅检索文本,还能检索图像、表格、音频等多种模态
  • RAG + CAG
     — 当上下文窗口足够大时,Cache-Augmented Generation 将部分高频的 RAG 查询转化为预加载的缓存
  • 长期记忆
     — RAG 从"一次性检索"转向具有记忆能力的持续学习系统

13.4 写在最后

RAG 不是银弹,它有自己的局限和适用边界。但它确实解决了 LLM 落地中最关键的问题——让模型能够引用外部知识来回答问题。在 Spring AI 的加持下,构建一个生产级的 RAG 系统已经从一件复杂的工作变成了一个标准化的工程实践。

关键在于理解每一层抽象背后的原理,知道何时使用默认配置、何时需要自定义扩展。希望本文能帮助你构建出真正可用的 RAG 系统。


本文基于 Spring AI 1.0+ 版本编写。框架版本迭代可能带来 API 变化,请以官方文档为准。