Spring AI 源码阅读

296 阅读5分钟

spring ai 源码

通过研究源码,知道 ai 项目一般有哪些概念。

昨天看了,spring ai 为了集成方便,做了自动装配。这些自动配置,只要你引入相关的依赖,就会自动装配相关的属性,但是有些属性还是必填的,比如 openai 的 token key 等。

那么接着我们看 openai 相关的包。

通过 git 提交记录来看源码

我们先根据 git 提交记录去。

在最前面,他首先考虑的是如何创建模版,prompt 的模版。模版,就是一个字符串模版,通过 ST 这个库,将想要的东西嵌入进去,比如 I want to know {city} weather. 那么这个 city 就是我们希望替换的。关于这个替换,看官方文档应该更清楚。

OpenAI Platform

看上面的文档,里面说的 """insert text here""” 大致就是我们要替换的。

prompt 是对 GPT 的输入,那么接着就是处理输出。关于 OpenAI API 的调用他使用的其他人的库:com.theokanning.openai…..这个库。

对于输出,就是 message,针对 openai api 所对应的message role,他也相应封装了对应的 message。

Untitled.png

Untitled (1).png

这样 prompt 和输出 message 都解析出来了。调用 openai client 即可。接入其他 model client 都是如此方式。

接着是spring ai文档和 github action 等等的工作。

embeding,最开始他打算自己去计算 embeding,通过 ONNX 调用 pytorch 的库去计算出 embeding 等。后面看起来放弃了,如果模型有 embeding 那么去调用model 的 embed 方法。

vector store,最开始在本地 map 缓存,自己实现算法来进行 simpleQuery。后面接入其他家的各种向量数据库。

OpenAI stream client,自己通过webFlux 异步的方式进行调用。

bedrock 接入

Ollama接入,testcontains 可以 Java 代码形式去启动一个 docker 容器,还能对容器执行命令。使用 project reactor 处理 stream response.

model option 完善,支持更多的参数修改

spring retry 库,如果调用失败,则进行重试

	public final RetryTemplate retryTemplate = RetryTemplate.builder()
		.maxAttempts(10)
		.retryOn(OpenAiApiException.class)
		.exponentialBackoff(Duration.ofMillis(2000), 5, Duration.ofMillis(3 * 60000))
		.withListener(new RetryListener() {
			public <T extends Object, E extends Throwable> void onError(RetryContext context,
					RetryCallback<T, E> callback, Throwable throwable) {
				logger.warn("Retry error. Retry count:" + context.getRetryCount(), throwable);
			};
		})
		.build();
		
	@Override
	public ChatResponse call(Prompt prompt) {

		return this.retryTemplate.execute(ctx -> {

			ChatCompletionRequest request = createRequest(prompt, false);

			ResponseEntity<ChatCompletion> completionEntity = this.openAiApi.chatCompletionEntity(request);

			var chatCompletion = completionEntity.getBody();
			if (chatCompletion == null) {
				logger.warn("No chat completion returned for request: {}", prompt);
				return new ChatResponse(List.of());
			}

			RateLimit rateLimits = OpenAiResponseHeaderExtractor.extractAiResponseHeaders(completionEntity);

			List<Generation> generations = chatCompletion.choices().stream().map(choice -> {
				return new Generation(choice.message().content(), Map.of("role", choice.message().role().name()))
					.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));
			}).toList();

			return new ChatResponse(generations,
					OpenAiChatResponseMetadata.from(completionEntity.getBody()).withRateLimit(rateLimits));
		});
	}

添加 images实现,即使文生图相关 API

添加更多的 model 提供商支持,添加更多的向量数据库支持

完善 openai api 其他边缘功能,看着像对于 openai 来说是 json 的改变,那么框架做的就是将请求json 改成对象,将响应 json 也解析成对象。这样就方便使用 spring ai 这个框架的人更好调用。

还有很多提交,都是为了完善文档的。文档工程师,最近看到一种新职位。

关于向量数据库的两个主要方法,add和similaritySearch。add 方法,将字符串通过比如 openai 的 embeding 接口进行向量化后存入数据库。similaritySearch方法,先将要 search 的字符串调用 openai 的 embeding 接口进行向量化,然后再去向量数据库中去查询。

要写这个 spring ai 框架,首先自己就要用 RAG 很熟悉,知道用户会怎么用,用户想要框架提供什么,用户想要配置哪些参数,哪些要开放给用户,哪些框架可以自动化。

add 音频翻译,音频翻译成成文字。

ETL pipeline,The Extract, Transform, and Load (ETL)。在做 RAG 时,我们需要将自己的知识库放到向量数据库中。而我们知识库的格式,可能是 pdf,doc,text 等等格式吧。那么首选我们需要 Extract 数据。接着我们需要 transform 数据,而对于数据的转换就有很多说法了,比如说数据块的大小的分割,如果太大,那么 search 精度太低。如果太小,则可能语义上下文错误。最后 Load 数据,就是通过比如 openai api embeding 将数据向量化后存到数据库。关于如何进行 embeding,openai api doc embedings文档有更好的描述。

add TTS,文本转语音。openai

ok,看到这里,基本上项目的所有功能都齐了。90% 的 commit 都是小的改动。10% 的改动是稍微重要的,是新代码,是在思考如何接入新的模块,如何设计新的模块。

项目大致模块:

  1. pdf 等等解析模块,也就是 document-reader 模块。这是ETL的 extract。
  2. prompt,也就是各种模型的输入,对各种 prompt 进行封装。prompt template,这是 prompt engineer。这里做的是字符串的替换等等。
  3. model client,给各种 model 发送请求(chat speech video),支持各种参数配置,function callback 等。
  4. message,响应的封装,在 chat 中各种 message role 的封装。
  5. embeding,各种 model 的 embeding 接入,和各种向量数据库 add 和 search。
  6. spring-ai-core,这个包里面是各个概念的抽象接口。不同概念之间调用通过 core 的抽象接口进行交互。比如向量数据库只需要知道他需要调用 model embed方法,不需要知道具体的 model类型。

通过源码阅读,主要知道 AIGC 项目一些相关的概念,RAG 大致该怎么做。各个 model 的接入的接口,大致有哪些概念和参数配置。

关于项目的代码设计,类的设计,还是有很多不懂的地方,还需要花时间详细体会。