ChatClient:对话 API 详解
上一章那一行 chatClient.prompt(q).call().content() 只是冰山一角。ChatClient 是流式 API(Builder 风格),能配系统提示词、管对话历史、流式输出。这一章把它掰开。
论call() vs stream():同步等 vs 流式吐
call():等大模型把整段回答生成完,一次性返回。像查字典——翻到那一页整页给你。适合短问题、后台任务。
stream():一边生成一边吐,返回 Flux<String>(响应式流)。像 ChatGPT 那种打字机效果,一个字一个字蹦出来。适合前端聊天界面,用户不用干等 5 秒。
流式输出:返回 Flux,前端用 SSE 接收打字机效果
系统提示词:给 AI 定人设
同样一个问题,让 AI 当"老师"和当"段子手",回答完全不同。系统提示词(System Prompt)就是开工前跟 AI 说"你是谁、你怎么说话"。
用 system() 设人设,用户问题用 user()
多轮对话:ChatMemory 管历史
大模型本身没有记忆——每次请求都是独立的,它不知道你上一句说了啥。想让它记得上下文,你得自己把历史消息每次都带上。Spring AI 提供 ChatMemory 帮你存对话历史。
用 ChatMemory 实现多轮对话(按 sessionId 区分用户)
Advisors:横切所有对话的"顾问"
Spring AI 用 Advisor 在每次调用前后插逻辑。MessageChatMemoryAdvisor 自动拼历史,QuestionAnswerAdvisor 自动做 RAG,还可以自定义 Advisor 做日志、审计。你在 .advisors(...) 里配的东西,每次请求都会生效,不用在每个接口里重复写。
| 内置 Advisor | 干什么 |
|---|---|
| MessageChatMemoryAdvisor | 自动存取对话历史,实现多轮。 |
| QuestionAnswerAdvisor | 自动从向量库检索拼上下文,实现 RAG。 |
| SimpleLoggerAdvisor | 记录每次 Prompt 和返回,调试用。 |
每次请求都会把整段历史发给大模型,历史消息占 token,token 就是钱。聊了 100 轮,每次请求都带 100 轮历史,费用指数上涨。生产上要做截断:只保留最近 N 轮,或者对老消息做摘要。MessageWindowChatMemory(内存版)重启就丢,生产换 Redis / JDBC 持久化(如 JdbcChatMemoryRepository)。
完整案例:流式对话 + 系统人设 + 多轮记忆 + Advisor 链
把这章三件套(system 人设、ChatMemory 多轮、stream 打字机)合成一个能跑的完整接口,带真实输出。
第一步:配 ChatMemory Bean(生产换 RedisChatMemory)
第二步:流式聊天接口,人设 + 记忆 + 逐字吐
第三步:两次请求看效果(同一个 sessionId)
论一次流式调用内部发生了啥
① MessageChatMemoryAdvisor 按 sessionId 把历史消息拼进 prompt;
② ChatClient 把"系统人设 + 历史 + 新问题"发给 DashScope;
③ 大模型一边生成一边通过 SSE 推 token,Spring 包成 Flux<String>;
④ 前端 EventSource 收到一个词渲染一个词,打字机效果;
⑤ 回答完,Advisor 把这轮问答存回 ChatMemory。
本章面试题
Q1. call() 和 stream() 区别?什么场景用哪个?
参考答案
call() 同步等整段回答一次性返回,适合后台任务、短问题;stream() 返回 Flux<String> 逐字吐,适合前端聊天打字机。前端接 stream 必须用 SSE(text/event-stream + EventSource)。
Q2. 大模型本身有记忆吗?怎么实现多轮对话?
参考答案
没有,每次请求无状态。多轮靠 ChatMemory:按 conversationId 把历史消息每次拼进 prompt。代价是历史占 token、就是钱,所以要截断/摘要,生产用 Redis 持久化。
Q3. Advisor 是什么?举两个内置 Advisor。
参考答案
Advisor 是横切每次调用的"顾问",在请求前后插逻辑。MessageChatMemoryAdvisor 自动拼历史;QuestionAnswerAdvisor 自动做 RAG 检索;SimpleLoggerAdvisor 记日志。好处是不用每个接口重复写。