Spring AI核心架构解析
用Spring AI写代码很简单,几行就能跑起来。但如果你想用好它,或者遇到问题能快速定位,就得搞清楚底层的架构设计。
这篇文章会带你深入Spring AI的核心组件,理解它们之间的关系和协作方式。
整体架构鸟瞰
先从宏观视角看看Spring AI的分层设计:
Spring AI采用了经典的分层架构,核心思想是面向接口编程。你的业务代码只需要和ChatClient或ChatModel打交道,完全不用关心底层对接的是哪家模型。
Spring AI 的核心是面向接口编程。ChatModel 是统一的模型抽象接口,ChatClient 是更高级的封装入口,业务代码与具体的模型实现完全解耦——换模型只需要换依赖,代码基本不用改。
ChatModel:模型统一抽象
ChatModel是Spring AI最核心的接口,它定义了与对话模型交互的标准方式。
接口设计
来看看它的接口定义:
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
// 最简单的调用方式:传入字符串,返回字符串
default String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
// 传入多条消息
default String call(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
// 核心方法:传入Prompt,返回完整响应
@Override
ChatResponse call(Prompt prompt);
// 流式调用
default Flux<ChatResponse> stream(Prompt prompt) {
throw new UnsupportedOperationException("streaming is not supported");
}
}
注意到它继承了两个接口:
- Model<Prompt, ChatResponse>:定义了基本的call方法
- StreamingChatModel:定义了stream方法,支持流式输出
为什么这样设计?
这种设计带来的好处是解耦。假设今天用阿里云的通义千问,明天要换成OpenAI的GPT-4,你只需要换一个依赖包,业务代码一行不改:
// 业务代码完全不关心用的是哪个模型
@Service
public class ProductService {
private final ChatModel chatModel; // 只依赖接口
public String generateDescription(String productName) {
return chatModel.call("为商品'" + productName + "'写一段吸引人的描述");
}
}
ChatClient:更友好的门面
ChatModel虽然功能完整,但用起来稍显繁琐。Spring AI又封装了一个ChatClient,提供了更流畅的API。
ChatClient vs ChatModel
打个比方:ChatModel像是JDBC,功能强大但用起来啰嗦;ChatClient像是Spring Data JPA,简洁优雅,大多数场景用它就够了。
日常开发中优先使用 ChatClient,它提供了更简洁的链式 API,内置了对 Advisor、默认参数的支持。只有当你需要精细控制底层请求(比如自定义 Prompt 构建),才有必要直接使用 ChatModel。
看看两种方式的代码对比:
// 使用ChatModel
Prompt prompt = new Prompt(
List.of(
new SystemMessage("你是订单分析助手"),
new UserMessage("分析这个订单的状态")
),
DashScopeChatOptions.builder().withModel("qwen-plus").build()
);
ChatResponse response = chatModel.call(prompt);
String result = response.getResult().getOutput().getText();
// 使用ChatClient - 链式调用,清爽多了
String result = chatClient.prompt()
.system("你是订单分析助手")
.user("分析这个订单的状态")
.options(DashScopeChatOptions.builder().withModel("qwen-plus").build())
.call()
.content();
ChatClient的构建方式
ChatClient通过Builder模式构建,可以预设各种默认配置:
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
// 默认系统提示词
.defaultSystem("你是一个专业的电商客服")
// 默认模型参数
.defaultOptions(
DashScopeChatOptions.builder()
.withTemperature(0.7)
.build()
)
// 默认Advisor(拦截器)
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
.build();
}
这些默认配置在每次调用时都会生效,但也可以在调用时覆盖。
Prompt:请求的载体
Prompt是发给大模型的请求对象,包含了对话内容和调用参数。
Prompt的结构
public class Prompt implements ModelRequest<List<Message>> {
// 对话内容:历史消息 + 当前消息
private final List<Message> messages;
// 调用参数:模型名称、温度值等
@Nullable
private ChatOptions chatOptions;
}
一个典型的Prompt长这样:
Message的类型
Message表示对话中的一条消息,根据角色不同有四种类型:
| 类型 | 说明 | 使用场景 |
|---|---|---|
| SystemMessage | 系统设定 | 定义AI的角色、行为准则 |
| UserMessage | 用户输入 | 用户的问题或指令 |
| AssistantMessage | AI回复 | 模型之前的回答 |
| ToolResponseMessage | 工具返回 | 工具调用的结果 |
在多轮对话中,Message列表会包含完整的对话历史,这就是大模型"记住"上下文的方式。
| 类型 | 说明 | 使用场景 |
|---|---|---|
| SystemMessage | 系统设定 | 定义AI的角色、行为准则 |
| UserMessage | 用户输入 | 用户的问题或指令 |
| AssistantMessage | AI回复 | 模型之前的回答 |
| ToolResponseMessage | 工具返回 | 工具调用的结果 |
多轮对话中,Message 列表需要包含完整的对话历史,大模型才能"理解"上下文。
ChatOptions:参数配置
ChatOptions用来设置调用大模型时的各种参数,不同模型厂商支持的参数有差异。
通用参数
// 使用通用的ChatOptions
ChatOptions options = ChatOptions.builder()
.model("qwen-plus") // 模型名称
.temperature(0.7) // 温度:越高越有创意
.maxTokens(2000) // 最大生成token数
.topP(0.9) // 核采样概率
.build();
厂商特有参数
如果要用某个厂商的特殊参数,需要用对应的Options实现类:
// 阿里云DashScope特有参数
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel("qwen-plus")
.withTemperature(0.7)
.withEnableSearch(true) // 开启联网搜索
.withResultFormat("text") // 结果格式
.build();
参数优先级
参数可以在多个地方设置,优先级从高到低是:
- 单次调用时指定 -
chatClient.prompt().options(xxx)— 最高优先级 - ChatClient构建时的默认值 -
ChatClient.builder().defaultOptions(xxx) - 配置文件 -
spring.ai.xxx.chat.options.xxx - 框架默认值 — 最低优先级
高优先级的配置会覆盖低优先级,灵活地利用这个机制可以让你的配置更整洁。
ChatResponse:响应解析
大模型返回的结果封装在ChatResponse中,结构稍微有点复杂。
响应结构
// 简化后的响应结构
public class ChatResponse {
private ChatResponseMetadata metadata; // 元数据
private List<Generation> results; // 生成结果(可能有多个)
public Generation getResult() {
return results.get(0); // 获取第一个结果
}
}
public class Generation {
private AssistantMessage output; // AI回复内容
private ChatGenerationMetadata metadata; // 生成元数据
}
常用的取值方式
ChatResponse response = chatModel.call(prompt);
// 获取文本内容 - 最常用
String text = response.getResult().getOutput().getText();
// 获取完整的消息对象
AssistantMessage message = response.getResult().getOutput();
// 获取token使用情况
Usage usage = response.getMetadata().getUsage();
System.out.println("输入token: " + usage.getPromptTokens());
System.out.println("输出token: " + usage.getCompletionTokens());
System.out.println("总计token: " + usage.getTotalTokens());
如果用ChatClient,取值更简单:
// 直接拿到文本
String content = chatClient.prompt("你好").call().content();
// 拿到完整响应(需要时)
ChatResponse response = chatClient.prompt("你好").call().chatResponse();
自动配置机制
Spring AI大量使用了Spring Boot的自动配置特性,让集成变得非常简单。
以阿里云DashScope为例,当你引入spring-ai-alibaba-starter-dashscope依赖后,会自动发生以下事情:
这一切都是自动完成的,你只需要配置API Key就行了。
Spring AI 大量借助 Spring Boot AutoConfiguration,引入 starter 依赖并配置 API Key 后,ChatModel Bean 会自动注册到容器,可直接注入使用,无需任何手动配置。
自定义Bean
如果默认配置不满足需求,可以自己定义Bean覆盖:
@Configuration
public class CustomAiConfig {
@Bean
public ChatModel customChatModel(DashScopeApi dashScopeApi) {
// 自定义ChatModel配置
return new DashScopeChatModel(dashScopeApi,
DashScopeChatOptions.builder()
.withModel("qwen-max")
.withTemperature(0.5)
.build());
}
}
调用流程全景
最后,我们把整个调用流程串起来看:
小结
这篇文章梳理了Spring AI的核心架构:
- ChatModel是模型抽象层,定义了统一的交互接口
- ChatClient是更高级的封装,提供流畅的链式API
- Prompt承载请求信息,包含Message列表和ChatOptions
- ChatResponse是响应对象,通过getResult().getOutput().getText()获取文本
理解了这些核心概念,你就能更好地使用Spring AI,也能在遇到问题时快速定位根因。
下一篇,我们来聊聊流式输出的实现原理,看看那个Flux到底是怎么回事。