Karpathy 用 Markdown 干掉 RAG 管线:48% 的错其实怪检索
AIAI Summary (BLUF)
这篇文章系统讲解了RAG(检索增强生成)的核心原理、三种架构模式以及全流程优化策略,并基于Spring AI Alibaba给出了完整的代码实现。无论你是刚接触RAG的开发者,还是正在优化线上RAG系统的工程师,都能从中找到实用的参考。
核心洞察
这篇文章最值得看的是 2.4 节那个反直觉的结论:Karpathy 用一堆 Markdown 文件干掉了整个 RAG 管线,效果还更好。如果你正在无脑上 RAG,建议先翻到选型决策那张表看看自己的场景到底该不该用。另外文中那个 48% 的数字挺扎心的——RAG 答错的问题里,将近一半是检索阶段就没找对材料,模型背了锅。
核心结论
RAG 的错误答案中,48% 根源在检索阶段而非模型本身。 2025 年一篇 arXiv 论文对政府文档 QA 任务的误差分析显示,近一半的 RAG 错误是因为检索阶段就漏掉了关键证据,模型只是“背了锅”。
小知识库(< 100K Token)直接全量加载到上下文窗口,效果优于 RAG。 Karpathy 用一组 Markdown 文件替代整个 RAG 管线(无向量数据库A database system designed to store and perform high-dimensional semantic similarity searches on vector embeddings of data.、无 Embedding、无分片),其 100 篇文章、40 万字的个人 Wiki 效果远超 RAG,召回率可达 85% 以上。
RAG、Skill预设提示词文件,相当于特定任务的使用手册,向AI详细描述如何完成某类任务,每个Skill代表一个专家技能。、MCPModel Context Protocol - a protocol that enables AI models to access external tools, data sources, and services to enhance their capabilities and context awareness. 是分工关系而非替代关系。 RAG 存储陈述性知识(Know-What,如“报销标准是 800 元/天”),Skill 封装过程性知识(Know-How,如“如何部署 K8s 集群”),MCP 处理连接性知识(Know-Where,如 API 接口调用)。
大多数团队的第一选择应是 RAG + BM25 混合检索。 GraphRAGRAG方法的高级变体,引入图结构数据,将信息表示为实体和关系的互联网络,以提高检索的完整性和准确性。 仅在业务确实需要跨文档多跳推理时才值得投入,其构建和维护成本是 VectorRAG 的 5-10 倍。
两步 RAG 中,AgentHookSpring AI Alibaba中ReactAgent的一种钩子,在Agent启动时仅执行一次检索,整个推理过程中复用检索结果,避免重复检索带来的性能开销。 是性能最优的检索钩子。 它在 Agent 启动时仅执行一次检索并在整个推理过程中复用结果,适用于绝大多数查询保持不变的场景;MessagesModelHookSpring AI Alibaba中ReactAgent的一种钩子,在每次模型调用前执行检索,适合需要在多轮推理中持续获取最新上下文的场景。 和 ModelInterceptor 则适合需要动态调整检索内容的复杂场景。
一、RAG 是什么
1.1 一句话定义
RAG(检索增强生成)结合信息检索和文本生成的技术,通过检索相关文档来增强大型语言模型的生成能力。是一种先查资料、再回答问题的技术范式。LLM 生成回答之前,先从外部知识库中检索出与问题相关的文档片段,把这些片段作为上下文注入 Prompt,让模型的回答有据可依。
用考试来类比:
- 不用 RAG = 闭卷考试,凭记忆作答,可能答错或编造
- 用 RAG = 开卷考试,先翻参考资料再作答,答案有据可查
1.2 RAG 的核心价值
RAG 解决了 LLM 的三大痛点:
痛点一:幻觉
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一个基于Spring AI的RAG解决方案框架,以ReactAgent为核心,支持三种RAG架构模式,并内置了从查询转换到文档后处理的全链路优化组件。 正是在这个背景下,提供了从基础到高级的完整 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,如果你的知识库在 100K1M Token 之间(约 2001500 页),直接全量加载到上下文即可,召回率 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 启动时仅执行一次检索,整个推理过程中复用检索结果,避免重复检索带来的性能开销:
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) {
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.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 的访问能力,可以修改请求参数、注入上下文、做异常重试等:
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 |
|
核心优势:
- 高度灵活:Agent 根据推理结果自主决定检索策略,可多轮检索
- 多源融合:可同时接入文档库、网络搜索、内部数据库等
- 可解释:工具调用过程透明,便于调试
模式三:混合 RAG(Hybrid RAG)
核心特点:结合两步 RAG 的可预测性和 Agentic RAG 的灵活性,引入质量验证机制。
| 维度 | 表现 |
|---|---|
| 控制性 | 中 |
| 灵活性 | 中 |
| 延迟 | 可变 |
| 典型场景 | 带质量验证的领域特定问答系统 |
先定义两个工具类。文档搜索工具负责从向量库里捞相关内容,网络搜索工具先留个空壳,后面接真实 API 的时候直接替换就行。
VectorStore vectorStore = ...;
ChatModel chatModel = ...;
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) {
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();
两个工具都注册成 ToolCallback 之后,Agent 就能根据用户问题自己决定该调哪个。
接下来是查询增强钩子。用户输入的问题往往不够精确,直接拿去检索效果一般。这个 Hook 挂在 Agent 执行之前,用一次 LLM 调用把原始 query 改写得更适合检索。
@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());
}
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);
}
}
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) {
return query;
}
}
enhanceQuery 方法目前是原样返回,你可以在这里接一个 prompt,让模型把口语化的提问转成检索友好的关键词组合。改写后的 query 会替换掉消息列表里的用户消息,同时存一份到 metadata 里,方便后续环节读取。
再来看答案验证拦截器。这个组件挂在模型调用层,每次 LLM 返回结果后检查一遍,不合格就带着修正提示重新调一次。
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) {
return answer != null && answer.length() > 20;
}
@Override
public String getName() {
return "answer_validation";
}
}
validateAnswer 现在只做了长度检查,实际项目里可以换成更复杂的逻辑,比如调一个轻量模型判断答案是否忠实于检索到的上下文,或者检查有没有编造内容。不通过就注入一条系统消息要求重新生成。
最后把所有组件组装到 ReactAgent 里。
ReactAgent hybridRAGAgent = ReactAgent.builder()
.name("hybrid_rag_agent")
.model(chatModel)
.instruction("""
你是一个智能助手,可以访问多个信息源来回答问题。
使用工具时:
1.
优先用 document_search 搜文档库,需要最新信息再上 web_search,然后基于检索结果生成答案。信息不够就多调几次工具。
.tools(documentSearchCallback, webSearchCallback)
.hooks(new QueryEnhancementHook(chatModel))
.interceptors(new AnswerValidationInterceptor(chatModel))
.build();
AssistantMessage response = hybridRAGAgent.call("Spring AI Alibaba支持哪些向量数据库?");
System.out.println("答案: " + response.getText());
3.2 基础 RAG 实现:从 ETL 到查询
在动手优化之前,先把一条完整的 RAG 链路跑通。Spring AI Alibaba 这套东西搭基础流程不算复杂。
Spring AI 提供了多种 DocumentReader,PDF、Markdown、JSON、HTML、纯文本都能读:
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(
new FileSystemResource("path/to/document.pdf"),
PagePdfDocumentReaderConfig.builder()
.withPageExtractedTextFormatter(new ExtractedTextFormatter.Builder()
.withNumberOfBottomTextLinesToDelete(0)
.withNumberOfTopPagesToSkipBeforeRead(0)
.build())
.withPagesPerDocument(1)
.build()
);
List<Document> documents = pdfReader.get();
MarkdownDocumentReader mdReader = new MarkdownDocumentReader(
new FileSystemResource("path/to/document.md"),
MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", "document.md")
.build()
);
List<Document> mdDocuments = mdReader.get();
TextReader textReader = new TextReader(new FileSystemResource("path/to/document.txt"));
textReader.getCustomMetadata().put("filename", "document.txt");
textReader.getCustomMetadata().put("category", "policy");
List<Document> textDocuments = textReader.get();
JsonReader jsonReader = new JsonReader(
new FileSystemResource("path/to/data.json"),
"description",
"content"
);
List<Document> jsonDocuments = jsonReader.get();
常用 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 提供了几种分割器:
TokenTextSplitter tokenSplitter = new TokenTextSplitter(
800, // chunk size
200, // min chunk size
100, // min chunk length
10000, // max num chunks
true // keep separators
);
List<Document> tokenChunks = tokenSplitter.apply(documents);
List<String> chineseSeparators = Arrays.asList(
"\n\n", "\n", "。", "!", "?", ";", ",",
". ", "! ", "? ",
" ",
""
);
RecursiveCharacterTextSplitter recursiveSplitter = new RecursiveCharacterTextSplitter(
800, // chunk size
150, // overlap
chineseSeparators // 中文分隔符
);
List<Document> recursiveChunks = recursiveSplitter.apply(documents);
SentenceSplitter sentenceSplitter = new SentenceSplitter(
chatModel, // 用模型判断句子边界
800, // chunk size
150 // overlap
);
List<Document> sentenceChunks = sentenceSplitter.apply(documents);
分割器选型表:
| 分割器 | 分割依据 | 适合场景 | 中文适配 |
|---|---|---|---|
TokenTextSplitter |
Token 数量 | 英文文档 | 一般(可能乱码) |
RecursiveCharacterTextSplitter |
多级分隔符递归 | 中文/混合文档 | 优秀 |
SentenceSplitter |
LLM 识别句子边界 | 高精度语义分割 | 好 |
自定义 TextSplitter |
自定义逻辑 | 结构化文档(Markdown 按标题、代码按函数) | 自定义 |
参数调优建议:
| 参数 | 推荐值(中文) | 说明 |
|---|---|---|
| chunkSize | 800~1000 字符 | 太小语义断裂,太大检索效率下降 |
| chunkOverlap | 150~250 字符 | 保持上下文连贯,防止关键信息被截断 |
| 分隔符优先级 | 段落 > 换行 > 句号 > 逗号 > 字符 | 优先在语义边界切分 |
第三步:向量化与存储(Load)
1 |
|
Spring AI Alibaba 支持多种向量数据库:Milvus、Redis、Elasticsearch、Pinecone 等。
第四步:检索与生成
基于 ReactAgent + AgentHook 的两步 RAG 实现(最省时方案):
1 | ReactAgent agent = ReactAgent.builder() |
这就是一个最基础的 RAG 系统。但在生产环境中,基础 RAG 往往不够用——用户问得模糊、检索结果不精准、答案可能遗漏关键信息。这就需要全链路优化。
3.3 RAG 全链路优化
Spring AI 通过模块化的 RAG 优化组件,覆盖四个阶段:
1 | 用户提问 |
3.3.1 Pre-Retrieval(检索前优化)
检索前优化的核心思想是:用户的原始提问往往不是最好的检索 query。可能太模糊、可能包含代词、可能用词与文档不一致。优化提问质量,是提升 RAG 效果的第一步。
(1)RewriteQueryTransformer——查询重写
将过长或含代词的模糊问题,重写为具体、检索友好的 query:
1 | RewriteQueryTransformer rewriteTransformer = RewriteQueryTransformer.builder() |
(2)CompressionQueryTransformer——查询压缩
将对话历史和当前问题压缩成一个独立、自包含的 query。这是处理多轮对话中代词指代的利器:
1 | CompressionQueryTransformer compressionTransformer = CompressionQueryTransformer.builder() |
(3)TranslationQueryTransformer——查询翻译
将非目标语言的查询翻译为知识库的主要语言:
1 | TranslationQueryTransformer translationTransformer = TranslationQueryTransformer.builder() |
(4)MultiQueryExpander——多查询扩展
为同一个问题生成多个不同表述的变体,多路检索后合并结果,提升召回率:
1 | MultiQueryExpander queryExpander = MultiQueryExpander.builder() |
3.3.2 Retrieval(检索优化)
检索阶段的核心组件是 VectorStoreDocumentRetriever,它负责从向量数据库中检索相关文档:
1 |
|
检索策略对比:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 纯向量检索 | 基于语义相似度 | 通用场景 |
| 纯关键词检索(BM25) | 基于关键词精确匹配 | 精确查找(订单号、航班号) |
| 混合检索 | 向量 + BM25 结合 | 生产环境推荐 |
3.3.3 Post-Retrieval(检索后优化)
检索回来的文档不一定都是高质量的,可能包含重复、不相关或冗余的内容。Post-Retrieval 阶段负责"精炼"检索结果。
(1)DocumentJoiner——文档合并器
将多路检索的结果合并为一个文档列表,支持去重:
1 |
|
(2)文档重排序(Reranking)
用更精确的模型对检索结果重新排序,将最相关的文档排到前面:
1 |
|
(3)文档压缩
检索回来的片段有时候特别长,但真正跟问题相关的可能就那两三句。压缩就是让模型先过一遍,把无关内容砍掉,只留核心信息,省 Token。
1 |
|
(4)文档去重
如果你用了多路检索或者查询扩展,不同路径很可能捞回来同一段文本。不去重的话,Prompt 里全是重复内容,白白浪费上下文窗口。
1 |
|
3.3.4 Generation(生成优化)
生成阶段的优化主要是如何将检索到的上下文更好地注入 Prompt。
ContextualQueryAugmenter——上下文查询增强器
1 | ContextualQueryAugmenter augmenter = ContextualQueryAugmenter.builder() |
3.3.5 完整管线构建
把上面这些组件串起来,就是一个完整的 RAG 优化管线:
1 | @Configuration |
构建 RAG 业务流:
1 | @Service |
完整数据流:
3.4 RAG + ChatMemory 的组合使用
多轮对话场景里,RAG 得跟 ChatMemory 搭着用。这里有个容易踩坑的地方:Advisor 的执行顺序。
1 | ChatClient chatClient = ChatClient.builder(chatModel) |
顺序这件事容易被忽略。RAG Advisor 要是排在 Memory Advisor 前面,CompressionQueryTransformer 拿不到对话历史,“这个小区”就变不成“碧海湾小区”,压缩也就无从谈起。
3.5 自定义 Prompt 模板
生成行为可以通过自定义模板来控制:
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.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();
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(customPromptTemplate)
.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) {
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
List<Document> documents = textReader.get();
List<Document> chunks = textSplitter.apply(documents);
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,按业务需求选合适的复杂度就行。
参考资源:
- Spring AI Alibaba 官方文档
- Spring AI Alibaba RAG 高级用法
- Spring AI RAG 官方文档
- Spring AI Alibaba Agent Framework
常见问题(FAQ)
RAG 答错问题,为什么说近一半是检索阶段的问题?
2025 年 arXiv 论文对政府文档 QA 的误差分析发现,48% 的 RAG 错误答案根源在检索阶段漏掉了关键证据,而非模型本身。这意味着优化检索层比换模型更关键。
小知识库还有必要上 RAG 吗?
如果知识库在 100K1M Token 之间(约 2001500 页),直接全量加载到上下文即可,召回率 85% 以上。此时上 RAG 反而可能引入 48% 概率出错的检索层,得不偿失。
Skill 和 RAG 到底该用哪个?
Skill 封装过程性知识(Know-How),如部署流程;RAG 存储陈述性知识(Know-What),如报销标准。两者是分工关系,不是替代关系,生产环境常组合使用。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



