ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HTTP Streaming:为什么 AI 回答经常要边生成边返回?

HTTP Streaming:为什么 AI 回答经常要边生成边返回? 专栏AI 全栈开发11前面几篇我们一直把 HTTP Response 当成“服务器处理完以后一次返回”。但生成式 AI 有一个新特点答案可能需要几秒甚至更久才生成完而且中间结果可以不断产生。Streaming 的价值就是让浏览器不用等全部完成先开始接收已经生成的部分。一、先看最普通的一次性实现哪里让人难受后端如果这样写answer model.generate(prompt) return {answer: answer}服务器只有等 model.generate() 完全结束才能把完整 JSON 返回。如果模型 8 秒后才生成完页面前 8 秒几乎拿不到正文内容。Streaming 改变的是返回方式模型生成一点↓后端转发一点↓浏览器接收一点↓页面展示一点这样用户不需要等到最终答案全部完成已经生成的内容可以先显示。图 1 Streaming 不会让总生成时间凭空消失但可以让用户更早看到第一批结果二、不是所有 AI 接口都必须 Streaming这是原稿里最需要收回的一句话。对长文本生成、聊天、代码生成这类交互Streaming 通常很有价值因为输出本来就是逐步产生的。但下面这些场景完全可以一次性返回• 分类只返回一个类别。• Embedding返回一个向量结果。• 很短的 Structured Output。• 后台批处理任务本来就不要求用户盯着页面等。所以正确结论是**生成时间较长而且中间结果对用户有价值时Streaming 往往能显著改善交互体验。**三、HTTP Streaming 到底是什么意思先不要急着背 Transfer-Encoding: chunked。从应用开发者角度HTTP Streaming 可以先理解成HTTP Response 建立以后Response Body 不需要等全部数据准备好再一次性发送而可以持续产生、持续被客户端读取。不同 HTTP 版本实现底层传输的方式不同协议对应用层最重要的认识HTTP/1.1未知长度的响应可以使用 chunked transfer coding 等机制传输HTTP/2使用自己的帧和 Stream 机制不使用 Transfer-Encoding: chunkedHTTP/3基于 QUIC 的 HTTP Stream也不是 HTTP/1.1 的 chunked 报文因此“HTTP Streaming Transfer-Encoding: chunked”是不准确的。chunked 是 HTTP/1.1 的一种消息传输方式现代应用通常把这些底层细节交给 Web Server / 框架处理。四、模型 token、后端 yield、浏览器 read() 不是一一对应图 2 模型输出、后端应用块和网络字节块是不同层不要假设“一次 token 一个 HTTP chunk 一次 read()”很多教程会说“模型生成一个 token服务器就发一个 chunk浏览器就收到一个 chunk。”作为直觉可以理解但工程上不能依赖这件事。真实链路可能是• 模型 SDK 一次回调给你几个字符或一个 delta。• 你的后端把多个 delta 合并后再 yield。• Web Server、HTTP/2 帧、代理、TLS 都可能重新分段。• 浏览器 reader.read() 拿到的字节块也可能被合并或拆分。因此真正需要稳定的是**应用协议**而不是网络 chunk 边界。下一篇 SSE 就是给流式数据加结构化事件边界的一种常见方式。五、先用 FastAPI 做一个最小裸 Streaming后端不接真实模型先用异步生成器模拟import asyncio from fastapi import FastAPI from fastapi.responses import StreamingResponse app FastAPI() async def fake_stream(): for piece in [你好, , 这是, 一个, 流式, 回答。]: await asyncio.sleep(0.3) yield piece.encode(utf-8) app.get(/stream) async def stream(): return StreamingResponse( fake_stream(), media_typetext/plain; charsetutf-8, )启动后用curl -N http://127.0.0.1:8000/stream你应该看到内容陆续出现而不是等全部字符串都生成完才一次打印。这里没有手工写 HTTP/1.1 chunk 长度也没有自己设置 Transfer-Encoding因为这类传输细节应该交给 ASGI Server 和协议栈。六、浏览器怎样消费一个 Streaming Responsefetch() 返回的 Response.body 是一个 ReadableStream。可以逐步读取const resp await fetch(/stream); if (!resp.body) { throw new Error(response body is not readable); } const reader resp.body.getReader(); const decoder new TextDecoder(); let text ; while (true) { const { done, value } await reader.read(); if (done) break; text decoder.decode(value, { stream: true }); output.textContent text; } text decoder.decode(); // flush decoder最后这句 decoder.decode() 很容易被忽略如果多字节字符刚好跨两个字节块TextDecoder 的 streaming 模式会保留残余状态结束时最好 flush 一次。同样要记住一次 reader.read() 返回的是一段字节不保证刚好是一句话、一个 token 或一次后端 yield。七、Streaming 和 SSE 到底是什么关系3 HTTP Streaming 解决“能持续传”SSE 在流上增加事件格式和浏览器约定裸 Streaming 只解决正文可以持续到达。至于每段数据代表什么由应用自己约定。例如我们现在只是连续发送普通文本你好这是一个流式回答。如果以后想表达开始文本片段Tool 状态错误完成就需要一层更明确的事件协议。SSE 是浏览器 Web 生态里很常见的一种方式使用 text/event-stream 和 data: 等字段组织事件。下一篇 12 专门讲 SSE这一篇先不把 EventSource、event/id、自动重连全部提前塞进来。八、Streaming 为什么在代理后面可能“看起来不流了”本地直连后端时每隔 300ms 都能看到新内容一挂代理结果可能攒一会儿再一起到。原因可能来自多层缓冲• 应用代码自己先攒数据。• 压缩/日志等中间件可能缓冲。• 反向代理或 CDN 可能改变转发节奏。• 客户端本身也可能有显示缓冲。所以“后端已经 yield”不代表用户一定立刻看到。现在只记住调试方法用 curl -N 先直连后端测试再经过代理测试。如果两边节奏明显不同优先检查中间层缓冲。Nginx 的具体配置、超时和部署问题放到后面的生产部署章节继续讲。九、流开始以后错误处理为什么变得不一样普通接口可以在执行失败时返回HTTP 500{error: ...}但流式响应一旦已经发送了 200 响应头和部分正文后面再失败就不能把 HTTP 状态码“改回 500”。这意味着流式协议通常要提前约定• 流内怎样表示错误。• 怎样表示完成。• 客户端怎样知道当前响应是正常结束还是中途失败。这正是下一篇 SSE Event Protocol 会自然解决的问题之一。十、客户端断开以后上游模型会自动停止吗不一定。浏览器关页面、Abort fetch 或网络断开只说明客户端不再继续消费这条连接。如果后端没有把取消继续传播到上游模型请求Provider 端仍可能继续生成和计费。所以完整取消链路应该是Browser cancel↓Backend 发现取消 / 断开↓停止读取并尽可能取消上游模型请求↓释放资源这一篇只建立这个边界。AbortController、request.is_disconnected()、Provider 是否真正支持取消会在后面的取消专题单独做。十一、几个最容易学错的说法• 误区 1所有 AI 接口都必须流式。分类、Embedding、短 JSON 等场景未必需要。• 误区 2Streaming 会减少模型总生成时间。它主要改变结果到达用户的节奏。• 误区 3HTTP Streaming 就是 Transfer-Encoding: chunked。HTTP/2/3 有不同的流传输机制。• 误区 4后端一次 yield 就等于浏览器一次 read。中间层可以拆分、合并或缓冲数据。• 误区 5Content-Length 和 Streaming 永远互斥。很多流式场景确实未知总长度但“是否流式”不是仅由一个 Header 判断。• 误区 6浏览器断开就一定停止模型生成。取消必须沿后端继续传播上游还要支持取消。十二、这一篇只记住 4 句话• Streaming 的价值是让尚未完成的响应可以逐步到达客户端。• 它特别适合长时间生成且中间结果有价值的交互不是所有 AI 请求的必选项。• 模型 token、后端 chunk 和浏览器读到的字节块不是一一对应。• 裸 Streaming 只解决“持续传输”SSE 会在下一篇解决“持续传输的数据怎样组织成事件”。十三、下一篇下一篇 12《SSE 到底是什么》会在这条 Streaming 链路上加一层事件协议text/event-stream、data:、事件结束、错误以及 EventSource 为什么能自动重连。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进