Spring AI 源码解析(六):自动配置扩展机制与多模型适配器
概述
Spring AI 最吸引人的地方不是它对某一家 AI 厂商的支持而是它用同一套接口屏蔽了十几家 AI 服务商的差异,并通过 Spring Boot 自动配置实现了"引入即用"。
换厂商只需改几行配置,业务代码几乎不用动。这背后是框架做了大量适配工作——接口抽象、SPI 自动发现、条件注入、消息格式转换、配置分层。这篇从源码层面把这些机制彻底理清楚。
核心目标:理解 Spring AI 如何通过 ChatModel 接口 + AutoConfiguration + MessageConverter ,实现"一套代码,多家厂商"。
模块化架构全景
Spring AI 把每个 AI 厂商拆成独立的 Maven 模块:
spring-ai-community/
├── spring-ai-core/ ← 核心抽象层(所有模块依赖它)
├── models/
│ ├── spring-ai-openai/ ← OpenAI / Azure OpenAI
│ ├── spring-ai-ollama/ ← Ollama(本地部署)
│ ├── spring-ai-anthropic/ ← Anthropic Claude
│ ├── spring-ai-mistral/ ← Mistral AI
│ ├── spring-ai-bedrock/ ← Amazon Bedrock
│ ├── spring-ai-vertex-ai/ ← Google Vertex AI
│ └── spring-ai-huggingface/ ← HuggingFace
└── vector-stores/ ← 向量数据库适配器(pgvector/Milvus/Chroma等)
关键设计原则:spring-ai-core 被所有厂商依赖,但它不依赖任何厂商。这是典型的依赖倒置原则(DIP)在框架级设计中的应用——核心抽象永远保持纯净,不会因某个厂商的特殊需求被污染。
模块依赖关系:
解读:业务代码只依赖 core,不知道底层用的是哪个厂商。自动配置模块作为"胶水层",同时了解 core 和各厂商适配器,通过条件注入把它们连接起来。三层各司其职:core 定义规范,adapters 实现规范,autoconfigure 组装二者。
实际踩坑提醒:引入
spring-ai-openai-spring-boot-starter会自动带上 autoconfigure 模块实现零配置。如果只引入spring-ai-openai(不带 starter),需要手动配置 Bean。
核心接口契约
ChatModel 接口族
// 同步调用
public interface ChatModel extends Model<Prompt, ChatResponse> {
ChatResponse call(Prompt prompt);
}
// 流式调用
public interface StreamingChatModel {
Flux<ChatResponse> stream(Prompt prompt);
}
两个设计细节值得关注:
为什么用接口而不是抽象类? 厂商 SDK 可能强制要求继承某个基类。Java 单继承限制下,用接口可以让适配器同时继承 SDK 基类并实现 Spring AI 接口——用抽象类就绑死了。
为什么 StreamingChatModel 是独立接口? 有些厂商只支持同步调用。如果 stream() 放在 ChatModel 里,不支持流式的厂商要么抛出 UnsupportedOperationException(不优雅),要么返回伪流式(误导调用方)。独立接口让能力声明更精确。
完整模型能力矩阵
| 能力类型 | 核心接口 | 返回类型 | 典型支持厂商 |
|---|---|---|---|
| 对话(同步) | ChatModel |
ChatResponse |
所有厂商 |
| 对话(流式) | StreamingChatModel |
Flux<ChatResponse> |
OpenAI, Ollama, Anthropic |
| 文本嵌入 | EmbeddingModel |
EmbeddingResponse |
OpenAI, Ollama, HuggingFace |
| 图片生成 | ImageModel |
ImageResponse |
OpenAI, Stability AI |
EmbeddingModel 同理:
public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {
EmbeddingResponse call(EmbeddingRequest request);
default float[] embed(String text) { // 便捷方法:单条文本直接返回向量
return call(new EmbeddingRequest(List.of(text), null))
.getResults().get(0).getOutput();
}
}
embed(String) 这个 default 方法让 RAG 场景中单条查询向量化变得非常简洁——不需要手动构造 EmbeddingRequest,一行 embeddingModel.embed("查询内容") 搞定。各厂商的嵌入适配器也遵循同样的三步转换模式,只是返回的是浮点数组而非自然语言。
OpenAI 适配器的标准实现模式
public class OpenAiChatModel implements ChatModel, StreamingChatModel {
private final OpenAiApi openAiApi;
private final OpenAiChatOptions defaultOptions;
@Override
public ChatResponse call(Prompt prompt) {
// 步骤1:Prompt → OpenAI 请求格式
ChatCompletionRequest request = createRequest(prompt, false);
// 步骤2:调用 API
ResponseEntity<ChatCompletion> response = this.openAiApi
.chatCompletionEntity(request);
// 步骤3:OpenAI 响应 → 统一 ChatResponse
return toChatResponse(response.getBody());
}
}
三步转换模式是所有厂商适配器的标准模板:格式转换 → API 调用 → 响应映射。流程一样,差异全在步骤 1 和步骤 3 的消息格式映射上。
自动配置 SPI 机制
加载链路
Spring AI 利用 Spring Boot 3.x 的自动配置机制实现"引入即用":
Spring Boot 启动
→ 扫描所有 jar 的 META-INF/spring/*.imports
→ 汇总候选配置类
→ 逐个检查 @ConditionalOnClass 等条件注解
→ 满足条件的注册 Bean,不满足的跳过
spring.factories → .imports 演进
Spring Boot 2.x 用 META-INF/spring.factories,3.x 改成 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(每行一个类):
# 新格式(Spring Boot 3.x,Spring AI 采用)
org.springframework.ai.openai.OpenAiAutoConfiguration
org.springframework.ai.ollama.OllamaAutoConfiguration
org.springframework.ai.anthropic.AnthropicAutoConfiguration
新格式每类一行,对 Git 合并友好得多。
条件注解的精准控制
以 Ollama 的自动配置为例:
@AutoConfiguration
@ConditionalOnClass(OllamaApi.class) // ① classpath 中有依赖才生效
@EnableConfigurationProperties({
OllamaConnectionProperties.class, // ② 绑定连接配置
OllamaChatProperties.class // ③ 绑定聊天参数
})
public class OllamaAutoConfiguration {
@Bean
@ConditionalOnMissingBean // ④ 允许用户覆盖
public OllamaApi ollamaApi(OllamaConnectionProperties properties) {
return new OllamaApi(properties.getBaseUrl());
}
}
四个关键机制:
@ConditionalOnClass:只有 pom 中引入了对应依赖,配置才生效。这是"按需加载"的核心。@EnableConfigurationProperties:将 yml 配置绑定到类型安全的 Java 对象,支持 IDE 补全。@ConditionalOnMissingBean:用户自定义了同类型 Bean,框架默认 Bean 就不创建——“约定大于配置,但允许覆盖”。@ConditionalOnProperty:配合使用,如只有配置了spring.ai.ollama.base-url才启用。
调试技巧:在 application.yml 中加 debug: true,启动后控制台打印完整的 CONDITIONS EVALUATION REPORT,哪些配置生效、为什么跳过一目了然。
多厂商并存时的 Bean 冲突
同时引入 OpenAI 和 Ollama,容器中会有两个 ChatModel Bean,直接 @Autowired 会抛 NoUniqueBeanDefinitionException。
三种解决方案:
// 方案一:@Primary(适合有明确主力模型)
@Bean @Primary
public ChatModel primaryChatModel(OpenAiChatModel model) { return model; }
// 方案二:@Qualifier(适合不同场景用不同模型)
@Autowired @Qualifier("openAiChatModel") private ChatModel fastModel;
// 方案三:ChatClient 指定模型(Spring AI 推荐)
@Bean
public ChatClient openAiClient(OpenAiChatModel model) {
return ChatClient.builder(model).build();
}
推荐方案三。
ChatClient是 Spring AI 1.0 的 Fluent API,可在调用链上动态覆盖 options、system prompt、advisor,比直接注入ChatModel灵活得多。
消息格式转换:适配器最核心的工作
各厂商消息格式对比
OpenAI:
{"model": "gpt-4o", "messages": [
{"role": "system", "content": "你是助手"},
{"role": "user", "content": "你好"}
]}
Anthropic:
{"system": "你是助手", "messages": [
{"role": "user", "content": "你好"}
]}
Ollama:
{"model": "llama3", "stream": false,
"messages": [{"role": "system", "content": "你是助手"}, {"role": "user", "content": "你好"}],
"options": {"temperature": 0.7, "num_predict": 2000}
}
关键差异总结:
| 差异点 | OpenAI | Anthropic | Ollama |
|---|---|---|---|
| system 消息位置 | messages 数组内 | 顶层 system 字段 | messages 数组内 |
| 温度参数路径 | 顶层 temperature | 顶层 temperature | options.temperature |
| 最大 token 参数 | max_tokens | max_tokens | options.num_predict |
| 流式开关 | 顶层 stream | 独立 SSE 端点 | 顶层 stream |
转换逻辑的核心差异
OpenAI 的适配器把 system 消息放在 messages 数组里,Anthropic 的适配器则把它提到请求体的顶层字段:
// Anthropic 适配器的差异处理(伪代码)
private AnthropicRequest toAnthropicRequest(Prompt prompt) {
String systemMessage = null;
List<AnthropicMessage> messages = new ArrayList<>();
for (Message msg : prompt.getInstructions()) {
if (msg.getMessageType() == MessageType.SYSTEM) {
systemMessage = msg.getContent(); // 提到顶层
} else {
messages.add(toAnthropicMessage(msg));
}
}
return AnthropicRequest.builder()
.system(systemMessage)
.messages(messages)
.build();
}
适配器模式的核心价值就在这里:厂商差异完全封装在各适配器内部,对外暴露统一的 ChatModel.call(Prompt)。调用方不需要知道底层消息格式长什么样。
Function Calling 的格式差异
工具调用是消息格式差异最复杂的部分。OpenAI 用 {type: "function", function: {name, parameters}},Anthropic 用 {name, input_schema},框架需要为每个厂商生成不同结构的 JSON,同时从各厂商不同格式的响应中提取统一的 ToolCall 对象——这是适配器代码中最容易出 Bug 的地方。
配置属性的分层设计
ChatOptions(通用:model, temperature, maxTokens, topP, stopSequences)
├── OpenAiChatOptions(扩展:frequencyPenalty, presencePenalty, responseFormat, seed)
├── AzureOpenAiChatOptions(继承 OpenAiChatOptions + deploymentName)
├── OllamaChatOptions(扩展:keepAlive, mirostat, numCtx, numPredict, repeatLastN)
└── AnthropicChatOptions(扩展:metadata, thinking)
配置示例对比:
# OpenAI
spring.ai.openai:
api-key: ${OPENAI_API_KEY}
chat.options:
model: gpt-4o
temperature: 0.7
# Ollama(本地部署,无需 api-key)
spring.ai.ollama:
base-url: http://localhost:11434
chat.options:
model: llama3:8b
num-predict: 4096
# DeepSeek(兼容 OpenAI 协议,复用 spring-ai-openai 适配器)
spring.ai.openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat.options:
model: deepseek-chat
注意 DeepSeek:它兼容 OpenAI 协议,所以直接用 spring-ai-openai 适配器,只需改 base-url 和 api-key。兼容 OpenAI 协议的厂商(DeepSeek、Groq、Together AI、LocalAI 等)都可以零额外开发成本接入——这是统一接口带来的最大红利。
Options 合并策略
Spring AI 中 Options 的生效优先级是一个容易踩坑的点。一次调用的最终参数由三层合并而来:
// 优先级:启动配置 < Bean 默认值 < Runtime 覆盖
// 1. application.yml 中的 chat.options(启动时加载)
// 2. ChatClientBuilder.defaultOptions()(Bean 级别默认值)
// 3. ChatClient.prompt().options()(运行时覆盖,优先级最高)
ChatClient.builder(model)
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4o-mini") // Bean 默认值
.temperature(0.5).build())
.build()
.prompt()
.options(OpenAiChatOptions.builder()
.temperature(0.9).build()) // 运行时覆盖,只改 temperature
.call();
// 最终生效:model=gpt-4o-mini, temperature=0.9
合并逻辑在 ChatModel.createRequest() 中实现:先取 defaultOptions,再用 runtime options 覆盖非 null 字段。这意味着 null 字段不覆盖——如果运行时 options 中 model 为 null,就沿用默认值的 model。这个设计很实用:可以只覆盖想改的参数,其他保持默认。
配置属性的类型安全绑定
@ConfigurationProperties(prefix = "spring.ai.ollama.chat.options")
public class OllamaChatOptions extends ChatOptions {
private String model = "llama3"; // 默认值
private String keepAlive = "5m";
private Integer numCtx;
private Integer numPredict; // null = 不限制
private Integer repeatLastN = 64;
}
配置→对象→注入的完整链路:
application.yml
→ @ConfigurationProperties 绑定
→ OllamaChatOptions 对象
→ OllamaAutoConfiguration 注入
→ OllamaChatModel.defaultOptions
配合 @ConfigurationPropertiesScan 或 @EnableConfigurationProperties,IDE 可以对 spring.ai.ollama.* 做自动补全——配置驱动开发的体验很好。
多模型路由:生产实战策略
生产项目中同时跑多个模型是常态:
- 成本控制:简单问题用便宜模型(GPT-4o-mini 或本地 Ollama),成本可降低 80%+
- 高可用兜底:旗舰模型挂了自动降级
- 延迟优化:对延迟敏感的场景用本地 Ollama,毫秒级响应
基于内容的路由
@Bean
@Primary
public ChatModel routingChatModel(
OpenAiChatModel gpt4o,
OpenAiChatModel gpt4oMini,
OllamaChatModel ollama) {
return prompt -> {
String text = extractText(prompt);
// 简单问题 → 本地 Ollama;复杂问题 → GPT-4o;常规 → GPT-4o-mini
ChatModel model = text.length() < 500 ? ollama
: text.length() < 2000 ? gpt4oMini : gpt4o;
return callWithFallback(model, gpt4oMini, prompt);
};
}
带熔断的健壮路由
@Component
public class ResilientModelRouter implements ChatModel {
private final List<NamedModel> models;
private final Map<String, AtomicInteger> failures = new ConcurrentHashMap<>();
private static final int THRESHOLD = 3;
@Override
public ChatResponse call(Prompt prompt) {
for (NamedModel nm : models) {
if (failures.getOrDefault(nm.name, new AtomicInteger(0)).get() >= THRESHOLD)
continue; // 熔断打开
try {
ChatResponse resp = nm.model.call(prompt);
failures.remove(nm.name);
return resp;
} catch (Exception e) {
failures.computeIfAbsent(nm.name, k -> new AtomicInteger()).incrementAndGet();
}
}
throw new RuntimeException("All models exhausted");
}
}
生产建议:手动熔断只适合简单场景。正式环境建议用 Resilience4j 或 Sentinel,它们提供滑动窗口统计、半开状态、事件通知等成熟能力。
基于 ChatClient 的声明式路由
对于简单的双模型场景,直接定义两个 ChatClient Bean 更清晰:
@Bean
public ChatClient fastClient(OllamaChatModel ollama) {
return ChatClient.builder(ollama)
.defaultSystem("你是简洁高效的助手,回复尽量简短")
.build();
}
@Bean
public ChatClient smartClient(OpenAiChatModel gpt4o) {
return ChatClient.builder(gpt4o)
.defaultSystem("你是深度推理助手,请给出详细分析")
.build();
}
// 业务代码按场景选择
@Service
public class ChatService {
@Qualifier("fastClient") private final ChatClient fastClient;
@Qualifier("smartClient") private final ChatClient smartClient;
public String answer(String question) {
ChatClient client = isComplex(question) ? smartClient : fastClient;
return client.prompt().user(question).call().content();
}
}
ChatClient 的方式优势在于:每个 Client 可以有独立的 system prompt、advisor 链、options 默认值,比操作裸 ChatModel 更语义化。
新增适配器检查清单
| 序号 | 组件 | 关键职责 | 必须 |
|---|---|---|---|
| 1 | XxxApi |
封装 REST API,处理认证和序列化 | 是 |
| 2 | XxxChatModel |
实现 ChatModel + StreamingChatModel,三步转换 | 是 |
| 3 | XxxChatOptions |
继承 ChatOptions,加厂商特有参数 | 是 |
| 4 | XxxConnectionProperties |
@ConfigurationProperties 绑定连接参数 | 是 |
| 5 | XxxAutoConfiguration |
条件注入 Bean | 是 |
| 6 | AutoConfiguration.imports |
META-INF/spring/…imports 中注册 | 是 |
| 7 | 消息格式转换逻辑 | 双向转换 Spring AI Message ↔ 厂商消息格式 | 是 |
| 8 | XxxEmbeddingModel |
实现 EmbeddingModel(如果厂商支持) | 否 |
| 9 | spring-ai-xxx-boot-starter |
Starter 模块,一键引入 | 推荐 |
开发建议:
- 先照抄:复制
spring-ai-openai模块,全局替换类名和包名,最快路径 - 重点改消息格式:90% 的差异化工作在这里,特别是 tool calling 格式
- 统一异常处理:各厂商错误码格式不同,需统一映射到 Spring AI 的异常体系
- 注意 SSE 格式:不同厂商的流式 SSE 格式可能不同(OpenAI 是
data: {...},Anthropic 是event: ...\ndata: {...}) - 用 RestClient 拦截器统一处理超时、重试、日志埋点,不要每个方法手写
总结
Spring AI 的多模型适配器 = 统一接口 + 条件注入 + 格式适配——用 ChatModel 等接口定义契约,用 Spring Boot 自动配置实现按需加载,用各厂商适配器封装消息格式差异,让业务代码与 AI 厂商彻底解耦。
关键设计决策回顾:
- 接口而非抽象类:给适配器最大继承灵活性
- 流式接口独立定义:允许厂商按能力声明,不强人所难
- 条件注入而非 if-else:用
@ConditionalOnClass等声明式配置替代大段分支判断 - 配置分层而非平铺:通用参数提取基类,厂商特有参数各管各的
@ConditionalOnMissingBean:用户随时可自定义覆盖框架默认行为
掌握了这些机制后,再看 Spring AI 的 RAG、Function Calling、Advisor 链,会发现它们都遵循同样的设计哲学——统一抽象、隔离差异、按需加载。
参考资源
本文为 Spring AI 源码解析系列第六篇。系列文章导航:
- Spring AI 源码解析(一):整体架构与设计理念
- Spring AI 源码解析(二):ChatClient 的构建与配置
- Spring AI 源码解析(三):Prompt 模板与消息系统
- Spring AI 源码解析(四):Tool Calling 机制与实现
- Spring AI 源码解析(五):流式响应与 SSE 源码实现
- Spring AI 源码解析(六):自动配置扩展机制与多模型适配器(本文)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)