本文系统讲解 RAG(检索增强生成)的核心原理、三种架构模式、全流程优化策略,并基于 Spring AI Alibaba 给出完整代码实现。无论你是刚接触 RAG 的开发者,还是正在优化线上 RAG 系统的工程师,都能从中找到实用的参考。


一、什么是 RAG?

1.1 一句话定义

RAG(Retrieval-Augmented Generation,检索增强生成)是一种先查资料、再回答问题的技术范式——在 LLM 生成回答之前,先从外部知识库中检索出与问题相关的文档片段,将这些片段作为上下文注入 Prompt,让模型的回答有据可依。

用考试来类比:

  • 不用 RAG = 闭卷考试,凭记忆作答,可能答错或编造
  • 用 RAG = 开卷考试,先翻参考资料再作答,答案有据可查

1.2 RAG 的核心价值

RAG 解决了 LLM 的三大痛点:

痛点一:幻觉(Hallucination)

LLM 在不确定时会"自信地编造"答案。比如你问"公司差旅报销标准是多少",模型可能凭训练数据中的通用信息给出一个完全不靠谱的数字。RAG 通过注入真实文档内容,让模型基于事实回答,大幅降低幻觉率。

痛点二:知识时效性

模型的训练数据有截止日期,无法回答训练之后的新信息。比如"2026年最新的员工病假政策是什么",模型的知识库中根本没有。RAG 让模型可以实时查询最新文档,回答永远基于最新信息。

痛点三:领域专业性

通用模型对企业内部知识(规章制度、产品文档、技术规范)了解有限。RAG 允许接入私有知识库,让模型输出专家级的精准回答。

1.3 RAG 的四大核心步骤

一个完整的 RAG 流程分为离线和在线两个阶段:

离线阶段(文档摄取 / ETL):

原始文档 → 文档切割(Chunking)→ 向量编码(Embedding)→ 写入向量数据库

在线阶段(检索增强生成):

用户提问 → 向量检索相似文档 → 上下文注入 Prompt → LLM 生成回答

拆开来看,四大步骤分别是:

  1. 文档切割:将海量文档转化为易检索的知识碎片。就像把厚重词典拆解成单词卡片,采用智能分块算法保持语义连贯性,给每个知识碎片打标签。优质的知识切割如同图书馆分类系统,决定了后续检索效率。

  2. 向量编码:用 Embedding 模型将文字转化为高维数学向量,使语义相近的内容产生相似的数学特征。比如"续航时间"和"电池容量"会被编码为相似向量,存入专用的向量数据库并建立快速检索索引。

  3. 相似检索:将用户问题同样转化为向量,在向量数据库中通过相似度算法(如余弦相似度)找到最相关的文档片段。

  4. 生成增强:将检索到的文档片段作为上下文,与用户问题一起注入 Prompt,由 LLM 基于这些事实生成精准回答。

1.4 RAG 的现状

2024-2025 年,RAG 几乎是企业 AI 落地的标配方案。但进入 2026 年,随着 LLM 上下文窗口突破 1M Token、Skill/MCP 等新范式兴起,RAG 的适用边界正在被重新审视。一个值得关注的数字:2025 年一篇 arXiv 论文对政府文档 QA 任务的误差分析发现,48% 的 RAG 错误答案根源不在模型,而在检索阶段就漏掉了关键证据

这意味着 RAG 仍然有效,但需要更精细的优化。Spring AI Alibaba 正是在这个背景下,提供了从基础到高级的完整 RAG 解决方案。


二、为什么会有 RAG?替代方案与场景选择

2.1 RAG 诞生的背景

RAG 诞生的直接原因是早期 LLM 的两个核心局限:

局限一:上下文窗口太小

  • GPT-3 时代:4K tokens
  • 早期 GPT-4:8K tokens
  • 问题:无法容纳完整的知识库,必须先筛选再输入

局限二:知识截止与幻觉

  • 模型训练数据有明确截止日期
  • 无法获取实时信息
  • 容易产生事实性幻觉

RAG 的核心逻辑:先用向量检索从大规模知识库中筛选出最相关的片段,再将这些片段作为上下文喂给 LLM

2.2 RAG 的替代方案

2026 年,让 LLM 获取外部能力的技术路线已经不止 RAG 一条。以下是主流方案的对比:

方案 本质 解决的问题 适用场景 成本
RAG 运行时检索外部知识 事实性问答、知识查询 文档问答、政策查询、FAQ 低(无需训练)
Skill 给 AI 的标准化"操作手册" 过程性知识(Know-How) 代码迁移、部署流程、审查流程 极低(Markdown 文件)
MCP AI 与外部系统的标准连接协议 实时数据获取、系统操作 查询实时数据、调用 API
微调(Fine-tuning) 把知识烧入模型参数 学习特定风格、行业术语 风格迁移、术语适配 高(需要 GPU)
长上下文 / Wiki 模式 直接全量加载到上下文窗口 小规模知识库 知识库 < 100K Token
GraphRAG 构建知识图谱 + 图遍历检索 多跳推理、实体关系问答 跨文档关联推理

2.3 深入理解:Skill vs RAG

Skill 是 2025 年底由 Anthropic 发起、2026 年被 27+ 主流 AI 工具采纳的开放标准。它的核心产物是一个 SKILL.md 文件——给 AI 看的"操作手册"。

关键区别在于知识类型:

知识类型 含义 典型例子 最佳载体
陈述性知识(Know-What) 事实、数据、规则 “公司差旅报销标准是 800 元/天” RAG / 知识库
过程性知识(Know-How) 操作步骤、工作流 “如何从零部署一个 K8s 集群” Skill
连接性知识(Know-Where) 系统在哪、API 怎么调 “ERP 系统订单接口是 /api/v2/orders” MCP

Skill 封装的是"怎么做",RAG 存储的是"是什么"。 两者不是替代关系,而是分工关系。

2.4 Wiki 模式:小知识库的最优解

2026 年 4 月,OpenAI 联合创始人 Andrej Karpathy 公开分享了自己用一组 Markdown 文件替代整个 RAG 管线的做法——没有向量数据库、没有 Embedding、没有分片。他的个人研究 Wiki 长到了 100 篇文章、40 万字,效果远超 RAG。

核心逻辑:2026 年主流模型的上下文窗口已达 1M Token,如果你的知识库在 100K~1M Token 之间(约 200~1500 页),直接全量加载到上下文即可,召回率 85% 以上。这时候上 RAG,等于给自己制造一个 48% 概率出错的检索层。

2.5 选型决策

你的场景 推荐方案 理由
文档问答、政策查询、FAQ RAG 最成熟、Spring AI 原生支持
代码迁移、部署流程、审查流程 Skill 过程性知识用操作手册更合适
实时数据查询、系统操作 MCP 需要连接外部系统
小知识库(< 100K Token) 长上下文 / Wiki 直接塞进上下文,无需检索
跨文档多跳推理 GraphRAG VectorRAG 天然做不到跨片段关联
通用生产环境 RAG + Skill + MCP 组合 知识 + 能力 + 连接 互补

工程判断:大多数团队的第一选择应该是 RAG + BM25 混合检索。GraphRAG 只在业务确实需要多跳推理时才值得投入——构建和维护成本是 VectorRAG 的 5-10 倍。


三、RAG 全流程实现与优化——基于 Spring AI Alibaba

Spring AI Alibaba 提供了完整的 RAG 解决方案,以 ReactAgent 为核心,支持三种架构模式,并内置了从查询转换到文档后处理的全链路优化组件。

3.1 三种 RAG 架构模式

模式一:两步 RAG(Two-Step RAG)

核心特点:检索操作总是在生成之前执行,流程可预测。

User Question

Retrieve Relevant
Documents

Generate Answer

Return Answer
to User

维度 表现
控制性
灵活性
延迟 快且可预测
典型场景 FAQ 系统、文档机器人

Spring AI Alibaba 提供了 ReactAgent 配合三种钩子来实现两步 RAG:AgentHook(启动时检索一次,最省时)、MessagesModelHook(每次模型调用前检索)、ModelInterceptor(提供完整请求信息访问能力)。

实现方式一:AgentHook(推荐,最省时)

AgentHook 在 Agent 启动时仅执行一次检索,整个推理过程中复用检索结果,避免重复检索带来的性能开销:

// 自定义 AgentHook:在 Agent 启动时执行一次检索
class KnowledgeBaseHook extends AgentHook {

    private final VectorStore vectorStore;
    private final int topK;

    public KnowledgeBaseHook(VectorStore vectorStore, int topK) {
        this.vectorStore = vectorStore;
        this.topK = topK;
    }

    @Override
    public String getName() {
        return "knowledge_base_hook";
    }

    @Override
    public AgentCommand beforeAgent(OverAllState state, RunnableConfig config) {
        // 从 state 中获取用户最新问题
        List<Message> messages = state.value("messages", new ArrayList<>());
        String userQuery = extractLatestUserQuery(messages);

        if (userQuery == null || userQuery.isEmpty()) {
            return new AgentCommand(state);
        }

        // 执行向量检索(仅一次)
        List<Document> docs = vectorStore.similaritySearch(
            SearchRequest.builder().query(userQuery).topK(topK).build()
        );

        // 将检索结果拼接到系统消息中
        String context = docs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));
        String systemPrompt = "你是企业知识助手。请基于以下参考资料回答用户问题:\n\n" + context;

        // 返回增强后的 state
        state.update("systemMessage", systemPrompt);
        return new AgentCommand(state);
    }
}

// 使用
ReactAgent agent = ReactAgent.builder()
    .name("two_step_rag_agent")
    .model(chatModel)
    .instruction("你是企业知识助手,请基于参考资料回答用户问题")
    .hooks(new KnowledgeBaseHook(vectorStore, 5))  // 启动时检索一次
    .build();

agent.invoke("公司的差旅报销标准是什么?");

实现方式二:MessagesModelHook(每次模型调用前检索)

MessagesModelHook 在每次模型调用前执行检索。适合需要在多轮推理中持续获取最新上下文的场景:

// 每次模型调用前都执行检索
@HookPositions({HookPosition.BEFORE_MODEL})
class PerModelRetrievalHook extends MessagesModelHook {

    private final VectorStore vectorStore;
    private final int topK;

    public PerModelRetrievalHook(VectorStore vectorStore, int topK) {
        this.vectorStore = vectorStore;
        this.topK = topK;
    }

    @Override
    public String getName() {
        return "per_model_retrieval_hook";
    }

    @Override
    public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
        // 提取最新用户问题
        String userQuery = extractLatestUserQuery(previousMessages);
        if (userQuery == null || userQuery.isEmpty()) {
            return new AgentCommand(previousMessages);
        }

        // 检索相关文档
        List<Document> docs = vectorStore.similaritySearch(
            SearchRequest.builder().query(userQuery).topK(topK).build()
        );
        String context = docs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));

        // 在用户消息前注入检索到的上下文
        Message contextMessage = new SystemMessage("参考资料:\n" + context);
        List<Message> newMessages = new ArrayList<>();
        newMessages.add(contextMessage);
        newMessages.addAll(previousMessages);
        return new AgentCommand(newMessages);
    }
}

// 使用
ReactAgent agent = ReactAgent.builder()
    .name("per_model_rag_agent")
    .model(chatModel)
    .instruction("你是企业知识助手")
    .hooks(new PerModelRetrievalHook(vectorStore, 5))  // 每次模型调用前检索
    .build();

实现方式三:ModelInterceptor(完整请求信息访问)

ModelInterceptor 提供对完整 ModelRequest 的访问能力,可以修改请求参数、注入上下文、做异常重试等:

// 自定义 ModelInterceptor:在模型调用前注入检索结果
class RAGContextInterceptor extends ModelInterceptor {

    private final VectorStore vectorStore;
    private final int topK;

    public RAGContextInterceptor(VectorStore vectorStore, int topK) {
        this.vectorStore = vectorStore;
        this.topK = topK;
    }

    @Override
    public String getName() {
        return "rag_context_interceptor";
    }

    @Override
    public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
        // 从请求中获取最新消息
        String userQuery = extractUserQueryFromRequest(request);
        if (userQuery == null || userQuery.isEmpty()) {
            return handler.call(request);  // 无问题,直接放行
        }

        // 检索相关文档
        List<Document> docs = vectorStore.similaritySearch(
            SearchRequest.builder().query(userQuery).topK(topK).build()
        );
        String context = docs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));

        // 构造增强后的请求:在系统消息后注入检索上下文
        ModelRequest enhancedRequest = augmentRequestWithContext(request, context);

        // 调用模型
        return handler.call(enhancedRequest);
    }
}

// 使用
ReactAgent agent = ReactAgent.builder()
    .name("interceptor_rag_agent")
    .model(chatModel)
    .instruction("你是企业知识助手")
    .interceptors(new RAGContextInterceptor(vectorStore, 5))  // 使用拦截器
    .build();

选择建议

钩子类型 触发时机 性能 适用场景
AgentHook Agent 启动时一次 最优 查询在 Agent 推理过程中保持不变(绝大多数场景)
MessagesModelHook 每次模型调用前 中等 需要根据推理结果动态调整检索内容
ModelInterceptor 每次模型调用前 中等 需要访问完整 ModelRequest 做更复杂的增强
模式二:Agentic RAG

核心特点:由 LLM 驱动的 Agent 自主决定何时、何地以及如何执行检索。

Yes

No

No

Yes

User Input / Question

Agent LLM

Need external info?

Search using tool s

Enough to answer?

Generate final answer

Return to user

维度 表现
控制性
灵活性
延迟 可变
典型场景 多工具研究助手、复杂推理任务

实现方式是为 ReactAgent 注册多个 ToolCallback(文档搜索、网络搜索、数据库查询等),Agent 根据推理结果自主决定调用哪个工具:

// 1. 文档检索工具
class DocumentSearchTool {
    public Response search(Request request) {
        // 从向量存储检索相关文档
        List<Document> docs = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(request.query())
                .topK(5)
                .similarityThreshold(0.6)
                .build()
        );
        // 合并文档内容作为返回结果
        String combinedContent = docs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));
        return new Response(combinedContent);
    }

    public record Request(String query) { }
    public record Response(String content) { }
}

// 2. 网页搜索工具
class WebSearchTool {
    public Response search(Request request) {
        // 实际实现中调用百度/Bing API
        // 这里简化为占位实现
        return new Response("从网络搜索到的信息: " + request.query());
    }

    public record Request(String query) { }
    public record Response(String content) { }
}

// 3. 数据库查询工具
class DatabaseQueryTool {
    public Response query(Request request) {
        // 实际实现中查询内部业务数据库
        return new Response("从数据库查询到的信息: " + request.query());
    }

    public record Request(String query) { }
    public record Response(String content) { }
}

// 构造工具实例
DocumentSearchTool docSearchTool = new DocumentSearchTool();
WebSearchTool webSearchTool = new WebSearchTool();
DatabaseQueryTool dbQueryTool = new DatabaseQueryTool();

// 4. 将工具封装为 ToolCallback
ToolCallback documentSearchCallback = FunctionToolCallback
    .builder("document_search",
        (Function<DocumentSearchTool.Request, DocumentSearchTool.Response>)
            req -> docSearchTool.search(req))
    .description("搜索内部文档库,获取与查询相关的文档片段。适用于查询公司政策、规章制度、产品文档等私有知识。")
    .inputType(DocumentSearchTool.Request.class)
    .build();

ToolCallback webSearchCallback = FunctionToolCallback
    .builder("web_search",
        (Function<WebSearchTool.Request, WebSearchTool.Response>)
            req -> webSearchTool.search(req))
    .description("搜索互联网获取最新信息。适用于查询新闻、实时事件、公开技术资料等。")
    .inputType(WebSearchTool.Request.class)
    .build();

ToolCallback databaseQueryCallback = FunctionToolCallback
    .builder("database_query",
        (Function<DatabaseQueryTool.Request, DatabaseQueryTool.Response>)
            req -> dbQueryTool.query(req))
    .description("查询内部业务数据库。适用于查询订单、库存、用户信息等结构化数据。")
    .inputType(DatabaseQueryTool.Request.class)
    .build();

// 5. 创建带多工具的 ReactAgent
ReactAgent multiSourceAgent = ReactAgent.builder()
    .name("multi_source_rag_agent")
    .model(chatModel)
    .instruction("你可以访问多个信息源,根据用户问题选择最合适的工具组合:\n" +
        "1. document_search - 查询公司内部文档(政策、规章、产品文档)\n" +
        "2. web_search - 查询最新互联网信息(新闻、公开资料)\n" +
        "3. database_query - 查询内部业务数据(订单、库存、用户)\n" +
        "如果第一次检索结果不充分,可以继续检索其他源。")
    .tools(documentSearchCallback, webSearchCallback, databaseQueryCallback)
    .build();

// 6. 调用 Agent
multiSourceAgent.invoke("比较我们产品文档中的功能升级和最近一个月市场上的竞品动态");

核心优势

  • 高度灵活:Agent 根据推理结果自主决定检索策略,可多轮检索
  • 多源融合:可同时接入文档库、网络搜索、内部数据库等
  • 可解释:工具调用过程透明,便于调试
模式三:混合 RAG(Hybrid RAG)

核心特点:结合两步 RAG 的可预测性和 Agentic RAG 的灵活性,引入质量验证机制。

不充分

充分

不通过

通过

用户问题

查询增强
重写/变体/扩展

检索

检索验证
结果是否充分?

调整查询

生成答案

答案验证
是否准确完整?

重新检索/调整

返回答案

维度 表现
控制性
灵活性
延迟 可变
典型场景 带质量验证的领域特定问答系统
// 假设你已经有一个配置好的向量存储和 ChatModel

VectorStore vectorStore = ...;

ChatModel chatModel = ...;

  

// ========== 1. 多工具检索(来自 Agentic RAG) ==========

  

// 文档搜索工具

class DocumentSearchTool {

  private final VectorStore vectorStore;

  public DocumentSearchTool(VectorStore vectorStore) {

      this.vectorStore = vectorStore;

  }

  public record Request(String query) {}

  public record Response(String content) {}

  public Response search(Request request) {

      List<Document> docs = vectorStore.similaritySearch(

          org.springframework.ai.vectorstore.SearchRequest.builder()

              .query(request.query())

              .topK(5)

              .build()

      );

      String content = docs.stream()

          .map(Document::getText)

          .collect(Collectors.joining("

  

"));

      return new Response(content);

  }

}

  

// 网络搜索工具(示例)

class WebSearchTool {

  public record Request(String query) {}

  public record Response(String content) {}

  public Response search(Request request) {

      // 实际实现中调用网络搜索 API

      return new Response("网络搜索结果: " + request.query());

  }

}

  

DocumentSearchTool docSearchTool = new DocumentSearchTool(vectorStore);

WebSearchTool webSearchTool = new WebSearchTool();

  

ToolCallback documentSearchCallback = FunctionToolCallback.builder("document_search",

  (Function<DocumentSearchTool.Request, DocumentSearchTool.Response>)

  req -> docSearchTool.search(req))

  .description("从文档库中搜索相关信息")

  .inputType(DocumentSearchTool.Request.class)

  .build();

  

ToolCallback webSearchCallback = FunctionToolCallback.builder("web_search",

  (Function<WebSearchTool.Request, WebSearchTool.Response>)

  req -> webSearchTool.search(req))

  .description("从互联网搜索最新信息")

  .inputType(WebSearchTool.Request.class)

  .build();

  

// ========== 2. 查询增强 Hook(来自两步 RAG) ==========

  

@HookPositions({HookPosition.BEFORE_AGENT})

class QueryEnhancementHook extends AgentHook {

  private final ChatModel chatModel;

  private static final String ENHANCED_QUERY_KEY = "enhanced_query";

  public QueryEnhancementHook(ChatModel chatModel) {

      this.chatModel = chatModel;

  }

  @Override

  public String getName() {

      return "query_enhancement";

  }

  @Override

  public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {

      // 从状态中提取用户查询

      Optional<Object> messagesOpt = state.value("messages");

      if (messagesOpt.isEmpty()) {

          return CompletableFuture.completedFuture(Map.of());

      }

      @SuppressWarnings("unchecked")

      List<Message> messages = (List<Message>) messagesOpt.get();

      // 提取最后一个用户消息作为查询

      String userQuery = messages.stream()

          .filter(msg -> msg instanceof UserMessage)

          .map(msg -> ((UserMessage) msg).getText())

          .reduce((first, second) -> second) // 获取最后一个

          .orElse("");

      if (userQuery.isEmpty()) {

          return CompletableFuture.completedFuture(Map.of());

      }

      // 使用 LLM 增强查询(只执行一次,在整个 Agent 执行过程中)

      // 简化示例:实际可以使用 RewriteQueryTransformer

      String enhancedQuery = enhanceQuery(userQuery);

      // 如果查询被增强,更新消息列表

      if (!enhancedQuery.equals(userQuery)) {

          List<Message> enhancedMessages = new ArrayList<>();

          // 保留系统消息和其他消息,只替换用户消息

          for (Message msg : messages) {

              if (msg instanceof UserMessage) {

                  enhancedMessages.add(new UserMessage(enhancedQuery));

              } else {

                  enhancedMessages.add(msg);

              }

          }

          // 将增强后的查询存储到 metadata 中,供后续使用

          config.metadata().ifPresent(meta -> {

              meta.put(ENHANCED_QUERY_KEY, enhancedQuery);

          });

          // 返回更新后的消息列表

          return CompletableFuture.completedFuture(Map.of("messages", enhancedMessages));

      }

      return CompletableFuture.completedFuture(Map.of());

  }

  private String enhanceQuery(String query) {

      // 简化示例:实际可以使用 RewriteQueryTransformer 或调用 LLM 进行查询重写

      // 这里只是示例,实际应该调用 LLM 增强查询

      // 例如:使用 RewriteQueryTransformer.builder().chatClientBuilder(...).build().transform(query)

      return query; // 实际实现中会调用 LLM 增强查询

  }

}

  

// ========== 3. 答案验证 Interceptor(来自两步 RAG) ==========

  

class AnswerValidationInterceptor extends ModelInterceptor {

  private final ChatModel chatModel;

  private static final double MIN_CONFIDENCE = 0.7;

  public AnswerValidationInterceptor(ChatModel chatModel) {

      this.chatModel = chatModel;

  }

  @Override

  public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {

      // 先调用模型生成答案

      ModelResponse response = handler.call(request);

      // 验证答案质量(简化示例)

      AssistantMessage answer = response.getResult().getOutput();

      boolean isValid = validateAnswer(answer.getText(), request);

      if (!isValid) {

          // 如果答案质量不足,可以添加提示要求重新生成

          SystemMessage validationPrompt = new SystemMessage(

              "请重新检查你的答案,确保基于提供的上下文信息,并且准确完整。"

          );

          ModelRequest retryRequest = ModelRequest.builder(request)

              .systemMessage(validationPrompt)

              .build();

          // 可以选择重试或返回当前答案

          return handler.call(retryRequest);

      }

      return response;

  }

  private boolean validateAnswer(String answer, ModelRequest request) {

      // 简化示例:实际可以使用 LLM 验证答案与上下文的一致性

      // 检查答案长度、是否包含关键信息等

      return answer != null && answer.length() > 20; // 简单验证

  }

  @Override

  public String getName() {

      return "answer_validation";

  }

}

  

// ========== 4. 创建混合 RAG Agent ==========

  

ReactAgent hybridRAGAgent = ReactAgent.builder()

  .name("hybrid_rag_agent")

  .model(chatModel)

  .instruction("""

      你是一个智能助手,可以访问多个信息源来回答问题。

      使用工具时:

      1. 优先使用 document_search 搜索文档库

      2. 如果需要最新信息,使用 web_search

      3. 基于检索到的信息生成准确、完整的答案

      4. 如果信息不足,可以多次调用工具

      """)

  .tools(documentSearchCallback, webSearchCallback)

  .hooks(new QueryEnhancementHook(chatModel))

  .interceptors(new AnswerValidationInterceptor(chatModel))

  .build();

  

// ========== 5. 使用混合 RAG Agent ==========

  

AssistantMessage response = hybridRAGAgent.call("Spring AI Alibaba支持哪些向量数据库?");

System.out.println("答案: " + response.getText());

3.2 基础 RAG 实现:从 ETL 到查询

在深入优化之前,先用 Spring AI Alibaba 搭建一个完整的 RAG 基础流程。

第一步:文档加载(Extract)

Spring AI 提供了多种 DocumentReader,支持 PDF、Markdown、JSON、HTML、纯文本等格式:

// 加载 PDF 文档(按页读取)

PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(

    new FileSystemResource("path/to/document.pdf"),        // PDF 文件资源

    PagePdfDocumentReaderConfig.builder()

        // 配置 PDF 文本提取时的格式选项

        .withPageExtractedTextFormatter(new ExtractedTextFormatter.Builder()

            .withNumberOfBottomTextLinesToDelete(0)        // 提取时跳过每页底部 N 行(去除页脚/页码)

            .withNumberOfTopPagesToSkipBeforeRead(0)      // 从第 N 页之后开始读取(跳过封面/目录)

            .build())

        .withPagesPerDocument(1)                           // 每 N 页打包为一个 Document 对象(1 = 每页一个)

        .build()

);

List<Document> documents = pdfReader.get();                // 执行读取,返回 Document 列表

  

// 加载 Markdown 文档(按结构分割)

MarkdownDocumentReader mdReader = new MarkdownDocumentReader(

    new FileSystemResource("path/to/document.md"),        // Markdown 文件资源

    MarkdownDocumentReaderConfig.builder()

        .withHorizontalRuleCreateDocument(true)           // 遇到 Markdown 水平规则(---)时拆分为新 Document

        .withIncludeCodeBlock(false)                      // 代码块(```)是否与周围段落合并(false = 独立成块)

        .withIncludeBlockquote(false)                     // 引用块(>)是否与周围段落合并(false = 独立成块)

        .withAdditionalMetadata("filename", "document.md")// 为生成的每个 Document 添加自定义元数据,便于后续过滤

        .build()

);

List<Document> mdDocuments = mdReader.get();               // 执行读取,返回 Document 列表

  

// 加载纯文本文件

TextReader textReader = new TextReader(new FileSystemResource("path/to/document.txt"));

// 为文档注入自定义元数据(key-value),后续可在向量检索中作为过滤条件

textReader.getCustomMetadata().put("filename", "document.txt");

textReader.getCustomMetadata().put("category", "policy");

List<Document> textDocuments = textReader.get();           // 读取文本内容,返回单个 Document

  

// 加载 JSON 文件(将 JSON 字段映射为 Document)

JsonReader jsonReader = new JsonReader(

    new FileSystemResource("path/to/data.json"),

    "description",                                        // 第一个参数:作为 metadata 的 JSON 字段名

    "content"                                             // 第二个参数:作为 Document 内容(text)的 JSON 字段名

);
List<Document> jsonDocuments = jsonReader.get();          // 逐条 JSON 记录生成 Document

常用 DocumentReader 类型一览

Reader 适用格式 主要配置项
TextReader 纯文本 自定义元数据
MarkdownDocumentReader Markdown 水平规则、代码块、引用块处理
PagePdfDocumentReader PDF(按页) 每页 Document 数、跳过页
ParagraphPdfDocumentReader PDF(按段落) 段落分割规则
JsonReader JSON 字段映射、JSON Pointer
HtmlReader HTML 标签过滤、CSS 选择器
TikaDocumentReader 多格式(DOCX/PPTX/HTML) 基于 Apache Tika
第二步:文本分割(Transform)

将大文档切分为适合检索的文本块,Spring AI Alibaba 提供了多种分割器:

// 方式一:基于 Token 的分割(适合英文)
TokenTextSplitter tokenSplitter = new TokenTextSplitter(
    800,    // chunkSize:每个块的目标 Token 数
    200,    // chunkOverlap:相邻块重叠的 Token 数,防止关键信息被截断
    100,    // minChunkSize:最小块大小,小于此值的块会被丢弃
    10000,  // maxChunkSize:最大块字符数(安全上限)
    true    // keepSeparators:是否保留分隔符
);
List<Document> tokenChunks = tokenSplitter.apply(documents);

// 方式二:递归字符分割(推荐中文场景)
// 分隔符优先级从高到低:段落 > 换行 > 中文句号 > 中文逗号 > 字符
List<String> chineseSeparators = Arrays.asList(
    "\n\n", "\n", "。", "!", "?", ";", ",",  // 中文分隔符
    ". ", "! ", "? ",                          // 英文分隔符
    " ",                                       // 空格
    ""                                         // 最终降级:逐字符
);
RecursiveCharacterTextSplitter recursiveSplitter = new RecursiveCharacterTextSplitter(
    800,              // chunkSize:每个块的目标字符数
    150,              // chunkOverlap:相邻块重叠字符数
    chineseSeparators // 分隔符优先级列表,从高到低
);
List<Document> recursiveChunks = recursiveSplitter.apply(documents);

// 方式三:基于语言模型的句子级分割(高精度场景)
SentenceSplitter sentenceSplitter = new SentenceSplitter(
    chatModel,        // 用 LLM 识别真正的句子边界
    800,              // chunkSize
    150               // chunkOverlap
);
List<Document> sentenceChunks = sentenceSplitter.apply(documents);

分割器选型表

分割器 分割依据 适合场景 中文适配
TokenTextSplitter Token 数量 英文文档 一般(可能乱码)
RecursiveCharacterTextSplitter 多级分隔符递归 中文/混合文档 优秀
SentenceSplitter LLM 识别句子边界 高精度语义分割
自定义 TextSplitter 自定义逻辑 结构化文档(Markdown 按标题、代码按函数) 自定义

参数调优建议

参数 推荐值(中文) 说明
chunkSize 800~1000 字符 太小语义断裂,太大检索效率下降
chunkOverlap 150~250 字符 保持上下文连贯,防止关键信息被截断
分隔符优先级 段落 > 换行 > 句号 > 逗号 > 字符 优先在语义边界切分
第三步:向量化与存储(Load)
// 将文档块写入向量数据库(自动完成 Embedding + 存储)
vectorStore.write(recursiveChunks);

Spring AI Alibaba 支持多种向量数据库:Milvus、Redis、Elasticsearch、Pinecone 等。

第四步:检索与生成

基于 ReactAgent + AgentHook 的两步 RAG 实现(最省时方案):

ReactAgent agent = ReactAgent.builder()
    .name("basic_rag_agent")
    .model(chatModel)
    .instruction("你是企业知识助手,请基于检索到的参考资料回答用户问题")
    .hooks(new KnowledgeBaseHook(vectorStore, 5))
    .build();

agent.invoke("公司的差旅报销标准是什么?");

这就是一个最基础的 RAG 系统。但在生产环境中,基础 RAG 往往不够用——用户问得模糊、检索结果不精准、答案可能遗漏关键信息。这就需要全链路优化。

3.3 RAG 全链路优化

Spring AI 通过模块化的 RAG 优化组件,覆盖四个阶段:

用户提问
  → [Pre-Retrieval:查询转换/扩展]    ← 优化提问
  → [Retrieval:文档检索]              ← 优化检索
  → [Post-Retrieval:文档后处理]       ← 优化结果
  → [Generation:查询增强/上下文注入]  ← 优化生成
  → 回答
3.3.1 Pre-Retrieval(检索前优化)

检索前优化的核心思想是:用户的原始提问往往不是最好的检索 query。可能太模糊、可能包含代词、可能用词与文档不一致。优化提问质量,是提升 RAG 效果的第一步。

(1)RewriteQueryTransformer——查询重写

将过长或含代词的模糊问题,重写为具体、检索友好的 query:

RewriteQueryTransformer rewriteTransformer = RewriteQueryTransformer.builder()
    .chatModel(chatModel)
    .build();

// 使用示例:
// 原始问题:"那个怎么弄来着?"
// 重写后:"2026年最新的员工病假申请流程是什么?"
Query originalQuery = new Query("那个怎么弄来着?");
Query rewritten = rewriteTransformer.transform(originalQuery);
System.out.println(rewritten.text()); // "2026年最新的员工病假申请流程是什么?"

(2)CompressionQueryTransformer——查询压缩

将对话历史和当前问题压缩成一个独立、自包含的 query。这是处理多轮对话中代词指代的利器:

CompressionQueryTransformer compressionTransformer = CompressionQueryTransformer.builder()
    .chatModel(chatModel)
    .build();

// 使用示例:
// 历史对话:
//   用户:"碧海湾小区的位置在哪?"
//   AI:"碧海湾小区位于XX区XX路..."
// 当前问题:"那这个小区的二手房均价是多少?"
// 
// 压缩后:"碧海湾小区的二手房均价是多少?"
// ("这个小区"被替换为"碧海湾小区",检索才能命中)
List<Message> history = List.of(
    new UserMessage("碧海湾小区的位置在哪?"),
    new AssistantMessage("碧海湾小区位于XX区XX路...")
);
Query currentQuery = Query.builder()
    .text("那这个小区的二手房均价是多少?")
    .history(history)
    .build();
Query compressed = compressionTransformer.transform(currentQuery);
System.out.println(compressed.text()); // "碧海湾小区的二手房均价是多少?"

(3)TranslationQueryTransformer——查询翻译

将非目标语言的查询翻译为知识库的主要语言:

TranslationQueryTransformer translationTransformer = TranslationQueryTransformer.builder()
    .chatModel(chatModel)
    .targetLanguage("zh")  // 翻译为中文
    .build();

// 使用示例:
// 原始问题:"What is the refund policy?"
// 翻译后:"退款政策是什么?"
Query englishQuery = new Query("What is the refund policy?");
Query translated = translationTransformer.transform(englishQuery);
System.out.println(translated.text()); // "退款政策是什么?"

(4)MultiQueryExpander——多查询扩展

为同一个问题生成多个不同表述的变体,多路检索后合并结果,提升召回率:

MultiQueryExpander queryExpander = MultiQueryExpander.builder()
    .chatModel(chatModel)
    .numberOfQueries(3)  // 生成 3 个变体
    .build();

// 使用示例:
// 原始问题:"怎么请假?"
// 扩展为:
//   1. "员工请假流程是什么?"
//   2. "病假申请需要哪些材料?"
//   3. "年假如何申请?"
// 三路检索 → 合并去重 → 更全面的检索结果
Query originalQuery = new Query("怎么请假?");
ExpandedQuery expanded = queryExpander.expand(originalQuery);
List<Query> variants = expanded.queries();
variants.forEach(q -> System.out.println(q.text()));
// 输出:
// 员工请假流程是什么?
// 病假申请需要哪些材料?
// 年假如何申请?
3.3.2 Retrieval(检索优化)

检索阶段的核心组件是 VectorStoreDocumentRetriever,它负责从向量数据库中检索相关文档:

// 构造检索器
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
    .vectorStore(vectorStore)       // 向量存储实例
    .similarityThreshold(0.5)       // 相似度阈值(0~1),低于此值的结果会被过滤
    .topK(5)                        // 返回最相关的 5 个文档
    .filterExpression("type == 'policy'")  // 过滤表达式(可选)
    .build();

// 使用
Query query = new Query("差旅报销标准");
List<Document> results = retriever.retrieve(query);

检索策略对比

策略 说明 适用场景
纯向量检索 基于语义相似度 通用场景
纯关键词检索(BM25) 基于关键词精确匹配 精确查找(订单号、航班号)
混合检索 向量 + BM25 结合 生产环境推荐
3.3.3 Post-Retrieval(检索后优化)

检索回来的文档不一定都是高质量的,可能包含重复、不相关或冗余的内容。Post-Retrieval 阶段负责"精炼"检索结果。

(1)DocumentJoiner——文档合并器

将多路检索的结果合并为一个文档列表,支持去重:

// 构造文档合并器
ConcatenationDocumentJoiner joiner = new ConcatenationDocumentJoiner();

// 使用:多查询扩展后,三路检索的结果合并去重
List<Query> queries = expanded.queries();  // 来自 MultiQueryExpander
List<List<Document>> retrievedResults = queries.stream()
    .map(retriever::retrieve)
    .collect(Collectors.toList());

List<Document> merged = joiner.join(retrievedResults);

(2)文档重排序(Reranking)

用更精确的模型对检索结果重新排序,将最相关的文档排到前面:

// Spring AI Alibaba 支持 DashScope 的 Rerank 模型
// 检索阶段先宽召回(topK=20),再用 Reranker 精排到 topK=5

// 构造重排序模型(DashScope GTE-Rerank)
DocumentRanker reranker = new DashScopeDocumentRanker(
    DashScopeDocumentRankerOptions.builder()
        .withModelName("gte-rerank")  // 使用 GTE-Rerank 模型
        .withTopN(5)                  // 精排后返回前 5 个
        .build()
);

// 完整流程:粗召回 → 重排序
List<Document> coarseResults = retriever.retrieve(query);  // 假设返回 20 个
List<Document> reranked = reranker.rank(query, coarseResults);  // 精排到 5 个

(3)文档压缩

对过长的文档片段进行摘要压缩,只保留与查询相关的核心信息,减少 Token 消耗:

// 使用 LLM 压缩文档
DocumentCompressor compressor = new LLMCompressor(
    chatModel,
    LLMCompressorOptions.builder()
        .withMaxLength(200)       // 压缩后最大字符数
        .withPreserveKeywords(true) // 保留关键词
        .build()
);

List<Document> compressed = compressor.compress(query, retrievedDocs);

(4)文档去重

多路检索或查询扩展后,不同检索路径可能返回相同的文档片段,需要去重:

// 基于内容相似度去重
DocumentDeduplicator deduplicator = new DocumentDeduplicator(
    DocumentDeduplicatorOptions.builder()
        .withSimilarityThreshold(0.9)  // 相似度大于 0.9 视为重复
        .build()
);
List<Document> deduplicated = deduplicator.deduplicate(retrievedDocs);
3.3.4 Generation(生成优化)

生成阶段的优化主要是如何将检索到的上下文更好地注入 Prompt。

ContextualQueryAugmenter——上下文查询增强器

ContextualQueryAugmenter augmenter = ContextualQueryAugmenter.builder()
    .allowEmptyContext(false)  // 没有检索到相关文档时,是否允许模型回答
    .build();

// allowEmptyContext = false 时:如果检索不到任何相关文档,模型会直接回复"无法回答"
// 而不是编造答案,这是防止幻觉的最后一道防线
3.3.5 完整管线构建

将上述所有优化组件组合起来,构建一个完整的 RAG 优化管线:

@Configuration
public class AdvancedRAGConfig {

    // Pre-Retrieval:查询压缩(处理多轮对话代词指代)
    @Bean
    public CompressionQueryTransformer compressionQueryTransformer(ChatModel chatModel) {
        return CompressionQueryTransformer.builder()
            .chatModel(chatModel)
            .build();
    }

    // Pre-Retrieval:多查询扩展(提升召回率)
    @Bean
    public MultiQueryExpander multiQueryExpander(ChatModel chatModel) {
        return MultiQueryExpander.builder()
            .chatModel(chatModel)
            .numberOfQueries(3)
            .build();
    }

    // Retrieval:向量检索
    @Bean
    public VectorStoreDocumentRetriever vectorStoreDocumentRetriever(VectorStore vectorStore) {
        return VectorStoreDocumentRetriever.builder()
            .vectorStore(vectorStore)
            .similarityThreshold(0.5)
            .topK(5)
            .build();
    }

    // Post-Retrieval:文档合并
    @Bean
    public ConcatenationDocumentJoiner concatenationDocumentJoiner() {
        return new ConcatenationDocumentJoiner();
    }

    // Post-Retrieval:文档重排序
    @Bean
    public DocumentRanker documentRanker() {
        return new DashScopeDocumentRanker(
            DashScopeDocumentRankerOptions.builder()
                .withModelName("gte-rerank")
                .withTopN(5)
                .build()
        );
    }

    // Generation:上下文增强
    @Bean
    public ContextualQueryAugmenter contextualQueryAugmenter() {
        return ContextualQueryAugmenter.builder()
            .allowEmptyContext(false)
            .build();
    }
}

构建 RAG 业务流:

@Service
public class AdvancedRAGService {

    private final CompressionQueryTransformer compressionTransformer;
    private final MultiQueryExpander queryExpander;
    private final VectorStoreDocumentRetriever retriever;
    private final ConcatenationDocumentJoiner joiner;
    private final DocumentRanker reranker;
    private final ContextualQueryAugmenter augmenter;
    private final ChatModel chatModel;

    public String query(String userQuestion, List<Message> history) {
        // 1. Pre-Retrieval:查询压缩
        Query compressedQuery = compressionTransformer.transform(
            Query.builder().text(userQuestion).history(history).build()
        );

        // 2. Pre-Retrieval:多查询扩展
        ExpandedQuery expandedQueries = queryExpander.expand(compressedQuery);

        // 3. Retrieval:多路检索
        List<List<Document>> retrievedResults = expandedQueries.queries().stream()
            .map(retriever::retrieve)
            .collect(Collectors.toList());

        // 4. Post-Retrieval:文档合并去重
        List<Document> joinedDocs = joiner.join(retrievedResults);

        // 5. Post-Retrieval:重排序精排
        List<Document> rerankedDocs = reranker.rank(compressedQuery, joinedDocs);

        // 6. Generation:上下文增强 + 生成答案
        Query finalQuery = augmenter.augment(compressedQuery, rerankedDocs);

        ChatClient chatClient = ChatClient.builder(chatModel).build();
        return chatClient.prompt()
            .user(finalQuery.text())
            .call()
            .content();
    }
}

完整数据流

用户提问:这个小区的二手房均价是多少?

CompressionQueryTransformer

结合对话历史压缩为:碧海湾小区的二手房均价是多少?

MultiQueryExpander

扩展为3个变体:
1. 碧海湾小区二手房均价
2. 碧海湾小区房价走势
3. 碧海湾小区成交价格

VectorStoreDocumentRetriever

三路检索,每路 topK=5

ConcatenationDocumentJoiner

合并去重,得到 12 个文档片段

DocumentRanker 重排序

精排到 top 5 个最相关文档

ContextualQueryAugmenter

将文档片段注入 Prompt 上下文

ChatModel 生成精准回答

3.4 RAG + ChatMemory 的组合使用

在多轮对话场景中,RAG 需要与对话记忆(ChatMemory)配合使用。关键点在于 Advisor 的执行顺序

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        // 1. 先加载对话历史
        MessageChatMemoryAdvisor(chatMemory),
        // 2. 再执行 RAG(此时 CompressionQueryTransformer 可以看到历史)
        ragAdvisor
    )
    .build();

为什么顺序重要? 如果 RAG Advisor 在 Memory Advisor 之前执行,CompressionQueryTransformer 就看不到对话历史,无法将"这个小区"替换为"碧海湾小区"。

3.5 自定义 Prompt 模板

可以通过自定义模板来控制生成行为:

// 构建自定义 Prompt 模板:用于控制 RAG 生成时的提示词拼接方式
PromptTemplate customPromptTemplate = PromptTemplate.builder()
    // 配置模板渲染器(基于 StringTemplate 引擎)
    .renderer(StTemplateRenderer.builder()
        .startDelimiterToken('<')                          // 模板变量起始分隔符:'<'  (默认是 '${')
        .endDelimiterToken('>')                            // 模板变量结束分隔符:'>'  (默认是 '}')
        .build())
    // 模板正文:必须包含 <query> 和 <question_answer_context> 两个占位符
    //  - <query> 会被替换为用户原始问题
    //  - <question_answer_context> 会被替换为检索到的文档片段
    .template("""
        <query>
        Context information is below.
        ---------------------
        <question_answer_context>
        ---------------------
        Given the context information and no prior knowledge, answer the query.
        Follow these rules:
        1. If the answer is not in the context, just say that you don't know.
        2. Avoid statements like "Based on the context..." or "The provided information...".
        """)
    .build();                                              // 构造 PromptTemplate 实例

// 将自定义模板应用到 RAG Advisor
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
    .promptTemplate(customPromptTemplate)                 // 注入自定义 Prompt 模板
    .build();

模板中必须包含两个占位符:

