Spring AI RAG -02 ChatClient 基础对话与流式输出
文章目录

引言
在构建 AI 应用时,与大模型的对话交互是最基础也是最核心的能力。Spring AI 提供了 ChatClient 这一高级抽象,它借鉴了 RestClient 和 WebClient 的 Fluent API 设计风格,让开发者可以用极少的代码完成与大模型的交互。
本篇将深入解析 ChatClient 的设计理念、构建方式、流式输出实现,以及如何通过 System Prompt 定制 AI 的行为角色。
设计说明
为什么选择 ChatClient 而非直接调用 ChatModel?
Spring AI 的架构分为两层:
- ChatModel:底层模型抽象,直接对接各厂商 API(OpenAI、DashScope 等),负责请求/响应的序列化
- ChatClient:高层客户端抽象,在 ChatModel 之上提供 Advisor 链、默认配置、流式支持等增强能力
ChatClient 的核心优势在于:
- Fluent API —— 链式调用,代码可读性极高
- Advisor 机制 —— 类似 Spring MVC 的拦截器,可插拔地增强对话能力(记忆、RAG、日志等)
- 默认配置 —— 通过
defaultSystem、defaultAdvisors等方法预设行为,避免每次调用重复配置 - 流式支持 —— 原生支持 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声明响应类型为 SSEFlux<String>会被 Spring 自动转换为text/event-stream格式- 前端使用
EventSource或fetchAPI 消费流式数据
代码解析
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();
}
}
代码逐行解析:
- 构造函数注入 ChatClient.Builder:Spring AI 自动装配提供了
ChatClient.BuilderBean,在构造函数中完成 ChatClient 的初始化配置 - defaultSystem:设置系统提示词,定义 AI 的角色和行为边界。这里将 AI 定位为"XS公司的客服代理"
- defaultAdvisors:注册默认的 Advisor 链。
MessageChatMemoryAdvisor负责将历史对话消息注入到当前请求中 - 敏感词过滤:在调用大模型之前,遍历敏感词库进行前置拦截。命中时直接返回提示信息,不会消耗 LLM 调用额度
- 用户隔离:通过
ChatMemory.CONVERSATION_ID参数传入当前用户 ID,确保不同用户的对话记忆互不干扰 - 动态 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 记住上下文,实现真正的多轮对话。

更多推荐



所有评论(0)