基于SpringAI的RAG全流程开发

0 阅读5分钟

SpringAI经历了2年时间,终于在前段时间发布了首个正版版本,也是帮助想要开发AI应用功能的Java小伙伴将AI集成,进行AI赋能。SpringAI提供了统一的大模型集成接口,用户只需要一行代码即可完成模型的调用,再也不用每次调用新的模型都需要跑去详细的阅读大模型厂商,然后通过http或者sdk写很多代码才能进行调用;其次就是提供了我觉得目前AI应用最有价值的检索增强生成(RAG),通过搭建RAG可以将ai打造成专属机器人,并且避免大模型出现幻觉的情况;然后就是提供了工具调用和mcp,这两个我目前觉得就是个玩具而已,对于个人开发者,意义并不大。
下面我将会简单的谈谈对RAG的理解:

  1. RAG的核心特性

RAG的工作流程:

首先需要获取到原始文档,将文档进行分割切片,通过Embedding模型将切片转换为对应的向量进行表示,并将向量和切片存储到向量数据库中,当用户提问检索的时候,会将用户问题进行向量表示,通过相似度搜索和过滤条件从向量数据库中得到相关的文档切片,接着通过rank模型对得到的文档进行排序,取出最相关的一些知识点,然后与用户的问题结合在一起,交给LLM进行回答。

    1. 文档收集和分割-ETL

在该阶段主要是进行将准备好的知识库文档进行处理,然后进行存储,该过程也叫做ETL(抽取、转换、加载)

文档:

在SpringAi中文档是一个Document对象,该文档不仅仅包含文本,还有一列的metadata和meadia

ETL:

在ETL中,对Document处理遵循以下流程:

  1. 读取文档:通过DocumentReader从数据源(本地、网络资源、数据库)加载文档
  2. 转换文档:通过DocumentTransform来将按照实际的需求将文档转换成适合的后续处理的格式,就比如去除冗余信息,分词等操作-----对应分片阶段
  3. 文档写入:使用DocumentWriter将文档转换为特定的格式进行存储,比如将文档以嵌入向量写入向量数据库中。或者是kv键值对写入redis中

抽取(EXtract):

SpringAI通过DocumentReader接口提供了文档抽取,也就是将文档加载到内存中,而该接口又继承了Supplier<List>接口, 用于从数据源中读取数据转换成一个Document列表,而Supplier其实就是用于获取或者生成的接口。在实际的开发中springai提供许多内置的DocumentReader的实现类 DocumentReader 实现类

转换(Transform)

Spring ai 通过DoucumentTransform接口实现了Function<List, List>,该接口也即是将一组文档转换成另一一组文档,也即是输入了的是一组大文档会被合理的拆分为便于检索的知识碎片(切片),Springai提供了多种实现类DocumentTransform实现类

加载(Load)

Spring ai通过DocumentWriter接口实现Consumner<List>接口,将处理后的文档写入到目标存储中,Consumer接口表示一个消费接口(只处理不返回),Springai提供了2种内容的DocumentWriter实现DocumentWriter实现类,写入文件文件系统,向量数据库

    1. 向量转换和存储

将文档转换为特定的向量并存储起来,以便后续进行高效的相似度搜索和过滤。Spring ai提供了向量数据库接口VectorStore和向量存储整合包,用于快速整合,比如pgVecotor,Redis等

Vecotor接口:

该接口继承了DocumentWriter接口,定义了向量存储的基本操作“增删改查+获取原生客户端”,Vector接口介绍

public interface VectorStore extends DocumentWriter {

    default String getName() {
		return this.getClass().getSimpleName();
	}

    void add(List<Document> documents);

    void delete(List<String> idList);

    void delete(Filter.Expression filterExpression);

    default void delete(String filterExpression) { ... };

    List<Document> similaritySearch(String query);

    List<Document> similaritySearch(SearchRequest request);

    default <T> Optional<T> getNativeClient() {
		return Optional.empty();
	}
}

搜索请求构建类:

在springai中提供了SearchRequest类,用于进行相似度请求:

public class SearchRequest {

	public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;

	public static final int DEFAULT_TOP_K = 4;

	private String query = "";

	private int topK = DEFAULT_TOP_K;

	private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;

	@Nullable
	private Filter.Expression filterExpression;

    public static Builder from(SearchRequest originalSearchRequest) {
		return builder().query(originalSearchRequest.getQuery())
			.topK(originalSearchRequest.getTopK())
			.similarityThreshold(originalSearchRequest.getSimilarityThreshold())
			.filterExpression(originalSearchRequest.getFilterExpression());
	}

	public static class Builder {

		private final SearchRequest searchRequest = new SearchRequest();

		public Builder query(String query) {
			Assert.notNull(query, "Query can not be null.");
			this.searchRequest.query = query;
			return this;
		}

		public Builder topK(int topK) {
			Assert.isTrue(topK >= 0, "TopK should be positive.");
			this.searchRequest.topK = topK;
			return this;
		}

		public Builder similarityThreshold(double threshold) {
			Assert.isTrue(threshold >= 0 && threshold <= 1, "Similarity threshold must be in [0,1] range.");
			this.searchRequest.similarityThreshold = threshold;
			return this;
		}

		public Builder similarityThresholdAll() {
			this.searchRequest.similarityThreshold = 0.0;
			return this;
		}

		public Builder filterExpression(@Nullable Filter.Expression expression) {
			this.searchRequest.filterExpression = expression;
			return this;
		}

		public Builder filterExpression(@Nullable String textExpression) {
			this.searchRequest.filterExpression = (textExpression != null)
					? new FilterExpressionTextParser().parse(textExpression) : null;
			return this;
		}

		public SearchRequest build() {
			return this.searchRequest;
		}

	}

	public String getQuery() {...}
	public int getTopK() {...}
	public double getSimilarityThreshold() {...}
	public Filter.Expression getFilterExpression() {...}
}

向量存储的工作原理:

  1. 嵌入转换:首先将文档通过嵌入模型(embeding模型)转换为向量
  2. 相似度计算:在查询时,会将查询文本同样转换为向量,然后统计计算该向量与存储的向量的相似度
  3. 相似度度量:余弦、欧式距离、点积
  4. 过滤与排序:根据相似度阈值过滤结果,并按照相似度返回文档

不论使用什么加载器,什么向量数据库,文档的获取与分割的开发步骤都是一样:先准备数据源->引入不同的整合包(如果md,json的读取器)->编写对应的配置->使用自动注入的VectorStore即可;也可以使用阿里云百炼平台参考 官方文档

文档过滤和检索:

springai提供了一个模块化的RAG架构,用于优化大模型的回复的准确度:

说白了,其实就是将整个文档过滤检索阶段分为了:检索前、检索时、检索后,三个阶段