  • <query>:接收用户问题
  • <question_answer_context>:接收检索到的上下文

3.6 完整 ETL 管道

以下是一个从文档加载到向量存储的完整 ETL 管道:

@Configuration
public class RAGConfiguration {

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        return new SimpleVectorStore(embeddingModel);
    }

    @Bean
    public TextSplitter textSplitter() {
        // 中文优先的递归字符分割器
        List<String> separators = Arrays.asList(
            "\n\n", "\n", "。", "!", "?", ";", ",",
            ". ", "! ", "? ", " ", ""
        );
        return new RecursiveCharacterTextSplitter(800, 150, separators);
    }
}

文档摄入服务:

@Service
public class DocumentIngestionService {

    private final VectorStore vectorStore;
    private final TextSplitter textSplitter;

    public void ingestDocument(Resource resource) {
        // 1. 加载文档
        TextReader textReader = new TextReader(resource);
        textReader.getCustomMetadata().put("filename", resource.getFilename());
        List<Document> documents = textReader.get();

        // 2. 分割文档
        List<Document> chunks = textSplitter.apply(documents);

        // 3. 写入向量数据库(自动完成 Embedding)
        vectorStore.write(chunks);
    }
}

3.7 最佳实践总结

文档处理

  • 优先使用 RecursiveCharacterTextSplitter 处理中文文档
  • chunkSize 设为 800~1000 字符,chunkOverlap 设为 150~250 字符
  • 为文档添加元数据(来源、类型、时间),便于后续过滤

