Spring AI 三层架构详解:ChatModel → ChatClient → Controller,同步调用 vs 流式调用(SSE)
随着大模型应用从"调 API"逐步走向工程化,Spring AI 提供了一套统一的编程模型,让 Java 开发者可以用一套 API 对接多家 AI 大模型(OpenAI、Ollama、Azure OpenAI 等)。其中,ChatModel → ChatClient → Controller 三层架构是最基础的代码组织方式,而同步调用与流式调用(SSE)则是交互层最重要的两个分支。
本文将从分层结构、代码示例、调用机制、适用场景四个角度,把这三层与两种调用方式讲清楚。
一、三层架构:职责分明,逐层封装
Spring AI 在设计上借鉴了 Spring Data 的分层思想,从上到下分为:
1. Controller 层(展示层 / 接口层)
职责:负责 HTTP 接口暴露、参数校验、结果格式化。
Controller 不直接操作模型,而是通过 ChatClient(或更低层的 ChatModel)完成业务逻辑。这样在切换模型或者调整提示词模板时,接口签名可以保持不变。
2. ChatClient 层(服务层 / 高级客户端)
职责:提供面向业务开发者友好的链式 API,封装了 Prompt 构造、消息历史、模型配置等细节。
ChatClient 类似于 Spring JDBC 中的 JdbcTemplate,让开发者专注于"用户说了什么"和"模型应该回复什么",而不是底层 API 参数。
3. ChatModel 层(技术层 / 低层适配器)
职责:直接与具体 AI 模型通信,负责发送 Prompt、接收 ChatResponse、处理模型供应商的差异。
它是整个架构的"地基",OpenAiChatModel、OllamaChatModel等都是它的实现。多数业务代码无需直接触碰 ChatModel,但在需要深度定制时,它可以提供最大的灵活性。
二、依赖与基础配置
以 OpenAI 为例,在 pom.xml中加入:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
然后配置 API Key:
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com
chat:
options:
model: gpt-4o-mini
temperature: 0.7
Spring Boot 容器会自动创建一个 ChatModelBean,我们可以直接用 ChatClient封装它。
三、同步调用:一步一出,结果完整
1. 使用 ChatModel 直接调用
最底层的方式:
kotlin
@RestController
public class ChatController {
private final ChatModel chatModel;
public ChatController(ChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/sync/chat-model")
public String chatModelSync(@RequestParam String message) {
return chatModel.call(message);
}
}
chatModel.call(message)方法会阻塞等待模型生成完整回复,然后一次性返回整个字符串。这种方式实现简单,但用户体验较差------如果生成时间较长,客户端会一直等待。
2. 使用 ChatClient 优雅调用
更推荐业务代码使用 ChatClient:
kotlin
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel).build();
}
@GetMapping("/sync/chat-client")
public String chatClientSync(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
这里 call()后返回 CallResponseSpec,.content()直接获取文本内容。如果想返回结构化对象,可以使用 .entity(MyBean.class),ChatClient 会自动将 JSON 映射为 Java 对象。
同步调用的特征
- 阻塞式:调用后线程阻塞,直到模型完整回复为止。
- 响应完整:最终拿到的是全部文本,无需处理中间状态。
- 适合短问答:生成时间可控的场景,如知识库问答、意图分类等。
- 异常处理简单:直接 try-catch 即可。
四、流式调用:边说边发,用 SSE 提升体验
大模型的回复往往很长,如果全部生成完再返回,用户可能需要等待 5~10 秒。流式调用可以让模型每生成一个 token 或一段文本,就立即推送给客户端,实现"打字机"效果。
1. 底层 ChatModel 的流式 API
ChatModel提供了 stream(Prompt prompt)方法,返回 Flux<ChatResponse>:
less
@GetMapping(value = "/stream/chat-model", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatModelStream(@RequestParam String message) {
return chatModel.stream(message);
}
默认情况下,ChatModel.stream(message)返回 Flux<String>,每个元素是增量文本片段。Spring WebFlux 会自动将其包装为 text/event-stream响应,浏览器或前端可以用 EventSource接收。
2. 使用 ChatClient 进行流式调用
ChatClient 提供了 .stream()方法,语义更丰富:
kotlin
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel).build();
}
@GetMapping(value = "/stream/chat-client", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatClientStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}
.stream().content()返回 Flux<String>,每次发射一段新生成的文本。
如果需要更高级的控制,比如返回元数据或自定义事件,可以这样写:
less
@GetMapping(value = "/stream/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamWithEvents(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.chatResponse()
.map(response -> ServerSentEvent.builder(response.getResult().getOutput().getText())
.event("message")
.build());
}
3. SSE 协议核心要点
SSE(Server-Sent Events)是一种基于 HTTP 的单向服务器推送协议。响应头必须包含:
yaml
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
服务端推送的数据格式为:
kotlin
data: 这是一段文本
data: 这是第二段文本
每个 data字段代表一条消息,以空行分割。Spring WebFlux 返回的 Flux会自动完成这些格式转换。
4. 传统 Servlet 栈中的 SSE 实现
如果你的项目是 Spring MVC(非 WebFlux),可以用 SseEmitter包装流式响应:
scss
@RestController
public class SseController {
private final ChatClient chatClient;
public SseController(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel).build();
}
@GetMapping("/stream/sse-emitter")
public SseEmitter streamSse(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(60_000L);
chatClient.prompt()
.user(message)
.stream()
.content()
.doOnNext(content -> {
try {
emitter.send(SseEmitter.event().name("message").data(content));
} catch (IOException e) {
emitter.completeWithError(e);
}
})
.doOnComplete(emitter::complete)
.doOnError(emitter::completeWithError)
.subscribe();
return emitter;
}
}
这样即使同步阻塞的 Spring MVC 容器,也能以异步方式处理流式响应。
五、同步 vs 流式:多维对比
| 维度 | 同步调用 | 流式调用(SSE) |
|---|---|---|
| 响应时间 | 首包时间长,需等全部生成完成 | 首包时间短,通常几百毫秒即可开始输出 |
| 用户体验 | 等待期空白,用户易认为网站卡顿 | 实时打字机效果,感知速度更快 |
| 资源占用 | 阻塞线程,高并发下需更多线程池资源 | 非阻塞,适合处理长时间生成任务 |
| 错误处理 | 一次 try-catch 即可捕获 | 需要处理流中段、取消、超时等场景 |
| 实现复杂度 | 低 | 中等,需要理解 Reactor 或 SseEmitter |
| 数据完整性 | 一次性获得完整 result,便于日志记录 | 需要自行拼接流片段,才能获得全文 |
| 适用场景 | 短文本、内部系统、批量处理 | 对话机器人、文章生成、代码补全等交互式场景 |
六、工程实践建议
1. 根据接口语义选择
- 对外 API 若面向终端用户,强烈建议使用流式 SSE,并以流式为主、同步为辅。
- 内部服务间调用,如果只是生成摘要、分类等短文本,同步足够。
- 如果 API 返回的是结构化 JSON 数据,流式并没有明显优势,同步更简单。
2. 设置超时与取消机制
流式接口需要特别关注连接时长。前端关闭页面时,后端的 Flux应自动取消订阅。可以结合 WebFlux 的 onCancel回调清理资源:
scss
return chatClient.prompt()
.user(message)
.stream()
.content()
.doOnCancel(() -> log.info("客户端取消连接"));
3. 文本拼接与去重
流式模型返回的片段可能是不连续的 token,如果希望记录完整回答,需要自行拼接:
go
StringBuilder fullContent = new StringBuilder();
flux.subscribe(
content -> fullContent.append(content),
error -> log.error("流式生成失败", error),
() -> log.info("生成完成,总长度:{}", fullContent.length())
);
4. 监控制造商限流
流式请求会占用模型提供商更多的资源,有些情况下会遇到速率限制。建议为不同模型配置不同的限流策略,并在响应中传递 usage信息(如 token 数)便于计费审计。
5. 结合提示词模板与记忆
不要将用户消息直接拼接,尽量使用 ChatClient.prompt()的 .system()和 .user()方法,或者引入 PromptTemplate来维护提示词:
ini
String template = """
你是{role}。请用{style}回答:
{question}
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
Message message = promptTemplate.createMessage(Map.of(
"role", "Java技术专家",
"style", "简洁犀利",
"question", question
));
七、总结
Spring AI 的三层架构清晰地区分了接口、业务和底层模型适配的关系:
- ChatModel 是引擎,决定了怎么跟模型说话;
- ChatClient 是驾驶舱,提供了舒适的操控体验;
- Controller 是方向盘,让外界按指定方式访问系统。
同步调用是基础,流式调用(SSE)是增强。两者并非互斥,一个成熟的应用完全可以同时暴露两种接口:即时消息走 SSE,数据量小的请求走同步接口。
更重要的是,Spring AI 将这两者统一在同一个 ChatClientAPI 之下,切换成本极低------大部分业务代码只需要从 .call()换成 .stream(),就能让响应体验从"等待"变为"实时"。这正是 Spring AI 在 Java 生态中快速流行的核心原因之一。