一、前言
Spring AI详解:【Spring AI详解】开启Java生态的智能应用开发新时代(附不同功能的Spring AI实战项目)-CSDN博客
二、RAG解析文档构建知识库
pom.xml
阿里云DashScope大模型:提供通义千问等大语言模型能力,用于文本生成和嵌入向量计算
Ollama本地大模型:支持在本地运行Llama2等开源模型,降低云服务依赖
文档处理组件:包含Markdown和PDF文档读取器,支持从不同格式文件中提取结构化内容
向量存储:为AI应用提供向量数据库存储和检索能力,支持语义搜索
Web支持:基于Spring Boot的Web开发框架,便于构建RESTful API
<!-- 依赖管理配置(BOM模式) -->
<dependencyManagement>
<dependencies>
<!-- 1. Spring AI Alibaba BOM -->
<!-- 功能:管理阿里云AI生态组件的版本(如Dashscope、百炼平台集成等) -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>1.0.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 2. Spring AI BOM -->
<!-- 功能:管理Spring AI核心模块版本(ChatClient、RAG、工具调用等) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 3. Spring Boot BOM -->
<!-- 功能:管理Spring Boot及其starter组件的版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 阿里云DashScope大模型集成 (百炼平台) -->
<!-- 提供与阿里云灵积平台(DashScope)的集成,支持通义千问等大语言模型 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- Ollama本地大模型集成 -->
<!-- 支持在本地运行Ollama服务的大语言模型(如Llama2等) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- Spring Web支持 -->
<!-- 提供Spring MVC和嵌入式Tomcat等Web开发基础功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 单元测试支持 -->
<!-- 包含JUnit、Mockito等测试框架集成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 向量存储支持 -->
<!-- 为AI应用提供向量数据库存储和检索能力 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<!-- Markdown文档读取器 -->
<!-- 支持从Markdown文件中提取结构化内容 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
<!-- PDF文档读取器 -->
<!-- 支持从PDF文件中提取文本内容 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
</dependencies>
application.properties
# 阿里云DashScope API密钥(建议通过环境变量ALI_AI_KEY配置)
spring.ai.dashscope.api-key=${ALI_AI_KEY}
# 指定文本嵌入(Embedding)模型使用text-embedding-v4版本
# 该模型用于将文本转换为向量表示,适用于语义搜索等场景
spring.ai.dashscope.embedding.options.model=text-embedding-v4
# 指定聊天模型使用通义千问3的4B参数版本(需提前在Ollama中拉取)
spring.ai.ollama.chat.model=qwen3:4b
# Ollama服务地址(默认本地11434端口)
spring.ai.ollama.base-url=http://localhost:11434
# 指定嵌入模型使用nomic-embed-text(轻量级开源嵌入模型)
spring.ai.ollama.embedding.model=nomic-embed-text
# 开启Spring AI模块的调试日志(可查看详细的API请求/响应)
logging.level.org.springframework.ai=debug
Application
原型作用域的RestClient构建器:为HTTP请求提供可定制的超时设置(连接和读取均为60秒)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer;
import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import java.time.Duration;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
@Scope("prototype") // 声明为原型作用域的Bean,每次依赖注入时都会创建新实例
RestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {
// 创建RestClient.Builder基础实例
RestClient.Builder builder = RestClient.builder()
// 配置请求工厂,设置超时参数
.requestFactory(ClientHttpRequestFactories.get(
ClientHttpRequestFactorySettings.DEFAULTS
.withReadTimeout(Duration.ofSeconds(60)) // 读取超时60秒
.withConnectTimeout(Duration.ofSeconds(60)) // 连接超时60秒
));
// 应用RestClientBuilderConfigurer的额外配置
return restClientBuilderConfigurer.configure(builder);
}
}
EmbaddingTest
验证不同AI模型的文本嵌入能力:
Ollama本地模型测试:评估本地运行的嵌入模型对中文文本的向量化效果
阿里云模型测试:测试云端服务的嵌入质量,对比向量维度和数值分布
输出分析:打印向量维度和具体数值,便于直观比较不同模型的嵌入结果
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;
/**
* 嵌入向量(Embedding)测试类
* 用于测试不同AI模型的文本嵌入(Embedding)能力
*/
@SpringBootTest
public class EmbaddingTest {
/**
* 测试Ollama本地模型的嵌入能力
* @param ollamaEmbeddingModel 自动注入的Ollama嵌入模型实例
* 使用本地运行的Ollama服务生成文本向量
*/
@Test
public void testEmbadding(@Autowired OllamaEmbeddingModel ollamaEmbeddingModel) {
// 对文本"我叫小小"生成嵌入向量
float[] embedded = ollamaEmbeddingModel.embed("我叫小小");
// 输出向量维度和内容
System.out.println("向量维度长度: " + embedded.length); // 打印向量维度数
System.out.println("向量内容: " + Arrays.toString(embedded)); // 打印向量具体数值
}
/**
* 测试阿里云百炼模型的嵌入能力
* @param embeddingModel 自动注入的DashScope嵌入模型实例
* 使用阿里云百炼平台生成文本向量
*/
@Test
public void testAliEmbadding(@Autowired DashScopeEmbeddingModel embeddingModel) {
// 对文本"我叫小小"生成嵌入向量
float[] embedded = embeddingModel.embed("我叫小小");
// 输出向量维度和内容
System.out.println("向量维度长度: " + embedded.length); // 打印向量维度数
System.out.println("向量内容: " + Arrays.toString(embedded)); // 打印向量具体数值
}
}
文档读取器
纯文本读取:处理简单的TXT文件,保留原始格式
Markdown解析:可配置是否包含代码块、引用块等特定元素,支持元数据添加
PDF处理:
按页读取:将每页内容作为独立文档
按段落读取:基于PDF目录结构进行智能分割,支持坐标调整和页眉过滤
统一接口:不同阅读器返回标准Document对象,便于后续处理
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
@SpringBootTest
public class ReaderTest {
/**
* 测试纯文本文件读取器
* @param resource 通过@Value注入的文本文件资源
*/
@Test
public void testReaderText(@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 创建文本阅读器实例
TextReader textReader = new TextReader(resource);
// 读取文档内容
List<Document> documents = textReader.read();
// 遍历并打印文档内容
for (Document document : documents) {
System.out.println(document.getText());
}
}
/**
* 测试Markdown文件读取器
* @param resource 通过@Value注入的Markdown文件资源
*/
@Test
public void testReaderMD(@Value("classpath:rag/9_横店影视股份有限公司_0.md") Resource resource) {
// 配置Markdown解析选项
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(false) // 是否将分割线作为文档分隔符
.withIncludeCodeBlock(false) // 是否包含代码块作为独立文档
.withIncludeBlockquote(false) // 是否包含引用块作为独立文档
.withAdditionalMetadata("filename", resource.getFilename()) // 添加文件名元数据
.build();
// 创建Markdown阅读器实例
MarkdownDocumentReader markdownDocumentReader = new MarkdownDocumentReader(resource, config);
// 读取文档内容
List<Document> documents = markdownDocumentReader.read();
// 遍历并打印文档内容
for (Document document : documents) {
System.out.println(document.getText());
}
}
/**
* 测试PDF按页读取器
* @param resource 通过@Value注入的PDF文件资源
*/
@Test
public void testReaderPdf(@Value("classpath:rag/平安银行2023年半年度报告摘要.pdf") Resource resource) {
// 创建PDF按页阅读器实例(默认配置)
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(resource,
PdfDocumentReaderConfig.builder().build());
// 读取文档内容(每页作为一个文档)
List<Document> documents = pdfReader.read();
// 遍历并打印文档内容
for (Document document : documents) {
System.out.println(document.getText());
}
}
/**
* 测试PDF按段落读取器(需要PDF包含目录结构)
* @param resource 通过@Value注入的PDF文件资源
*/
@Test
public void testReaderParagraphPdf(@Value("classpath:rag/平安银行2023年半年度报告.pdf") Resource resource) {
// 配置PDF段落解析选项
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(resource,
PdfDocumentReaderConfig.builder()
// 修正某些PDF工具的坐标系问题
.withReversedParagraphPosition(true)
// 设置页面上边距
.withPageTopMargin(0)
// 配置文本提取格式
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
// 设置删除页面顶部N行(处理页眉等)
.withNumberOfTopTextLinesToDelete(0)
.build())
.build());
// 读取文档内容(按段落/章节作为文档)
List<Document> documents = pdfReader.read();
// 遍历并打印文档内容
for (Document document : documents) {
System.out.println(document.getText());
}
}
}
文档切割器
基础Token分割:基于CL100K编码的标准分割方式
中文优化分割器:针对中文特点改进,考虑标点符号和段落结构
元数据增强:
关键词提取:使用AI模型自动生成文档关键词
摘要生成:创建前/中/后三段式摘要,增强检索效果
向量存储集成:处理后的文档可直接存入向量数据库
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.transformer.KeywordMetadataEnricher;
import org.springframework.ai.model.transformer.SummaryMetadataEnricher;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import java.util.List;
@SpringBootTest
public class SplitterTest {
/**
* 测试基于Token的文本分割器(通用版)
* @param resource 测试文本资源
*/
@Test
public void testTokenTextSplitter(@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 1. 读取原始文档
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.read();
// 2. 使用TokenTextSplitter进行分割
TokenTextSplitter splitter = new TokenTextSplitter(); // 默认配置
List<Document> splitDocuments = splitter.apply(documents);
// 3. 输出分割结果
splitDocuments.forEach(System.out::println);
}
/**
* 测试中文优化的Token文本分割器
* @param resource 测试文本资源
*/
@Test
public void testChineseTokenTextSplitter(@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 1. 读取原始文档
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.read();
// 2. 使用中文优化分割器
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter(); // 中文专用实现
List<Document> splitDocuments = splitter.apply(documents);
// 3. 输出分割结果
splitDocuments.forEach(System.out::println);
}
/**
* 测试关键词元数据增强器
* @param vectorStore 向量存储
* @param chatModel 聊天模型(用于生成关键词)
* @param resource 测试文本资源
*/
@Test
public void testKeywordMetadataEnricher(
@Autowired VectorStore vectorStore,
@Autowired DashScopeChatModel chatModel,
@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 1. 读取文档并添加文件名元数据
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
List<Document> documents = textReader.read();
// 2. 使用中文分割器预处理
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter();
documents = splitter.apply(documents);
// 3. 使用关键词增强器(生成5个关键词)
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(chatModel, 5);
documents = enricher.apply(documents);
// 4. 存储到向量数据库
vectorStore.add(documents);
// 5. 基于关键词进行检索
/* 假如允许自定义提示词:KeywordMetadataEnricher.KEYWORDS_TEMPLATE= """
给我按照我提供的内容{context_str},生成%s个关键字;
允许的关键字有这些:
['退票','预定']
只允许在这个关键字范围进行选择。
""";*/
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.filterExpression("filename in ('退票')") // 文件名过滤
.filterExpression("excerpt_keywords in ('退票')") // 关键词过滤
.build());
// 6. 输出检索结果
for (Document document : results) {
System.out.println(document.getText());
System.out.println(document.getText().length());
}
}
/**
* 测试配置类 - 提供向量存储Bean
*/
@TestConfiguration
static class TestConfig {
@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
/**
* 测试摘要元数据增强器
* @param chatModel 聊天模型(用于生成摘要)
* @param resource 测试文本资源
*/
@Test
public void testSummaryMetadataEnricher(
@Autowired DashScopeChatModel chatModel,
@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 1. 读取文档并添加元数据
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
List<Document> documents = textReader.read();
// 2. 使用中文分割器(带精细参数配置)
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter(
130, // 块大小
10, // 块重叠
5, // 最小块大小
10000, // 最大输入长度
true // 是否计算token
);
List<Document> splitDocuments = splitter.apply(documents);
// 3. 应用摘要增强器(生成前/中/后三段摘要)
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(
chatModel,
List.of(
SummaryMetadataEnricher.SummaryType.PREVIOUS,
SummaryMetadataEnricher.SummaryType.CURRENT,
SummaryMetadataEnricher.SummaryType.NEXT
)
);
List<Document> enhancedDocuments = enricher.apply(splitDocuments);
// 4. 输出增强后的文档
System.out.println(enhancedDocuments);
}
}
自定义中文优化分割器
智能断句:优先在句号、问号等中文标点处分割
可配置参数:
块大小、重叠区域、最小长度阈值
最大分块数限制,避免长文本处理过载
编码处理:基于CL100K_BASE编码,准确计算中英混合文本的token数量
格式保留:可选是否保留换行符等分隔符
package com.xushu.springai.rag.ELT;
import com.knuddels.jtokkit.Encodings;
import com.knuddels.jtokkit.api.Encoding;
import com.knuddels.jtokkit.api.EncodingRegistry;
import com.knuddels.jtokkit.api.EncodingType;
import com.knuddels.jtokkit.api.IntArrayList;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.List;
public class ChineseTokenTextSplitter extends TextSplitter {
private static final int DEFAULT_CHUNK_SIZE = 800;
private static final int MIN_CHUNK_SIZE_CHARS = 350;
private static final int MIN_CHUNK_LENGTH_TO_EMBED = 5;
private static final int MAX_NUM_CHUNKS = 10000;
private static final boolean KEEP_SEPARATOR = true;
private final EncodingRegistry registry = Encodings.newLazyEncodingRegistry();
private final Encoding encoding = this.registry.getEncoding(EncodingType.CL100K_BASE);
// The target size of each text chunk in tokens
private final int chunkSize;
// The minimum size of each text chunk in characters
private final int minChunkSizeChars;
// Discard chunks shorter than this
private final int minChunkLengthToEmbed;
// The maximum number of chunks to generate from a text
private final int maxNumChunks;
private final boolean keepSeparator;
public ChineseTokenTextSplitter() {
this(DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE_CHARS, MIN_CHUNK_LENGTH_TO_EMBED, MAX_NUM_CHUNKS, KEEP_SEPARATOR);
}
public ChineseTokenTextSplitter(boolean keepSeparator) {
this(DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE_CHARS, MIN_CHUNK_LENGTH_TO_EMBED, MAX_NUM_CHUNKS, keepSeparator);
}
public ChineseTokenTextSplitter(int chunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks,
boolean keepSeparator) {
this.chunkSize = chunkSize;
this.minChunkSizeChars = minChunkSizeChars;
this.minChunkLengthToEmbed = minChunkLengthToEmbed;
this.maxNumChunks = maxNumChunks;
this.keepSeparator = keepSeparator;
}
public static Builder builder() {
return new Builder();
}
@Override
protected List<String> splitText(String text) {
return doSplit(text, this.chunkSize);
}
protected List<String> doSplit(String text, int chunkSize) {
if (text == null || text.trim().isEmpty()) {
return new ArrayList<>();
}
List<Integer> tokens = getEncodedTokens(text);
List<String> chunks = new ArrayList<>();
int num_chunks = 0;
// maxNumChunks多能分多少个块, 超过了就不管了
while (!tokens.isEmpty() && num_chunks < this.maxNumChunks) {
// 按照chunkSize进行分隔
List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));
String chunkText = decodeTokens(chunk);
// Skip the chunk if it is empty or whitespace
if (chunkText.trim().isEmpty()) {
tokens = tokens.subList(chunk.size(), tokens.size());
continue;
}
// Find the last period or punctuation mark in the chunk
int lastPunctuation =
Math.max(chunkText.lastIndexOf('.'),
Math.max(chunkText.lastIndexOf('?'),
Math.max(chunkText.lastIndexOf('!'),
Math.max(chunkText.lastIndexOf('\n'),
Math.max(chunkText.lastIndexOf('。'),
Math.max(chunkText.lastIndexOf('?'),
chunkText.lastIndexOf('!')
))))));
// 按照句子截取之后长度 > minChunkSizeChars
if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {
// 保留按照句子截取之后的内容
chunkText = chunkText.substring(0, lastPunctuation + 1);
}
// 按照句子截取之后长度 < minChunkSizeChars 保留原块
// keepSeparator=true 替换/r/n =false不管
String chunkTextToAppend = (this.keepSeparator) ? chunkText.trim()
: chunkText.replace(System.lineSeparator(), " ").trim();
// 替换/r/n之后的内容是不是<this.minChunkLengthToEmbed 忽略
if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {
chunks.add(chunkTextToAppend);
}
// Remove the tokens corresponding to the chunk text from the remaining tokens
tokens = tokens.subList(getEncodedTokens(chunkText).size(), tokens.size());
num_chunks++;
}
// Handle the remaining tokens
if (!tokens.isEmpty()) {
String remaining_text = decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();
if (remaining_text.length() > this.minChunkLengthToEmbed) {
chunks.add(remaining_text);
}
}
return chunks;
}
private List<Integer> getEncodedTokens(String text) {
Assert.notNull(text, "Text must not be null");
return this.encoding.encode(text).boxed();
}
private String decodeTokens(List<Integer> tokens) {
Assert.notNull(tokens, "Tokens must not be null");
var tokensIntArray = new IntArrayList(tokens.size());
tokens.forEach(tokensIntArray::add);
return this.encoding.decode(tokensIntArray);
}
public static final class Builder {
private int chunkSize = DEFAULT_CHUNK_SIZE;
private int minChunkSizeChars = MIN_CHUNK_SIZE_CHARS;
private int minChunkLengthToEmbed = MIN_CHUNK_LENGTH_TO_EMBED;
private int maxNumChunks = MAX_NUM_CHUNKS;
private boolean keepSeparator = KEEP_SEPARATOR;
private Builder() {
}
public Builder withChunkSize(int chunkSize) {
this.chunkSize = chunkSize;
return this;
}
public Builder withMinChunkSizeChars(int minChunkSizeChars) {
this.minChunkSizeChars = minChunkSizeChars;
return this;
}
public Builder withMinChunkLengthToEmbed(int minChunkLengthToEmbed) {
this.minChunkLengthToEmbed = minChunkLengthToEmbed;
return this;
}
public Builder withMaxNumChunks(int maxNumChunks) {
this.maxNumChunks = maxNumChunks;
return this;
}
public Builder withKeepSeparator(boolean keepSeparator) {
this.keepSeparator = keepSeparator;
return this;
}
public ChineseTokenTextSplitter build() {
return new ChineseTokenTextSplitter(this.chunkSize, this.minChunkSizeChars, this.minChunkLengthToEmbed,
this.maxNumChunks, this.keepSeparator);
}
}
}
testVectorStore向量数据库
演示向量数据库的核心功能:
文档存储:将文本转换为向量并索引
语义搜索:基于余弦相似度查找相关内容
实战示例:航班预订和取消政策的存储与查询
结果排序:按相似度得分返回最匹配的文档片段
阈值过滤:排除低相关性结果,提高答案质量
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* 向量存储测试类
* 演示如何使用VectorStore进行文档的向量化存储和语义搜索
*/
@SpringBootTest
public class VectorStoreTest {
/**
* 测试向量存储和语义搜索功能
* @param vectorStore 自动注入的向量存储实例
* 支持文档的向量化存储和相似度搜索
*/
@Test
public void testVectorStore(@Autowired VectorStore vectorStore) {
// 创建第一个文档:关于航班预订的条款
Document doc = Document.builder()
.text("""
预订航班:
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
""")
.build();
// 创建第二个文档:关于取消预订的条款
Document doc2 = Document.builder()
.text("""
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
""")
.build();
// 将文档添加到向量存储中(会自动进行文本向量化)
vectorStore.add(List.of(doc, doc2));
// 构建搜索请求
SearchRequest searchRequest = SearchRequest.builder()
.query("退票") // 搜索关键词
.topK(2) // 返回最相似的2个结果
.similarityThreshold(0.5) // 相似度阈值,过滤低分结果
.build();
// 执行相似度搜索
List<Document> documents = vectorStore.similaritySearch(searchRequest);
// 打印搜索结果
for (Document document : documents) {
System.out.println("匹配文档内容: \n" + document.getText());
System.out.println("相似度得分: " + document.getScore());
System.out.println("----------------------------------");
}
}
}
检索重排序
两阶段检索:先获取大量候选文档,再进行精细排序
专用重排序模型:使用DashScope的gte-rerank-v2模型优化结果
import com.alibaba.cloud.ai.advisor.RetrievalRerankAdvisor;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel;
import com.xushu.springai.rag.ELT.ChineseTokenTextSplitter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import java.util.List;
/**
* 检索重排序(Rerank)功能测试类
* 演示如何结合DashScope的嵌入模型、重排序模型和聊天模型实现高质量检索增强生成(RAG)
*/
@SpringBootTest
public class RerankTest {
/**
* 初始化测试数据
* 1. 读取文本文件
* 2. 使用中文分词器分割文档
* 3. 将分割后的文档向量化并存储
*/
@BeforeEach
public void init(@Autowired VectorStore vectorStore,
@Value("classpath:rag/terms-of-service.txt") Resource resource) {
// 读取文本文件(如服务条款文档)
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
List<Document> documents = textReader.read();
// 使用自定义中文分词器分割文档(优化中文处理)
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter(
80, // 每个分块最大token数
10, // 分块重叠token数
5, // 最小分块token数
10000,// 最大输入长度
true // 是否启用详细日志
);
List<Document> chunkedDocuments = splitter.apply(documents);
// 向量化并存储文档(自动调用DashScope嵌入模型)
vectorStore.add(chunkedDocuments);
}
/**
* 测试检索重排序流程
* 1. 通过向量存储检索相关文档
* 2. 使用DashScope重排序模型优化结果
* 3. 生成最终回答
*/
@Test
public void testRerank(@Autowired VectorStore vectorStore,
@Autowired DashScopeRerankModel dashScopeRerankModel,
@Autowired DashScopeChatModel dashScopeChatModel) {
// 构建ChatClient(使用DashScope聊天模型)
ChatClient chatClient = ChatClient.builder(dashScopeChatModel).build();
// 创建检索重排序顾问(核心组件)
RetrievalRerankAdvisor retrievalRerankAdvisor = new RetrievalRerankAdvisor(
vectorStore,
dashScopeRerankModel, // 使用gte-rerank-v2模型
SearchRequest.builder()
.topK(200) // 初步检索200个候选文档
.build()
);
// 执行RAG流程(带重排序优化)
String content = chatClient.prompt()
.user("退费费用?") // 用户查询
.advisors(retrievalRerankAdvisor) // 注入重排序逻辑
.call()
.content();
System.out.println("最终回答: " + content);
}
/**
* 测试配置类
* 初始化基于Ollama的向量存储(可替换为DashScopeEmbeddingModel)
*/
@TestConfiguration
static class TestConfig {
@Bean
public VectorStore vectorStore(OllamaEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
}
基础RAG流程和高级RAG特性
基础检索增强:简单直接的问答流程
查询重写:优化用户问题的表述方式
多语言支持:通过翻译增强跨语言检索能力
元数据过滤:基于文档属性精确定位信息
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.preretrieval.query.translation.TranslationQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* Spring AI RAG功能集成测试类
* 演示如何结合DashScope大模型和向量存储实现检索增强生成
* 核心功能包括:基础检索增强、查询重写、多语言翻译增强等
*/
@SpringBootTest
public class ChatClientRagTest {
private ChatClient chatClient;
/**
* 初始化测试数据
* 向向量存储中添加航班预订和取消政策的文档
*/
@BeforeEach
public void init(@Autowired DashScopeChatModel chatModel,
@Autowired VectorStore vectorStore) {
// 构建测试文档
Document doc = Document.builder()
.text("""
预订航班:
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生25的费用。
""")
.build();
Document doc2 = Document.builder()
.text("""
取消预订:
- 最晚在航班起飞前48小时取消。
- 取消费用:经济舱75美元,豪华经济舱50美元,商务舱25美元。
- 退款将在7个工作日内处理。
""")
.build();
// 将文档向量化并存储
vectorStore.add(List.of(doc, doc2));
}
/**
* 基础RAG测试
* 使用QuestionAnswerAdvisor实现简单的检索增强
*/
@Test
public void testRag(@Autowired DashScopeChatModel dashScopeChatModel,
@Autowired VectorStore vectorStore) {
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(SimpleLoggerAdvisor.builder().build()) // 添加日志记录
.build();
String content = chatClient.prompt()
.user("退票需要多少费用?")
.advisors(
// 组合使用日志顾问和检索增强顾问
SimpleLoggerAdvisor.builder().build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5) // 返回最相关的5个文档片段
.similarityThreshold(0.6) // 相似度阈值0.6
.build())
.build()
)
.call()
.content();
System.out.println("RAG回答结果: " + content);
}
/**
* 带过滤条件的RAG测试
* 演示如何通过元数据过滤检索结果
*/
@Test
public void testRag2(@Autowired DashScopeChatModel dashScopeChatModel,
@Autowired VectorStore vectorStore) {
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(SimpleLoggerAdvisor.builder().build())
.build();
String content = chatClient.prompt()
.user("退票需要多少费用?")
.advisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.1) // 更宽松的相似度阈值
// 可添加元数据过滤:.filterExpression("category == 'cancellation'")
.build())
.build()
)
.call()
.content();
System.out.println("带过滤的RAG回答: " + content);
}
/**
* 高级RAG流程测试
* 使用RetrievalAugmentationAdvisor实现多阶段增强
*/
@Test
public void testRag3(@Autowired VectorStore vectorStore,
@Autowired DashScopeChatModel dashScopeChatModel) {
chatClient = ChatClient.builder(dashScopeChatModel)
.defaultAdvisors(SimpleLoggerAdvisor.builder().build())
.build();
// 构建完整的检索增强流程
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.0) // 接受所有相似度结果
.vectorStore(vectorStore)
.build())
// 上下文增强配置
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(false) // 禁止无上下文回答
.emptyContextPromptTemplate(new PromptTemplate("未找到相关知识,请重新提问"))
.build())
// 查询重写转换器(去除情绪化内容)
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(dashScopeChatModel))
.targetSearchSystem("航空票务助手")
.build())
// 查询翻译转换器(支持多语言)
.queryTransformers(TranslationQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(dashScopeChatModel))
.targetLanguage("english")
.build())
// 文档后处理器(可添加日志或过滤)
.documentPostProcessors((query, documents) -> {
System.out.println("优化后查询: " + query.text());
System.out.println("检索到文档数: " + documents.size());
return documents;
})
.build();
// 测试包含情绪化表达的查询
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user("我今天心情不好,不想去玩了,能不能告诉我退票需要多少钱?")
.call()
.content();
System.out.println("高级RAG回答: " + answer);
}
/**
* 测试配置类
* 初始化内存型向量存储
*/
@TestConfiguration
static class TestConfig {
@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
}
RAG效果评估
RetrievalAugmentationAdvisor:负责检索与用户查询相关的文档
VectorStoreDocumentRetriever:基于向量数据库实现语义搜索
RelevancyEvaluator:评估生成回答与问题和上下文的关联程度
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.evaluation.RelevancyEvaluator;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.evaluation.EvaluationRequest;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* RAG(检索增强生成)效果评估测试类
* 演示如何评估AI回答与用户问题及检索内容的相关性
*/
@SpringBootTest
public class RagEvalTest {
/**
* RAG流程相关性评估测试
* 完整流程:文档检索 -> 生成回答 -> 相关性评估
*/
@Test
public void testRag(@Autowired VectorStore vectorStore,
@Autowired DashScopeChatModel dashScopeChatModel) {
// 构建测试知识库文档(航班预订相关条款)
List<Document> documents = List.of(
new Document("""
1. 预订航班
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生25的费用。
"""),
new Document("""
2. 更改预订
- 允许在航班起飞前24小时更改。
- 通过在线更改或联系我们的支持人员。
- 改签费:经济舱50,豪华经济舱30,商务舱免费。
"""),
new Document("""
3. 取消预订
- 最晚在航班起飞前48小时取消。
- 取消费用:经济舱75美元,豪华经济舱50美元,商务舱25美元。
- 退款将在7个工作日内处理。
"""));
// 将文档存入向量数据库
vectorStore.add(documents);
// 构建RAG增强顾问(负责检索相关文档)
RetrievalAugmentationAdvisor retrievalAugmentationAdvisor =
RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.build())
.build();
// 测试查询(与文档内容无关的问题)
String query = "我叫什么名字";
// 执行RAG流程
ChatResponse chatResponse = ChatClient.builder(dashScopeChatModel)
.build()
.prompt(query)
.advisors(retrievalAugmentationAdvisor)
.call()
.chatResponse();
/**
* 构建评估请求参数:
* 1. 原始用户问题
* 2. RAG流程检索到的上下文文档
* 3. AI生成的最终回答
*/
EvaluationRequest evaluationRequest = new EvaluationRequest(
query,
chatResponse.getMetadata().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT),
chatResponse.getResult().getOutput().getText()
);
// 使用DashScope模型作为评估器
RelevancyEvaluator evaluator = new RelevancyEvaluator(
ChatClient.builder(dashScopeChatModel)
);
// 执行相关性评估
EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest);
// 输出评估结果和AI回答
System.out.println("=== 评估结果 ===");
System.out.println(evaluationResponse);
System.out.println("\n=== AI回答 ===");
System.out.println(chatResponse.getResult().getOutput().getText());
}
/**
* 测试配置类
* 初始化基于Ollama的向量存储
*/
@TestConfiguration
static class TestConfig {
@Bean
public VectorStore vectorStore(OllamaEmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
}
}
AI模型评估
AI模型评估测试类专注于对AI回答进行事实检查和相关性验证,确保回答的准确性和适用性
事实检查(Fact Checking)
将AI回答与参考文档进行比对,验证事实一致性
FactCheckingEvaluator使用DashScope模型评估回答的准确性
相关性评估(Relevancy)
原理:评估回答与上下文/问题的语义相关性
实现:RelevancyEvaluator分析声明与上下文的逻辑关联
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.evaluation.FactCheckingEvaluator;
import org.springframework.ai.chat.evaluation.RelevancyEvaluator;
import org.springframework.ai.document.Document;
import org.springframework.ai.evaluation.EvaluationRequest;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Collections;
import java.util.List;
/**
* AI模型评估测试类
* 包含事实检查(Fact Checking)和相关性(Relevancy)评估测试
*/
@SpringBootTest
public class FactCheckingTest {
/**
* 事实检查评估测试
* 验证AI回答是否与提供的参考文档一致
* @param chatModel 自动注入的DashScope聊天模型实例
*/
@Test
void testFactChecking(@Autowired DashScopeChatModel chatModel) {
// 创建事实检查评估器,使用DashScope模型作为评估器
var factCheckingEvaluator = new FactCheckingEvaluator(ChatClient.builder(chatModel));
// 构建参考文档(事实依据)
Document doc = Document.builder()
.text("""
取消预订:
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在 7 个工作日内处理。
""")
.build();
// AI生成的回答(待验证)
String response = "经济舱取消费用75 美元";
// 创建评估请求,包含参考文档和AI回答
EvaluationRequest evaluationRequest = new EvaluationRequest(List.of(doc), response);
// 执行事实检查评估
EvaluationResponse evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
// 输出评估结果(包含通过/失败和评估详情)
System.out.println("事实检查评估结果: " + evaluationResponse);
}
/**
* 相关性评估测试
* 验证AI回答是否与用户查询相关
* @param chatModel 自动注入的DashScope聊天模型实例
*/
@Test
void testRelevancyEvaluator(@Autowired DashScopeChatModel chatModel) {
// 创建相关性评估器,使用DashScope模型作为评估器
var evaluator = new RelevancyEvaluator(ChatClient.builder(chatModel));
// 测试数据
String context = "地球是距离太阳的第三颗行星,也是已知唯一孕育生命的天文物体。";
String claim = "地球是距离太阳的第四颗行星,也是已知唯一孕育生命的天文物体。";
// 创建评估请求,包含上下文和待评估声明
EvaluationRequest evaluationRequest = new EvaluationRequest(context, Collections.emptyList(), claim);
// 执行相关性评估
EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest);
// 输出评估结果(包含相关度评分和评估详情)
System.out.println("相关性评估结果: " + evaluationResponse);
}
}
上一篇:【Spring AI快速上手 (三)】Tool实现业务系统对接-CSDN博客
下一篇:【Spring AI快速上手 (五)】Agent复杂任务智能体初探-CSDN博客
实战示例:【Spring AI实战】ChatPDF实现RAG知识库_chatpdf 关键技术及实现-CSDN博客
该文章展示了从文档处理到智能问答的完整技术栈,特别针对中文场景进行了优化,同时支持本地和云端AI服务的灵活组合,为企业级知识库应用提供了可靠参考实现。
有任何问题或建议欢迎评论区留言讨论!