原文链接# SpringAI(GA)的chat:快速上手+自动注入源码解读
教程说明
说明:本教程将采用2025年5月20日正式的GA版,给出如下内容
- 核心功能模块的快速上手教程
- 核心功能模块的源码级解读
- Spring ai alibaba增强的快速上手教程 + 源码级解读
版本:JDK21 + SpringBoot3.4.5 + SpringAI 1.0.0 + SpringAI Alibaba最新
将陆续完成如下章节教程。本章是第一章:chat初体验
chat快速上手
[!TIP] 通过自然语言的句子和 AI 模型进行会话交流
实战代码可见:github.com/GTyingzi/sp… 下的 chat
pom 文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8080
spring:
application:
name: advisor-base
ai:
openai:
api-key: ${DASHSCOPEAPIKEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-max
OPENAI 由于封禁的原因,国内无法很好的获取其 api-key,国内厂商阿里的百炼可进行平替,只需要替换对应的 api-key、base-url 即可,同时可选对应的模型
controller
ChatController
package com.spring.ai.tutorial.chat.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/call")
public String call(@RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?")String query) {
return chatClient.prompt(query).call().content();
}
@GetMapping("/stream")
public Flux<String> stream(@RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?")String query) {
return chatClient.prompt(query).stream().content();
}
}
效果
call 调用
stream 调用
ChatOptionController
package com.spring.ai.tutorial.chat.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yingzi
* @date 2025/5/24 16:52
*/
@RestController
@RequestMapping("/chat/option")
public class ChatOptionController {
private final ChatClient chatClient;
public ChatOptionController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultOptions(
OpenAiChatOptions.builder()
.temperature(0.9)
.build()
)
.build();
}
@GetMapping("/call")
public String call(@RequestParam(value = "query", defaultValue = "你好,请为我创造一首以“影子”为主题的诗")String query) {
return chatClient.prompt(query).call().content();
}
@GetMapping("/call/temperature")
public String callOption(@RequestParam(value = "query", defaultValue = "你好,请为我创造一首以“影子”为主题的诗")String query) {
return chatClient.prompt(query)
.options(
OpenAiChatOptions.builder()
.temperature(0.0)
.build()
)
.call().content();
}
}
chatClient 全局配置 temperature=0.9
- /call:使用的是 temperature=0.9
- /call/temperature:当前请求覆盖配置,temperature=0.0
效果
/call 接口的 temperature=0.9
/call/temperature 接口的 temperature=0.0
ChatClient + ChatModel自动注入篇
pom.xml 文件
入 ChatClient 依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
选择 chat 模型,这里使用 openai
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>
</dependency>
ChatClient 自动注入
ChatClientBuilderProperties
类的作用:
- 控制是否提供 ChatClient.Builder 聊天客户端构建器的 Bean,默认为 true
- 配置观测日志的行为,如是否记录提示词内容
package org.springframework.ai.model.chat.client.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("spring.ai.chat.client")
public class ChatClientBuilderProperties {
public static final String _CONFIG_PREFIX _= "spring.ai.chat.client";
private boolean enabled = true;
private final Observations observations = new Observations();
public Observations getObservations() {
return this.observations;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public static class Observations {
private boolean logPrompt = false;
public boolean isLogPrompt() {
return this.logPrompt;
}
public void setLogPrompt(boolean logPrompt) {
this.logPrompt = logPrompt;
}
}
}
ChatClientBuilderConfigurer
类的作用:
- 用于对 ChatClient.Builder 聊天客户端构建器进行扩展性配置
- 通过注册不同的 ChatClientCustomizer 实现,可动态调整聊天客户端
package org.springframework.ai.model.chat.client.autoconfigure;
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
public class ChatClientBuilderConfigurer {
private List<ChatClientCustomizer> customizers;
void setChatClientCustomizers(List<ChatClientCustomizer> customizers) {
this.customizers = customizers;
}
public ChatClient.Builder configure(ChatClient.Builder builder) {
this.applyCustomizers(builder);
return builder;
}
private void applyCustomizers(ChatClient.Builder builder) {
if (this.customizers != null) {
for(ChatClientCustomizer customizer : this.customizers) {
customizer.customize(builder);
}
}
}
}
ChatClientCustomizer
可通过实现 ChatClientCustomizer 函数式接口,自定义调整 ChatClient.Builder 的相关配置
package org.springframework.ai.chat.client;
@FunctionalInterface
public interface ChatClientCustomizer {
void customize(ChatClient.Builder chatClientBuilder);
}
ChatClientAutoConfiguration
类上重点注解说明
- 在 ObservationAutoConfiguration 类之后加载,确保观测基础设施已就绪
- 当类路径 ChatClient 类时才启用该自动配置
- 启用 ChatClientBuilderProperties 配置属性的支持,将配置文件中的
spring.ai.chat.client.*映射到该类实例 - 只有当配置项
spring.ai.chat.client.enabled=true时,才启用该自动配置,默认为 true
对外提供 Bean
-
ChatClientBuilderConfigurer:从容器中获取所有 ChatClientCustomizer 实例,配置 ChatClient.Builder 信息
-
ChatClient.Builder:使用 ChatModel 初始化 ChatClient.Builder,再
- @Scope("prototype"):每次注入都会生成新实例
内部配置配 TracerPresentObservationConfiguration、TracerNotPresentObservationConfiguration
-
配置项:
spring.ai.chat.client.observations.log-prompt=true -
当项目中存在 Tracer 时,启用带追踪能力的日志记录处理器
- 注册带有追踪能力的日志处理器,用于记录提示词内容,输出安全警告日志
-
当项目中不存在 Tracer 时,启用普通日志处理器
- 未使用追踪框架的情况下,仅记录提示词内容,输出安全警告日志
package org.springframework.ai.model.chat.client.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@AutoConfiguration(
afterName = {"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration"}
)
@ConditionalOnClass({ChatClient.class})
@EnableConfigurationProperties({ChatClientBuilderProperties.class})
@ConditionalOnProperty(
prefix = "spring.ai.chat.client",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public class ChatClientAutoConfiguration {
private static final Logger _logger _= LoggerFactory._getLogger_(ChatClientAutoConfiguration.class);
private static void logPromptContentWarning() {
_logger_.warn("You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}
@Bean
@ConditionalOnMissingBean
ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClientCustomizer> customizerProvider) {
ChatClientBuilderConfigurer configurer = new ChatClientBuilderConfigurer();
configurer.setChatClientCustomizers(customizerProvider.orderedStream().toList());
return configurer;
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ChatClientObservationConvention> observationConvention) {
ChatClient.Builder builder = ChatClient._builder_(chatModel, (ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry._NOOP_), (ChatClientObservationConvention)observationConvention.getIfUnique(() -> null));
return chatClientBuilderConfigurer.configure(builder);
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Tracer.class})
@ConditionalOnBean({Tracer.class})
static class TracerPresentObservationConfiguration {
@Bean
@ConditionalOnMissingBean(
value = {ChatClientPromptContentObservationHandler.class},
name = {"chatClientPromptContentObservationHandler"}
)
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = {"log-prompt"},
havingValue = "true"
)
TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientPromptContentObservationHandler(Tracer tracer) {
ChatClientAutoConfiguration._logPromptContentWarning_();
return new TracingAwareLoggingObservationHandler(new ChatClientPromptContentObservationHandler(), tracer);
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingClass({"io.micrometer.tracing.Tracer"})
static class TracerNotPresentObservationConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = {"log-prompt"},
havingValue = "true"
)
ChatClientPromptContentObservationHandler chatClientPromptContentObservationHandler() {
ChatClientAutoConfiguration._logPromptContentWarning_();
return new ChatClientPromptContentObservationHandler();
}
}
}
ChatModel 自动注入
OpenAiParentProperties
从 OpenAI 的开发者平台获取,基础配置信息
- apiKey(必填):密钥
- baseUrl(选填):调用 url,若没填会自动填充,详情可见 OpenAiConnectionProperties 类的_DEFAULT_BASE_URL_字段
- projectId(选填):项目 Id
- organizationId(选填):组织 Id
package org.springframework.ai.model.openai.autoconfigure;
class OpenAiParentProperties {
private String apiKey;
private String baseUrl;
private String projectId;
private String organizationId;
public String getApiKey() {
return this.apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getBaseUrl() {
return this.baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getProjectId() {
return this.projectId;
}
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public String getOrganizationId() {
return this.organizationId;
}
public void setOrganizationId(String organizationId) {
this.organizationId = organizationId;
}
}
OpenAiConnectionProperties
Connection 配置类,默认 baseUrl 为_DEFAULT_BASE_URL_,若配置文件有 baseUrl 配置则会覆盖
package org.springframework.ai.model.openai.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("spring.ai.openai")
public class OpenAiConnectionProperties extends OpenAiParentProperties {
public static final String _CONFIG_PREFIX _= "spring.ai.openai";
public static final String _DEFAULT_BASE_URL _= "https://api.openai.com";
public OpenAiConnectionProperties() {
super.setBaseUrl("https://api.openai.com");
}
}
OpenAiChatProperties
Chat 配置类。
-
配置 Chat Model,默认为“gpt-4o-mini”
-
配置 Chat 接口路径,默认为“/v1/chat/completions”
-
配置 temperature,默认为 0.7(值范围一般在 0~1,部分模型会大于 1)
- 值越低输出越确定(0,代表每次相同输入产生相同输出)
- 值越高随机性越强(产生更开放或不常见的回答,适用于创意写作等场景)
package org.springframework.ai.model.openai.autoconfigure;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties("spring.ai.openai.chat")
public class OpenAiChatProperties extends OpenAiParentProperties {
public static final String _CONFIG_PREFIX _= "spring.ai.openai.chat";
public static final String _DEFAULT_CHAT_MODEL _= "gpt-4o-mini";
public static final String _DEFAULT_COMPLETIONS_PATH _= "/v1/chat/completions";
private static final Double _DEFAULT_TEMPERATURE _= 0.7;
private String completionsPath = "/v1/chat/completions";
@NestedConfigurationProperty
private OpenAiChatOptions options;
public OpenAiChatProperties() {
this.options = OpenAiChatOptions._builder_().model("gpt-4o-mini").temperature(_DEFAULT_TEMPERATURE_).build();
}
public OpenAiChatOptions getOptions() {
return this.options;
}
public void setOptions(OpenAiChatOptions options) {
this.options = options;
}
public String getCompletionsPath() {
return this.completionsPath;
}
public void setCompletionsPath(String completionsPath) {
this.completionsPath = completionsPath;
}
}
OpenAiChatAutoConfiguration
类上重点注解说明
- 确保网络客户端(RestClient、WebClient)、重试机制、工具调用就绪后再注入
- 当类路径有 OpenAiApi 类时才启用该自动配置
- 启用 OpenAiConnectionProperties、OpenAiChatProperties 配置属性的支持
- 只有当配置项
spring.ai.model.chat.openai=true时,才会启用该自动配置,默认为 true
对外提供了 OpenAiChatModel 的 Bean
-
使用 openAiApi 方法构建底层 API 实例,通过 OpenAiChatModel.builder()构建 Chat 模型,另外配置了默认选项、工具调用、重试策略、观测注册表
-
openAiApi 侧封装了 OpenAI API 的构建逻辑,包括基础 URL、API Key、请求头、请求路径、HTTP 客户端等配置
- 注:非公开 Bean
package org.springframework.ai.model.openai.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import java.util.Objects;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate;
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
@AutoConfiguration(
after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class}
)
@ConditionalOnClass({OpenAiApi.class})
@EnableConfigurationProperties({OpenAiConnectionProperties.class, OpenAiChatProperties.class})
@ConditionalOnProperty(
name = {"spring.ai.model.chat"},
havingValue = "openai",
matchIfMissing = true
)
@ImportAutoConfiguration(
classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, ToolCallingAutoConfiguration.class}
)
public class OpenAiChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OpenAiChatModel openAiChatModel(OpenAiConnectionProperties commonProperties, OpenAiChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider, ObjectProvider<WebClient.Builder> webClientBuilderProvider, ToolCallingManager toolCallingManager, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ChatModelObservationConvention> observationConvention, ObjectProvider<ToolExecutionEligibilityPredicate> openAiToolExecutionEligibilityPredicate) {
OpenAiApi openAiApi = this.openAiApi(chatProperties, commonProperties, (RestClient.Builder)restClientBuilderProvider.getIfAvailable(RestClient::_builder_), (WebClient.Builder)webClientBuilderProvider.getIfAvailable(WebClient::_builder_), responseErrorHandler, "chat");
OpenAiChatModel chatModel = OpenAiChatModel._builder_().openAiApi(openAiApi).defaultOptions(chatProperties.getOptions()).toolCallingManager(toolCallingManager).toolExecutionEligibilityPredicate((ToolExecutionEligibilityPredicate)openAiToolExecutionEligibilityPredicate.getIfUnique(DefaultToolExecutionEligibilityPredicate::new)).retryTemplate(retryTemplate).observationRegistry((ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry._NOOP_)).build();
Objects._requireNonNull_(chatModel);
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) {
OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = OpenAIAutoConfigurationUtil._resolveConnectionProperties_(commonProperties, chatProperties, modelType);
return OpenAiApi._builder_().baseUrl(resolved.baseUrl()).apiKey(new SimpleApiKey(resolved.apiKey())).headers(resolved.headers()).completionsPath(chatProperties.getCompletionsPath()).embeddingsPath("/v1/embeddings").restClientBuilder(restClientBuilder).webClientBuilder(webClientBuilder).responseErrorHandler(responseErrorHandler).build();
}
}
工具类:OpenAIAutoConfigurationUtil
- 校验 apiKey、baseUrl 最后拼接到 OpenAiApi 时不为空
- 根据 projectId、organizationId 设置请求头
package org.springframework.ai.model.openai.autoconfigure;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
public final class OpenAIAutoConfigurationUtil {
private OpenAIAutoConfigurationUtil() {
}
@NotNull
public static ResolvedConnectionProperties resolveConnectionProperties(OpenAiParentProperties commonProperties, OpenAiParentProperties modelProperties, String modelType) {
String baseUrl = StringUtils._hasText_(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() : commonProperties.getBaseUrl();
String apiKey = StringUtils._hasText_(modelProperties.getApiKey()) ? modelProperties.getApiKey() : commonProperties.getApiKey();
String projectId = StringUtils._hasText_(modelProperties.getProjectId()) ? modelProperties.getProjectId() : commonProperties.getProjectId();
String organizationId = StringUtils._hasText_(modelProperties.getOrganizationId()) ? modelProperties.getOrganizationId() : commonProperties.getOrganizationId();
Map<String, List<String>> connectionHeaders = new HashMap();
if (StringUtils._hasText_(projectId)) {
connectionHeaders.put("OpenAI-Project", List._of_(projectId));
}
if (StringUtils._hasText_(organizationId)) {
connectionHeaders.put("OpenAI-Organization", List._of_(organizationId));
}
Assert._hasText_(baseUrl, "OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai." + modelType + ".base-url property.");
Assert._hasText_(apiKey, "OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai." + modelType + ".api-key property.");
return new ResolvedConnectionProperties(baseUrl, apiKey, CollectionUtils._toMultiValueMap_(connectionHeaders));
}
public static record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap<String, String> headers) {
}
}