AI Agent 前端自学路线(firstDay)
·
1.LLM 大模型、流式输出 SSE / Websocket(前端重中之重!对话页面核心)
在大模型(LLM)应用中,流式输出是提升用户体验的核心技术,它能将首字延迟从数十秒缩短至毫秒级,实现“打字机”效果。针对 SSE (Server-Sent Events) 和 WebSocket 两种主流协议,以下是基于 2026 年工程实践的深度解析与选型建议。
a.核心协议对比与选型
SSE 和 WebSocket 各有优劣,选择取决于具体的交互场景和部署环境:
- SSE (Server-Sent Events):基于 HTTP 协议的单向通信(服务器→客户端)。它是 LLM 文本生成场景的首选方案,因为大模型输出通常是单向的。其优势在于开发复杂度极低、天然支持自动重连,且能无缝穿透企业防火墙和 CDN,无需特殊配置 。
- WebSocket:独立的全双工通信协议。适用于需要高频双向交互的场景,如实时语音对话(允许用户打断 AI)、多人协同编辑或复杂的 Agent 工具调用。虽然性能略优,但需要手动处理心跳、重连和状态同步,运维成本较高 。
- 性能差异:在 HTTP/2 普及的当下,SSE 的延迟表现已非常接近 WebSocket。除非追求极致吞吐(如微服务内部调用)或必须双向通信,否则 SSE 的工程性价比更高 。
b.关键工程实现细节
无论选择哪种协议,在生产环境中都需注意以下核心要点,以避免常见陷阱:
- 禁用反向代理缓冲:在使用 Nginx 等反向代理时,必须显式禁用缓冲(如设置
X-Accel-Buffering: no或proxy_buffering off),否则数据会被缓存直到生成完毕,导致流式效果失效,用户仍看到“一次性返回” 。 - 中断与资源释放:当用户停止生成时,后端必须立即感知并终止 LLM 推理任务,释放显存(KV Cache)。WebSocket 和 gRPC 可通过流控制实现,SSE 则需监听连接断开事件 。
- 心跳与超时管理:长连接需配置心跳机制以防超时断开。SSE 协议自带简易心跳,而 WebSocket 需手动实现 ping/pong 逻辑,gRPC 则有内置 keepalive 参数 。
c.核心协议选择:为什么首选 SSE?
在 90% 的 LLM 文本对话场景中,SSE (Server-Sent Events) 是最佳选择。
- LLM 的特性:单向通信(服务端推 Token -> 客户端展示)。
- SSE 优势:基于 HTTP,天然穿透防火墙/CDN,浏览器原生支持自动重连,开发成本极低。
- WebSocket 适用场景:仅当你需要实时语音双向对讲、高频中断控制或复杂 Agent 工具调用反馈时才考虑。
d.SSE 实战:从入门到生产级稳定
1. 基础 API 使用 (EventSource)
浏览器原生提供了 EventSource API,这是最标准的实现方式。
// 基本用法
const eventSource = new EventSource('/api/chat/stream');
eventSource.onmessage = (event) => {
// event.data 是服务器推送的字符串
console.log('收到数据:', event.data);
// 注意:LLM 通常返回 JSON 字符串,需要解析
try {
const chunk = JSON.parse(event.data);
if (chunk.content) {
appendToUI(chunk.content);
}
} catch (e) {
// 处理非 JSON 数据或结束标记 [DONE]
if (event.data === '[DONE]') {
eventSource.close();
}
}
};
eventSource.onerror = (err) => {
console.error('SSE 连接错误', err);
// 浏览器会自动重连,但你可以在这里做 UI 提示
};
2. 关键痛点与解决方案
痛点 A:中文乱码与 UTF-8 截断
- 现象:
EventSource默认假设数据是 UTF-8 文本。如果后端发送的二进制流被错误切割,或者包含非 UTF-8 字符,可能会乱码。 - 解决:确保后端设置
Content-Type: text/event-stream; charset=utf-8。前端通常无需额外处理,但若遇到乱码,需检查后端是否直接传输了二进制 Buffer 而未正确编码。
痛点 B:消息格式规范
- 规范:SSE 要求每条消息以
\n\n结尾。 - 陷阱:如果后端忘记加双换行,前端会一直缓冲直到超时或连接关闭,导致“一次性吐出”而非流式。
- 调试技巧:在浏览器 Network 面板查看 Response 流,确认是否有连续的
data: {...}\n\n。
痛点 C:自动重连与状态恢复
- 机制:SSE 断开后,浏览器会自动重连,并发送
Last-Event-ID头。 - 前端任务:如果你希望断线后从上次位置继续,后端需支持
id字段。对于 LLM 对话,通常不建议依赖此机制恢复长对话上下文(因为后端可能已释放资源),而是应在onerror中提示用户“连接中断,请重试”。
痛点 D:携带认证信息
- 限制:原生
EventSource不支持自定义 Header(如Authorization: Bearer token)。 - 解决方案:
- URL 参数传参:
new EventSource('/api/stream?token=xyz')(注意安全性,Token 会出现在日志中)。 - Cookie:将 Token 放在 HttpOnly Cookie 中,浏览器自动携带。
- Polyfill/Fetch 方案:放弃原生
EventSource,使用fetch+ReadableStream手动实现(见下文高级部分)。
- URL 参数传参:
e.高级方案:Fetch + ReadableStream(推荐用于复杂场景)
当你需要自定义 Header、更好的错误处理或取消请求时,原生 EventSource 不够用。现代前端应掌握基于 fetch 的手动流式解析。
async function streamChat(prompt, signal) {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token' // 支持自定义 Header
},
body: JSON.stringify({ prompt }),
signal: signal // 支持 AbortController 取消请求
});
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码字节流
buffer += decoder.decode(value, { stream: true });
// 按行分割处理 SSE 格式
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留最后一行不完整的部分
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
if (jsonStr === '[DONE]') return;
try {
const chunk = JSON.parse(jsonStr);
handleChunk(chunk); // 更新 UI
} catch (e) {
console.warn('解析失败', line);
}
}
}
}
}
// 取消请求示例
const controller = new AbortController();
streamChat('Hello', controller.signal);
// 用户点击停止按钮时:
// controller.abort();
掌握要点:
- TextDecoder:处理流式字节到字符串的转换,注意
{ stream: true }选项以处理多字节字符(如中文)被截断的情况。 - Buffer 管理:网络包可能截断 SSE 的行,必须维护一个缓冲区,只处理完整的行。
- AbortController:LLM 生成时间长,用户常需“停止生成”,必须能立即中断网络请求并释放后端资源。
f.WebSocket 实战(仅在必要时使用)
如果项目强制要求 WebSocket,你需要掌握:
-
心跳保活 (Ping/Pong):
- LLM 生成可能耗时数十秒,中间无数据传输,代理服务器(如 Nginx)可能因超时而切断连接。
- 前端任务:每隔 30s 发送
{ type: 'ping' },或监听后端的心跳。
-
手动重连策略:
- WebSocket 断开后不会自动重连。
- 实现:使用指数退避算法(Exponential Backoff)进行重连,避免雪崩效应。
-
消息队列与顺序保证:
- 网络抖动可能导致消息乱序。虽然 TCP 保证顺序,但应用层可能需要处理“停止”指令优先于“后续 Token”的逻辑。
g、 UI/UX 渲染优化:让“打字机”更丝滑
拿到数据只是第一步,如何展示才是用户体验的关键。
1. 防抖与批量渲染
- 问题:LLM 可能每秒推送 10-50 个 Token,频繁操作 DOM 会导致页面卡顿。
- 解决:
- React/Vue:不要每个 Token 都触发一次
setState。可以使用requestAnimationFrame或简单的节流(Throttle),每 50-100ms 更新一次 UI。 - 累积字符串:在内存中累积完整文本,定期刷新视图。
- React/Vue:不要每个 Token 都触发一次
2. Markdown 实时渲染
- 挑战:流式输出的 Markdown 是不完整的(如
**bold只有开头),直接渲染会导致闪烁或样式错误。 - 解决方案:
- 使用支持增量解析的 Markdown 库(如
react-markdown配合remark-gfm)。 - 技巧:在渲染前,对未闭合的标签进行临时补全(如自动添加缺失的
**或}),或在 CSS 中隐藏未完成的块。
- 使用支持增量解析的 Markdown 库(如
3. 光标与滚动
- 自动滚动:确保容器在有新内容时自动滚动到底部,但如果用户手动向上滚动查看历史,应暂停自动滚动。
- 光标效果:在最后一个字符后添加一个闪烁的光标动画,增强“正在生成”的心理暗示。
4. 处理特殊内容
- 代码块:流式输出代码时,语法高亮库可能在代码未完成时报错。建议仅在代码块闭合(检测到 ```)后才进行完整高亮,过程中使用纯文本显示。
- 思维链 (Reasoning):如果模型返回
<think>...</think>内容,前端需识别并折叠/展开显示,避免干扰主回答。
h.异常处理与兜底
- 超时处理:设置前端超时(如 60s)。如果首字延迟过高,提示用户“网络繁忙”。
- 不完整 JSON 处理:网络中断可能导致最后一个 JSON 片段截断。前端解析时需
try-catch,忽略最后一条无效数据。 - 敏感词过滤:如果后端返回的内容包含被拦截标记,前端需友好提示“内容不符合规范”,而不是直接报错崩溃。
更多推荐



所有评论(0)