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 系统都可以分解为两个阶段、四个步骤:
css
graph LR
A[用户查询] --> B[检索阶段]
B --> C[检索结果]
C --> D[生成阶段]
D --> E[最终答案]
subgraph 索引管线(离线)
F[原始文档] --> G[文档分割]
G --> H[Embedding]
H --> I[(向量数据库)]
end
I -.-> B
索引管线(Indexing Pipeline) --- 离线执行,将原始文档转化为可检索的向量索引。 查询时管线(Query-time Pipeline) --- 在线执行,处理用户查询并生成答案。
2. RAG 与 Fine-tuning 的博弈与协同
在讨论 RAG 的架构之前,必须澄清一个常被混淆的问题:RAG 和 Fine-tuning 到底是什么关系?
2.1 本质差异
| 维度 | RAG | Fine-tuning |
|---|---|---|
| 知识来源 | 外部检索 + 模型能力 | 模型内部参数 |
| 更新成本 | 更新索引库即可,无需重新训练 | 需要重新训练或微调 |
| 幻觉控制 | 强(基于检索事实) | 弱(依赖模型记忆) |
| 推理成本 | 增加检索延迟和 Token 消耗 | 基本不变 |
| 适合场景 | 知识密集型、频繁更新、高准确性要求 | 风格适配、指令遵循、稳定性要求 |
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 提供了完整的构建块:
scss
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 定义了几个关键接口,理解这些接口是掌握整个框架的基础:
java
// 文档表示
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 流程只需要几行代码:
java
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return new PgVectorStore(jdbcTemplate, embeddingModel);
}
@Service
public 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 提供了丰富的文档读取器,覆盖了大部分常见格式:
java
// 读取 PDF
DocumentReader pdfReader = new PdfDocumentReader(
new FileSystemResource("document.pdf"));
// 读取 JSON
DocumentReader jsonReader = new JsonDocumentReader(
new ClassPathResource("data.json"));
// 读取 Markdown
DocumentReader mdReader = new MarkdownDocumentReader(
new FileSystemResource("README.md"));
// 读取 CSV
DocumentReader csvReader = new CsvDocumentReader(
new FileSystemResource("data.csv"));
// 读取 HTML
DocumentReader htmlReader = new HtmlDocumentReader(
new UrlResource("https://example.com"));
4.2 文档解析的深度策略
PDF 解析的陷阱
PDF 是 RAG 中最常见的输入格式,也是最容易出问题的格式。PDF 本质上是一个排版格式,不是内容格式,解析质量参差不齐。
Spring AI 的 PdfDocumentReader 基于 Apache PDFBox。对于扫描版 PDF(图片格式),需要使用 OCR 进行预处理:
java
// 原生 PDF(文本可选中)
DocumentReader reader = new PdfDocumentReader(resource);
// 扫描版 PDF(需先 OCR 处理)
// Spring AI 本身不内置 OCR,可以结合 Tesseract 预处理
// 或者使用 AWS Textract、Azure Document Intelligence 等云服务
实际经验:对于 PDF 中的表格,原生解析几乎一定会丢失结构信息。建议使用专门的文档解析服务或自定义表格提取逻辑。
元数据的价值
文档加载时保留的元数据(Metadata)在后续的检索和过滤中至关重要:
java
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:
java
// 基于 Token 的分割(推荐)
DocumentTransformer splitter = TokenTextSplitter.builder()
.defaultMaxTokenSize(500) // 每个分割块最多 500 token
.minChunkSizeChars(350) // 最小字符数
.minOverlap(50) // 块间重叠(保持上下文连续性)
.build();
List<Document> chunks = splitter.apply(originalDocuments);
分割参数调优指南:
| 场景 | 推荐 chunk size | overlap | 理由 |
|---|---|---|---|
| 问答型 FAQ | 200-300 tokens | 20-50 | 答案本身较短,精确匹配更重要 |
| 文档分析 | 500-1000 tokens | 50-100 | 需要更多上下文来理解 |
| 代码检索 | 300-500 tokens | 30-50 | 函数/类通常在此长度 |
| 法律条文 | 400-600 tokens | 50-80 | 条款间有交叉引用 |
| 长文档摘要 | 800-1500 tokens | 100-200 | 需要完整段落理解 |
5.4 高级分割策略
语义分割------不基于固定的 Token 数,而是基于内容的语义边界(段落、标题、列表):
java
// 自定义语义分割器示例(概念)
List<Document> semanticChunks = documents.stream()
.flatMap(doc -> splitByMarkdownHeaders(doc).stream())
.flatMap(doc -> splitByParagraphs(doc).stream())
.collect(Collectors.toList());
递归字符分割------从粗粒度到细粒度递归分割,直到块大小满足要求:
java
// 递归分割:先按 ## 分割,再按段落分割,最后按句子分割
// Spring AI 的 TokenTextSplitter 内置了类似递归逻辑
实际建议:在实践中,最好根据你的文档特性定制分割策略。比如 Markdown 文档可以按标题层级分割,代码可以按函数或类分割,PDF 可以按页面或段落分割。没有万能的分割策略。
6. Embedding:将语义转化为向量
6.1 Embedding 的核心作用
Embedding 模型将文本映射到高维向量空间,使得语义相近的文本在向量空间中距离较近。
arduino
"苹果发布了新款手机" [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 抽象
java
public interface EmbeddingModel {
// 单条文本 embedding
EmbeddingResponse embed(EmbeddingRequest request);
// 批量 embedding(通常有优化)
List<EmbeddingResponse> embed(List<EmbeddingRequest> requests);
// 获取向量维度
int dimensions();
}
6.3 Embedding 模型选型
Spring AI 支持多种 Embedding 实现:
| Embedding 模型 | 维度 | 适用场景 | Spring AI 支持 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 通用、英文为主 | ✅ |
| OpenAI text-embedding-3-large | 3072 | 高精度需求 | ✅ |
| BAAI/bge-large-zh-v1.5 | 1024 | 中文场景 | ✅ (ONNX) |
| Ollama (nomic-embed-text) | 768 | 本地部署 | ✅ |
| Alibaba Qwen Embeddings | 1024 | 中文/多语言 | ✅ |
中文场景的现实考量:
java
// 中文推荐:使用 BAAI/bge 系列或 Qwen Embeddings
@Bean
public 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 的最佳实践
java
// 批量处理优于逐条处理(大多数模型支持 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 接口抽象了所有向量数据库的操作:
java
public interface VectorStore {
void add(List<Document> documents);
void delete(List<String> idList);
List<Document> similaritySearch(SearchRequest request);
}
7.3 主流实现对比
| 解决方案 | 部署方式 | 适合规模 | Spring AI 支持度 | 特点 |
|---|---|---|---|---|
| PGVector | 自托管 | 百万级 | 原生支持 | PostgreSQL 插件,善用已有数据库 |
| Redis Stack | 自托管 | 百万级 | 原生支持 | 缓存+向量,低延迟 |
| Pinecone | SaaS | 亿级 | 原生支持 | 全托管,无需运维 |
| Chroma | 嵌入式 | 十万级 | 原生支持 | 开发环境首选 |
| Milvus | 自托管 | 十亿级 | 原生支持 | 大规模分布场景 |
| Qdrant | 自托管 / SaaS | 亿级 | 原生支持 | Rust 实现,性能优秀 |
| Elasticsearch | 自托管 | 亿级 | 原生支持 | 全文检索+向量混合 |
| Weaviate | 自托管 / SaaS | 亿级 | 原生支持 | 自带 schema 管理 |
7.4 生产选型建议
java
// 开发环境:Chroma(零配置嵌入式)
@Bean
public VectorStore chromaVectorStore(EmbeddingModel embeddingModel) {
return new ChromaVectorStore(chromaApi, embeddingModel, "collection_name");
}
// 中小规模生产:PGVector(复用 PostgreSQL 基础设施)
@Bean
public VectorStore pgVectorStore(EmbeddingModel embeddingModel,
JdbcTemplate jdbcTemplate) {
return new PgVectorStore(jdbcTemplate, embeddingModel,
PgVectorStore.PgDistanceType.COSINE_DISTANCE,
PgIndexType.HNSW, // HNSW 索引,支持 ANN 搜索
false); // 不移除现有文档
}
// 大规模生产:Pinecone(全托管)
@Bean
public VectorStore pineconeVectorStore(PineconeApi api,
EmbeddingModel embeddingModel) {
return new PineconeVectorStore(api, embeddingModel,
"my-index", PineconeVectorStore.MetadataFields.NONE);
}
7.5 元数据过滤:提升检索精度的关键
纯向量搜索找到的是"语义相似"的文档。结合元数据过滤,可以找到"语义相似且符合条件"的文档。
java
// 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 基础检索:向量相似性搜索
最简单的检索方式是余弦相似度搜索:
java
// 余弦距离: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 策略
java
// K 值的影响
K = 1-3: 高精度,低召回(严谨问答场景)
K = 5-10: 适中(通用知识问答)
K = 10-20: 高召回,低精度(摘要、综合分析场景)
动态 Top-K:根据问题的复杂度动态调整 K 值。
java
// 概念:问题越复杂,需要的上下文越多
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 相似度阈值
只保留超过相似度阈值的文档,避免引入噪音:
java
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(20)
.similarityThreshold(0.7) // 低于 0.7 的结果将被丢弃
.build();
合理的阈值通常通过实验确定。先用 0.7 作为起点,根据实际效果上浮或下调。
8.4 Spring AI 的 Advisor 机制
Spring AI 通过 Advisor 模式实现灵活的检索增强控制:
java
// 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 将用户问题改写成更适合检索的形式。
arduino
用户问题: "去年的营收怎么样?"
重写后: "2025年公司年度营收数据"
查询分解(Query Decomposition)------将复杂问题分解为多个子问题分别检索:
arduino
用户问题: "苹果和微软2025年的营收对比"
子问题1: "苹果公司2025年营收是多少"
子问题2: "微软公司2025年营收是多少"
HyDE(Hypothetical Document Embedding)------先生成一个假设的理想文档,再基于这个假设文档检索:
makefile
用户问题: "什么是量子计算?"
假设文档: "量子计算是一种利用量子力学原理进行信息处理的计算方式..."
检索: 用假设文档的 embedding 去匹配真实文档
这些高级策略在 Spring AI 中可以通过自定义 Advisor 或 Processor 实现。框架提供了扩展点,策略本身需要开发者根据场景实现。
9. 增强生成:让 LLM 基于上下文作答
9.1 Prompt 模板设计
检索到的文档如何呈现给 LLM 是影响生成质量的关键因素。
java
// 自定义上下文模板
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 内部执行以下步骤:
markdown
1. 接收用户请求
2. 调用 VectorStore.similaritySearch() 检索相关文档
3. 将检索结果注入 Prompt(通过 ContentFormatter)
4. 构造增强后的 Prompt:
System: [原始 System Message + 检索到的上下文]
User: [用户问题]
5. 调用 ChatModel
6. 返回基于上下文的回答
9.3 流式输出与 RAG
java
// 流式 RAG 响应
@Bean
public 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 引用溯源
在生产系统中,告诉用户答案的来源是建立信任的关键:
java
// 在 Prompt 中要求标注来源
// 同时在后处理中解析引用
@Service
public 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:上下文消歧
arduino
用户: "什么是量子计算?"
助手: "量子计算是..."
用户: "它和传统计算有什么区别?"
↑ 这里的"它"指的是量子计算,需要从对话历史推断
解决方案:上下文压缩(Contextual Compression)
java
// 在检索前,将对话历史压缩为最新的上下文
// 或者让 LLM 将用户问题补全为独立问题
@Component
public 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)
结合多种检索策略,综合利用各自优势:
markdown
用户问题
│
├──→ 向量检索(语义匹配)
├──→ 关键词检索(精确匹配,如 Elasticsearch)
├──→ 图检索(实体关系,如 Neo4j)
└──→ SQL 查询(结构化数据)
│
└──→ 融合排序 → 最终上下文
Spring AI 实现思路:
java
@Component
public 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)是一种将文档构建成层次化摘要树的方法。
css
┌─────────────┐
│ 全局摘要 │ ← Level 2(最抽象)
└──────┬──────┘
│
┌─────────┼─────────┐
│ │ │
┌────┴───┐ ┌───┴────┐ ┌──┴────┐
│摘要集群A │ │摘要集群B │ │摘要集群C │ ← Level 1
└────┬───┘ └───┬────┘ └───┬───┘
│ │ │
┌────┴───┐ ... ...
│原始块A1 │
│原始块A2 │
└────────┘ ← Level 0(原始文档块)
检索时,从顶层开始逐层匹配,找到最合适的粒度。这种模式在处理超长文档(如一本书或一份研究报告)时特别有效。
10.4 Self-RAG:让 LLM 自我评估
Self-RAG 是一种让 LLM 在生成过程中自我反思的范式:
markdown
1. 检索:根据问题检索相关文档
2. 评估:判断检索到的文档是否与问题相关
- 相关 → 基于文档生成答案
- 不相关 → 拒绝回答或请求补充信息
3. 事实核查:检查生成的答案是否基于检索到的文档
4. 最终输出:如果有冲突,修正或标注不确定性
Spring AI 中可以通过自定义 Advisor 实现 Self-RAG 的核心逻辑。
11. Spring AI 的 RAG 生产化实践
11.1 完整的 RAG 管线
一个生产级的 RAG 系统通常包含以下步骤:
java
// 索引管线(IndexingPipeline)
@Configuration
public class IndexingPipelineConfig {
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
@Bean
public 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 增量索引
生产环境中,文档会持续更新。全量重建索引成本太高,需要增量索引策略:
java
@Service
public 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);
}
// 监听文件变化(适用于本地文件)
@EventListener
public void onFileChange(FileChangeEvent event) {
updateDocument(event.getFileId(), event.getContent());
}
}
11.3 缓存策略
RAG 系统中,相同或相似的问题往往会被反复问到。缓存可以有效降低延迟和成本:
java
@Configuration
public class RagCacheConfig {
@Bean
public CacheManager ragCacheManager() {
// 使用 Caffeine 作为本地缓存
CaffeineCacheManager cacheManager = new CaffeineCacheManager("rag-cache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.recordStats());
return cacheManager;
}
@Bean
public ChatClient cachedChatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
.build();
}
}
@Service
public class CachedRagService {
@Cacheable(value = "rag-cache", key = "#question")
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
11.4 异步索引和批量处理
java
@Service
public class AsyncIndexingService {
private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();
@Async
public 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 设计
java
@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 和应用监控:
java
@Component
public 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 失败模式
css
失败模式 表现形式 解决方案
─────── ──────── ────────
检索失败 返回了完全不相关的文档 调整分割策略、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 变化,请以官方文档为准。