双状态模型 + ReadableStream:流式 AI 对话前端的架构内幕 前言:一个反直觉的问题你打开 ChatGPT,问了一个问题。AI 的回答不是一次性蹦出来的——而是一个字一个字在屏幕上"打"出来。你的第一反应可能是"后端用 WebSocket 推过来的"。但打开浏览器 DevTools,你会发现它用的就是普通的 HTTP 请求——POST /api/chat,只是响应体是一个持续到达的字节流。为什么一个普通 HTTP 请求能做到"边收边显示"?答案在ReadableStream——它是 Fetch API 中被低估最严重的一个能力。这篇文章拆解一个流式 AI 对话前端的完整架构:从后端 SSE 解析,到前端双状态消息模型,到 Markdown 实时渲染。核心机制:一条完整的流式数据管道整个流式链路分为四层,数据像水一样逐层流过:OpenAI API 后端 Astro Server 前端 Solid.js 组件 (SSE stream) ──→ (SSE 解析 → ReadableStream) ──→ (fetch → getReader → 逐字渲染) │ │ eventsource-parser messageList (已完成) 提取 delta.content + currentAssistantMessage (流式中) │ │ 逐 token enqueue Markdown 实时渲染 │ │ new Response(stream) markdown-it + highlight.js每一层都有自己的职责,下面逐层拆解。第一层:后端 SSE 解析(eventsource-parser)OpenAI 的 Chat API 在stream: true模式下,返回的是标准的 SSE(Server-Sent Events)格式:data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"你"},"index":0}]} data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"好"},"index":0}]} data: [DONE]每行以data:开头,用空行分隔事件。后端不是简单地把这个字符串透传给前端——那样前端还得自己解析 SSE 格式。后端用eventsource-parser把 SSE 格式解析成纯文本流,再喂给 ReadableStream:import{createParser}from"eventsource-parser";constparseOpenAIStream=(rawResponse:Response):Response={conststream=newReadableStream({asyncstart(controller){constparser=createParser((event)={if(event.type!=="event")return;// 1. 判断结束信号if(event.data==="[DONE]"){controller.close();return;}// 2. 提取 delta.contentconstjson=JSON.parse(event.data);consttext=json.choices[0]