Chat 功能是 LLMs 应用的最基础功能,任何功能都要向 LLMs 应用给输入,之后将获得 LLMs 的返回。其底层是 NLP 的实现。这篇文章中不会分析 LLMs 的底层实现,会分析一波 Spring AI Albaba 的 Chat 功能实现和 Spring AI 提供的 Chat 接口实现。
Spring AI Chat 接口定义
从之前的 Spring AI Alibaba 使用示例中,我们可以看到,Spring AI 提供了两种 Chat Model 的实现,一种是比较低级的 ChatModel API,一种是高级的 ChatClient API。
其两者的区别是:ChatModel 是针对于各个模型的客户端,而 ChatClient 是屏蔽底层模型差异性的客户端接口,基于 ChatClient,Spring AI 提供了强大的 Advisors 机制来扩展功能。Advisors API 的实现类似于 Spring 的 AOP,应用了 AOP 的编程思想。
ChatModel API
下面以 Spring AI Alibaba 的 DashScope ChatModel 为例来了解一下 ChatModel 的源码设计。
从 IDEA 生成的关系图来看:DashScopeChatModel 继承了 AbstractToolCallSupport 实现了 ChatModel 接口。其中 AbstractToolCallSupport 实现了 工具函数调用的 抽象逻辑,ChatModel 提供了 call() 和 stream() 方法,用来和 LLMs 进行交互。从四个维度来理解一下 ChatModel 做的事情:
- Options,模型配置参数的处理;
- call 和 stream 的调用;
- Function 处理;
- 可观测处理。
Options 处理
在 Spring AI 中,将大模型的一些参数抽象成了 Options 的概念,其类结构如下图:
在 Spring AI Alibaba 中,对 ChatOptions 的参数支持在 application.yml 中指定,也支持在代码中通过 DashScopeChatOptions 提供的 Builder 方法进行构建:
在 Spring AI Alibaba DascopeChatModel#createRequest 方法中:
DashScopeChatOptions options = DashScopeChatOptions.builder().build();
if (prompt.getOptions() != null) {
DashScopeChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(),
ChatOptions.class, DashScopeChatOptions.class);
enabledToolsToUse.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions));
options = ModelOptionsUtils.merge(updatedRuntimeOptions, options, DashScopeChatOptions.class);
}
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) {
enabledToolsToUse.addAll(this.defaultOptions.getFunctions());
}
options = ModelOptionsUtils.merge(options, this.defaultOptions, DashScopeChatOptions.class);
if (!CollectionUtils.isEmpty(enabledToolsToUse)) {
options.setTools(this.getFunctionTools(enabledToolsToUse));
}
对 模型调用参数进行了一系列的 merge 操作。以通过代码设置的 options 为准,其次是以 application.yml 中配置的 options 参数。也就是说,通过代码设置的参数优先级高于配置文件。
Call 和 Stream 方法
在之前的版本中,Spring AI Alibaba 通过 SDK 来适配通义大模型,现在都使用 openapi 规范的接口去调用大模型。
在 ChatModel 中,定义了 Call 和 Stream 方法,用来调用大模型,前者是朴素的调用方式,后者是流式调用。
在 Model 接口中定义了 Call 方法:
public interface Model<TReq extends ModelRequest<?>, TRes extends ModelResponse<?>> {
TRes call(TReq request);
}
在 StreamingModel 中定义了 stream 方法:
public interface StreamingModel<TReq extends ModelRequest<?>, TResChunk extends ModelResponse<?>> {
Flux<TResChunk> stream(TReq request);
}
通过代码可以看到在默认的 ChatModel 中组合了 Model 和 StreamingModel:
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
@Override
ChatResponse call(Prompt prompt);
ChatOptions getDefaultOptions();
default Flux<ChatResponse> stream(Prompt prompt) {
throw new UnsupportedOperationException("streaming is not supported");
}
}
有了接口,且适配了不同模型之后,通过 createRequest 构造一个 DashScopeApi#ChatCompletionRequest 的 request 对象,用于发起调用:()其中包含对 options 参数的处理和对 function 的处理)
# 构建请求参数
DashScopeApi.ChatCompletionRequest request = createRequest(prompt, false);
# 调用 LLMs API
ResponseEntity<ChatCompletion> completionEntity = this.retryTemplate
.execute(ctx -> this.dashscopeApi.chatCompletionEntity(request));
# 准备接受 LLMs 响应
var chatCompletion = completionEntity.getBody();
在对 stream 方法的调用中,和 call 相同,细心的同学们,肯定发现了 createRequest 方法有两个参数,第一个是 prompt,第二个就是决定此次请求是否是流式调用:
ChatCompletionRequest request = createRequest(prompt, true);
继而,再去调用 dashScopeApi 的 chatCompletionStream 方法完成请求。最后接受响应。
至此我们已经完成了对 call 和 stream 请求方式的解析,从源码实现上非常简单。现在来分析一下 Spring AI 中定义的用来接受模型响应的类 ChatResponse:
其中元数据接口专注于提供 AI 模型生成结果的更多背景信息和见解。它可能包括计算时间、模型版本和 Usage 等其他相关详细信息,以增强对各种应用中的 AI 模型输出的理解和管理。
Generation 类是一个记录 AI 返回信息的实体类。有两个类成员:一个为 ChatGenerationMetadata,一个是 AssistantMessage。
其中 AssistantMessage 是让 LLMs 知晓此类型的消息类型是作为用户响应生成的。实现了 Message 和 Content 类型:类图如下:
我们可以看到有四种类型的实现,这里我们不讨论实现,后续文章会介绍。
至此便是整个 Spring AI 的 ChatReponse 的源码实现,可以看到 Spring AI 定义了一系列的接口来约束实现类的行为。使得整个 Spring AI 体系逻辑更加严密。
Function 处理
在 LLMs 中为了解决实时性的问题,提供了函数调用的方式,可以调用外部的一些工具函数来补充自身知识,给用户正确的响应。在 Spring AI 中也提供了对函数调用的集成。
下面以 DashScope 的 ChatModel 中的 Function 实现来分析一波具体的实现。在 IDEA 中关系图如下:
我们可以看到 Spring AI 主要通过 AbstractToolCallSupport 这个抽象类来完成整个函数调用的工具链抽象。通过构造组合相关的类,其中类说明如下:
-
AbstractToolCallSupport:(此类已经经过好几轮迭代,不知道后续会不会在变动,之前的名字是 AbstractFunctionCallSupport)其中维护一组 Map 集合的 FunctionCallback 对象,提供用于处理函数回调和运行函数的功能;
-
FunctionCallback:接口实现,内部定义了一个 Function 必须要的一些属性和方法,如 Name,Desc 等;
public interface FunctionCallback { String getName(); String getDescription(); String getInputTypeSchema(); String call(String functionInput); }
-
FunctionCallbackContext:实现 Spring 的 ApplicationContextAware 接口,从 Spring context 中获取 Function Bean;
-
AbstractFunctionCallback:承上启下,对大模型需要转换为大模型需要的函数调用协议,对于3rd,需要包装调用第三方服务或者函数。在使用上对该类是无感知的;
-
FunctionCallbackWrapper:将输出转换为模型可以使用的格式,默认实现是将输出发送给模型之前将其转换为字符串。可以提供一个自定义的函数
responseConverter
实现来覆盖此操作。
此小节中,将从以下几个方面来分析:
- 函数是如何定义以及如何被 LLMs 感知到的?
- Spring AI 在调用 LLMs 时对 F