![SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档](https://pic.songma.com/blogimg/20251117/bb7f84cb58134484aa7653bb134f1be0.jpg)
目标:构建一个真正可用、具备上下文感知能力的知识库问答系统,让大模型基于你提供的私有文档生成准确、可靠的回答。
在上一章《嵌入模型与向量化存储》中,我们深入学习了如何将文本转化为向量、如何计算语义类似度,以及如何使用 VectorStore 存储和检索这些向量。这些技术不仅是独立的能力,更是本章核心——RAG(Retrieval-Augmented Generation,检索增强生成) 的基石。
通用大模型虽然强劲,但其知识是“静态”且“通用”的。它无法回答诸如“我们公司2025年差旅报销标准是多少?”这类依赖私有、动态知识的问题。而 RAG 正是解决这一痛点的关键技术。
通过本章,你将掌握如何将文档知识注入大模型的推理过程,构建一个真正可用的企业级智能问答系统。
8.1 RAG 原理详解:检索 + 增强 + 生成
RAG 的核心思想是:将大模型的生成能力与外部知识库的检索能力相结合,形成“先查资料,再作答”的智能行为。
整个流程分为三步:
8.1.1 检索(Retrieval)
当用户提出问题时,系统使用上一章学习的 Embedding 模型 将问题转化为向量,然后在 VectorStore 中进行类似度搜索,找出与问题最相关的文档片段。
8.1.2 增强(Augmentation
将检索到的相关文档内容(原文)作为上下文,拼接到 Prompt 中,形成一个“增强版”的输入。这相当于告知模型:“请参考以下信息来回答问题。”
8.1.3 生成(Generation
大模型基于增强后的 Prompt(包含问题 + 上下文)生成最终回答。由于模型有了明确的信息依据,生成的答案更准确、更可信,大幅降低“幻觉”风险。
✅ RAG 的本质:
它不是让模型“记住”所有知识,而是让它“会查资料”。这正是人类专家的工作方式。
![SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档](https://pic.songma.com/blogimg/20251117/0e996fbc8b344a44aa871696281bd2a0.jpg)
8.2 ETL 管道(Pipeline):数据处理的核心流程
从文档到向量数据库是RAG的前置条件,而文档到数据库有几个步骤超级重大:
- 文档读取:文档可能包括pdf、Excel、图片等等各种各样的内容,而这些内容中包括许多不同的格式,列如pdf中可能还包括表格和图片,因此选择哪些文档读取的工具超级重大
- 文档分块:由于大模型有token长度限制同时过长的token实则对于大模型的理解和推理会变慢或者不准确,因此需要将文档分块。对于如何分块,如何不会将原本有关联的语句分隔开也是文档向量化的重大步骤。
- 文档embedding:对于不同语言,不同的embedding模型有着不同效果.根据实际使用场景,选择适合自己的embedding模型
- 向量数据库:向量数据库的存储和类似度查询的准确性也可能会影响RAG最终的结果。
那么在这一套流程中,Spring AI称为ETL(Extract-Transform-Load) Pipeline,流程如下图(官方提供):
ETL 管道协调了从原始数据源到结构化向量存储的完整流程,确保数据最终转换为 AI 模型检索所需的最优格式。其三大核心阶段为:
- 提取(Extract):从 PDF、TXT、Word、HTML 等多种源中读取原始内容。
- 转换(Transform):对提取的文本进行清洗、分割、元数据增强等操作。
- 加载(Load):将处理后的文档写入向量数据库(VectorStore)或其他存储。
![SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档](https://pic.songma.com/blogimg/20251117/3dcd42461de649e6a5cd3ad383e565da.jpg)
Spring AI 为 ETL 流程定义了三个核心接口,体现了清晰的职责分离:
|
接口 |
功能 |
对应阶段 |
|
DocumentReader |
实现 Supplier<List<Document>> |
Extract |
|
DocumentTransformer |
实现 Function<List<Document>, List<Document>> |
Transform |
|
DocumentWriter |
实现 Consumer<List<Document>> |
Load |
下面是三个接口的定义:
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return (List)this.get();
}
}
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> transform) {
return (List)this.apply(transform);
}
}
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
this.accept(documents);
}
}
关于Document,在上一章中已经学习过了。
8.3 Document Reader接口
Spring AI,也集成了许多不同文档解析工具,以下是已经支持的文档读取器:
|
DocumentReaders |
描述 |
需要引入依赖 |
|
JsonReader |
用于读取Json格式的数据,并将其转换为一系列Document对象。 |
内置 |
|
TextReader |
处理纯文本文档,并将其转换为一系列Document对象。 |
内置 |
|
JsoupDocumentReader |
用于处理 HTML 文档,并利用 JSoup 库将这些文档转换为一系列的 Document 对象。 |
spring-ai-jsoup-document-reader |
|
MarkdownDocumentReader |
会处理 Markdown格式文档,并将其转换为一系列的 Document 对象。 |
spring-ai-markdown-document-reader |
|
PagePdfDocumentReader |
使用 Apache PdfBox 库来解析 PDF 文档。 |
spring-ai-pdf-document-reader |
|
ParagraphPdfDocumentReader |
同样也是基于 Apache PdfBox 库,但是不同的是 |
spring-ai-pdf-document-reader |
|
TikaDocumentReader |
通过使用 Apache Tika 技术从各种文档格式(如 PDF、DOC/DOCX、PPT/PPTX 和 HTML)中提取文本。 |
spring-ai-tika-document-reader |
8.4 DocumentTransformer接口
DocumentTransformer 可以对一批文档进行转换处理,它是一个接口,主要实现其transform方法,将一组Document转换为另外一组Document。实则现类有:
![SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档](https://pic.songma.com/blogimg/20251117/44d92b23e3d443f5a9f6101b76e08dbd.jpg)
- TextSplitter:是一个抽象基类,它有助于将文档进行分割,以适应人工智能模型的上下文窗口。
- TokenTextSplitter:是“TextSplitter”的一种实现方式,它根据词的数量将文本分割成多个部分,并使用“CL100K_BASE”编码进行处理。
- ContentFormatTransformer:可以将文档中的元数据内容变成键值对字符串,从而确保所有文档中的内容格式保持统一。
- KeywordMetadataEnricher:它调用大模型从文档内容中提取关键词,并向Metadata中添加一个keywords字段,该字段包含了文档的关键词信息。
- SummaryMetadataEnricher:它调用大模型为文档生成摘要,并将其作为Metadata添加到文档中。它能够为当前文档生成摘要,也能为相邻文档(即前文和后文的文档)生成摘要。
8.5 DocumentWriter接口
DocumentWriter Spring的Writers支持Flie以及向量数据库。实则现有:
![SpringAI[8]:RAG(检索增强生成)实战 — 让 AI_看懂_你的文档](https://pic.songma.com/blogimg/20251117/df99ddff77704bfd8a9b8ea437e74827.jpg)
可以看到 VectorStore实际上是 DocumentWriter的子接口。
8.6 实战:对PDF文件进行ETL处理
第一找一个样例 pdf文件,
chapter08/src/main/resources/Embedding-Models.pdf
- 引入依赖
chapter08/pom.xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
- 配置 Milvus VectorStore
// chapter08/src/main/java/com/kaifamiao/chapter08/configuration/VectorStoreConfig.java
@Configuration
public class VectorStoreConfig {
@Bean
public VectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingModel embeddingModel) {
return MilvusVectorStore.builder(milvusClient, embeddingModel)
.collectionName("default")
.databaseName("default")
.indexType(IndexType.IVF_FLAT)
.metricType(MetricType.COSINE)
.initializeSchema(true)
.build();
}
@Bean
public MilvusServiceClient milvusClient() {
return new MilvusServiceClient(ConnectParam.newBuilder()
.withHost("192.168.31.254")
.withPort(19530)
.build());
}
}
- 编写一个Service类,这个类中一次完成 pdf文件读取, 文本分割,最后保存到 Milvus 向量数据库中。
//chapter08/src/main/java/com/kaifamiao/chapter08/service/PDFEmbeddingService.java
@Service
@Slf4j
public class PDFEmbeddingService {
private final VectorStore vectorStore;
public PDFEmbeddingService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void processPDFAndStoreToMilvus(Resource pdfResource) {
try {
// 1. 读取PDF文档
PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)//如果设置为0,则表明所有页都变成一个文档
.build();
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(
pdfResource, config);
List<Document> documents = pdfReader.get();
// 处理文档内容中的非UTF-8字符
List<Document> cleanedDocuments = documents.stream()
.map(this::cleanNonUTF8Characters)
.collect(Collectors.toList());
// 2. 文本分割
TextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(cleanedDocuments);
// for(Document doc:splitDocuments){
// log.info("media:{}, metadata:{} content:{}",doc.getMedia(),doc.getMetadata().keySet(),doc.getText());
// }
// 分批处理,每批不超过10个文档,避免超出嵌入模型的批量大小限制
int batchSize = 10;
for (int i = 0; i < splitDocuments.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, splitDocuments.size());
List<Document> batch = splitDocuments.subList(i, endIndex);
// 3. 存储到Milvus向量数据库(分批处理)
vectorStore.add(batch);
log.info("Processed batch {} to {}", i / batchSize + 1, (endIndex - 1) / batchSize + 1);
}
} catch (Exception e) {
throw new RuntimeException("Failed to process PDF and store to Milvus", e);
}
}
// 修改清理非UTF-8字符的方法以适应新的API
private Document cleanNonUTF8Characters(Document document) {
String text = document.getText();
// 清理非UTF-8字符
String cleanedText = text.replaceAll("[^p{Print}s]", "?");
// 创建新的Document对象
return new Document(cleanedText, document.getMetadata());
}
}
- 测试用例
//chapter08/src/test/java/com/kaifamiao/chapter08/PDFEmbeddingServiceTest.java
@SpringBootTest
@Slf4j
public class PDFEmbeddingServiceTest {
@Autowired
private PDFEmbeddingService pdfEmbeddingService;
@Test
public void testProcessPDFAndStoreToMilvus() {
Resource resource = new ClassPathResource("Embedding-Models.pdf");
pdfEmbeddingService.processPDFAndStoreToMilvus(resource);
}
}
8.7 Advisor 机制与 QuestionAnswerAdvisor
在 Spring AI 中,Advisor 是一种强劲的拦截器机制,用于在请求发送给 AI 模型之前或之后修改请求或响应。它在 RAG 场景中扮演着核心角色。
Advisor 允许你在不修改核心业务逻辑的情况下,动态地:
- 修改 Prompt
- 注入上下文
- 记录日志
- 添加安全检查
- 处理异常
QuestionAnswerAdvisor 是 Spring AI 提供的专门用于 RAG 的 Advisor。它自动完成了“检索 + 增强”的过程。使用它,必须要引入依赖:
<!-- chapter08/pom.xml -->
<!-- 引入向量数据库插件 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
QuestionAnswerAdvisor类实现了BaseAdvisor接口,该BaseAdvisor接口主要有2个方法before和after,这个两个方法分别是调用Advisor的核心方法之前和之后执行的方法
// QuestionAnswerAdvisor 类部分源码
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
// 1. Search for similar documents in the vector store.
// 组装查询条件,并从向量数据库查询文档
var searchRequestToUse = SearchRequest.from(this.searchRequest)
.query(chatClientRequest.prompt().getUserMessage().getText())
.filterExpression(doGetFilterExpression(chatClientRequest.context()))
.build();
List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);
// 2. Create the context from the documents.
// 将文档作为上下文,传递到其它 advisor或者after 方法中使用
Map<String, Object> context = new HashMap<>(chatClientRequest.context());
context.put(RETRIEVED_DOCUMENTS, documents);
String documentContext = documents == null ? ""
: documents.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator()));
// 3. Augment the user prompt with the document context.
// 将文档使用promptTemplate组装为提示语
UserMessage userMessage = chatClientRequest.prompt().getUserMessage();
String augmentedUserText = this.promptTemplate
.render(Map.of("query", userMessage.getText(), "question_answer_context", documentContext));
// 4. Update ChatClientRequest with augmented prompt.
// 访问大模型
return chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
.context(context)
.build();
}
8.7.1 QuestionAnswerAdvisor工作原理
- 拦截用户的提问。
- 使用配置的 Retriever 在 VectorStore 中检索相关文档。
- 将检索到的文档内容作为上下文,注入到原始 Prompt 中。
- 将增强后的 Prompt 发送给大模型。
- 返回生成的答案。
8.7.2 SearchRequest是一个封装的查询
public class SearchRequest {
/**
* Similarity threshold that accepts all search scores. A threshold value of 0.0 means
* any similarity is accepted or disable the similarity threshold filtering. A
* threshold value of 1.0 means an exact match is required.
*/
public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;
/**
* Default value for the top 'k' similar results to return.
*/
public static final int DEFAULT_TOP_K = 4;
/**
* Default value is empty string.
*/
private String query = "";
private int topK = DEFAULT_TOP_K;
private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;
@Nullable
private Filter.Expression filterExpression;
...
}
- query:查询的问题
- topK:返回前几个文档
- similarityThreshold:类似度,大于等于这个值之上的类似度才会匹配
- filterExpression:元数据的过滤条件,主要是过滤Document的metadata元数据属性
8.7.3 实战RAG
只需要为 ChatClient配置一个 QuestionAnswerAdvisor即可,而构建 QuestionAnswerAdvisor的时候,是需要和 VectorStore绑定的。
// chapter08/src/main/java/com/kaifamiao/chapter08/controller/RAGController.java
@RestController
public class RAGController {
private final ChatClient chatClient;
@Autowired
public RAGController(ChatClient.Builder chatClientBuilder,
VectorStore vectorStore) {
//通过Advisors方式,对向量数据库进行封装
Advisor questionAdvisor =QuestionAnswerAdvisor
.builder(vectorStore)
.build();
this.chatClient = chatClientBuilder
.defaultAdvisors(questionAdvisor)
.build();
}
@GetMapping(value="/chat/rag"
, produces = "text/html;charset=UTF-8"
)
public String generate(@RequestParam(value = "question") String question) {
return this.chatClient.prompt()
.user(question)
.call()
.content();
}
}
这样,当用户提问的时候,就会第一到 向量数据库中对问题进行类似度搜索,然后将搜索到的相关提示词再组装后发送给大模型,最终大模型生成答案。
上面只是简单使用了QuestionAnswerAdvisor实际上,还可以对 QuestionAnswerAdvisor进行更详细的配置,列如:
@Autowired
public RAGController(ChatClient.Builder chatClientBuilder,
VectorStore vectorStore) {
String promptTmp = """
{query}
参考信息如下,使用“---------------------”标识包裹在里面的为参考信息。
---------------------
{question_answer_context}
---------------------
鉴于当前的参考信息以及所提供的历史信息(而非任何先入为主的了解),请回复用户评论。如果答案不在上述背景信息中,告知用户您无法回答该问题。
""";
PromptTemplate promptTemplate = PromptTemplate.builder()
.template(promptTmp)
.build();
var b = new FilterExpressionBuilder().eq("source", "官方网站").build();
QuestionAnswerAdvisor questionAdvisor =QuestionAnswerAdvisor
.builder(vectorStore)
.promptTemplate(promptTemplate)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.2) //类似度阈值,只有大于等于该值才会被返回(取值范围:0-1),默认是0(没有类似度排除)
.topK(2) //返回类似度排名前2的文档,默认是4
.filterExpression(b) // 通过文档的元数据过滤
.build())
.build();
this.chatClient = chatClientBuilder
.defaultAdvisors(questionAdvisor)
.build();
}














暂无评论内容