在这里插入图片描述

Pre

大模型开发 - 01 Spring AI 核心特性一览

大模型开发 - 02 Spring AI Concepts

大模型开发 - 03 QuickStart_借助DeepSeekChatModel实现Spring AI 集成 DeepSeek

大模型开发 - 04 QuickStart_DeepSeek 模型调用流程源码解析:从 Prompt 到远程请求

大模型开发 - 06 QuickStart_本地大模型私有化部署实战:Ollama + Spring AI 全栈指南

大模型开发 - 07 ChatClient:构建统一、优雅的大模型交互接口


概述

在 Spring AI 框架中,ChatClient 是开发者与大语言模型(LLM)交互的核心入口。它提供了一套流畅(Fluent)的 API,支持同步调用与流式响应,并屏蔽了底层不同模型(如 OpenAI、Anthropic、DeepSeek、Groq 等)的差异,让你“一次编写,多模型运行”。

本文将全面解析 ChatClient 的设计理念、核心功能、高级用法及注意事项,高效构建企业级 AI 应用。


一、ChatClient 是什么?

ChatClient 是 Spring AI 提供的高层封装,用于与 AI 模型进行对话。它基于底层的 ChatModel 接口(如 OpenAiChatModelAnthropicChatModel)构建,但提供了更易用、更强大的编程模型:

  • 统一 API:无论底层是 GPT-4、Claude 还是 Llama3,代码写法一致
  • 流畅链式调用.prompt().user("...").system("...").call()
  • 支持同步与流式.call() 返回完整结果,.stream() 返回 Flux
  • 内置模板、工具调用、记忆、顾问(Advisors)等高级能力

📌 核心理念面向 ChatClient 编程,而非具体模型实现


二、快速入门:创建并使用 ChatClient

2.1 自动配置(单模型场景)

Spring Boot 会自动配置一个 ChatClient.Builder 原型 Bean:

@RestController
public class MyController {

    private final ChatClient chatClient;

    public MyController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping("/ai")
    public String ask(@RequestParam String userInput) {
        return chatClient.prompt()
                .user(userInput)
                .call()
                .content();
    }
}

用户输入设置用户消息的内容。call() 方法向 AI 模型发送请求,content() 方法将 AI 模型的响应作为 String 返回。

⚠️ 注意:必须通过 ChatClient.Builder 构建,不能直接 new

2.2 多模型场景:如何管理多个 ChatClient?

当应用需要同时使用多个模型(如 OpenAI + Anthropic),需禁用自动配置

# application.properties
spring.ai.chat.client.enabled=false

设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 自动配置

然后手动创建多个 ChatClient

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient openAiClient(OpenAiChatModel model) {
        return ChatClient.create(model);
    }

    @Bean
    public ChatClient anthropicClient(AnthropicChatModel model) {
        return ChatClient.create(model);
    }
}

在使用时通过 @Qualifier 注入:

@Service
public class AiService {

    @Qualifier("openAiClient")
    private final ChatClient openAi;

    @Qualifier("anthropicClient")
    private final ChatClient anthropic;
    
    // 根据业务逻辑选择模型
}
@Configuration
public class ChatClientExample {

    @Bean
    CommandLineRunner cli(
            @Qualifier("openAiChatClient") ChatClient openAiChatClient,
            @Qualifier("anthropicChatClient") ChatClient anthropicChatClient) {

        return args -> {
            var scanner = new Scanner(System.in);
            ChatClient chat;

            // Model selection
            System.out.println("\nSelect your AI model:");
            System.out.println("1. OpenAI");
            System.out.println("2. Anthropic");
            System.out.print("Enter your choice (1 or 2): ");

            String choice = scanner.nextLine().trim();

            if (choice.equals("1")) {
                chat = openAiChatClient;
                System.out.println("Using OpenAI model");
            } else {
                chat = anthropicChatClient;
                System.out.println("Using Anthropic model");
            }

            // Use the selected chat client
            System.out.print("\nEnter your question: ");
            String input = scanner.nextLine();
            String response = chat.prompt(input).call().content();
            System.out.println("ASSISTANT: " + response);

            scanner.close();
        };
    }
}

2.3 多个 OpenAI 兼容端点(如 Groq + OpenAI)