检索增强策略

  • 多轮对话场景必须使用 CompressionQueryTransformer
  • 召回率不足时使用 MultiQueryExpander
  • 生产环境推荐向量 + BM25 混合检索

系统配置

  • 相似度阈值建议 0.5~0.8,过低会引入噪声,过高会漏掉相关文档
  • topK 建议 5~10,需要平衡精度与 Token 消耗
  • allowEmptyContext = false 防止无检索结果时的幻觉

钩子选择

  • 简单两步 RAG 优先使用 AgentHook(最省时)
  • 多轮推理中需要动态调整检索时使用 MessagesModelHookModelInterceptor

Advisor 执行顺序

  • Memory Advisor 必须在 RAG Advisor 之前,确保查询压缩能访问对话历史

总结

RAG 不是万能的,但在大规模知识库问答场景中,它仍然是最成熟、最实用的方案。关键在于:

  1. 选对方案:小知识库用长上下文,过程性知识用 Skill,实时数据用 MCP,事实性问答用 RAG
  2. 选对架构:FAQ 用两步 RAG,复杂推理用 Agentic RAG,高质量要求用混合 RAG
  3. 做好优化:查询压缩解决代词指代,多查询扩展提升召回率,重排序精炼结果,空上下文防护防止幻觉
  4. 选对分割器:中文场景优先 RecursiveCharacterTextSplitter,参数需要根据实际文档调优

Spring AI Alibaba 的模块化设计让这些优化可以灵活组合——从最简的 AgentHook 两步 RAG,到完全定制的 RetrievalAugmentationAdvisor,开发者可以根据业务需求选择合适的复杂度。


参考资源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