概述

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)在框架级设计中的应用——核心抽象永远保持纯净,不会因某个厂商的特殊需求被污染。

模块依赖关系

业务代码

spring-ai-core
ChatModel / EmbeddingModel / ImageModel

spring-ai-openai

spring-ai-ollama

spring-ai-anthropic

spring-ai-spring-boot-autoconfigure

解读:业务代码只依赖 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-urlapi-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.* 做自动补全——配置驱动开发的体验很好。

多模型路由:生产实战策略

生产项目中同时跑多个模型是常态:

  1. 成本控制:简单问题用便宜模型(GPT-4o-mini 或本地 Ollama),成本可降低 80%+
  2. 高可用兜底:旗舰模型挂了自动降级
  3. 延迟优化:对延迟敏感的场景用本地 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 模块,一键引入 推荐

开发建议

  1. 先照抄:复制 spring-ai-openai 模块,全局替换类名和包名,最快路径
  2. 重点改消息格式:90% 的差异化工作在这里,特别是 tool calling 格式
  3. 统一异常处理:各厂商错误码格式不同,需统一映射到 Spring AI 的异常体系
  4. 注意 SSE 格式:不同厂商的流式 SSE 格式可能不同(OpenAI 是 data: {...},Anthropic 是 event: ...\ndata: {...}
  5. 用 RestClient 拦截器统一处理超时、重试、日志埋点,不要每个方法手写

总结

Spring AI 的多模型适配器 = 统一接口 + 条件注入 + 格式适配——用 ChatModel 等接口定义契约,用 Spring Boot 自动配置实现按需加载,用各厂商适配器封装消息格式差异,让业务代码与 AI 厂商彻底解耦。

关键设计决策回顾

  • 接口而非抽象类:给适配器最大继承灵活性
  • 流式接口独立定义:允许厂商按能力声明,不强人所难
  • 条件注入而非 if-else:用 @ConditionalOnClass 等声明式配置替代大段分支判断
  • 配置分层而非平铺:通用参数提取基类,厂商特有参数各管各的
  • @ConditionalOnMissingBean:用户随时可自定义覆盖框架默认行为

掌握了这些机制后,再看 Spring AI 的 RAG、Function Calling、Advisor 链,会发现它们都遵循同样的设计哲学——统一抽象、隔离差异、按需加载

参考资源


本文为 Spring AI 源码解析系列第六篇。系列文章导航:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