利用 mutate() 方法派生新实例:

// 基于同一个 OpenAiChatModel 派生不同配置
OpenAiApi groqApi = baseOpenAiApi.mutate()
    .baseUrl("https://api.groq.com/openai")
    .apiKey(System.getenv("GROQ_API_KEY"))
    .build();

OpenAiChatModel groqModel = baseChatModel.mutate()
    .openAiApi(groqApi)
    .defaultOptions(OpenAiChatOptions.builder()
        .model("llama3-70b-8192")
        .temperature(0.5)
        .build())
    .build();

ChatClient groqClient = ChatClient.builder(groqModel).build();
@Service
public class MultiModelService {

    private static final Logger logger = LoggerFactory.getLogger(MultiModelService.class);

    @Autowired
    private OpenAiChatModel baseChatModel;

    @Autowired
    private OpenAiApi baseOpenAiApi;

    public void multiClientFlow() {
        try {
            // Derive a new OpenAiApi for Groq (Llama3)
            OpenAiApi groqApi = baseOpenAiApi.mutate()
                .baseUrl("https://api.groq.com/openai")
                .apiKey(System.getenv("GROQ_API_KEY"))
                .build();

            // Derive a new OpenAiApi for OpenAI GPT-4
            OpenAiApi gpt4Api = baseOpenAiApi.mutate()
                .baseUrl("https://api.openai.com")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .build();

            // Derive a new OpenAiChatModel for Groq
            OpenAiChatModel groqModel = baseChatModel.mutate()
                .openAiApi(groqApi)
                .defaultOptions(OpenAiChatOptions.builder().model("llama3-70b-8192").temperature(0.5).build())
                .build();

            // Derive a new OpenAiChatModel for GPT-4
            OpenAiChatModel gpt4Model = baseChatModel.mutate()
                .openAiApi(gpt4Api)
                .defaultOptions(OpenAiChatOptions.builder().model("gpt-4").temperature(0.7).build())
                .build();

            // Simple prompt for both models
            String prompt = "What is the capital of France?";

            String groqResponse = ChatClient.builder(groqModel).build().prompt(prompt).call().content();
            String gpt4Response = ChatClient.builder(gpt4Model).build().prompt(prompt).call().content();

            logger.info("Groq (Llama3) response: {}", groqResponse);
            logger.info("OpenAI GPT-4 response: {}", gpt4Response);
        }
        catch (Exception e) {
            logger.error("Error in multi-client flow", e);
        }
    }
}

三、ChatClient 流畅 API 详解

3.1 构建 Prompt 的三种方式

// 1. 无参:链式构建
chatClient.prompt().user("Hello").system("You are helpful").call()

// 2. 传入 Prompt 对象
Prompt prompt = new Prompt("Hello");
chatClient.prompt(prompt).call()

// 3. 直接传用户消息(最简)
chatClient.prompt("Hello").call()

3.2 响应格式:多种返回类型

方法 返回类型 说明
.content() String 纯文本响应
.chatResponse() ChatResponse 包含元数据(token 数、生成选项等)
.entity(Actor.class) Actor 自动反序列化为 Java 对象
.entity(new ParameterizedTypeReference<List<Actor>>(){}) List<Actor> 支持泛型集合
.responseEntity() ResponseEntity<?> 包含 HTTP 状态码、头等(底层调试用)
ChatResponse chatResponse = chatClient.prompt()
    .user("Tell me a joke")
    .call()
    .chatResponse();
record ActorFilms(String actor, List<String> movies) {}

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);

💡 要求模型支持 JSON 输出,并在提示中明确要求返回 JSON 格式。

List<ActorFilms> actorFilms = chatClient.prompt()
    .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});

Flux<String> flux = this.chatClient.prompt()
    .user(u -> u.text("""
                        Generate the filmography for a random actor.
                        {format}
                      """)
            .param("format", this.converter.getFormat()))
    .stream()
    .content();

String content = this.flux.collectList().block().stream().collect(Collectors.joining());

List<ActorsFilms> actorFilms = this.converter.convert(this.content);

四、流式响应(Streaming)

适用于长文本生成、实时聊天等场景:

Flux<String> stream = chatClient.prompt()
    .user("Write a poem about spring")
    .stream()
    .content();

