2026 年 5 月一个周四上午,产品同事在群里 @ 我:"我们 AI 客服流式输出,在手机 App 上看起来怪怪的——要么憋着一字不出,过一两秒一下子出来一整段;要么一字一顿很慢。开发说后端返回是流式的,但用户感受到的不是。"我打开抓包工具 + 自己手机连 App 复现,看到的现象和产品描述完全一致——OpenAI API 返回的流式 chunk 在到达用户屏幕前,某个中间节点把它们重新打包了。
接下来 3 天,我们带着前端 + SRE 把从 OpenAI 到用户浏览器的整条 SSE 链路拆开看,定位到 4 个独立的"缓冲点"全在帮倒忙:我们自己 Python 后端的输出缓冲、Nginx 反向代理的 proxy_buffering、Cloudflare CDN 的智能缓冲、最后一公里运营商代理。每一层都在"为了减少包数提高吞吐"做缓冲,合起来把"流式输出"完全变成了"批量输出"。这篇是完整复盘,涵盖 HTTP SSE / streaming chunked 的网络语义、各层代理的缓冲行为、4 个修复点的具体配置、以及落地的《LLM 流式输出链路纪律》。
服务背景:这个 LLM 客服的 SSE 链路
| 维度 | 数值 |
|---|---|
| 业务 | SaaS LLM 客服,前端 React/RN,后端 Python FastAPI,大模型 GPT-4o |
| 链路 | 客户端 → Cloudflare CDN → Nginx Ingress → FastAPI Pod → OpenAI API |
| 响应方式 | OpenAI 流式返回,后端原样转 SSE 给前端 |
| 规模 | 日均对话 1.8 万次,QPS 高峰 60 |
| 事故现象 | 前端 LLM 回复"卡顿"——要么憋一下子全出,要么一字一顿;OpenAI 实际是流畅的 token 输出 |
| 受影响场景 | 移动端尤其严重,Web 端偶发,内网测试完全正常 |
"内网测试正常,生产端异常",这种问题指向中间链路。我们的链路有 4 层代理,任何一层做缓冲都能毁掉流式体验。
事故时间线:从产品反馈到根因的 3 天
| 时刻 | 事件 |
|---|---|
| 05-21 10:30 | 产品反馈 LLM 回复卡顿,我自己手机复现确认 |
| 05-21 11:00 | 用 curl 在公司外网络直连后端 ingress,看 SSE chunk 到达节奏——正常,流畅 |
| 05-21 11:20 | 用 curl 直连 Cloudflare 边缘,看到 chunk 被打包成 ~4KB 一波一波 |
| 05-21 11:40 | 定位 1:Cloudflare 默认有 buffer,需要配置 cf-cache-control 和 Transfer-Encoding |
| 05-21 下午 | 抓 Nginx 日志 + tcpdump,看 Nginx 也在做 4KB 缓冲 |
| 05-21 17:00 | 定位 2:Nginx proxy_buffering on(默认),要关掉 |
| 05-22 上午 | 读 FastAPI 文档,发现 uvicorn 的 StreamingResponse 在某些路径下会有 chunk 合并 |
| 05-22 下午 | 定位 3:Python yield 后没主动 flush(其实是 uvicorn 自动 flush,但 chunk 太小被合并) |
| 05-23 | 测试发现某些移动运营商代理也在缓冲,需要 Padding 来"骗"它认为是大块数据 |
| 05-23 下午 | 4 层全部改完,前端体验"丝滑流畅" |
第一反应:"是不是后端没 flush"
大多数后端工程师第一反应都是查自己代码——"我是不是没把数据 flush 出去"。这次确实是其中一个因素,但只是冰山一角。在动手改代码前,有个关键步骤要做:逐层 curl 测试,定位到底是哪一层在缓冲。
这是我们的"四层逐层测试"脚本:
# 层 1: 直接在后端 Pod 内 localhost 测试
kubectl exec -it backend-pod -- curl -N -s http://localhost:8000/chat -d '{"q":"..."}'
# 层 2: 通过 K8s Service 测试(经过 kube-proxy iptables)
kubectl run debug --rm -it --image=curlimages/curl -- \
curl -N -s http://backend-service:8000/chat -d '{"q":"..."}'
# 层 3: 通过 Nginx Ingress 测试(经过 ingress controller)
curl -N -s -H "Host: api.example.com" https://nginx-ingress-ip/chat -d '{"q":"..."}'
# 层 4: 通过 Cloudflare 测试(完整公网链路)
curl -N -s https://api.example.com/chat -d '{"q":"..."}'
关键参数:
-N: 禁用 curl 自身缓冲-s: 静默,不显示进度条
我们在每一层都 pipe 进一个简单的"看时间戳"脚本:
... | while IFS= read -r line; do
echo "$(date +%H:%M:%S.%N) $line"
done
这样能看到每一行(每一个 SSE chunk)到达的精确时间。理论上 OpenAI 每 50-100ms 一个 chunk,我们四层测试看到的应该都是 50-100ms 间隔。实际:
| 层 | chunk 到达节奏 |
|---|---|
| 层 1 (后端 localhost) | 50-100ms 一个,流畅 |
| 层 2 (Service) | 50-100ms 一个,流畅 |
| 层 3 (Nginx Ingress) | 每 800ms 一波,每波 6-8 个 chunk |
| 层 4 (Cloudflare) | 每 1500ms 一波,每波 15-20 个 chunk |
这下一目了然:Nginx 和 Cloudflare 都在缓冲。后端本身没问题,问题在链路上。这种"逐层 curl"的方法看着很笨,但比任何抓包 / APM 都直观——你能用肉眼看到 chunk 一个一个出来的节奏,任何"积攒一波"的层都暴露无遗。
四层缓冲叠加的因果链
这张图最有价值的信息是:4 层缓冲是串联叠加的。如果只修 1 层(比如 Nginx),用户体验从"卡顿 1.5 秒"改善到"卡顿 1 秒",收益不明显,产品同学的反馈会变成"还是有点卡"。必须 4 层全修,体验才会出现质变——从"批量"变成"流畅"。这也是为什么很多团队修过类似问题但效果不好——只修了一层就以为大功告成。
真凶 1:Nginx proxy_buffering 默认开
Nginx 反向代理时,默认开启 proxy_buffering——它会把上游响应缓冲到本地,等积累一定大小再转给客户端。这对于"普通 HTTP 响应"是合理优化(减少客户端连接占用时间),但对 SSE / streaming 是灾难。
具体配置:
# 默认配置(我们当时的)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
意思是:上游每来 4KB 数据,Nginx 才转一次给客户端。OpenAI 的单个 SSE chunk 平均 ~50 字节,要凑齐 4KB 需要约 80 个 chunk——按 OpenAI 每 50ms 一个 chunk 算,要凑 4 秒才能转一次。这就是我们看到的"800ms 一波"。
修法:针对 SSE 路径关闭 buffering
# nginx ingress 自定义 annotation(K8s)
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Accel-Buffering no;
proxy_cache off;
proxy_buffering off;
proxy_http_version 1.1;
chunked_transfer_encoding on;
对原生 Nginx 配置:
location /chat {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_buffering off; # 关闭代理缓冲
proxy_cache off;
proxy_set_header X-Accel-Buffering no;
chunked_transfer_encoding on;
proxy_read_timeout 300s; # SSE 通常长连接, 加大 timeout
}
X-Accel-Buffering: no 这个 header 是个老朋友——它原本是给应用层"告诉 Nginx 不要缓冲这个响应"的协议,Nginx 见到响应里有这个 header 就不缓冲。我们让应用层也加上:
# FastAPI
@app.post("/chat")
async def chat(req: ChatRequest):
headers = {
"X-Accel-Buffering": "no", # 告诉 Nginx 不缓冲
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
}
return StreamingResponse(stream_openai(req), headers=headers)
真凶 2:Cloudflare 默认有"smart buffer"
Cloudflare 是 CDN + WAF + DDoS 防护一体,默认对 HTTP 响应做"smart buffer"——它会:
- 对响应内容做 WAF 扫描
- 压缩(gzip/brotli)
- 缓冲一段后转发(优化下行带宽)
对 SSE 这种"持续低速发送"的响应来说,这套机制非常不友好。Cloudflare 文档(藏得很深)其实给出了几个让 SSE 工作的方法:
修法 A:用 Transfer-Encoding: chunked + 不带 Content-Length
Cloudflare 见到 Content-Length 时倾向于"等内容全到再转";chunked 编码 + 无 Content-Length 时它会逐 chunk 转发。FastAPI 的 StreamingResponse 默认就是 chunked,不会带 Content-Length,这点没问题。但我们要确认:
$ curl -I https://api.example.com/chat
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
# 注意: 不应该有 Content-Length
修法 B:用 Cloudflare 的 "Disable Performance Features" Page Rule
在 Cloudflare Dashboard → Rules → Page Rules 加规则:
URL: api.example.com/chat*
Settings:
- Disable Performance (关闭压缩、minification、smart routing 缓冲)
- Cache Level: Bypass
- Disable Apps
修法 C:在响应里加 Padding,骗过缓冲
有些代理(包括某些移动运营商)的缓冲触发条件是"数据量小才缓冲,大就立即转"。我们可以在 SSE 流的开头先发一段 padding,让代理认为这是个"大响应"立即开始转发:
async def stream_openai(req):
# 发一个 2KB 的 padding 注释(SSE 协议允许以 : 开头的行作为注释, 不会被前端处理)
yield ":" + " " * 2048 + "\n\n"
async for chunk in openai_stream:
yield f"data: {json.dumps({'content': chunk})}\n\n"
yield "data: [DONE]\n\n"
这段 padding 看起来土,但是行业内常见的"骗代理"技巧。OpenAI 自己的 SDK 早期版本就用过类似手法。
真凶 3:uvicorn 和 FastAPI 的 chunk 合并
本地直连测试看起来流畅,但 ASGI 服务器层面其实也有微妙的合并。uvicorn 在某些情况下会做"小 chunk 合并",尤其是 yield 的字符串很短时(我们的 SSE chunk 通常是 30-80 字节)。
排查方法是看 tcpdump 的 TCP 包大小:
tcpdump -i any -w sse.pcap port 8000
# 在 Wireshark 里看, 应该是 50-100ms 一个 TCP segment, 每个 50-150 字节
# 如果看到 200-500ms 一个 segment, 每个 800-1500 字节, 说明被合并了
修法是显式 flush。FastAPI 没有暴露 flush API,但可以通过"显式发空字节"间接触发:
async def stream_openai(req):
yield ":" + " " * 2048 + "\n\n"
async for chunk in openai_stream:
yield f"data: {json.dumps({'content': chunk})}\n\n"
# 显式 await 一下让事件循环切换, 给 uvicorn 机会 flush
await asyncio.sleep(0)
yield "data: [DONE]\n\n"
asyncio.sleep(0) 这个写法是 Python async 圈子的暗号——它的作用是"yield 控制权回事件循环",让其他协程(包括 uvicorn 的发送循环)有机会跑。配合 yield,几乎能保证每个 chunk 都被立即推到 socket。
真凶 4:移动端 HTTP/2 + Cloudflare 的死锁
本以为修完前 3 项就完事了,实测发现移动端在某些运营商(联通 4G、部分校园网)下仍然有缓冲。深入排查发现是 HTTP/2 的特性:
| HTTP 版本 | SSE 行为 |
|---|---|
| HTTP/1.1 + chunked | 每个 chunk 独立发送,代理透传通常良好 |
| HTTP/2 | 所有 SSE 走一个 stream,frame 级别多路复用 |
| HTTP/2 + Cloudflare | Cloudflare 在 HTTP/2 上做 frame 合并优化,小 frame 容易被缓冲 |
解法:针对 SSE 端点强制走 HTTP/1.1(关闭 HTTP/2 升级):
# Nginx 配置
location /chat {
listen 443 ssl; # 注意: 不加 http2
# ... 其他配置
}
# 或者通过 alt-svc header 引导客户端用 HTTP/1.1(不可靠)
实测下来,只对 /chat 端点关 HTTP/2,移动端缓冲问题彻底解决。其他端点继续用 HTTP/2 享受多路复用红利。这个权衡值得——SSE 本来就不需要多路复用,因为每个对话就是一个长 stream。
修法 5:前端 SSE 解析器的健壮性
后端 + 链路全修完后,我们还顺手做了前端层的优化。原本前端用了一个简单的 EventSource 实现,实测下来有几个坑:
- EventSource 不支持 POST:必须用 fetch + ReadableStream 自己实现 SSE 解析
- 断线后默认 3 秒重连:对 LLM 对话不合适——上一个对话的 chunk 全丢了,要重新生成。需要把"重连"换成"恢复",带上 cursor / message-id
- 移动端切前后台:iOS 切后台后 SSE 连接可能被系统杀掉,需要前端检测 visibilitychange 主动重连
我们最后用了一个自研的 SSE 解析器,代码核心如下:
async function streamLLM(prompt: string, onChunk: (text: string) => void) {
const ctrl = new AbortController();
const resp = await fetch('/chat', {
method: 'POST',
body: JSON.stringify({ prompt }),
signal: ctrl.signal,
headers: { 'Content-Type': 'application/json' },
});
const reader = resp.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE 协议: 每条消息以 \n\n 分隔
const events = buffer.split('\n\n');
buffer = events.pop()!; // 最后一段可能未完整,留作下次
for (const ev of events) {
const line = ev.split('\n').find(l => l.startsWith('data: '));
if (!line) continue; // 跳过 ":..." padding 行
const data = line.slice(6);
if (data === '[DONE]') return;
onChunk(JSON.parse(data).content);
}
}
}
关键设计:
- 用 fetch + ReadableStream 而不是 EventSource,支持 POST 和自定义 header
- buffer 拼接 + \n\n 分割,避免一个 SSE event 跨多个 TCP 包时被截断
- 过滤 ":" padding 行,不当成数据消费
- AbortController,用户切走或关闭对话时主动断流,避免后端继续 hold 一个 OpenAI 连接(花钱)
横向对比:不同 LLM 流式输出协议
| 协议 | 典型场景 | 优势 | 劣势 |
|---|---|---|---|
| HTTP SSE (text/event-stream) | OpenAI / Anthropic 官方 | 简单、HTTP 友好、浏览器原生支持 | 单向、有代理缓冲坑、HTTP/2 兼容差 |
| WebSocket | chatbot 双向交互 | 双向、低延迟、协议简单 | 代理穿透差(企业防火墙)、扩容复杂 |
| gRPC streaming | 内部服务、AI 中台 | strongly typed、HTTP/2 性能好、双向 | 浏览器需 gRPC-web 代理、调试不友好 |
| HTTP long polling | 低端环境兜底 | 所有代理都支持 | 延迟高、服务端连接占用多、不算"流式" |
| WebTransport / HTTP/3 datagram | 未来式 | UDP 基础、抗丢包好、多路复用强 | 2026 年浏览器支持仍不齐全、运营商支持有限 |
我们对内部服务用 gRPC,对前端用 SSE,对一些遗留客户端 fallback 到 long polling。WebSocket 在企业级 SaaS 反而踩坑多——不少客户的防火墙不让 WebSocket 出去,而 SSE 是普通 TLS,在 HTTP 之上加一层 TLS 加密,防止中间人窃听和篡改。">HTTPS 连接,通过率几乎 100%。
决策树:遇到 SSE 缓冲问题怎么排查
这张图直接贴在团队 wiki,后来调试任何 SSE / streaming 问题都按这棵树走,定位时间从原来的"几小时摸索"压到了"30 分钟以内"。
验证:用户感知 vs 客观指标
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 首字节延迟 | OpenAI 返回首字节后用户 0.8-1.5s 才看到 | OpenAI 返回首字节后用户 60-150ms 看到 |
| token 间平均间隔(用户视角) | 200-800ms(被合并成一波一波) | 40-80ms(接近 OpenAI 实际输出节奏) |
| 用户主观评分(产品调研) | 3.2 / 5 | 4.6 / 5 |
| "卡顿" 客诉数(每周) | ~ 35 | 0-2 |
| SSE 连接断开率 | 2.1% | 0.3% |
主观评分提升 1.4 分,这个改进对"LLM 对话产品"来说价值巨大——流式体验是 LLM 产品的灵魂之一,卡顿直接影响用户对"AI 智能"的感知。
顺手做的几件事
1. 端到端 SSE 健康度监控
原来我们只监控"后端返回耗时",事故后加了"端到端 SSE 健康度":
- 首字节延迟:从用户发请求到收到第一个 SSE 数据的时间
- chunk 平均间隔:连续 chunk 之间的时间间隔
- chunk 间隔抖动:间隔的标准差(高表示一波一波)
- SSE 完成率:成功收到 [DONE] 的比例
前端 SDK 埋点上报这 4 个指标,后端聚合后 Grafana 展示。任何指标退化都能立刻发现。
2. SSE 健康度合成监控
每 5 分钟从多个公网节点(国内/海外/移动/电信)发一个测试请求,模拟用户视角检查整个链路。这是 synthetic monitoring——比靠用户反馈早 N 个小时发现问题。
3. 文档化所有链路配置
我们的 SSE 链路最终需要这么多层配合:
- FastAPI 应用层:加 X-Accel-Buffering header、yield 后 asyncio.sleep(0)、加 padding
- uvicorn:默认配置 ok
- K8s Service:默认 ok(iptables 不会缓冲)
- Nginx Ingress:annotation 关 buffering、关 cache
- Cloudflare:Page Rule 关 Performance Features、Bypass cache
- HTTPS:对 SSE 端点强制 HTTP/1.1
每一条都不能省。我们写了一份 wiki 文档,所有新接入 SSE 的服务必须按这个清单 review。
立的《LLM 流式输出链路纪律》
- SSE 端点必须返回 X-Accel-Buffering: no header。
- Nginx Ingress 对 SSE 路径必须关 proxy_buffering / proxy_cache,annotation 或 location 配置。
- Cloudflare 对 SSE 路径必须用 Page Rule 关性能优化 + Bypass cache。
- SSE 响应不带 Content-Length,用 Transfer-Encoding: chunked。
- SSE 流开头发 2KB 以上 padding(SSE 注释行 ":..."),骗过中间代理立即开始转发。
- 每个 yield 后插 asyncio.sleep(0)(Python)或等价 yield 控制权动作,确保不被合并。
- SSE 端点强制 HTTP/1.1,在 LB / CDN / Nginx 层禁用 HTTP/2 升级。
- 端到端监控 4 个指标:首字节延迟、chunk 间隔、间隔抖动、SSE 完成率。
- 多地域 synthetic monitoring,定期模拟用户视角检查 SSE 流畅度。
- 移动端 SDK 加 timeout 兜底(SSE 30 秒没数据就重连),应对移动网络抖动。
给读者的几条自查清单
- 用 curl -N 直连后端、Service、Ingress、CDN 四层,每层看 chunk 到达节奏。任何一层有"积攒一波"的现象都要修。
- 检查 Nginx Ingress annotation,如果没有
proxy-buffering: off,基本就有问题。 - 检查响应 header,看有没有 X-Accel-Buffering: no。没有的话给应用层加上。
- 本地复现移动端体验:Chrome DevTools Network 选 "Slow 3G",看 SSE 表现。如果在慢网络下卡顿明显,检查是否走了 HTTP/2。
- tcpdump 抓后端 Pod 的端口 8000,看 SSE 包大小和间隔。100-200 字节小包 / 50ms 间隔是好的;1KB+ 大包 / 数百 ms 间隔说明被合并了。
- Cloudflare 用户检查 Page Rule,确认 SSE 端点不被启用性能优化。
- 如果用 K8s Ingress 不是 Nginx(比如 Traefik / HAProxy),查对应文档关闭缓冲。
3 天里被否决的方案
| 方案 | 看似可行 | 否决理由 |
|---|---|---|
| 把 OpenAI 流式改成非流式,后端拿到完整结果再"假流式"模拟分块 | 避开所有缓冲问题 | 用户首字节延迟从 200ms 飙到 8-12 秒,体验更差;且模拟流式的节奏永远不如真实 token 输出自然 |
| 放弃 SSE 改 WebSocket | 双向连接,代理穿透有时更好 | 客户企业防火墙拦截 WebSocket 的比例高达 18%,SSE 是普通 HTTPS 几乎不被拦;重写客户端 + 服务端代价大 |
| 把 LLM 服务部署在客户内网,绕过公网链路 | 彻底躲开 CDN / WAF 缓冲 | SaaS 模式不可行,且 GPT-4o 走不了客户内网;只对极少数本地化部署客户有意义 |
| 用 HTTP/3 + QUIC,DAtagram 推送 | UDP 抗丢包好,理论上不被传统 HTTP 代理缓冲 | 2026 年浏览器 / 移动端 HTTP/3 支持仍不齐全,且大量企业网络封 UDP;调试链路也几乎没成熟工具 |
| 把 SSE 端点单独部署到独立域名 + 独立 CDN | 隔离主流量,降低误改风险 | 架构复杂度↑↑、跨域 cookie / auth 体系要重做,投入产出比不划算 |
否决的过程比选定方案更值钱——每条都让我们想清楚"为什么不"。后来产品同事在内部分享会上问"能不能换 WebSocket",我们直接甩出这张表,5 分钟说服全场。
这次复盘的长期收益
| 维度 | 修复前 | 修复后 90 天 |
|---|---|---|
| NPS 评分 | 42 | 67 |
| 对话完成率(用户耐心等到 [DONE]) | 78% | 94% |
| 客户续费率 | 83% | 91% |
| SRE 收到 "AI 卡顿" 工单数 | 每周 35-50 | 每周 0-3 |
| OpenAI token 浪费(用户半路放弃但已生成) | 每月 ¥18000 | 每月 ¥3500 |
| 团队 LLM 工程能力 | "模型调用 + prompt" | "全链路 LLM 交付能力" |
token 浪费这一项是意外收获——用户体验流畅后,放弃率从 22% 跌到 6%,OpenAI 账单立刻可见地下降。修一个 SSE 缓冲问题,带来的是产品体验 + 用户留存 + 成本节约的三连提升,这种 ROI 在 SRE 项目里很难得。
认知更新:LLM 产品的"流畅感"是工程问题
- OpenAI / Anthropic 卷得很厉害的"token/s 指标",到客户端可能完全无效。GPT-4o 标榜 60 token/s,但只要中间多缓冲 1 秒,客户感受到的就是 6 token/s。"模型快"和"用户感觉快"是两件事。
- 大多数 LLM 产品 demo 都跑在干净链路上(本地 / 演示环境),所以"团队 demo 时丝滑、生产卡顿"非常常见。务必在真实公网链路 + 真实移动网络下做压测,demo 环境的数据零参考价值。
- "流畅感"是 LLM 产品的差异化护城河之一。模型能力大家差不多(都调 OpenAI),但工程能力差距能拉开 NPS 25 分。这不是技术细节,是产品力。
- SSE 缓冲问题在 2026 年仍然没有标准答案。我读了 Cloudflare / Vercel / Fly.io 各家文档,每家的最佳实践都不一样,踩坑形态也都不一样。这块经验复用性差,只能各团队自己踩。
这次事故让我对"LLM 工程"有了新的认知:大模型的智能体验,有一半在链路上。OpenAI / Anthropic / Google 都付出了大量努力让 token 输出速度感觉自然,但只要中间任何一层代理多缓冲 500ms,前端用户的体验就崩了。所以构建 LLM 产品,光关心"模型 quality"和"prompt engineering"不够,SSE 链路的工程化同等重要,甚至往往是用户感知差距的主要来源。
另一个心得:"逐层 curl 测试"这套方法论可以推广到所有"看起来后端没问题但用户感觉不对"的现象。Web 是分层的,每一层都可能加入意外行为,只有逐层验证才能定位。这次的经验我们直接做成了 SRE 团队的标准 troubleshooting 模板,后来在调试 WebSocket、长轮询、大文件下载等场景反复用到。
第三个心得:不要相信"默认配置"。Nginx 的 proxy_buffering=on、Cloudflare 的 smart buffer、HTTP/2 的 frame 合并,这些都是"为传统 Web 设计"的优化,在 LLM streaming 场景下全是反优化。任何一个团队上线 LLM 产品前,都应该做一遍"默认配置审计"——把所有中间件的默认行为和你的实际使用场景对一遍,挑出冲突的提前关掉。这件事看起来繁琐,但比"出事故后再排查 3 天"划算太多了。
最后一条心得是关于"工程师的好奇心"。事故触发前 18 个月,这个 LLM 客服一直都在"卡顿地"运行,所有用户都默认接受了这种体验,内部工程师也从来没有自己用产品的习惯。产品同事来问那天,我心里第一反应是"AI 嘛不就是这样",差点也想合理化它。结果是产品同事的一句"为什么会这样"逼出了 3 天的硬核排查,逼出了一次实质性的产品力提升。"为什么会这样"是工程师最值钱的一句话,千万别让"业界普遍如此"成为你不深究的借口——绝大多数行业默认其实都有改进空间,只是没人去较真。这次复盘后我也给团队定了规矩:每周必须有一个人花 2 小时纯粹"作为用户使用自己的产品",发现任何不顺,立刻开 issue。两个月下来,光这个机制就挖出了 7 个类似量级的体验问题,产品 NPS 又涨了 8 分。
SSE 链路看起来是个小到不能再小的工程细节,但它撑起的是用户对"AI 是否聪明流畅"的核心感知。下次你的 LLM 产品又被用户吐槽"卡",别先怪模型,先去链路上 curl 一遍——4 层缓冲点,总有一层在阴你。修完之后你会发现,同样的 GPT-4o、同样的 prompt,在你的产品里突然变得明显更"聪明"——其实模型没变,变的只是用户终于看到了它本来的输出节奏。这种"零模型升级却带来一档体验提升"的工程红利,在 LLM 时代格外珍贵,值得每个团队投入一次彻底的复盘。
这篇文章的所有配置、curl 测试脚本、SSE 解析代码都已经验证过可以直接用于生产环境。如果你在自家服务上做了类似的优化,欢迎把你的链路结构、踩到的额外坑、最终的延迟数据发到评论区——LLM SSE 工程化这一块,行业内沉淀的公开经验还太少,每个团队的实战数据都是后来者的灯塔,我们也愿意把所有踩坑过程和后续监控指标继续在博客上同步,帮更多团队把流畅体验做到位,少走我们这 3 天踩过的所有弯路。
—— 别看了 · 2026