本文系统讲解 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 文件)
MCPAI 与外部系统的标准连接协议实时数据获取、系统操作查询实时数据、调用 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 选型决策

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

选择建议:

钩子类型触发时机性能适用场景
AgentHookAgent 启动时一次最优查询在 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纯文本自定义元数据
MarkdownDocumentReaderMarkdown水平规则、代码块、引用块处理
PagePdfDocumentReaderPDF(按页)每页 Document 数、跳过页
ParagraphPdfDocumentReaderPDF(按段落)段落分割规则
JsonReaderJSON字段映射、JSON Pointer
HtmlReaderHTML标签过滤、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);

分割器选型表:

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

参数调优建议:

参数推荐值(中文)说明
chunkSize800~1000 字符太小语义断裂,太大检索效率下降
chunkOverlap150~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(最省时)
  • 多轮推理中需要动态调整检索时使用 MessagesModelHook 或 ModelInterceptor

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

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