// WebFlux 控制器中直接返回 Flux
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamPoem() {
    return stream;
}

⚠️ 注意:流式必须依赖 spring-boot-starter-webflux,即使你的应用是 Servlet 架构。


五、Prompt 模板与参数化

支持运行时动态替换变量:

String movies = chatClient.prompt()
    .user(u -> u
        .text("List 5 movies by composer {composer}")
        .param("composer", "John Williams"))
    .call()
    .content();

默认使用 {} 作为占位符,但可自定义分隔符(避免与 JSON 冲突):

// 使用 < > 作为分隔符
chatClient.prompt()
    .user(u -> u.text("Composer: <composer>").param("composer", "Hans Zimmer"))
    .templateRenderer(
        StTemplateRenderer.builder()
            .startDelimiterToken('<')
            .endDelimiterToken('>')
            .build()
    )
    .call()
    .content();

六、默认配置:简化运行时代码

@Configuration 中设置默认行为,避免重复代码。

6.1 默认系统提示

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
                .build();
    }

}

控制器中只需写用户消息:

@RestController
class AIController {

	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai/simple")
	public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
		return Map.of("completion", this.chatClient.prompt().user(message).call().content());
	}
}

6.2 带参数的默认系统提示

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
                .build();
    }

}

@RestController
class AIController {
	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@GetMapping("/ai")
	Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
		return Map.of("completion",
				this.chatClient.prompt()
						.system(sp -> sp.param("voice", voice))
						.user(message)
						.call()
						.content());
	}

}

6.3 其他默认项

  • defaultOptions():设置温度、模型名等
  • defaultFunctions():注册工具函数
  • defaultAdvisors():配置 RAG、记忆等顾问

七、高级特性:Advisors

Advisors 是 Spring AI 的核心扩展机制,用于拦截并增强 Prompt

7.1 常见 Advisor

Advisor 作用
MessageChatMemoryAdvisor 自动注入对话历史
QuestionAnswerAdvisor 实现 RAG(检索增强生成)
SimpleLoggerAdvisor 记录请求/响应日志

7.2 使用示例:RAG + 记忆

ChatClient client = ChatClient.builder(chatModel)
    .build();

String response = client.prompt()
    .advisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(),
        QuestionAnswerAdvisor.builder(vectorStore).build()
    )
    .user("What did I ask earlier about Spring AI?")
    .call()
    .content();

执行顺序:先加记忆,再做检索,最后生成回答。

7.3 日志记录

// 启用 DEBUG 日志
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

// 代码中添加
.prompt()
.advisors(new SimpleLoggerAdvisor())
.user("...")

八、聊天记忆(Chat Memory)

ChatMemory 用于存储对话历史,解决 LLM 无状态问题。

内置实现:MessageWindowChatMemory

  • 默认保留最近 20 条消息
  • 系统消息始终保留
  • 支持多种存储后端:内存、JDBC、Cassandra、Neo4j
ChatMemory chatMemory = new MessageWindowChatMemory(
    new InMemoryChatMemoryRepository()
);

配合 MessageChatMemoryAdvisor 自动注入上下文。


九、重要实现说明

Spring AI 的 ChatClient 同时支持命令式(Servlet)和响应式(Reactive) 编程模型,但需注意:

场景 要求
流式响应 必须引入 spring-boot-starter-webflux
非流式调用 需要 spring-boot-starter-web(Servlet)
工具调用(Function Calling) 是阻塞操作,会中断 Micrometer 链路追踪
HTTP 客户端 需同时配置 RestClientWebClient
Spring Boot 3.4 Bug 需设置 spring.http.client.factory=jdk

📌 建议:即使你只做同步调用,也建议引入 WebFlux 以支持未来流式需求。


十、总结

ChatClient 是 Spring AI 的最佳实践入口,它通过:

  • 统一 API 屏蔽模型差异
  • Fluent 链式调用提升开发体验
  • 内置模板、记忆、RAG、工具调用等企业级能力

让你专注于业务逻辑,而非模型细节。

开发原则

优先使用 ChatClient,仅在需要深度定制时才使用 ChatModel

通过合理配置默认项、Advisors 和记忆机制,你可以快速构建出功能强大、可维护、可扩展的 AI 应用。


延伸阅读

Logo

更多推荐