Spring AI 2.0 源码解析(二):Starter 如何自动装配 ChatClient ?

Builder 的出现

第一章的示例只引入了一个 Spring AI 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

业务代码里没有声明 ChatModel,也没有声明 ChatClient.Builder

java 复制代码
public ChatController(ChatClient.Builder builder) {
    this.chatClient = builder.build();
}

应用却能正常启动。

是不是很神奇?

所以第二篇文章,我们仍然保持节奏,先不深追 prompt(),往前退一小步:这个 Builder 到底是怎么进入 Spring 容器的?

从源码可知,其实 Starter 自己几乎没有实现代码。它负责把模型实现、ChatClient API 和自动配置模块放进 classpath;Spring Boot 再从各个自动配置模块的 AutoConfiguration.imports 中找到配置类,根据条件创建 Bean。

完整主链是:

rust 复制代码
Starter POM
  -> 依赖进入 classpath
  -> AutoConfiguration.imports
  -> ToolCallingAutoConfiguration
  -> OpenAiChatAutoConfiguration
  -> OpenAiChatModel
  -> ChatClientAutoConfiguration
  -> prototype ChatClient.Builder

明面上看起来,似乎引入依赖后 ChatClient 就会自动出现,实际上是多组模块和条件配置共同完成的。

这个依赖的源码在哪里

读完第一章的朋友,心里应该也有疑惑------ model-openai 的源码在哪里?我怎么没找到?

spring-ai-starter-model-openai 并不是一个独立仓库,它就在 Spring AI 的 monorepo 中:

markdown 复制代码
spring-ai/
└── starters/
    └── spring-ai-starter-model-openai/
        └── pom.xml

但是这个目录在 v2.0.0 中只有一个 pom.xml,没有 src/main/java

所以打开这个模块时看不到 Java 代码,因为这里的 Starter 只是承担依赖聚合的职责。

它引入的依赖很多,核心模块主要是下面几项:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-autoconfigure-model-openai</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-client-chat</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>

四个模块的具体分工如下:

模块 职责
spring-ai-openai 提供 OpenAiChatModel 和 OpenAI SDK 适配代码
spring-ai-autoconfigure-model-openai 根据配置创建 OpenAI 模型 Bean
spring-ai-client-chat 提供 ChatClient、Advisor Chain 等通用客户端代码
spring-ai-autoconfigure-model-chat-client 创建 ChatClient.Builder 及相关 Bean

模块关系可以画成这样:

flowchart TD A[&#34;spring-ai-starter-model-openai&#34;] --> B[&#34;spring-ai-openai<br>OpenAI 运行时实现&#34;] A --> C[&#34;spring-ai-autoconfigure-model-openai<br>模型自动配置&#34;] A --> D[&#34;spring-ai-client-chat<br>ChatClient API&#34;] A --> E[&#34;spring-ai-autoconfigure-model-chat-client<br>Client 自动配置&#34;] C --> F[&#34;Tool Calling、Retry、Observation<br>配套自动配置&#34;]

Spring AI 把运行时实现和 Spring Boot 自动配置拆成了不同模块。

这样,核心实现便不必强制依赖自动配置。于是不使用 Spring Boot 时,也可以直接调用 ChatClient.builder(chatModel) 手动组装。

Boot 从哪里发现自动配置

依赖进入 classpath 还不够,Spring Boot 需要知道有哪些自动配置类。

OpenAI 自动配置的模块里面有这个文件:

bash 复制代码
META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件内容是自动配置类的全限定名:

复制代码
org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration

ChatClient 自动配置模块也有一份同名文件:

复制代码
org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration

Spring Boot 启动时会自动收集 classpath 中的这些候选配置,再判断 @Conditional... 的条件。配置类出现在 imports 文件里,只代表它有资格参与自动配置,不代表一定会创建 Bean。

这是 Spring Boot 自动装配的基础知识,在这里不做赘述。

OpenAiChatModel 的创建过程

ChatClient.Builder 依赖 ChatModel,所以先看 OpenAiChatModel 的创建过程。

OpenAiChatAutoConfiguration 类上的条件如下:

java 复制代码
@AutoConfiguration
@EnableConfigurationProperties({
        OpenAiCommonProperties.class,
        OpenAiChatProperties.class
})
@ConditionalOnProperty(
        name = SpringAIModelProperties.CHAT_MODEL,
        havingValue = SpringAIModels.OPENAI,
        matchIfMissing = true)
public class OpenAiChatAutoConfiguration {
}

SpringAIModelProperties.CHAT_MODEL 对应的配置项是:

复制代码
spring.ai.model.chat

它要求值为 openai,但 matchIfMissing = true。因此哪怕只引入 OpenAI Starter、不配置这个属性时,OpenAI Chat 的自动配置也会默认生效。

真正创建模型的 Bean 方法是:

java 复制代码
@Bean
@ConditionalOnMissingBean
public OpenAiChatModel openAiChatModel(
        OpenAiCommonProperties commonProperties,
        OpenAiChatProperties chatProperties,
        ToolCallingManager toolCallingManager,
        ObjectProvider<ObservationRegistry> observationRegistry,
        ObjectProvider<MeterRegistry> meterRegistry,
        ObjectProvider<ChatModelObservationConvention> convention,
        ObjectProvider<OpenAiHttpClientBuilderCustomizer> customizers) {

    var resolvedProperties =
            OpenAiAutoConfigurationUtil.resolveCommonProperties(
                    commonProperties, chatProperties);

    OpenAIClient openAIClient = this.openAiClient(...);
    OpenAIClientAsync openAIClientAsync = this.openAiClientAsync(...);

    return OpenAiChatModel.builder()
            .openAiClient(openAIClient)
            .openAiClientAsync(openAIClientAsync)
            .options(chatProperties.toOptions())
            .toolCallingManager(toolCallingManager)
            .observationRegistry(...)
            .build();
}

这里做了四件事:

  • 绑定 spring.ai.openaispring.ai.openai.chat 配置;
  • 合并公共连接配置与 Chat 专属配置;
  • 创建同步、异步 OpenAI SDK Client;
  • 使用这些对象构建了 OpenAiChatModel

有个细节很容易看错,一定要注意:OpenAIClientOpenAIClientAsync 只是方法内的局部变量。对应的两个创建方法是 private,也没有 @Bean

Spring 容器里默认暴露的是 OpenAiChatModel,并不是这两个独立的 OpenAI SDK Client Bean。

如果需要修改底层 HTTP Client,也有预留的扩展点: OpenAiHttpClientBuilderCustomizer。自动配置会按顺序先收集这些 Bean,再交给 OpenAiSetup

ToolCallingManager 为什么先创建

openAiChatModel(...) 的参数中直接依赖 ToolCallingManager

这个 Bean 来自 ToolCallingAutoConfiguration

java 复制代码
@Bean
@ConditionalOnMissingBean
ToolCallingManager toolCallingManager(
        ToolCallbackResolver toolCallbackResolver,
        ToolExecutionExceptionProcessor exceptionProcessor,
        ObjectProvider<ObservationRegistry> observationRegistry,
        ObjectProvider<ToolCallingObservationConvention> convention) {

    return ToolCallingManager.builder()
            .observationRegistry(...)
            .toolCallbackResolver(toolCallbackResolver)
            .toolExecutionExceptionProcessor(exceptionProcessor)
            .build();
}

它还会准备 ToolCallbackResolverToolExecutionExceptionProcessor

回看第一章的内容,虽然只调用了一句"你好",但 Tool Calling 的基础 Bean 已经在启动阶段准备好了。真正执行工具仍然发生在运行时 Advisor Chain 中,这里只是先把依赖装配完成。

ChatClientAutoConfiguration 承接模型

有了 OpenAiChatModelChatClientAutoConfiguration 才能创建 Builder。

先看类上的条件:

java 复制代码
@AutoConfiguration(after = ToolCallingAutoConfiguration.class)
@ConditionalOnClass(ChatClient.class)
@EnableConfigurationProperties(
        ChatClientBuilderProperties.class)
@ConditionalOnProperty(
        prefix = "spring.ai.chat.client",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true)
public class ChatClientAutoConfiguration {
}

它要求:

  • classpath 中存在 ChatClient
  • spring.ai.chat.client.enabled 没有被设置成 false

Starter 已经引入 spring-ai-client-chat,所以第一个条件成立。第二个属性默认是 true

接着创建三个关键 Bean:

Bean 作用 Scope
ChatClientBuilderConfigurer 统一应用 ChatClientBuilderCustomizer singleton
ToolCallingAdvisor.Builder ToolCallingManager 接入 ChatClient singleton
ChatClient.Builder 绑定默认 ChatModel,供业务创建 ChatClient prototype

Builder 的创建代码是:

java 复制代码
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
ChatClient.Builder chatClientBuilder(
        ChatClientBuilderProperties properties,
        ChatClientBuilderConfigurer configurer,
        ChatModel chatModel,
        ObjectProvider<ObservationRegistry> observationRegistry,
        ObjectProvider<ChatClientObservationConvention> clientConvention,
        ObjectProvider<AdvisorObservationConvention> advisorConvention,
        ObjectProvider<ToolCallingAdvisor.Builder<?>> toolAdvisorBuilder) {

    ChatClient.Builder builder = ChatClient.builder(
            chatModel,
            observationRegistry.getIfUnique(
                    () -> ObservationRegistry.NOOP),
            clientConvention.getIfUnique(),
            advisorConvention.getIfUnique(),
            toolAdvisorBuilder.getIfAvailable());

    return configurer.configure(builder);
}

这里并没有 builder.build()

自动配置到此为止,只把 prototype 的 ChatClient.Builder 放进容器。最终的 ChatClient 仍由业务代码创建。

为什么使用 prototype

假设一个系统需要两个 ChatClient:

java 复制代码
@Bean
ChatClient customerServiceClient(ChatClient.Builder builder) {
    return builder
            .defaultSystem("你是客服助手")
            .build();
}

@Bean
ChatClient codeReviewClient(ChatClient.Builder builder) {
    return builder
            .defaultSystem("你是 Java 代码审查助手")
            .build();
}

两个 Bean 方法拿到的是不同 Builder。各自设置默认 System Prompt,不会修改同一个可变 Builder。

可以用这段代码来直接验证:

java 复制代码
@Bean
ApplicationRunner inspect(ApplicationContext context) {
    return args -> {
        ChatModel model = context.getBean(ChatModel.class);
        ChatClient.Builder first =
                context.getBean(ChatClient.Builder.class);
        ChatClient.Builder second =
                context.getBean(ChatClient.Builder.class);

        System.out.println(model.getClass().getName());
        System.out.println(first == second);
    };
}

输出结果:

arduino 复制代码
org.springframework.ai.openai.OpenAiChatModel
false

prototype 解决的是 Builder 配置隔离,不代表每次请求都从容器重新获取 Builder。通常在配置类或构造方法中拿到一次 Builder,构建出可复用的 ChatClient 即可。

自定义配置如何生效

ChatClient.Builder 返回前还会经过 ChatClientBuilderConfigurer

java 复制代码
public ChatClient.Builder configure(
        ChatClient.Builder builder) {
    applyCustomizers(builder);
    return builder;
}

它会按顺序执行容器里的 ChatClientBuilderCustomizer

例如给所有自动配置的 Builder 增加默认 System Prompt:

java 复制代码
@Bean
ChatClientBuilderCustomizer defaultSystemCustomizer() {
    return builder -> builder.defaultSystem(
            "回答必须准确,不确定时直接说明");
}

业务代码后续仍可以继续修改 Builder。

Customizer 适合设置全局约定,至于具体的场景配置更适合放在各自的 ChatClient Bean 中。

另外,主要 Bean 都带有 @ConditionalOnMissingBean。业务代码自己声明同类型 Bean 后,默认自动配置会退让,而不是再创建一份参与竞争。

这是 Spring Boot Starter 最核心的约定:提供能直接运行的默认值,同时给业务保留覆盖入口。

启动时序

sequenceDiagram participant B as &#34;Spring Boot&#34; participant I as &#34;AutoConfiguration.imports&#34; participant T as &#34;ToolCallingAutoConfiguration&#34; participant O as &#34;OpenAiChatAutoConfiguration&#34; participant C as &#34;ChatClientAutoConfiguration&#34; B->>I: 收集 classpath 中的自动配置候选 I-->>B: 返回 Tool、OpenAI、ChatClient 配置类 B->>T: 创建 Tool Calling 基础 Bean T-->>B: ToolCallingManager B->>O: 绑定配置并创建 SDK Client O-->>B: OpenAiChatModel B->>C: 注入 ChatModel 和 ToolCallingManager C-->>B: prototype ChatClient.Builder

这张图只表示当前主链。Retry、Observation、Chat Memory 等自动配置也会参与启动,只是它们不影响"Builder 为什么出现"这个问题,先不展开。

Starter 不只导入 Chat

OpenAI 模块的 AutoConfiguration.imports 还列出了 Embedding、Image、Audio 和 Moderation 自动配置。

这些配置类在 2.0.0 中同样使用 matchIfMissing = true。只使用 Chat 时,可以明确关闭不需要的模型类型:

yaml 复制代码
spring:
  ai:
    model:
      embedding: none
      image: none
      audio:
        speech: none
        transcription: none
      moderation: none

这些都不是调用 ChatClient 的必要配置,但有助于让容器中的模型 Bean 与实际业务保持一致。

容易出问题的三个位置

Builder 根本没有创建

先检查:

yaml 复制代码
spring:
  ai:
    chat:
      client:
        enabled: true

如果配置成 falseChatClientAutoConfiguration 整体不会生效。

OpenAiChatModel 没有创建

检查模型选择:

yaml 复制代码
spring:
  ai:
    model:
      chat: openai

设置成 none 或其他供应商后,OpenAiChatAutoConfiguration 不会创建 OpenAiChatModel

同时存在多个 ChatModel

自动配置的 Builder 需要注入一个确定的 ChatModel。如果容器里有多个候选 Bean,应当指定一个 @Primary,或者按模型手动创建不同的 ChatClient。

这类问题出现后,大概率是发生在启动装配阶段,说明业应用还没进入到 prompt() ,因此不必耗费过多精力在排查 Fluent API 上。

怎么确认自动配置是否生效

最直接的方法是开启 Spring Boot 的 Condition Evaluation Report:

bash 复制代码
java -jar app.jar --debug

重点搜索:

复制代码
OpenAiChatAutoConfiguration
ChatClientAutoConfiguration
ToolCallingAutoConfiguration

如果向自己看源码来跟进,可以参考我上面讲解的顺序,建议这样来打断点:

顺序 断点 观察内容
1 ToolCallingAutoConfiguration#toolCallingManager 工具调用基础依赖怎样创建
2 OpenAiChatAutoConfiguration#openAiChatModel 配置、SDK Client 和 ChatModel 怎样组装
3 ChatClientAutoConfiguration#toolCallingAdvisorBuilder Tool Calling 怎样接入 Builder
4 ChatClientAutoConfiguration#chatClientBuilder ChatModel 怎样进入 ChatClient.Builder
5 ChatClientBuilderConfigurer#configure 全局 Customizer 怎样应用

回到开头

现在可以回答最初的问题了。

spring-ai-starter-model-openai 并不会直接扫描出一个 ChatClient,它只是把需要的模块放进 classpath,后面的创建工作仍然交由 Spring Boot 自动配置完成:

rust 复制代码
spring-ai-starter-model-openai
  -> spring-ai-autoconfigure-model-openai
  -> OpenAiChatAutoConfiguration
  -> OpenAiChatModel
  -> ChatClientAutoConfiguration
  -> ChatClient.Builder
  -> 业务代码 build()
  -> ChatClient

Starter 解决的仅仅是默认装配问题。

它让最小项目少写一批配置代码,同时依靠条件注解、Customizer 和 @ConditionalOnMissingBean 来提供出可扩展的空间。当然也有代价,那就是对象创建过程被分散到了 POM、imports 文件和多个自动配置类里,不容易阅读和理解。这都是老生常谈的问题。

下一篇我们继续深入 spring-ai-client-chat,单独拆解 ChatClient ,看看它包装的这套 Fluent API 怎样把 system、user、options、advisors 和 tools 收集成一次请求。

相关源码

  • spring-ai-starter-model-openai/pom.xml
  • OpenAI AutoConfiguration.imports
  • OpenAiChatAutoConfiguration.java
  • ToolCallingAutoConfiguration.java
  • ChatClient AutoConfiguration.imports
  • ChatClientAutoConfiguration.java
  • ChatClientBuilderConfigurer.java
相关推荐
_小柏_1 小时前
总结下最近面试出现的问题
c++·面试
饼干哥哥1 小时前
重生之我是导演:爆改成「牛来版」黑客帝国?附3D预演台保姆级教程!
人工智能·后端·深度学习
MacroZheng1 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
世界哪有真情2 小时前
AI 写代码两年多,我发现自己越来越"看不进去"了
前端·后端·ai编程
星栈2 小时前
被 Rust async 纠正的三个异步认知
前端·后端·rust
夏雪coding2 小时前
openpyxl 对账实战:金额浮点、前导零丢失、20 位单号科学计数法
人工智能·后端
禁止摆烂_才浅2 小时前
前端 AI 面试题
前端·面试·ai编程
程序员老赵2 小时前
Docker 部署 ZLMediaKit:轻松搭建高性能流媒体服务平台
前端·javascript·后端
SamDeepThinking2 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员