Spring Ai超简单教程系列 - 02流式对话
在网上找了一些Spring AI相关的教程,因为官方API更新较快的原因,大部分教程的内容都已过时。所以在参照官方参考文档学习的同时,沉淀每篇文章,为同样热爱学习的你,提供参考。
本系列教程参考Spring AI官方1.1.x版本文档:https://docs.spring.io/spring-ai/reference/1.1/index.html
流式对话
快速入门中我们通过阻塞式API调用的方式,实现了大模型调用。受限于大模型响应速度的原因,阻塞式调用对用户体验感不好,主流调用方式还是通过SSE协议实现流式响应。
流式响应接口
@GetMapping(value = "/ai/chat/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> stream(@RequestParam(value = "message") String message) {
return this.chatModel.stream(new Prompt(message))
.filter(chatResponse -> chatResponse.getResult() != null
&& chatResponse.getResult().getOutput().getText() != null)
.map(chatResponse -> chatResponse.getResult().getOutput().getText())
.filter(StringUtils::hasText);
}
接口调用

data:是SSE(Server-Sent Events)本身的协议格式响应头。
前端调用
AI生成的SSE前端调用代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue SSE 流式 Markdown 渲染</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.box { border: 1px solid #ddd; padding: 12px; border-radius: 8px; }
.output { margin-top: 10px; min-height: 80px; }
.output pre { background: #f6f8fa; padding: 10px; border-radius: 6px; overflow-x: auto; }
.output code { background: #f6f8fa; padding: 2px 4px; border-radius: 3px; }
.output pre code { background: none; padding: 0; }
.output table { border-collapse: collapse; }
.output th, .output td { border: 1px solid #ddd; padding: 4px 8px; }
.status { color: #888; margin-bottom: 8px; }
</style>
</head>
<body>
<div id="app">
<chat-stream></chat-stream>
</div>
<script>
const { createApp, ref, computed, onMounted, onBeforeUnmount } = Vue;
const ChatStream = {
template: `
<div class="box">
<h3>SSE 流式输出(Markdown)</h3>
<div class="status">
<span v-if="running">正在接收…</span>
<span v-else-if="error" style="color: #c00">{{ error }}</span>
<span v-else-if="output">完成</span>
<span v-else>等待中…</span>
</div>
<div class="output" v-html="rendered"></div>
</div>
`,
setup() {
const output = ref('');
const running = ref(false);
const error = ref('');
let abortController = null;
// 将流式拼接的原文按 markdown 渲染
const rendered = computed(() =>
output.value ? marked.parse(output.value) : ''
);
const stopStream = () => {
running.value = false;
if (abortController) {
abortController.abort();
abortController = null;
}
};
// 页面加载即从 query 参数读取 message,编码后请求 SSE 接口
const startStream = async () => {
const params = new URLSearchParams(window.location.search);
const message = params.get('message') || '';
const url = '/ai/chat/stream?message=' + encodeURIComponent(message);
output.value = '';
error.value = '';
running.value = true;
abortController = new AbortController();
try {
const res = await fetch(url, {
headers: { 'Accept': 'text/event-stream' },
signal: abortController.signal
});
if (!res.ok || !res.body) {
throw new Error('请求失败:HTTP ' + res.status);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
// 按块读取并解析 SSE,读到流关闭(done)自然结束,不会重连
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE 事件以空行分隔
const events = buffer.split('\n\n');
buffer = events.pop(); // 最后一段可能不完整,留到下一轮
for (const event of events) {
const data = event
.split('\n')
.filter(line => line.startsWith('data:'))
// EventSource 语义:多个 data: 行用换行拼接
.map(line => line.slice(5).replace(/^ /, ''))
.join('\n');
if (data) {
output.value += data;
}
}
}
} catch (e) {
// 手动停止不算错误
if (e.name !== 'AbortError') {
error.value = e.message || '连接中断';
}
} finally {
running.value = false;
abortController = null;
}
};
onMounted(startStream);
onBeforeUnmount(stopStream);
return { output, running, error, rendered };
}
};
createApp({
components: { ChatStream }
}).mount('#app');
</script>
</body>
</html>
添加Web模版引擎
引入thymeleaf模版引擎组件依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
增加thymeleaf配置项:
spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8
页面跳转端点
@Controller
public class PageController {
@GetMapping("/page/stream")
String toStream() {
return "chat_stream";
}
}
页面调用


浙公网安备 33010602011771号