GEOZ

Karpathy 用 Markdown 干掉 RAG 管线:48% 的错其实怪检索

2026/10/11
Karpathy 用 Markdown 干掉 RAG 管线:48% 的错其实怪检索

AIAI Summary (BLUF)

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

核心洞察

这篇文章最值得看的是 2.4 节那个反直觉的结论:Karpathy 用一堆 Markdown 文件干掉了整个 RAG 管线,效果还更好。如果你正在无脑上 RAG,建议先翻到选型决策那张表看看自己的场景到底该不该用。另外文中那个 48% 的数字挺扎心的——RAG 答错的问题里,将近一半是检索阶段就没找对材料,模型背了锅。


核心结论

  1. RAG 的错误答案中,48% 根源在检索阶段而非模型本身。 2025 年一篇 arXiv 论文对政府文档 QA 任务的误差分析显示,近一半的 RAG 错误是因为检索阶段就漏掉了关键证据,模型只是“背了锅”。

  2. 小知识库(< 100K Token)直接全量加载到上下文窗口,效果优于 RAG。 Karpathy 用一组 Markdown 文件替代整个 RAG 管线(无向量数据库、无 Embedding、无分片),其 100 篇文章、40 万字的个人 Wiki 效果远超 RAG,召回率可达 85% 以上。

  3. RAG、Skill、MCP 是分工关系而非替代关系。 RAG 存储陈述性知识(Know-What,如“报销标准是 800 元/天”),Skill 封装过程性知识(Know-How,如“如何部署 K8s 集群”),MCP 处理连接性知识(Know-Where,如 API 接口调用)。

  4. 大多数团队的第一选择应是 RAG + BM25 混合检索。 GraphRAG 仅在业务确实需要跨文档多跳推理时才值得投入,其构建和维护成本是 VectorRAG 的 5-10 倍。

  5. 两步 RAG 中,AgentHook 是性能最优的检索钩子。 它在 Agent 启动时仅执行一次检索并在整个推理过程中复用结果,适用于绝大多数查询保持不变的场景;MessagesModelHook 和 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 生成回答

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

  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,如果你的知识库在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

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) { }
}


class WebSearchTool {
public Response search(Request request) {


return new Response("从网络搜索到的信息: " + request.query());
}

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


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();


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();


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();


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

核心优势:

