Spring AI RAG 深度技术解析 — 从原理到生产实践
摘要: 检索增强生成(Retrieval-Augmented Generation, RAG)是当前大语言模型应用中最核心的架构范式之一。本文从 Spring AI 框架的视角出发,系统性地拆解 RAG 的技术原理、架构设计、核心组件、进阶策略及生产化落地要点,帮助读者建立起从理论到实践的完整知识体系。
1. RAG 的本质:为什么需要检索增强生成
1.1 大模型的核心痛点
大语言模型(LLM)虽然能力强大,但在实际应用中面临三个根本性局限:
- 知识截止日期(Knowledge Cutoff)
:模型训练完成后,无法自动获取最新信息。一个在 2025 年训练的模型不知道 2026 年发生了什么事。 - 幻觉(Hallucination)
:模型在不确定时会"编造"答案,尤其在处理长尾知识或专业领域问题时。这不是 bug,而是语言模型基于概率生成的天性。 - 缺乏内部知识访问能力
:模型无法"查阅"企业内部的数据库、文档库、知识库。它的知识完全来自于训练语料。
RAG 的出现正是为了解决这三个问题——它不试图让模型记住更多,而是让模型学会"查资料"。
1.2 RAG 的核心思想
RAG 的核心逻辑极其简洁,可以用一句话概括:
在回答之前,先检索与问题相关的信息,然后将这些信息作为上下文提供给 LLM,让 LLM 基于检索到的内容生成答案。
这个简单的思路带来了三个关键转变:
传统 LLM 调用: 用户问题 → LLM → 答案(依赖模型内部知识)RAG 模式: 用户问题 → 检索器 → 相关知识 + 问题 → LLM → 基于事实的答案
1.3 RAG Pipeline 的抽象视角
从最抽象的层面看,任何 RAG 系统都可以分解为两个阶段、四个步骤:
graph LRA[用户查询] --> B[检索阶段]B --> C[检索结果]C --> D[生成阶段]D --> E[最终答案]subgraph 索引管线(离线)F[原始文档] --> G[文档分割]G --> H[Embedding]H --> I[(向量数据库)]endI -.-> B
索引管线(Indexing Pipeline) — 离线执行,将原始文档转化为可检索的向量索引。查询时管线(Query-time Pipeline) — 在线执行,处理用户查询并生成答案。
2. RAG 与 Fine-tuning 的博弈与协同
在讨论 RAG 的架构之前,必须澄清一个常被混淆的问题:RAG 和 Fine-tuning 到底是什么关系?
2.1 本质差异
2.2 何时用 RAG
- 需要引用具体数据源
(法律条文、财报数据、产品文档) - 知识频繁变化
(实时新闻、内部政策更新) - 需要精确追溯信息来源
- 长尾知识
(罕见问题、小众领域)
2.3 何时用 Fine-tuning
- 改变模型的输出风格或语调
(让模型更像客服或专家) - 让模型遵循特定指令格式
(始终输出 JSON) - 少量高频任务
(分类、实体抽取) - 减少推理时的 Prompt 长度
(将行为编码进参数)
2.4 RAG + Fine-tuning 的最佳协同
在实践中,RAG 和 Fine-tuning 不是互斥的,而是互补的。理想的架构是:
Fine-tuning 负责"如何回答",RAG 负责"回答什么"。
举个例子:一个法律咨询 AI
- Fine-tuning
让模型学会法律文书的严谨语气和引用格式 - RAG
从法律数据库中检索最新的法条和判例
在 Spring AI 中,这意味着你可以同时使用 ChatClient 和 VectorStore —— 微调后的模型回答风格更专业,RAG 提供的事实更准确。
3. Spring AI RAG 整体架构
3.1 Spring AI 的定位
Spring AI 是 Spring 生态中面向 AI 应用的抽象层。它的设计哲学沿袭了 Spring 一贯的风格:
提供抽象接口,让开发者用最少的代码切换不同实现。
在 RAG 领域,Spring AI 提供了完整的构建块:
Spring AI RAG 组件层次┌─────────────────────────────────────────────┐│ 应用层 ││ ChatClient / StreamingChatClient │├─────────────────────────────────────────────┤│ 增强层 ││ QuestionAnswerAdvisor ││ RetrievalAugmentationAdvisor │├─────────────────────────────────────────────┤│ 检索层 ││ VectorStore (PGVector / Pinecone / Redis) ││ DocumentRetriever │├─────────────────────────────────────────────┤│ 处理层 ││ DocumentReader / DocumentTransformer ││ TokenTextSplitter / ContentFormatter │├─────────────────────────────────────────────┤│ 基础设施层 ││ EmbeddingModel (OpenAI / Ollama / BAAI) ││ ChatModel / StreamingChatModel │└─────────────────────────────────────────────┘
3.2 核心接口与抽象
Spring AI 为 RAG 定义了几个关键接口,理解这些接口是掌握整个框架的基础:
// 文档表示public class Document {private String id;private String content; // 文档文本内容private Metadata metadata; // 元数据(来源、时间、类型等)private List<Double> embedding; // 向量表示}// 向量存储抽象public interface VectorStore {void add(List<Document> documents);void delete(List<String> idList);List<Document> similaritySearch(SearchRequest request);}// 检索增强建议器(核心 RAG 组件)public interface RetrievalAugmentationAdvisor {ChatResponse advise(ChatRequest request);}// 文档检索器public interface DocumentRetriever {List<Document> retrieve(RetrievalRequest request);}
3.3 最小化 RAG 实现
在 Spring AI 中,实现一个完整的 RAG 流程只需要几行代码:
@Beanpublic VectorStore vectorStore(EmbeddingModel embeddingModel) {return new PgVectorStore(jdbcTemplate, embeddingModel);}@Servicepublic class RagService {private final ChatClient chatClient;public RagService(ChatClient.Builder builder, VectorStore vectorStore) {this.chatClient = builder.defaultSystem("""你是一个专业的AI助手。请基于提供的上下文信息回答问题。如果上下文信息不足以回答问题,请明确说明,不要编造答案。回答时请引用信息来源。""").defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)).build();}public String ask(String question) {return chatClient.prompt().user(question).call().content();}}
这段代码背后发生的事情:
QuestionAnswerAdvisor拦截用户请求 调用 VectorStore.similaritySearch()执行向量检索将检索到的文档格式化为上下文,注入 Prompt 调用 LLM 生成基于上下文的回答
4. 文档加载与解析:RAG 的入口
RAG 的质量上限在很大程度取决于输入数据的质量。垃圾进,垃圾出 在 RAG 中体现得尤为明显。
4.1 Spring AI 的 DocumentReader 体系
Spring AI 提供了丰富的文档读取器,覆盖了大部分常见格式:
// 读取 PDFDocumentReader pdfReader = new PdfDocumentReader(new FileSystemResource("document.pdf"));// 读取 JSONDocumentReader jsonReader = new JsonDocumentReader(new ClassPathResource("data.json"));// 读取 MarkdownDocumentReader mdReader = new MarkdownDocumentReader(new FileSystemResource("README.md"));// 读取 CSVDocumentReader csvReader = new CsvDocumentReader(new FileSystemResource("data.csv"));// 读取 HTMLDocumentReader htmlReader = new HtmlDocumentReader(new UrlResource("https://example.com"));
4.2 文档解析的深度策略
PDF 解析的陷阱
PDF 是 RAG 中最常见的输入格式,也是最容易出问题的格式。PDF 本质上是一个排版格式,不是内容格式,解析质量参差不齐。
Spring AI 的 PdfDocumentReader 基于 Apache PDFBox。对于扫描版 PDF(图片格式),需要使用 OCR 进行预处理:
// 原生 PDF(文本可选中)DocumentReader reader = new PdfDocumentReader(resource);// 扫描版 PDF(需先 OCR 处理)// Spring AI 本身不内置 OCR,可以结合 Tesseract 预处理// 或者使用 AWS Textract、Azure Document Intelligence 等云服务
实际经验:对于 PDF 中的表格,原生解析几乎一定会丢失结构信息。建议使用专门的文档解析服务或自定义表格提取逻辑。
元数据的价值
文档加载时保留的元数据(Metadata)在后续的检索和过滤中至关重要:
Document doc = new Document("content");doc.getMetadata().put("source", "financial-report-2026-q1.pdf");doc.getMetadata().put("page", 42);doc.getMetadata().put("category", "quarterly");doc.getMetadata().put("created_at", "2026-04-15");doc.getMetadata().put("author", "finance-team");
为什么元数据重要? 元数据允许你在检索时进行预过滤——只检索特定分类、特定时间范围、特定来源的文档,大大提升检索精准度。
5. 文档分割:决定检索质量的隐形之手
5.1 为什么需要分割?
- LLM 上下文窗口有限
:即使 GPT-4 有 128K 上下文,检索几百个文档也不现实 - 检索精度要求
:一个 100 页的文档,不可能作为一个向量整体检索——相关性会稀释 - 成本控制
:Token 就是成本,塞入过多无关上下文既浪费又降低质量
5.2 分割粒度的权衡
太细(句子级) ←→ 太粗(章节级)检索精度高 上下文完整上下文碎片化 检索噪声大丢失跨句关系 可能含无关内容
经验法则:检索的"原子单位"应该是一个自包含的思想单元——能够独立被理解且包含足够上下文的最小段落。
5.3 Spring AI 的分割器策略
Spring AI 提供了 DocumentTransformer 接口以及开箱即用的 TokenTextSplitter:
// 基于 Token 的分割(推荐)DocumentTransformer splitter = TokenTextSplitter.builder().defaultMaxTokenSize(500) // 每个分割块最多 500 token.minChunkSizeChars(350) // 最小字符数.minOverlap(50) // 块间重叠(保持上下文连续性).build();List<Document> chunks = splitter.apply(originalDocuments);
分割参数调优指南:
5.4 高级分割策略
语义分割——不基于固定的 Token 数,而是基于内容的语义边界(段落、标题、列表):
// 自定义语义分割器示例(概念)List<Document> semanticChunks = documents.stream().flatMap(doc -> splitByMarkdownHeaders(doc).stream()).flatMap(doc -> splitByParagraphs(doc).stream()).collect(Collectors.toList());
递归字符分割——从粗粒度到细粒度递归分割,直到块大小满足要求:
// 递归分割:先按 ## 分割,再按段落分割,最后按句子分割// Spring AI 的 TokenTextSplitter 内置了类似递归逻辑
实际建议:在实践中,最好根据你的文档特性定制分割策略。比如 Markdown 文档可以按标题层级分割,代码可以按函数或类分割,PDF 可以按页面或段落分割。没有万能的分割策略。
6. Embedding:将语义转化为向量
6.1 Embedding 的核心作用
Embedding 模型将文本映射到高维向量空间,使得语义相近的文本在向量空间中距离较近。
"苹果发布了新款手机" [0.23, -0.45, 0.78, ...] ←→"iPhone 16 正式发布" [0.21, -0.42, 0.80, ...] ← 语义相近,向量距离小"天气预报说明天下雨" [0.67, 0.12, -0.34, ...] ← 语义无关,向量距离大
6.2 Spring AI 的 Embedding 抽象
public interface EmbeddingModel {// 单条文本 embeddingEmbeddingResponse embed(EmbeddingRequest request);// 批量 embedding(通常有优化)List<EmbeddingResponse> embed(List<EmbeddingRequest> requests);// 获取向量维度int dimensions();}
6.3 Embedding 模型选型
Spring AI 支持多种 Embedding 实现:
中文场景的现实考量:
// 中文推荐:使用 BAAI/bge 系列或 Qwen Embeddings@Beanpublic EmbeddingModel embeddingModel() {// 方案1:本地部署(Ollama)return new OllamaEmbeddingModel(ollamaApi).withModel("bge-m3"); // 支持中英文// 方案2:阿里通义千问return new TongyiEmbeddingModel(tongyiApi);// 方案3:OpenAI(英文为主场景)return new OpenAiEmbeddingModel(openAiApi).withModel("text-embedding-3-small");}
选择 Embedding 模型的关键维度:
- 语义理解能力
:模型是否真正理解你所在领域的语义 - 维度大小
:高维度更精确但计算和存储成本更高 - 语言支持
:中文场景需要专门的中文 embedding 模型 - 延迟和吞吐
:在线服务 vs 本地推理
6.4 Embedding 的最佳实践
// 批量处理优于逐条处理(大多数模型支持 batch)List<Document> batch = chunkedDocuments;List<List<Double>> embeddings = embeddingModel.embed(batch); // 一次调用// 缓存 embedding 结果(避免重复计算)// 向量数据库通常会自动处理缓存
7. 向量数据库:RAG 的记忆存储
7.1 向量数据库 vs 传统数据库
向量数据库专门为向量相似性搜索优化,但不存在"完美"的向量数据库——只有适合你场景的。
7.2 Spring AI 的 VectorStore 实现
Spring AI 通过 VectorStore 接口抽象了所有向量数据库的操作:
public interface VectorStore {void add(List<Document> documents);void delete(List<String> idList);List<Document> similaritySearch(SearchRequest request);}
7.3 主流实现对比
| PGVector | ||||
| Redis Stack | ||||
| Pinecone | ||||
| Chroma | ||||
| Milvus | ||||
| Qdrant | ||||
| Elasticsearch | ||||
| Weaviate |
7.4 生产选型建议
// 开发环境:Chroma(零配置嵌入式)@Beanpublic VectorStore chromaVectorStore(EmbeddingModel embeddingModel) {return new ChromaVectorStore(chromaApi, embeddingModel, "collection_name");}// 中小规模生产:PGVector(复用 PostgreSQL 基础设施)@Beanpublic VectorStore pgVectorStore(EmbeddingModel embeddingModel,JdbcTemplate jdbcTemplate) {return new PgVectorStore(jdbcTemplate, embeddingModel,PgVectorStore.PgDistanceType.COSINE_DISTANCE,PgIndexType.HNSW, // HNSW 索引,支持 ANN 搜索false); // 不移除现有文档}// 大规模生产:Pinecone(全托管)@Beanpublic VectorStore pineconeVectorStore(PineconeApi api,EmbeddingModel embeddingModel) {return new PineconeVectorStore(api, embeddingModel,"my-index", PineconeVectorStore.MetadataFields.NONE);}
7.5 元数据过滤:提升检索精度的关键
纯向量搜索找到的是"语义相似"的文档。结合元数据过滤,可以找到"语义相似且符合条件"的文档。
// Spring AI 中实现带过滤的检索SearchRequest request = SearchRequest.builder().query("2026年第一季度财报数据") // 查询文本.topK(10) // 返回前10条.similarityThreshold(0.75) // 相似度阈值.filterExpression("category == 'quarterly' && created_at > '2026-01-01'").build();List<Document> results = vectorStore.similaritySearch(request);
元数据过滤的核心价值:
- 权限控制
:只检索用户有权访问的文档 - 时效性控制
:只检索特定时间范围内的信息 - 分类过滤
:只检索特定类别的文档 - 混合搜索前置条件
:缩小搜索范围后再执行向量搜索
8. 检索策略:如何找到最相关的信息
检索是 RAG 的核心环节。检索质量直接决定了生成质量。
8.1 基础检索:向量相似性搜索
最简单的检索方式是余弦相似度搜索:
// 余弦距离:1 - cos(A, B)// 值域 [0, 2],越小越相似PgVectorStore.PgDistanceType.COSINE_DISTANCE// 内积距离:常用于归一化向量// 值域 [-1, 1],越大越相似PgVectorStore.PgDistanceType.NEGATIVE_INNER_PRODUCT// 欧几里得距离:适合低维向量PgVectorStore.PgDistanceType.EUCLIDEAN_DISTANCE
经验结论:对于文本 Embedding,余弦相似度通常是默认选择。如果使用 OpenAI Embedding(已经 L2 归一化),内积和余弦等价。
8.2 Top-K 策略
// K 值的影响K = 1-3: 高精度,低召回(严谨问答场景)K = 5-10: 适中(通用知识问答)K = 10-20: 高召回,低精度(摘要、综合分析场景)
动态 Top-K:根据问题的复杂度动态调整 K 值。
// 概念:问题越复杂,需要的上下文越多int dynamicTopK(String question) {int words = question.split(" ").length;int tokens = estimateTokens(question);if (tokens < 10) return 3; // 简单问题if (tokens < 50) return 5; // 一般问题return 10; // 复杂问题}
8.3 相似度阈值
只保留超过相似度阈值的文档,避免引入噪音:
SearchRequest request = SearchRequest.builder().query(question).topK(20).similarityThreshold(0.7) // 低于 0.7 的结果将被丢弃.build();
合理的阈值通常通过实验确定。先用 0.7 作为起点,根据实际效果上浮或下调。
8.4 Spring AI 的 Advisor 机制
Spring AI 通过 Advisor 模式实现灵活的检索增强控制:
// 1. QuestionAnswerAdvisor:最简单的 RAG// 自动执行检索并将结果注入 Prompt.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))// 2. RetrievalAugmentationAdvisor:更灵活的配置.defaultAdvisors(RetrievalAugmentationAdvisor.builder().documentRetriever(DefaultDocumentRetriever.builder().vectorStore(vectorStore).similarityThreshold(0.75).topK(5).build()).contentFormatter(DefaultContentFormatter.builder().withTemplate("""上下文信息:------------{documents}------------请基于上述上下文回答以下问题。""").build()).build())
8.5 高级检索策略
查询重写(Query Rewriting)——用户的问题往往不适合直接用于检索。比如"能告诉我这个怎么用?"中的"这个"是代词,需要解析。通过 LLM 将用户问题改写成更适合检索的形式。
用户问题: "去年的营收怎么样?"重写后: "2025年公司年度营收数据"
查询分解(Query Decomposition)——将复杂问题分解为多个子问题分别检索:
用户问题: "苹果和微软2025年的营收对比"子问题1: "苹果公司2025年营收是多少"子问题2: "微软公司2025年营收是多少"
HyDE(Hypothetical Document Embedding)——先生成一个假设的理想文档,再基于这个假设文档检索:
用户问题: "什么是量子计算?"假设文档: "量子计算是一种利用量子力学原理进行信息处理的计算方式..."检索: 用假设文档的 embedding 去匹配真实文档
这些高级策略在 Spring AI 中可以通过自定义 Advisor 或 Processor 实现。框架提供了扩展点,策略本身需要开发者根据场景实现。
9. 增强生成:让 LLM 基于上下文作答
9.1 Prompt 模板设计
检索到的文档如何呈现给 LLM 是影响生成质量的关键因素。
// 自定义上下文模板ContentFormatter formatter = DefaultContentFormatter.builder().withTemplate("""你是一个专业的AI助手。## 上下文信息以下是与你需要回答的问题相关的参考信息。请仔细阅读这些信息,它们来自我们公司的知识库:{% for document in documents %}[来源:{{ document.metadata.source }}]{{ document.content }}{% endfor %}## 回答要求1. 如果上下文信息充分,请基于上下文给出准确、详细的回答2. 如果上下文信息不足,请明确说明"根据现有信息无法回答"3. 回答时请在句末标注信息来源:[[来源:xxx]]4. 不要编造上下文之外的信息5. 使用专业、清晰的语言## 用户问题{{ userInput }}""").build();
9.2 Prompt 注入的工作流程
Spring AI 的 QuestionAnswerAdvisor 内部执行以下步骤:
1. 接收用户请求2. 调用 VectorStore.similaritySearch() 检索相关文档3. 将检索结果注入 Prompt(通过 ContentFormatter)4. 构造增强后的 Prompt:System: [原始 System Message + 检索到的上下文]User: [用户问题]5. 调用 ChatModel6. 返回基于上下文的回答
9.3 流式输出与 RAG
// 流式 RAG 响应@Beanpublic ChatClient streamingChatClient(ChatClient.Builder builder) {return builder.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)).build();}// Controller@GetMapping("/chat/stream")public Flux<String> streamChat(@RequestParam String question) {return streamingChatClient.prompt().user(question).stream().content();}
9.4 引用溯源
在生产系统中,告诉用户答案的来源是建立信任的关键:
// 在 Prompt 中要求标注来源// 同时在后处理中解析引用@Servicepublic class CitedRagService {public AnswerWithSources answer(String question) {String response = chatClient.prompt().user(question).call().content();// 解析引用来源(从元数据中提取)List<String> sources = extractSources(response);return new AnswerWithSources(response, sources);}private List<String> extractSources(String response) {// 从 LLM 输出中解析 [[来源: xxx]] 格式的引用Pattern pattern = Pattern.compile("\\[\\[来源:(.+?)\\]\\]");Matcher matcher = pattern.matcher(response);return matcher.results().map(r -> r.group(1)).distinct().collect(Collectors.toList());}}public record AnswerWithSources(String answer,List<String> sources) {}
10. 高级 RAG 模式
10.1 多轮对话中的 RAG
单轮 RAG 相对简单,多轮对话中的 RAG 则面临更多挑战:
挑战 1:上下文消歧
用户: "什么是量子计算?"助手: "量子计算是..."用户: "它和传统计算有什么区别?"↑ 这里的"它"指的是量子计算,需要从对话历史推断
解决方案:上下文压缩(Contextual Compression)
// 在检索前,将对话历史压缩为最新的上下文// 或者让 LLM 将用户问题补全为独立问题@Componentpublic class ContextualQueryEnricher {public String enrichQuery(String currentQuestion,List<Message> history) {if (history.isEmpty()) {return currentQuestion;}// 使用 LLM 将问题补全String enriched = chatClient.prompt().system("""根据对话历史,将用户的最后一句话改写为可以独立检索的查询。只需要返回改写后的查询,不要有其他内容。""").user("""历史对话:%s用户最新问题:%s""".formatted(formatHistory(history), currentQuestion)).call().content();return enriched != null ? enriched : currentQuestion;}}
挑战 2:历史信息的累积
随着对话进行,历史消息越来越多。如果不加控制:会超出上下文窗口、检索噪音增加、成本线性增长。
解决方案:滑动窗口和摘要化
10.2 多路检索(Multi-Route Retrieval)
结合多种检索策略,综合利用各自优势:
用户问题│├──→ 向量检索(语义匹配)├──→ 关键词检索(精确匹配,如 Elasticsearch)├──→ 图检索(实体关系,如 Neo4j)└──→ SQL 查询(结构化数据)│└──→ 融合排序 → 最终上下文
Spring AI 实现思路:
@Componentpublic class HybridRetriever {private final VectorStore vectorStore;private final ElasticsearchRestClient esClient;public List<Document> retrieve(String query, int topK) {// 1. 向量检索List<Document> vectorResults = vectorStore.similaritySearch(SearchRequest.builder().query(query).topK(topK).build());// 2. 关键词检索List<Document> keywordResults = keywordSearch(query, topK);// 3. 融合排序(Reciprocal Rank Fusion)return reciprocalRankFusion(vectorResults, keywordResults, topK);}private List<Document> reciprocalRankFusion(List<Document>... rankings) {// RRF: score = Σ 1/(k + rank)// 融合多路检索结果,被多路都命中的文档排名更高// ...}}
10.3 RAPTOR:层次化摘要检索
RAPTOR(Recursive Abstractive Processing for Tree-Organized Retrieval)是一种将文档构建成层次化摘要树的方法。
┌─────────────┐│ 全局摘要 │ ← Level 2(最抽象)└──────┬──────┘│┌─────────┼─────────┐│ │ │┌────┴───┐ ┌───┴────┐ ┌──┴────┐│摘要集群A │ │摘要集群B │ │摘要集群C │ ← Level 1└────┬───┘ └───┬────┘ └───┬───┘│ │ │┌────┴───┐ ... ...│原始块A1 ││原始块A2 │└────────┘ ← Level 0(原始文档块)
检索时,从顶层开始逐层匹配,找到最合适的粒度。这种模式在处理超长文档(如一本书或一份研究报告)时特别有效。
10.4 Self-RAG:让 LLM 自我评估
Self-RAG 是一种让 LLM 在生成过程中自我反思的范式:
1. 检索:根据问题检索相关文档2. 评估:判断检索到的文档是否与问题相关- 相关 → 基于文档生成答案- 不相关 → 拒绝回答或请求补充信息3. 事实核查:检查生成的答案是否基于检索到的文档4. 最终输出:如果有冲突,修正或标注不确定性
Spring AI 中可以通过自定义 Advisor 实现 Self-RAG 的核心逻辑。
11. Spring AI 的 RAG 生产化实践
11.1 完整的 RAG 管线
一个生产级的 RAG 系统通常包含以下步骤:
// 索引管线(IndexingPipeline)@Configurationpublic class IndexingPipelineConfig {private final EmbeddingModel embeddingModel;private final VectorStore vectorStore;@Beanpublic CommandLineRunner indexDocuments(@Value("${documents.path}") String docPath) {return args -> {// 1. 扫描文档List<File> files = scanDocuments(docPath);for (File file : files) {// 2. 文档加载DocumentReader reader = createReader(file);List<Document> documents = reader.read();// 3. 文档分割DocumentTransformer splitter = TokenTextSplitter.builder().defaultMaxTokenSize(500).minOverlap(50).build();List<Document> chunks = splitter.apply(documents);// 4. 向量化并存储vectorStore.add(chunks);log.info("Indexed: {} ({} chunks)", file.getName(), chunks.size());}};}private DocumentReader createReader(File file) {String name = file.getName().toLowerCase();Resource resource = new FileSystemResource(file);if (name.endsWith(".pdf")) return new PdfDocumentReader(resource);if (name.endsWith(".md")) return new MarkdownDocumentReader(resource);if (name.endsWith(".csv")) return new CsvDocumentReader(resource);// 扩展更多格式throw new UnsupportedOperationException("Unsupported format: " + name);}}
11.2 增量索引
生产环境中,文档会持续更新。全量重建索引成本太高,需要增量索引策略:
@Servicepublic class IncrementalIndexingService {private final VectorStore vectorStore;private final DocumentIdStrategy idStrategy;// 文档更新时调用public void updateDocument(String docId, String newContent) {// 1. 删除旧索引vectorStore.delete(List.of(docId));// 2. 重新分割和索引Document doc = Document.builder().id(docId).content(newContent).metadata(Map.of("updated_at", Instant.now().toString())).build();List<Document> chunks = splitter.apply(List.of(doc));vectorStore.add(chunks);}// 监听文件变化(适用于本地文件)@EventListenerpublic void onFileChange(FileChangeEvent event) {updateDocument(event.getFileId(), event.getContent());}}
11.3 缓存策略
RAG 系统中,相同或相似的问题往往会被反复问到。缓存可以有效降低延迟和成本:
@Configurationpublic class RagCacheConfig {@Beanpublic CacheManager ragCacheManager() {// 使用 Caffeine 作为本地缓存CaffeineCacheManager cacheManager = new CaffeineCacheManager("rag-cache");cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1, TimeUnit.HOURS).recordStats());return cacheManager;}@Beanpublic ChatClient cachedChatClient(ChatClient.Builder builder) {return builder.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)).build();}}@Servicepublic class CachedRagService {@Cacheable(value = "rag-cache", key = "#question")public String ask(String question) {return chatClient.prompt().user(question).call().content();}}
11.4 异步索引和批量处理
@Servicepublic class AsyncIndexingService {private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();@Asyncpublic CompletableFuture<Void> indexDocumentAsync(MultipartFile file) {return CompletableFuture.runAsync(() -> {// 异步处理文档索引indexDocument(file);}, executor);}// 批量索引public void batchIndex(List<File> files) {List<CompletableFuture<Void>> futures = files.stream().map(file -> CompletableFuture.runAsync(() -> indexDocument(file), executor)).toList();// 等待所有完成CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();}}
11.5 API 设计
@RestController@RequestMapping("/api/rag")public class RagController {private final RagService ragService;// 标准问答@PostMapping("/ask")public ResponseEntity<RagResponse> ask(@RequestBody @Valid RagRequest request) {RagResponse response = ragService.ask(request);return ResponseEntity.ok(response);}// 流式问答@PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> askStream(@RequestBody RagRequest request) {return ragService.askStream(request).map(content -> ServerSentEvent.<String>builder().data(content).build());}// 带引用来源的问答@PostMapping("/ask/cited")public ResponseEntity<CitedResponse> askWithCitations(@RequestBody @Valid RagRequest request) {CitedResponse response = ragService.askWithCitations(request);return ResponseEntity.ok(response);}// 文档上传和索引@PostMapping("/documents/upload")public ResponseEntity<Void> uploadDocument(@RequestParam("file") MultipartFile file) {ragService.indexDocument(file);return ResponseEntity.accepted().build();}}public record RagRequest(@NotBlank String question,@Min(1) @Max(20) Integer topK,Double similarityThreshold,String filterExpression) {}public record RagResponse(String answer,long processingTimeMs) {}public record CitedResponse(String answer,List<Source> sources,long processingTimeMs) {public record Source(String title, String content, double score) {}}
12. 评估与持续优化
12.1 RAG 评估框架
评估 RAG 系统需要从两个维度进行:
检索质量
- 命中率(Hit Rate)
: 检索结果中是否包含正确答案 - 平均倒数排名(MRR)
: 正确答案在结果中的位置 - NDCG(归一化折损累计增益)
: 排序质量的综合指标
生成质量
- 忠实度(Faithfulness)
: 答案是否基于检索到的上下文,而非幻觉 - 答案相关性(Answer Relevance)
: 答案是否回答了问题 - 上下文精度(Context Precision)
: 检索结果中的噪音比例
Spring AI 本身不内置评估工具,但可以集成 Spring Boot Actuator 和应用监控:
@Componentpublic class RagMetricsCollector {private final MeterRegistry meterRegistry;public void recordQuery(String question, RagResponse response) {// 记录查询延迟meterRegistry.timer("rag.query.latency").record(response.processingTimeMs(), TimeUnit.MILLISECONDS);// 记录检索结果数量meterRegistry.gauge("rag.retrieval.topk", response.sources().size());// 记录平均相似度分数double avgScore = response.sources().stream().mapToDouble(Source::score).average().orElse(0.0);meterRegistry.gauge("rag.retrieval.avg_score", avgScore);}}
12.2 RAGAS 评估框架集成
RAGAS(Retrieval Augmented Generation Assessment)是专门评估 RAG 系统的框架。虽然它不是 Spring AI 原生组件,但可以作为独立服务集成。
12.3 常见的 RAG 失败模式
失败模式 表现形式 解决方案─────── ──────── ────────检索失败 返回了完全不相关的文档 调整分割策略、embedding模型、检索参数上下文窗口溢出 Token 使用超过限制 减小 Top-K、压缩上下文幻觉 答案包含检索结果中没有的信息 优化 Prompt、增加 faithfulness 检查引用错误 标注了错误的来源 加强引用解析逻辑更新延迟 文档已修改但索引未更新 实现增量索引机制冷启动问题 新知识库没有高质量检索结果 预置种子数据,逐步优化嵌入
13. 总结与展望
13.1 RAG 的核心原则
回顾全文,RAG 成功的关键可以归结为几点:
- 数据质量决定了 RAG 的天花板
— 花最多的精力在数据清洗、分割和元数据标注上 - 检索策略比模型选择更重要
— 一个中等水平的 LLM + 优秀的检索机制,远好于一个顶级 LLM + 糟糕的检索 - 评估驱动优化
— 不测量就无法改进。建立 RAG 评估指标(RAGAS)是上线前最重要的工作 - RAG + Fine-tuning 是最佳组合
— 两者解决不同层次的问题,合力才能达到最佳效果 - 监控和反馈闭环
— 生产环境中的 RAG 需要持续的监控、日志分析和用户反馈循环来不断提升
13.2 Spring AI 的优势
Spring AI 在 RAG 领域的独特价值在于:
- 完整的抽象层
— 切换向量数据库、Embedding 模型、LLM 提供商时只需修改配置 - 与 Spring 生态深度融合
— 事务管理、缓存、异步处理、监控、安全等开箱即用 - 模块化设计
— Advisor 机制允许灵活组合 RAG 策略 - 企业级特性
— 生产化部署所需的一切都已经在 Spring 生态中
13.3 未来趋势
RAG 技术仍在快速演进,值得关注的方向包括:
- Agentic RAG
— RAG 系统从被动检索转变为主动推理,如自动判断是否需要补充检索 - Graph RAG
— 利用知识图谱的结构化关系增强检索的相关性和可解释性 - 多模态 RAG
— 不仅检索文本,还能检索图像、表格、音频等多种模态 - RAG + CAG
— 当上下文窗口足够大时,Cache-Augmented Generation 将部分高频的 RAG 查询转化为预加载的缓存 - 长期记忆
— RAG 从"一次性检索"转向具有记忆能力的持续学习系统
13.4 写在最后
RAG 不是银弹,它有自己的局限和适用边界。但它确实解决了 LLM 落地中最关键的问题——让模型能够引用外部知识来回答问题。在 Spring AI 的加持下,构建一个生产级的 RAG 系统已经从一件复杂的工作变成了一个标准化的工程实践。
关键在于理解每一层抽象背后的原理,知道何时使用默认配置、何时需要自定义扩展。希望本文能帮助你构建出真正可用的 RAG 系统。
本文基于 Spring AI 1.0+ 版本编写。框架版本迭代可能带来 API 变化,请以官方文档为准。
夜雨聆风