96SEO 2026-08-03 20:57 3
打开任何一个 AI Chatbot——ChatGPT、Claude、DeepSeek——你输入一个问题。它不像传统网页那样转圈等 几秒 接下来啪地一下弹出全部回答。相反,文字像打字机一样一个字一个字地往外蹦。
这不是动画效果,也不是前端做了一个“打字机 CSS 动画”。

这是 HTTP 协议层的一条水流:LLM 服务端每生成一个 token。就通过一条长连接推给客户端,客户端收到一段就渲染一段。
第三十五天我们写了一个 Vue 前端来消费 DeepSeek 的流式 API。但这篇文章不止于“怎么在前端调 stream 接口”——我们要从 HTTP 协议栈的最底层一路往上,把流式输出的完整技术链路拆解清楚。不过,因为面试官不会只问你 stream: true 怎么写。他会追问:
这些问题,我们一个个回答。
大语言模型的主要是 Transformer 架构。它在生成文本时采用自回归解码每次预测下一个 token。接下来把新 token 拼回输入,再预测下一个——像一个接龙游戏。
输入:"今天的天气"
→ 预测下一个 token:"真"
→ 把"真"拼回去:"今天的天气真"
→ 预测下一个 token:"好"
→ 把"好"拼回去:"今天的天气真好"
→ ...直到预测出
每个 token 都要跑一次前向传播,涉及数十亿参数的矩阵运算。这就是 LLM 推理慢的根本原因——不是一次慢,而是要重复很多次。
使用者痛点:当模型需要生成上百个 token 时总耗时往往超过 10 秒。远超使用者耐心阈值,导致页面卡死或直接关闭。
痛点示例:如果不做流式输出。一个复杂问题可能要等 10 秒以上——这远超使用者耐心极限,导致高跳出率。
第一个 token 的出现时间比总耗时关键得多。
流式输出不减少总耗时但它改变了感知性能使用者在第 .5 秒 就看到第一个字。大脑立刻知道“它在工作”,等待焦虑瞬间消失。说起来,
非流式:
→ 一次性返回全部文字
从流式来看。→ 逐字流出 → → 结束
↑ 使用者在这里就安心了
痛点对策:通过实现 TTFT 最小化,让使用者感受到即时反馈,从而明显提高留存率和满意度。
传统 HTTP 响应需要先知道完整体积才能发送:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123
{"choices":}
LLM 必须边生成边发送。却在生成完之前根本不知道总长度,这直接导致阻塞。
痛点:If server waits for full response before setting Content‑Length,client experiences a long “blank” period.
Transfer-Encoding: chunked 为“未知长度但需要逐步发送”的场景提供了方法:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
7\r
Hello\r
6\r
World!\r
0\r
\r
痛点:If upstream proxy buffers chunks before forwarding,“real‑time” feel disappears.
curl -N -X POST https://api.deepseek.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" \
-d '{
"model": "deepseek-v4-flash"。"messages":,"stream": true
}'
-N 参数让 curl 不缓冲,直接打印每个 SSE 块:
data: {"id":"...","choices":}
data这方面,{"id":"...","choices":}
至于data,{"id":"...","choices":}
data的观点是,
SSE是 HTML5 标准的一部分,定义在 W3C EventSource spec 中。它允许服务端通过单向 HTTP 长连接持续向客户端推送数据。
| 规则说明 | |
|---|---|
| 字段前缀 | 含义 |
| data: | 事件内容 |
| event: | 自定义事件类型 |
| id: | 事件 ID。用于自动重连 |
| retry: | 服务器建议的重连间隔毫秒 |
| 空行 | 标记一次事件结束 |
| 以 ":" 开头的行 | 注释,客户端忽略 |
A typical SSE event example:
id: abc123
从event来看,message
data的观点是,{"token":"你"}
从data来看,{"token":"好"}
两行 `data:` 会被客户端拼接为单条消息。
LLM API 中的 SSE 格式
{
说到"id","chatcmpl-xxx","object": "chat.completion.chunk"。"created": 1724000000,"model": "deepseek-v4-flash","choices":
}
从关键字段来看,
-
deltа.content: 本次增量文本。与完整回复里的 .message.content**不同**。
-
`finish_reason`: 为 `"stop"` 时表示生成结束,通常伴随 ``。
-
`index`: 当 `n> 1` 时区分不同候选答案。
为什么是 SSE 而不是 WebSocket
维度 SSE WebSocket
通信方向 单向:服务端 → 客户端 双向:服务端 ↔ 客户端
协议层 纯 HTTP,无需升级握手 / td> 需要 Upgrade 到 websocket 协议 / td>
基础设施兼容性 / td> 穿透所有 HTTP/HTTPS Proxy、CDN、负载均衡器 / td> 部分代理不支持。需要特殊配置 / td>
连接建立成本 / td> 普通 HTTP 请求就可以完成握手 / td> 必须额外进行 Switching Protocols 握手 / td>
自动重连机制 / td> EventSource 原生自动重连 / td> 需自行实现 reconnect logic / td>
< td >Coding complexity
-
SSE 完全匹配 LLM 流式需求:“一次请求,持续推送”。老实说,
-
The 双向特性和额外握手会带来 **过度设计** 与 **维护成本**。老实说,
-
SSE 在防火墙/公司网络中更友好。不易被拦截或降级,<\/ul>
面试要点 : 当被问到 “ChatGPT 为什么用 SSE 而不是 WebSocket”。不要只说 “单向就够了”,要展开说明 **协议简洁性** 、 **代理友好** 、 **原生自动重连** 与 **实现成本低** 四个维度。其实,
EventSource vs fetch + ReadableStream
-
`EventSource`只能发 GET 请求且不支持自定义 Header;而 LLM API 必须 POST 并携带 Authorization Header。
-
`EventSource` 自动重连机制对 LLM 对话不友好——重连后无法带上上下文。实际工程中几乎都采用 `fetch` + `ReadableStream` 手动消费 SSE。<\/ul>
javascript
const es = new EventSource // ❌ 无法满足 LLM 要求
// 正确姿势
const resp = await fetch(endpoint。{
至于method,'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${apiKey}`},body:JSON.stringify
});
四、服务端:LLM 如何 token‑by‑token 生成并推送
自回归解码与流式写入
LLM 推理引擎在每一步产生 token 后通过回调或 generator 将 token 抛给上层框架;框架再把 token 包装成 SSE 格式写入响应体,实现“边算边传”。下面分别展示 Python FastAPI 与 Node.js Express 的实现方式。
python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json,asyncio
app = FastAPI
async def generate_tokens:
# 实际调用模型推理引擎,此处模拟
for token in :
chunk = {
"choices":
}
yield f"data:{json.dumps}
"
await asyncio.sleep # 模拟计算耗时
yield "data:
"
@app.post
async def chat_stream:
return StreamingResponse(
generate_tokens)。media_type='text/event-stream',headers={
'Cache-Control':'no-cache','Connection':'keep-alive','X-Accel-Buffering':'no' # 禁止 nginx 缓冲,否则会失去实时性
}
)
javascript
// node_express_example.js
app.post=>{
res.setHeader;res.setHeader;res.setHeader;res.setHeader;
const tokens =;for{
const chunk = JSON.stringify({
choices:
});res.write,await new Promise);}
res.write,res.end;
});
Pain point:: 若忘记禁用代理缓存。即使后端已经逐块写入,前端仍会等到缓存满后才收到数据,体验退化为非流式。
从stream来看,true 到底改变了什么
stream:false stream:true
响应头 Content‑Type: application/json Content‑Type: text/event-stream
Body 一次性完整 JSON Chunked SSE 事件序列
传输方式 一次性发送 Transfer‑Encoding: chunked
客户端处理 response.json response.body.getReader + 手动解析
TTFB 必须等全部 Token 完成后才返回首字节 首 Token 完成即返回 ⇒ TTFT 大幅下降
主要 API 链路
``javascript
const response = await fetch(endpoint。{
说到method,'POST',headers:{
'Content-Type':'application/json','Authorization':Bearer ${apiKey}`
},body的观点是,JSON.stringify({
model这方面,'deepseek-v4-flash',messages:,stream:true // 告诉服务器开启 SSE 流模式
})
});
// 第一步先:拿到可读流
const reader = response.body.getReader;
// 接下来:创建 TextDecoder
const decoder = new TextDecoder;
// 然后:循环读取并处理
let buffer = '';while{
const {done,value}=await reader.read;if break,怎么说呢,
// 将 Uint8Array 转成字符串。同时保留未完成字符
buffer += decoder.decode;按理说,// 按行切分 – 注意最终一行可能是不完整的
const lines = buffer.split;老实说,buffer = lines.pop||'';// 保存残余片段
for{
if) continue;const raw=line.slice.trim;// 去掉 “data:” 前缀
if return;// 流结束标记
try{
const chunk=JSON.parse;
const txt=chunk.choices?.,.delta?.content||'';不过,// 将 txt 累加到 UI 中。例如 Vue state.push
}catch{
console.warn;// 保持稳健,不抛异常终止整个读取循环
}
}
}
// 循环结束后处理可能残留在 buffer 中的数据
if){
// 一样尝试解析剩余部分…}
Pain point:: 若省略 decoder.decode,中文或 Emoji 等多字节字符会出现乱码;若不做 buffer 合并。会因 TCP 分片导致 JSON 无法完整解析,引发崩溃。
为什么 TextDecoder 要加 { stream:true }
-
多字节 UTF‑8 字符可能跨越两个 TCP 包。例如中文 “好” 编码为
E5 A5 BD;若第一包只收到 E5 A5没有 { stream:false } 会直接抛出替换字符。而 { stream:true } 会保存未完成序列,下次解码时拼接恢复完整字符。
javascript
const enc=new TextEncoder;话说回来,const bytes=enc.encode;//
const part1=bytes.slice;//
const part2=bytes.slice;//
const dec=new TextDecoder;console.log);// ''
console.log);// '好'
最终一段数据的处理
即使 reader.read 返回 {done:true}仍有可能携带最终一块 payload。循环退出后必须检查 buffer 中残留内容并完成一次解析。
javascript
if{
const line=buffer.trim;if){
const raw=line.slice.trim;if{
const chunk=JSON.parse;content+=chunk.choices?.,.delta?.content||'';}
}
-
AbortController :让使用者随时取消正在进行的对话。
-
错误分类 :HTTP 错误 | 网络中断 | SSE 解码错误,并给出对应恢复策略。
-
断点续传 :短回答直接全局重试;长回答缓存已生成内容并放进
messages 重启,以期续写。 -
背压 :浏览器内部已基于 TCP 流控处理,一般无需手动干预;若渲染逻辑非常耗时可考虑将解析搬至 Web Worker。
-
代理配置 :nginx 使用
proxy_buffering off 或响应头 X‑Accel‑Buffering:no;话说回来,设置足够大的 proxy_read_timeout;话说回来,Cloudflare/AWS ALB 相应调高 idle timeout。 -
HTTP/ vs HTTP/1. :HTTP/ 的多路复用让同一连接可以并发多个请求,提高整体吞吐;怎么说呢,但某些老旧 CDN 对长连接支持不足,需要监测兼容性。<\/ul>
Pain point 示例:: 在生产环境未关闭 nginx 缓冲导致 “第一次 token 延迟30秒”,经排查发现是默认 proxy_buffering on;添加 proxy_buffering off; 或响应头即解决。
-
Agent 工具调用透明化 :每一步工具调用都可以使用一样的 SSE 格式实时推送,让人类观察 AI 思考过程并及时干预。
css
至于data。{\"type\":\"thinking\",\"content\":\"正在搜索…\"}
再看data,{\"type\":\"tool_call\"。\"tool\":\"search\",\"args\":{\"query\":\"…\"}}
data的观点是,{\"type\":\"tool_result\",\"tool\":\"search\"。\"result\":\"找到12条结果\"}
...
-
RAG 与流式结合 :检索阶段快速返回后立即进入模型流水线进行长文本生成;也可以探索 Speculative Streaming —— 在检索尚未完成时先返回模型猜测出的首句,以进一步压缩感知延迟。其实,
-
MCP 与大文件读取 :虽然 MCP 本身基于 JSON-RPC。不强制使用流,但可以把“大文件读取”实现为分块返回,从而避免一次性占满内存。
<\/ul>
基础层
-
**为什么要用流式输出?** —— 自回归解码导致总耗时长,TTFT 是决定使用者是否继续的关键指标。话说回来,
-
**HTTP 层如何实现?** —— 用
Transfer-Encoding:\ chunked
每个 chunk 即为 SSE 行。
-
格式怎样?** ——
text/event-stream
data的观点是,\ …支持 `.id`,`.event`。`.retry`.
对比层
-
**SSE vs WebSocket?** — 单向数据流场景下 SSE 更轻量、更兼容公司网络;按理说,WebSocket 属于过度设计。怎么说呢,
-
**EventSource vs fetch+ReadableStream?** — EventSource 不支持 POST 与自定义 Header。而 LLM API 必须 POST 并携带 Authorization,所以只能使用 fetch+ReadableStream。
实现层
-
**ReadableStream 怎么用?** — `.getReader`,循环 `.read`。用 `.TextDecoder`,管理 buffer 并按行解析 SSE。其实,
-
**为何要做 buffer 管理?** — TCP 分片随意切割,会导致 JSON 被截断;没有 buffer 会产生解析错误甚至崩溃。
-
**TextDecoder 的 `.decode`**: 保证多字节字符跨包仍能正确拼接。老实说,
高阶层
-
**如何取消正在进行的流?**: 使用 AbortController + reader.cancel 捕获 AbortError。
-
**网络中断后如何续传?**: 短回答直接全局重试;按理说,长回答缓存已得内容并放进 messages 重启。对话状态保持一致但会产生额外 Token 消耗。
-
**nginx/Egress Proxy 如何配置才能真正做到实时?**: 加 `X‑Accel‑Buffering:no`,`proxybuffering off`。增大 `proxyread_timeout`.
-
**哪些场景不适合使用流式?**: 必须保证事务原子性的操作、小体积一次性返回的数据还有不支持 Stream API 的嵌入设备。
LLM 流式输出看似只是前端“把接口改成 stream”。实则是一条横跨 **硬件算力 → Transformer 推理 → HTTP Chunked Transfer → SSE 协议 → 浏览器 ReadableStream → UI 渲染** 的完整技术链路,每一步都有潜在「卡顿」或「丢失」风险,需要有针对性的工程措施来消除。
-
User Experience 层:
Pain point:: 超过 ~1 秒无反馈,即出现高跳出率。SOLUtion:: 实现可靠且低延迟的首 Token 推送。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback