
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 |
模块关系可以画成这样:
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.openai和spring.ai.openai.chat配置; - 合并公共连接配置与 Chat 专属配置;
- 创建同步、异步 OpenAI SDK Client;
- 使用这些对象构建了
OpenAiChatModel。
有个细节很容易看错,一定要注意:OpenAIClient 和 OpenAIClientAsync 只是方法内的局部变量。对应的两个创建方法是 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();
}
它还会准备 ToolCallbackResolver 和 ToolExecutionExceptionProcessor。
回看第一章的内容,虽然只调用了一句"你好",但 Tool Calling 的基础 Bean 已经在启动阶段准备好了。真正执行工具仍然发生在运行时 Advisor Chain 中,这里只是先把依赖装配完成。
ChatClientAutoConfiguration 承接模型
有了 OpenAiChatModel,ChatClientAutoConfiguration 才能创建 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 最核心的约定:提供能直接运行的默认值,同时给业务保留覆盖入口。
启动时序
这张图只表示当前主链。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
如果配置成 false,ChatClientAutoConfiguration 整体不会生效。
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