在这里插入图片描述

引言

在构建 AI 应用时,与大模型的对话交互是最基础也是最核心的能力。Spring AI 提供了 ChatClient 这一高级抽象,它借鉴了 RestClientWebClient 的 Fluent API 设计风格,让开发者可以用极少的代码完成与大模型的交互。

本篇将深入解析 ChatClient 的设计理念、构建方式、流式输出实现,以及如何通过 System Prompt 定制 AI 的行为角色。

设计说明

为什么选择 ChatClient 而非直接调用 ChatModel?

Spring AI 的架构分为两层:

  • ChatModel:底层模型抽象,直接对接各厂商 API(OpenAI、DashScope 等),负责请求/响应的序列化
  • ChatClient:高层客户端抽象,在 ChatModel 之上提供 Advisor 链、默认配置、流式支持等增强能力

ChatClient 的核心优势在于:

  1. Fluent API —— 链式调用,代码可读性极高
  2. Advisor 机制 —— 类似 Spring MVC 的拦截器,可插拔地增强对话能力(记忆、RAG、日志等)
  3. 默认配置 —— 通过 defaultSystemdefaultAdvisors 等方法预设行为,避免每次调用重复配置
  4. 流式支持 —— 原生支持 Reactor 的 Flux<String>,天然适配 SSE(Server-Sent Events)

整体交互流程

前端 (SSE) ←→ Controller (Flux<String>) ←→ ChatClient ←→ ChatModel ←→ DashScope API

原理方案

Spring AI Alibaba DashScope 集成

项目通过 spring-ai-alibaba-starter-dashscope 自动装配 DashScope 的 ChatModel 实现。只需在 application.yml 中配置 API Key:

spring:
  ai:
    dashscope:
      api-key: ${ALI_AI_KEY}

Spring Boot 自动装配会创建 DashScopeChatModel Bean,它实现了 ChatModel 接口,支持同步调用(call)和流式调用(stream)。

ChatClient 的构建模式

ChatClient 支持两种构建方式:

方式一:通过 ChatClient.Builder 自动注入

@Autowired
private ChatClient chatClient;

public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
    this.chatClient = builder
            .defaultSystem("你是一名AI助手")
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
}

方式二:通过 ChatModel 手动构建

public AiRagController(ChatModel chatModel, ChatMemory chatMemory) {
    this.chatClient = ChatClient.builder(chatModel)
            .defaultSystem("你是知识库系统的对话助手")
            .defaultAdvisors(
                PromptChatMemoryAdvisor.builder(chatMemory).build(),
                SimpleLoggerAdvisor.builder().build()
            )
            .build();
}

两种方式的区别在于:方式一使用 Spring 容器中预配置的 Builder(可通过 @Bean 全局定制),方式二直接从 ChatModel 创建,更灵活。

流式输出与 SSE

流式输出的核心是 ChatClient.stream().content() 方法,它返回 Flux<String>,每个元素是大模型生成的一个 token 片段。配合 Spring WebFlux 的 SSE 支持,前端可以实时接收生成内容:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .content();
}

关键点:

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE 声明响应类型为 SSE
  • Flux<String> 会被 Spring 自动转换为 text/event-stream 格式
  • 前端使用 EventSourcefetch API 消费流式数据

代码解析

ChatController —— 基础对话控制器

@Tag(name = "AiRagController", description = "chat对话接口")
@Slf4j
@RestController
@RequestMapping(ApplicationConstant.API_VERSION + "/chat")
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private SensitiveWordService sensitiveWordService;

    public ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
        this.chatClient = builder
                .defaultSystem("""
                    你是一家名为"XS公司"的知识库系统的客户客服代理。
                    请友好乐于助人,充满喜悦地回复。
                    """)
                .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();
    }

    @Operation(summary = "stream", description = "流式对话接口")
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @Loggable("message")
    public Flux<String> streamRagChat(
            @RequestParam(value = "message", defaultValue = "你好") String message,
            @RequestParam(value = "prompt", defaultValue = "你是一名AI助手") String prompt) {

        // 敏感词前置过滤
        List<SensitiveWord> list = sensitiveWordService.list();
        for (SensitiveWord sensitiveWord : list) {
            if (message.contains(sensitiveWord.getWord())) {
                return Flux.just("包含敏感词:" + sensitiveWord.getWord());
            }
        }

        Long userId = BaseContext.getCurrentId();
        return chatClient.prompt()
                .system(prompt)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
                .user(message)
                .stream()
                .content();
    }
}

代码逐行解析:

  1. 构造函数注入 ChatClient.Builder:Spring AI 自动装配提供了 ChatClient.Builder Bean,在构造函数中完成 ChatClient 的初始化配置
  2. defaultSystem:设置系统提示词,定义 AI 的角色和行为边界。这里将 AI 定位为"XS公司的客服代理"
  3. defaultAdvisors:注册默认的 Advisor 链。MessageChatMemoryAdvisor 负责将历史对话消息注入到当前请求中
  4. 敏感词过滤:在调用大模型之前,遍历敏感词库进行前置拦截。命中时直接返回提示信息,不会消耗 LLM 调用额度
  5. 用户隔离:通过 ChatMemory.CONVERSATION_ID 参数传入当前用户 ID,确保不同用户的对话记忆互不干扰
  6. 动态 System Prompt:接口支持前端传入自定义 prompt,覆盖默认系统提示词,实现灵活的角色切换

ApplicationConfig —— ChatClient Bean 配置

@Configuration
public class ApplicationConfig implements WebMvcConfigurer {

    @Bean
    public TokenTextSplitter tokenTextSplitter() {
        return new TokenTextSplitter();
    }

    @Bean
    ChatClient chatclient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你是一个乐于助人解决问题的AI机器人")
                .build();
    }
}

这里通过 @Bean 方法创建了一个全局的 ChatClient 实例,设置了默认的系统提示词。其他 Controller 可以直接 @Autowired 注入使用。

System Prompt 的参数化

在 AiRagController 中,System Prompt 支持参数化模板:

private static final String DEFAULT_SYSTEM_PROMPT = """
    你是"Artisan"知识库系统的对话助手,请以乐于助人的方式进行对话,
    {rag_message}
    今天的日期:{current_data}
    """;

调用时通过 .system(a -> a.param(...)) 动态填充参数:

chatClient.prompt()
    .user(message)
    .system(a -> a.param("current_data", LocalDate.now().toString()))
    .system(a -> a.param("rag_message", "请严格基于知识库内容回答"))
    .stream()
    .content();

这种设计让同一个 ChatClient 实例可以根据不同场景动态调整行为,而无需重新构建。

验证结果

基础对话测试

请求:

GET /api/v1/chat/stream?message=你好,请介绍一下你自己

响应(SSE 流式):

data: 你好
data: !我是
data: Artisan公司
data: 知识库系统
data: 的客服
data: 助手,
data: 很高兴
data: 为您
data: 服务。
data: 我可以
data: 帮助您
data: 解答
data: 关于
data: 知识库
data: 中的
data: 各类
data: 问题...

自定义 Prompt 测试

请求:

GET /api/v1/chat/stream?message=什么是RAG&prompt=你是一名技术专家,请用简洁的语言回答

响应:
AI 会以技术专家的口吻简洁回答 RAG 的定义,而非默认的客服风格。

敏感词拦截测试

请求:

GET /api/v1/chat/stream?message=包含某敏感词的内容

响应:

包含敏感词:xxx

请求被前置拦截,未调用大模型,响应延迟极低。

小结

本篇介绍了 Spring AI ChatClient 的核心设计:

  • ChatClient 是 Spring AI 的高层对话抽象,提供 Fluent API 和 Advisor 扩展机制
  • 流式输出通过 Flux<String> + SSE 实现,前端可实时展示生成内容
  • System Prompt 支持参数化模板,同一实例可动态切换角色
  • 敏感词过滤作为前置逻辑,在调用 LLM 之前完成拦截

下一篇将深入对话记忆(ChatMemory)的实现,看看如何让 AI 记住上下文,实现真正的多轮对话。

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