一文吃透 Spring AI Alibaba RAG:三种架构模式 + 全流程优化详解
本文系统讲解 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 生成回答
拆开来看,四大步骤分别是:
-
文档切割:将海量文档转化为易检索的知识碎片。就像把厚重词典拆解成单词卡片,采用智能分块算法保持语义连贯性,给每个知识碎片打标签。优质的知识切割如同图书馆分类系统,决定了后续检索效率。
-
向量编码:用 Embedding 模型将文字转化为高维数学向量,使语义相近的内容产生相似的数学特征。比如"续航时间"和"电池容量"会被编码为相似向量,存入专用的向量数据库并建立快速检索索引。
-
相似检索:将用户问题同样转化为向量,在向量数据库中通过相似度算法(如余弦相似度)找到最相关的文档片段。
-
生成增强:将检索到的文档片段作为上下文,与用户问题一起注入 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)
核心特点:检索操作总是在生成之前执行,流程可预测。
| 维度 | 表现 |
|---|---|
| 控制性 | 高 |
| 灵活性 | 低 |
| 延迟 | 快且可预测 |
| 典型场景 | 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 自主决定何时、何地以及如何执行检索。
| 维度 | 表现 |
|---|---|
| 控制性 | 低 |
| 灵活性 | 高 |
| 延迟 | 可变 |
| 典型场景 | 多工具研究助手、复杂推理任务 |
实现方式是为 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();
}
}
完整数据流:
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 不是万能的,但在大规模知识库问答场景中,它仍然是最成熟、最实用的方案。关键在于:
- 选对方案:小知识库用长上下文,过程性知识用 Skill,实时数据用 MCP,事实性问答用 RAG
- 选对架构:FAQ 用两步 RAG,复杂推理用 Agentic RAG,高质量要求用混合 RAG
- 做好优化:查询压缩解决代词指代,多查询扩展提升召回率,重排序精炼结果,空上下文防护防止幻觉
- 选对分割器:中文场景优先
RecursiveCharacterTextSplitter,参数需要根据实际文档调优
Spring AI Alibaba 的模块化设计让这些优化可以灵活组合——从最简的 AgentHook 两步 RAG,到完全定制的 RetrievalAugmentationAdvisor,开发者可以根据业务需求选择合适的复杂度。
参考资源:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)