  • 高度灵活: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
2

vectorStore.write(recursiveChunks);

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

第四步:检索与生成

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

1
2
3
4
5
6
7
8
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 优化组件,覆盖四个阶段:

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

3.3.1 Pre-Retrieval(检索前优化)

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

(1)RewriteQueryTransformer——查询重写

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

1
2
3
4
5
6
7
8
9
10
RewriteQueryTransformer rewriteTransformer = RewriteQueryTransformer.builder()
.chatModel(chatModel)
.build();




Query originalQuery = new Query("那个怎么弄来着?");
Query rewritten = rewriteTransformer.transform(originalQuery);
System.out.println(rewritten.text());

(2)CompressionQueryTransformer——查询压缩

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CompressionQueryTransformer compressionTransformer = CompressionQueryTransformer.builder()
.chatModel(chatModel)
.build();









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——查询翻译

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

1
2
3
4
5
6
7
8
9
10
11
TranslationQueryTransformer translationTransformer = TranslationQueryTransformer.builder()
.chatModel(chatModel)
.targetLanguage("zh")
.build();




Query englishQuery = new Query("What is the refund policy?");
Query translated = translationTransformer.transform(englishQuery);
System.out.println(translated.text());

(4)MultiQueryExpander——多查询扩展

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatModel(chatModel)
.numberOfQueries(3)
.build();








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,它负责从向量数据库中检索相关文档:

1
2
3
4
5
6
7
8
9
10
11

VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(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——文档合并器

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

1
2
3
4
5
6
7
8
9
10

ConcatenationDocumentJoiner joiner = new ConcatenationDocumentJoiner();


List<Query> queries = expanded.queries();
List<List<Document>> retrievedResults = queries.stream()
.map(retriever::retrieve)
.collect(Collectors.toList());

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

(2)文档重排序(Reranking)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14




DocumentRanker reranker = new DashScopeDocumentRanker(
DashScopeDocumentRankerOptions.builder()
.withModelName("gte-rerank")
.withTopN(5)
.build()
);


List<Document> coarseResults = retriever.retrieve(query);
List<Document> reranked = reranker.rank(query, coarseResults);

(3)文档压缩

检索回来的片段有时候特别长,但真正跟问题相关的可能就那两三句。压缩就是让模型先过一遍,把无关内容砍掉,只留核心信息,省 Token。

1
2
3
4
5
6
7
8
9
10

DocumentCompressor compressor = new LLMCompressor(
chatModel,
LLMCompressorOptions.builder()
.withMaxLength(200)
.withPreserveKeywords(true)
.build()
);

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

(4)文档去重

如果你用了多路检索或者查询扩展,不同路径很可能捞回来同一段文本。不去重的话,Prompt 里全是重复内容,白白浪费上下文窗口。

1
2
3
4
5
6
7

DocumentDeduplicator deduplicator = new DocumentDeduplicator(
DocumentDeduplicatorOptions.builder()
.withSimilarityThreshold(0.9)
.build()
);
List<Document> deduplicated = deduplicator.deduplicate(retrievedDocs);

3.3.4 Generation(生成优化)

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

ContextualQueryAugmenter——上下文查询增强器

1
2
3
4
5
6
ContextualQueryAugmenter augmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build();



3.3.5 完整管线构建

把上面这些组件串起来,就是一个完整的 RAG 优化管线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Configuration
public class AdvancedRAGConfig {


@Bean
public CompressionQueryTransformer compressionQueryTransformer(ChatModel chatModel) {
return CompressionQueryTransformer.builder()
.chatModel(chatModel)
.build();
}


@Bean
public MultiQueryExpander multiQueryExpander(ChatModel chatModel) {
return MultiQueryExpander.builder()
.chatModel(chatModel)
.numberOfQueries(3)
.build();
}


@Bean
public VectorStoreDocumentRetriever vectorStoreDocumentRetriever(VectorStore vectorStore) {
return VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.topK(5)
.build();
}


@Bean
public ConcatenationDocumentJoiner concatenationDocumentJoiner() {
return new ConcatenationDocumentJoiner();
}


@Bean
public DocumentRanker documentRanker() {
return new DashScopeDocumentRanker(
DashScopeDocumentRankerOptions.builder()
.withModelName("gte-rerank")
.withTopN(5)
.build()
);
}


@Bean
public ContextualQueryAugmenter contextualQueryAugmenter() {
return ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.build();
}
}

构建 RAG 业务流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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) {

Query compressedQuery = compressionTransformer.transform(
Query.builder().text(userQuestion).history(history).build()
);


ExpandedQuery expandedQueries = queryExpander.expand(compressedQuery);


List<List<Document>> retrievedResults = expandedQueries.queries().stream()
.map(retriever::retrieve)
.collect(Collectors.toList());


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


List<Document> rerankedDocs = reranker.rank(compressedQuery, joinedDocs);


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 的执行顺序。

1
2
3
4
5
6
7
8
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(

MessageChatMemoryAdvisor(chatMemory),

ragAdvisor
)
.build();

顺序这件事容易被忽略。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 不是万能的。但在大规模知识库问答这个场景里,它依然是最成熟、最实用的方案。几个关键点:

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

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


参考资源:

常见问题(FAQ)

RAG 答错问题,为什么说近一半是检索阶段的问题?

2025 年 arXiv 论文对政府文档 QA 的误差分析发现,48% 的 RAG 错误答案根源在检索阶段漏掉了关键证据,而非模型本身。这意味着优化检索层比换模型更关键。

小知识库还有必要上 RAG 吗?

如果知识库在 100K1M Token 之间(约 2001500 页),直接全量加载到上下文即可,召回率 85% 以上。此时上 RAG 反而可能引入 48% 概率出错的检索层,得不偿失。

Skill 和 RAG 到底该用哪个?

Skill 封装过程性知识(Know-How),如部署流程;RAG 存储陈述性知识(Know-What),如报销标准。两者是分工关系,不是替代关系,生产环境常组合使用。

阿凯广州
本文由 阿凯 审核,最后更新于 2026年10月11日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。