SpringAI(GA)的chat:快速上手+自动注入源码解读

500 阅读7分钟

原文链接# SpringAI(GA)的chat:快速上手+自动注入源码解读

教程说明

说明:本教程将采用2025年5月20日正式的GA版,给出如下内容

  1. 核心功能模块的快速上手教程
  2. 核心功能模块的源码级解读
  3. 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 即可,同时可选对应的模型

资料地址:大模型服务平台百炼:如何获取 API Key

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

类上重点注解说明

  1. 在 ObservationAutoConfiguration 类之后加载,确保观测基础设施已就绪
  2. 当类路径 ChatClient 类时才启用该自动配置
  3. 启用 ChatClientBuilderProperties 配置属性的支持,将配置文件中的 spring.ai.chat.client.* 映射到该类实例
  4. 只有当配置项 spring.ai.chat.client.enabled=true 时,才启用该自动配置,默认为 true

对外提供 Bean

  1. ChatClientBuilderConfigurer:从容器中获取所有 ChatClientCustomizer 实例,配置 ChatClient.Builder 信息

  2. 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

类上重点注解说明

  1. 确保网络客户端(RestClient、WebClient)、重试机制、工具调用就绪后再注入
  2. 当类路径有 OpenAiApi 类时才启用该自动配置
  3. 启用 OpenAiConnectionProperties、OpenAiChatProperties 配置属性的支持
  4. 只有当配置项 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
  1. 校验 apiKey、baseUrl 最后拼接到 OpenAiApi 时不为空
  2. 根据 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) {
    }
}