什么是 Spring AI Alibaba
“Spring AI Alibaba(SAA) 是一款以 Spring AI 为基础,深度集成百炼平台,支持 ChatBot、工作流、多智能体应用开发模式的 AI 框架。”
总的来说spring-ai-alibaba就是在spring-AI所拥有的功能基础上,对百炼平台进行了适配,个人觉得后面可能会和spring-cloud-alibaba一样,深受国内外开发者喜爱,目前的最新版本1.0.0.2对标的是Spring-AI的1.0.0GA版,详细教程可以去spring-ai-alibaba官方文档查看,这里不做详细赘述。
什么是RAG
检索增强生成(Retrieval Augmented Generation, RAG)是一种有用的技术,用于克服大型语言模型在处理长篇内容、事实准确性和上下文感知能力方面的局限性。 Spring AI 通过提供模块化架构来支持 RAG,该架构允许您自己构建自定义 RAG 流程,或者使用 Advisor API 来使用开箱即用的 RAG 流程。
举个例子,下面有这样一个场景:
情景:你正在写一篇关于“量子计算的未来影响”的报告。
你手头有一个最新的大型语言模型(LLM),比如 GPT-4o 或 Qwen。
没有 RAG 的情况(纯 LLM 的局限性):
你直接向 LLM 提问:“量子计算对全球经济的未来影响是什么?请列举具体的公司和技术突破。”
LLM 可能会这样回答:
-
“量子计算有望彻底改变多个行业,例如金融、医药和材料科学。它可能加速新材料的发现,优化复杂的金融模型,并为药物开发提供前所未有的能力。一些领先的公司正在投资这一领域,如 IBM、Google 和 Microsoft。”
-
问题所在:
- 通用性强,但缺乏深度和特异性: 回答听起来合理,但信息非常泛泛,没有提到具体的“未来影响”的细节、量化数据或最新的研究进展。
- 事实准确性可能不足: 提到的公司是正确的,但它可能无法提供最新的公司动态、具体的投资项目,或者某些公司在该领域面临的挑战。
- 缺乏最新上下文: LLM 的训练数据有截止日期(例如,到 2023 年初),它无法访问 2024 年甚至 2025 年最新的量子计算研究报告、公司财报或行业分析。
- 无法溯源: 你无法知道这些信息是从哪里来的,它是否真实可靠。 有了 RAG 的情况(检索增强生成):
现在,你有了 RAG 系统。这个系统连接了:
-
你的 LLM
-
一个知识库/检索系统:这个知识库包含了大量高质量、最新的、与“量子计算”相关的文档,例如:
- 最新的行业研究报告(如高盛 2024 年关于量子计算的分析)
- 顶尖大学(如 MIT、Stanford)的最新论文
- 权威科技新闻网站的深度报道
- 特定公司(如 Quantinuum、IonQ)的官方白皮书和年度报告。
RAG 的工作流程:
-
用户提问: 你向 RAG 系统提问:“量子计算对全球经济的未来影响是什么?请列举具体的公司和技术突破。”
-
检索(Retrieval): RAG 系统会首先分析你的问题,并将其转化为一个或多个查询,去知识库中搜索相关且高质量的文档片段。
-
例如,它可能会检索到:
- 一篇 2024 年关于“量子加密标准发展”的NIST报告片段。
- Quantinuum 公司 2025 年 Q1 财报中关于其 H-Series 处理器性能提升的段落。
- 一篇分析量子算法在药物研发中应用潜力的最新科学论文的摘要。
- 高盛 2024 年关于量子经济影响的报告中,提到金融模型加速的预测数据。
-
-
增强(Augmentation): RAG 系统将这些检索到的具体、最新、有事实依据的文档片段作为“上下文”或“参考资料”,连同你的原始问题一起,提供给 LLM。
- LLM 接收到的是:“[用户问题] + [多篇相关文档片段]”。
-
生成(Generation): LLM 接收到这些高质量的上下文信息后,利用其强大的语言理解和生成能力,结合这些信息来回答你的问题。
LLM 使用 RAG 后可能给出的回答:
-
“量子计算预计将在未来五年内对全球经济产生显著影响,特别是在以下几个关键领域:
-
金融建模: 据高盛 2024 年报告预测,量子算法能将某些复杂金融衍生品的定价速度提高 100 倍,从而实现更精准的风险管理和高频交易策略。这可能重塑投资银行的量化分析部门。
-
新药发现: 最新研究表明,量子模拟能够以前所未有的精度模拟分子行为,加速新化合物的发现。例如,某 2025 年论文展示了量子算法在设计新型抗癌药物方面的突破,可能将药物研发周期缩短 30%。
-
网络安全: 随着量子计算机的进步,当前的加密标准面临挑战。NIST 正在推动后量子加密(PQC)标准的制定,这将在全球范围内引发大规模的加密系统升级,预计未来三年内相关产业投资将达数十亿美元。
-
领先公司及其技术:
- Quantinuum: 其最新的 H-Series 量子处理器在 2025 年 Q1 取得了XX量子体积(Quantum Volume)的突破,这使其在优化化学模拟和金融模型方面具有领先优势。
- IonQ: 正专注于离子阱技术的商业化,并与[具体公司名称]合作开发针对[具体行业]的量子解决方案。
- IBM: 持续推进其量子系统路线图,并推出了[具体新平台或软件],旨在降低企业采用门槛。”
-
上面的场景案例其实不难看出,RAG的作用:
-
提升事实准确性: 基于实时、权威的外部知识生成答案,减少 LLM“胡说八道”(hallucination)的可能性。
-
提供最新信息: 即使 LLM 的训练数据已经过时,RAG 也能通过检索最新文档来提供时效性强的信息。
-
增强上下文感知: 为 LLM 提供了具体、相关的背景信息,使其生成更精准、更深入的回答。
-
可溯源性: 由于答案是基于检索到的文档生成的,通常可以展示引用的来源(例如,来自哪篇报告、哪篇论文),提高了答案的透明度和可信度。
-
处理长篇内容: LLM 的输入有长度限制。RAG 可以智能地只检索相关的“片段”,而不是将整个长文档都喂给 LLM,从而有效利用 LLM 的上下文窗口。
所以,RAG 就像是给 LLM 配备了一个“图书馆管理员”和“研究助理”,让它在回答问题时能随时查阅最新的、最相关的资料,而不是仅仅依赖自己脑中的“记忆”,这点在我们利用构建本地知识库进行知识检索的时候尤为重要。
Spring-ai-Alibaba中的RAG
Spring-ai-Alibaba的RAG沿用了Spring-ai的内容,下面我带大家来做个入门案例,方便大家理解RAG的过程和使用。
相关依赖包
下面以Gradle举例:
ext{
springAiAlibabaVersion = '1.0.0.2'
springAiVersion = '1.0.0'
dashScopeSDKVersion = '2.20.5'
protobufJavaVersion = '3.25.3'
openAIJavaVersion = '2.7.0'
}
//在dependencies里面加入下面这些依赖包
implementation platform("org.springframework.ai:spring-ai-bom:${springAiVersion}")
implementation 'com.zaxxer:HikariCP:5.0.1'
// 引入springAiAlibaba依赖
implementation platform("com.alibaba.cloud.ai:spring-ai-alibaba-bom:${springAiAlibabaVersion}")
// 引入springAiAlibaba依赖包
implementation 'com.alibaba.cloud.ai:spring-ai-alibaba-starter-dashscope'
//引入springAI相关依赖包
implementation 'org.springframework.ai:spring-ai-deepseek'
//引入RAG检索增强生成相关依赖
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
implementation 'org.springframework.ai:spring-ai-jsoup-document-reader'
implementation 'org.springframework.ai:spring-ai-markdown-document-reader'
implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
implementation 'org.springframework.ai:spring-ai-tika-document-reader'
implementation 'org.springframework.ai:spring-ai-tika-document-reader'
//引入Milvus VectorStore依赖包
implementation 'org.springframework.ai:spring-ai-starter-vector-store-milvus'
implementation "com.google.protobuf:protobuf-java:${protobufJavaVersion}"
implementation "com.alibaba:dashscope-sdk-java:${dashScopeSDKVersion}"
implementation "com.openai:openai-java:${openAIJavaVersion}"
配置文件
首先是chat配置,这里使用qwen-plus。
注:形似${xxx}为环境变量,具体值各位自行补充,这里不方便透露,apikey可以去百炼平台获取。
第二个就是向量数据库的配置了,这里使用milvus作为向量存储的数据库,当然也可以用es
注:SpringAI官网支持的向量数据库:
配置类
ChatClient配置:这里为了后续可以灵活切换大模型因此使用了手动注册,其实配置文件写好后可以直接使用主动注入获取ChatClient。
嵌入模型配置:
RAG相关配置:
向量存储数据库的初始化
根据实际使用时遇到的问题发现,只配置向量存储是不会自动初始化数据库的,需要我们自行初始化,下面代码是我调试很多遍后,milvus的初始化:
@Component
@Slf4j
public class VectorDBInit {
@Value("${spring.ai.vectorstore.milvus.collectionName}")
private String collectionName;
// 替换为您的嵌入模型维度,具体维度可以看下各个嵌入模型的官方文档说明
private int dimension = 1536;
@Autowired
private MilvusClient milvusClient;
@PostConstruct
void run() {
// 检查 Collection 是否存在
if (!milvusClient.hasCollection(HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()).getData()) {
// 定义 Collection 的 Schema
CreateCollectionParam.Builder collectionBuilder = CreateCollectionParam.newBuilder()
.withCollectionName(collectionName)
.withDescription("Vector store for Spring AI documents");
collectionBuilder
.addFieldType(FieldType.newBuilder()
.withName("id")
.withDataType(DataType.VarChar)
.withMaxLength(256)
.withPrimaryKey(true)
.withAutoID(false)
.build())
.addFieldType(FieldType.newBuilder()
.withName("embedding")
.withDataType(DataType.FloatVector)
.withDimension(dimension)
.build())
.addFieldType(FieldType.newBuilder()
.withName("text") // 添加原始文本字段
.withDataType(DataType.VarChar)
.withMaxLength(16777216)
.build())
.addFieldType(FieldType.newBuilder() // 添加元数据字段
.withName("metadata")
.withDataType(DataType.VarChar)
.withMaxLength(165535)
.build());
// 创建 Collection
milvusClient.createCollection(collectionBuilder.build());
log.info(String.format("Collection '{}' created successfully.", collectionName));
// 创建索引
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName("embedding")
.withIndexType(IndexType.HNSW)
.withMetricType(MetricType.COSINE) // 根据您的需求选择距离度量
.withExtraParam("{"M":8,"efConstruction":64}") // 索引参数
.build();
milvusClient.createIndex(indexParam);
log.info(String.format("Index created on collection '%s' field 'vector'.", collectionName));
// 加载 Collection
milvusClient.loadCollection(LoadCollectionParam.newBuilder().withCollectionName(collectionName).build());
log.info(String.format("Collection '%s' loaded.", collectionName));
} else {
log.info(String.format("Collection '%s' already exists.", collectionName));
// 确保 Collection 已加载
milvusClient.loadCollection(LoadCollectionParam.newBuilder().withCollectionName(collectionName).build());
log.info(String.format("Collection '%s' loaded.", collectionName));
}
}
}
好了,上面就是我们使用RAG需要的启动步骤了,接下来我们开始正式介绍RAG,想要实现RAG,就必须对用输入的文本或者文件进行信息提取、数据清洗以及数据向量化并向量存储,官方提供了ETL方式。
“抽取、转换和加载(Extract, Transform, Load, ETL)框架是检索增强生成(RAG)用例中数据处理的支柱。 ETL 管线编排了数据从原始数据源到结构化向量存储的流动,确保数据以最佳格式存在,以便 AI 模型进行检索。 RAG 用例旨在通过从数据主体中检索相关信息来增强生成模型的能力,从而提高生成输出的质量和相关性。”
首先是文件/文本读取并构造成Document对象,这里我封装了方法供大家使用,但是目前没有详细优化,效果可能很一般,但是作为入门学习够了。
/**
* @author ashao
*/
@Service
@Slf4j
public class DocumentReaderServiceImpl implements DocumentReaderService {
// 文件上传目录
// 建议使用绝对路径,例如 "/opt/app_data/uploads/"
// 或者基于当前工作目录的相对路径,例如 "./uploads/"
// 生产环境中,请确保此目录有写入权限且位于数据盘而非应用部署盘
private static final String UPLOAD_DIR = "./uploads/";
@Override
public Resource uploadFile(MultipartFile file) {
Resource resource = null;
if (file.isEmpty()) {
log.warn("接收到一个空文件上传请求。");
// 这里不返回给前端,但可以在日志中记录
throw new RuntimeException("空文件");
}
try {
// 确保上传目录存在
Path uploadPath = Paths.get(UPLOAD_DIR);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
log.info("创建上传目录: {}", uploadPath.toAbsolutePath());
}
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 获取文件扩展名 (例如 .txt, .pdf)
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < originalFilename.length() - 1) {
fileExtension = originalFilename.substring(dotIndex);
}
// **推荐:生成一个唯一的文件名以避免冲突和安全问题**
String uniqueFileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
+ "_" + (int)(Math.random() * 1000) + fileExtension;
// 构建目标文件路径
Path filePath = Paths.get(UPLOAD_DIR + uniqueFileName);
// 将文件数据写入到服务器文件系统
Files.copy(file.getInputStream(), filePath); // 使用 Files.copy 更高效和健壮
log.info("文件上传成功。原始文件名: '{}', 保存路径: '{}'", originalFilename, filePath.toAbsolutePath());
// 之后,您可以在这里对这个文件 (filePath) 进行后续操作
// 例如:解析文件内容、将其路径存入数据库、触发其他业务逻辑等
// processFile(filePath); // 调用其他服务方法处理文件
resource = new UrlResource(filePath.toUri());
} catch (IOException e) {
log.error("文件上传失败: {}", e.getMessage(), e);
// 异常时同样不直接返回给前端错误消息,但会在日志中记录
// 如果需要,也可以抛出一个自定义异常,由全局异常处理器处理
}
return resource;
}
/**
* 将该记录中名为 "description" 的字段的值作为新的 Document 对象的主体内容。
* 将该记录中名为 "content" 的字段的值作为新的 Document 对象的元数据。
* 将所有生成的 Document 对象收集到一个列表中并返回。
* @param resource
* @return
*/
@Override
public List<Document> loadJsonDocuments(Resource resource) {
JsonReader jsonReader = new JsonReader(resource, "description", "content");
return jsonReader.get();
}
@Override
public List<Document> loadText(Resource resource) {
TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
return textReader.read();
}
@Override
public List<Document> loadHtml(Resource resource) {
JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
// 指定要提取的 HTML 元素(article p)。
.selector("article p")
// 设置字符编码(UTF-8)
.charset("UTF-8")
// 选择是否包含链接 URL。
.includeLinkUrls(true)
// 定义要从 meta 标签中提取的元数据(author, date)
.metadataTags(List.of("author", "date"))
// 添加额外的、通用的元数据(source)
.additionalMetadata("source", resource.getFilename())
.build();
JsoupDocumentReader reader = new JsoupDocumentReader(resource, config);
return reader.get();
}
/**
* @descrpition 读取markdown文件
* @author ashao
* @date 2025-06-11 10:02
**/
@Override
public List<Document> loadMarkdown(Resource resource) {
// 排除非必要代码块和引用块,只保留原始文本内容
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
// 指定水平分隔线作为文档边界
.withHorizontalRuleCreateDocument(true)
// 排除代码块(即代码块内容会被过滤)
.withIncludeCodeBlock(false)
// 排除引用块(即引用块内容会被过滤)
.withIncludeBlockquote(false)
// 添加一个名为 "filename" 的额外元数据
.withAdditionalMetadata("filename", resource.getFilename())
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
return reader.get();
}
/**
* @descrpition 明确按页面分割pdf文件,粗颗粒度,对pdf文件格式没那么敏感,但噪音也会较大
* @author ashao
* @date 2025-06-11
**/
@Override
public List<Document> loadDocsFromPdfWithCatalog(Resource resource) {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(resource,
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build());
return pdfReader.read();
}
/**
* @descrpition 明确按段落分割pdf文件,细颗粒度,对pdf文件格式敏感,但噪音较小
* @author ashao
* @date 2025-06-11
**/
@Override
public List<Document> loadDocsFromPdf(Resource resource) {
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(resource,
PdfDocumentReaderConfig.builder()
// 设置页面顶部边距为 0,不跳过顶部内容。
.withPageTopMargin(0)
// 配置文本提取器,确保不删除每页顶部的任何行。
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
// 限制每个生成的 Document 对象的内容最多只来自 PDF 的一页。
.withPagesPerDocument(13)
.build());
return pdfReader.read();
}
/**
* @descrpition 这个方法也可以用来解析pdf、docx等类型的文件,具体支持的文件类型可以去查看apache tika官网
* @author ashao
* @date 2025-06-11
**/
@Override
public List<Document> loadOtherFiles(Resource resource) {
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(resource);
return tikaDocumentReader.read();
}
@Override
public List<Document> loadMultiFile(Resource resource) throws IOException {
String fileType = resource.getFilename()
.substring(resource.getFilename().indexOf(".") + 1, resource.getFilename().length());
switch (fileType) {
case "pdf":
return loadDocsFromPdfWithCatalog(resource);
case "txt":
return loadText(resource);
case "html":
return loadHtml(resource);
case "md":
return loadMarkdown(resource);
case "json":
return loadJsonDocuments(resource);
default:
return loadOtherFiles(resource);
}
}
}
其次就是重点的数据清洗了,将上面提取到的Document集合进行内容分快,官方提供了五种分块方法:
- TextSplitter
- TokenTextSplitter
- ContentFormatTransformer
- KeywordMetadataEnricher
- SummaryMetadataEnricher
同样的,我这里也提供一些封装的方法,不过也是简单实现,具体的RAG分块方案很多,大家后续可以自行查找并学习:
/**
* @author ashao
*/
@Service
public class DocumentTransformersServiceImpl implements DocumentTransformersService {
private final SummaryMetadataEnricher enricher;
@Autowired
private ChatModel chatModel;
public DocumentTransformersServiceImpl (SummaryMetadataEnricher enricher) {
this.enricher = enricher;
}
@Override
public List<Document> splitDocuments(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter();
return splitter.apply(documents);
}
@Override
public List<Document> splitCustomized(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter(10000, 400, 10, 50000, true);
return splitter.apply(documents);
}
/**
* TokenTextSplitter 是 TextSplitter 的一个实现,它使用 CL100K_BASE 编码,根据 token 数量将文本分割成块。
* @param documents 需要处理的原Document集合
* @param defaultChunkSize 这设置了每个文本块的目标大小,以 tokens(标记)为单位计量。默认情况下,它目标是每个块 800 个 token。
* 这是一个关键设置,因为大型语言模型(LLM)在单次处理时可以处理的 token 数量是有限的。
* @param minChunkSizeChars 这指定了在分割器寻找自然断点之前,一个文本块必须满足的最小字符数。
* 如果一个块的字符数少于 350(默认值),分割器会尝试扩展它以达到这个最小值,然后再尝试分割。
* @param minChunkLengthToEmbed 这定义了一个块要被包含在输出中所需的绝对最小长度(以字符为单位)。
* 如果在所有分割和格式化之后,一个块的长度最终少于 5 个字符(默认值),它就会被认为太小而没有意义,并被丢弃。
* @param maxNumChunks 这个参数设置了可以从单个输入文本生成的最大块数。
* 默认值非常高(10,000),这意味着除非你处理极长的文档并希望限制总段数,否则不太可能达到这个上限。
* @param keepSeparator 这是一个布尔(真/假)设置,它决定了分割后是否在块中保留分隔符(如换行符、句号等)。
* 默认是 true,这意味着分隔符将保留在块的文本中。将其设置为 false 将会移除它们。
* @return 处理完成的扩充Document集合
*/
@Override
public List<Document> splitCustomized(List<Document> documents, int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator) {
TokenTextSplitter splitter = new TokenTextSplitter(defaultChunkSize, minChunkSizeChars, minChunkLengthToEmbed, maxNumChunks, keepSeparator);
return splitter.apply(documents);
}
@Override
public List<Document> enrichDocumentsByKeywordMetadataEnricher(List<Document> documents) {
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);
return enricher.apply(documents);
}
/**
* KeywordMetadataEnricher 是一个 DocumentTransformer(文档转换器),
* 它利用一个生成式 AI 模型(Generative AI Model)从文档内容中提取关键词,并将这些关键词作为元数据添加到文档中
* @param documents 输入的需要处理的Document集合
* @param chatModel 指定用于生成关键字的AI模型
* @param keywordCount 指定生成后每个Document提取的关键字个数
* @return 返回处理好的扩充Document集合
*/
@Override
public List<Document> enrichDocumentsByKeywordMetadataEnricher(List<Document> documents, ChatModel chatModel, int keywordCount) {
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(chatModel , keywordCount);
return enricher.apply(documents);
}
/**
* SummaryMetadataEnricher 是一个 DocumentTransformer(文档转换器),它利用一个生成式 AI 模型(Generative AI Model)为文档创建摘要,并将这些摘要作为元数据添加到文档中。
* 它能够为当前文档生成摘要,也能为相邻文档(前一个和后一个)生成摘要
* @param documents
* @return
*/
@Override
public List<Document> enrichDocumentsBySummaryMetadataEnricher(List<Document> documents) {
return this.enricher.apply(documents);
}
}
最后,将上面数据清洗过的Document集合进行向量化写入向量存储数据库中:
/**
* @author ashao
*/
@Service
public class DocumentWriterAndStoreServiceImpl implements DocumentWriterAndStoreService {
@Autowired
private VectorStore vectorStore;
/**
* 将对象列表的内容写入文件的实现
*
* withDocumentMarkers:是否在输出中包含文档标记(默认值:false)。
* metadataMode:指定要写入文件的文档内容(默认值:MetadataMode.NONE)。
* append:如果为 true,则数据将写入文件末尾而不是开头(默认值:false)。
*
* @param documents 准备写入的document集合
* @param fileName 要将文档写入到的文件的名称
*
*/
@Override
public void writeDocuments(List<Document> documents, String fileName) {
FileDocumentWriter writer = new FileDocumentWriter(fileName, true, MetadataMode.ALL, false);
writer.accept(documents);
}
@Override
public void delete(List<String> idList) {
vectorStore.delete(idList);
}
@Override
public void delete(Filter.Expression filterExpression) {
vectorStore.delete(filterExpression);
}
}
接下来我们编写一个接口可以测试一下效果:
@PostMapping("/ragFiles")
public String ragFiles(@RequestParam("file") MultipartFile file,@RequestParam(value = "userInput",defaultValue = "请帮我分析下文档") String userInput) throws IOException {
Resource resource = documentReaderService.uploadFile(file);
List<Document> documents1 = documentReaderService.loadMultiFile(resource);
log.info("documents : {}",documents1);
List<Document> documents2 = documentTransformersService.transformDocuments(documents1, chatModel, 5,userInput);
vectorStore.doAdd(documents2);
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
<query>
下面是上下文信息。
---------------------
<context>
---------------------
给定上下文信息且没有先验知识,回答查询。
遵循以下规则:
1. 如果答案不在上下文中,就说“我不知道,因此不知道怎么回答”。
2. 避免使用诸如“根据上下文……”或“所提供的信息……”之类的语句。
""")
.build();
QueryAugmenter augmenter = ContextualQueryAugmenter.builder()
//允许上下文为空
.allowEmptyContext(true)
.promptTemplate(customPromptTemplate)
.build();
VectorStoreDocumentRetriever vectorStoreDocumentRetriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.50)
.build();
RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(vectorStoreDocumentRetriever)
.queryAugmenter(augmenter)
.build();
ChatOptions options = ChatOptions.builder()
.model("qwen-long")
.build();
ChatClient.Builder builder = chatClient.mutate().defaultOptions(options);
return builder.build().prompt()
.user(userInput)
.advisors(retrievalAugmentationAdvisor)
.call()
.content();
}
简单介绍一下这个代码吧
1. QueryAugmenter augmenter = ContextualQueryAugmenter.builder()...
这部分是配置一个查询增强器(Query Augmenter) 。在 RAG 流程中,原始的用户查询可能不够明确,不足以直接用于检索或生成。查询增强器就是用来优化或改写用户原始查询的。
-
ContextualQueryAugmenter: 这是一个具体的查询增强器实现类。它的名字“Contextual”暗示它可能会利用上下文信息来改写或细化查询。 -
.builder(): 这是一个典型的构建器模式(Builder Pattern)的用法,用于创建ContextualQueryAugmenter的实例,提供了更灵活和可读的配置方式。 -
.allowEmptyContext(true):- 作用: 这个配置允许在某些情况下,即使没有检索到相关文档(即上下文为空),查询增强器仍然可以处理查询。
- 含义: 通常,如果
allowEmptyContext为false,当检索结果为空时,整个 RAG 流程可能会中断或抛出错误。设置为true意味着,即使没有检索到任何相关文档,LLM 仍然可以尝试根据其自身知识来回答问题(尽管准确性可能降低),而不是直接报错。
-
.promptTemplate(customPromptTemplate):- 作用: 指定用于查询增强的提示模板。
customPromptTemplate: 这是一个变量,它应该是一个预定义的字符串,包含了如何将用户查询和可能的上下文信息组合起来,形成一个更适合 LLM 处理的完整提示。例如,它可能包含占位符,让 LLM 知道如何“消化”检索到的信息。
简而言之: augmenter 的作用是根据一个自定义的提示模板来处理或改写用户查询,并且即使没有外部上下文也能工作。
2. VectorStoreDocumentRetriever vectorStoreDocumentRetriever = VectorStoreDocumentRetriever.builder()...
这部分是配置一个文档检索器(Document Retriever) ,它是 RAG 流程中“检索”阶段的核心组件,负责从知识库中找出与用户查询相关的文档。
-
VectorStoreDocumentRetriever: 这是一个基于向量存储的文档检索器。它依赖于嵌入模型将文本转化为向量,并利用向量数据库进行相似性搜索。 -
.vectorStore(vectorStore):-
作用: 指定用于存储和检索文档向量的向量存储实例。
-
vectorStore: 这是一个变量,它应该是一个已经配置好的VectorStore对象。这个vectorStore负责:- 存储所有知识库文档的嵌入向量。
- 接收用户查询的嵌入向量,并在其中执行相似性搜索,找到最相关的文档片段。
-
-
.similarityThreshold(0.50):- 作用: 设置一个相似性阈值。
- 含义: 当检索器进行向量相似性搜索时,它会计算用户查询向量与知识库中文档片段向量之间的相似度得分(通常是余弦相似度)。只有当相似度得分高于这个阈值(0.50)时,相应的文档片段才会被认为是相关的,并被返回给 LLM 作为上下文。
- 重要性: 这个阈值决定了检索结果的相关性严格程度。值越高,要求越严格,返回的文档越少但越精确;值越低,返回的文档越多但可能包含更多不相关的。
简而言之: vectorStoreDocumentRetriever 的作用是从指定的向量存储中,根据相似性阈值检索与查询最相关的文档。
3. RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()...
这部分是 Spring AI 中 RAG 流程的核心协调器,它将查询增强器和文档检索器结合起来,形成一个完整的 RAG 管道。
-
RetrievalAugmentationAdvisor: 这是 Spring AI 提供的高级 API,用于封装和协调 RAG 流程。它不是直接执行检索或生成,而是作为这些子组件的“顾问”或“协调者”。 -
.documentRetriever(vectorStoreDocumentRetriever):- 作用: 将之前配置好的文档检索器 (
vectorStoreDocumentRetriever) 注册到RetrievalAugmentationAdvisor中。这意味着当需要检索文档时,Advisor会调用这个检索器。
- 作用: 将之前配置好的文档检索器 (
-
.queryAugmenter(augmenter):- 作用: 将之前配置好的查询增强器 (
augmenter) 注册到RetrievalAugmentationAdvisor中。这意味着在执行检索之前,Advisor可能会先通过这个增强器来优化用户查询。
- 作用: 将之前配置好的查询增强器 (
简而言之: retrievalAugmentationAdvisor 整合了文档检索器和查询增强器,它代表了一个完整的、可用的 RAG 管道。当你向这个 Advisor 提交一个用户查询时,它会内部协调:
- 可能先通过
queryAugmenter处理查询。 - 然后通过
documentRetriever检索相关文档。 - 最终将原始查询和检索到的文档一起提供给底层的 LLM,以生成增强的答案。
整体总结:
这段代码是在 Spring AI 框架下,构建一个可配置的 RAG 管道。它定义了:
- 如何处理原始用户查询(通过
QueryAugmenter进行增强和改写)。 - 如何从向量数据库中检索相关文档(通过
VectorStoreDocumentRetriever和相似度阈值)。 - 如何将这两个核心功能组合起来,形成一个端到端的 RAG 解决方案(通过
RetrievalAugmentationAdvisor)。
这使得开发者能够灵活地定制 RAG 流程的不同阶段,以适应特定的应用需求。
ok啦,以上就是RAG检索相关的内容,有什么疑问和建议都可以在评论区告知。