流式输出
流式把一次生成拆成多个增量推送,首字更快、长回答体感更好。协议与 OpenAI 一致,客户端无需改造。
流式响应先返回 200 再逐块推送,因此「请求成功」不等于「生成成功」。 故障可能出现在已经收到部分内容之后,必须在解析循环里单独处理。
开启流式
在请求体里加 "stream": true 即可。其余参数与非流式一致,返回形态从一个完整 JSON 变成一串 SSE 事件。
SSE 数据格式
每个事件以 data: 开头,后接一个 JSON 片段, 事件之间用空行分隔。文本增量在 choices[0].delta.content 里。
data: {"choices":[{"delta":{"role":"assistant","content":""}}]}
data: {"choices":[{"delta":{"content":"SSE"}}]}
data: {"choices":[{"delta":{"content":" 是"}}]}
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]有三点容易踩坑:首个事件的 delta 可能只带角色而没有内容;结束前会出现带 finish_reason 而内容为空的事件; 最后的 [DONE] 不是合法 JSON,直接丢给解析器会抛错,需要先判断再解析。
用 SDK 消费增量
官方 SDK 已经处理了分帧与 [DONE],直接迭代即可。
from openai import OpenAI
client = OpenAI(api_key="...", base_url="https://chuanapi.com/v1")
stream = client.chat.completions.create(
model="MODEL_ID",
messages=[{"role": "user", "content": "用三句话解释 SSE"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.WANCHUAN_API_KEY,
baseURL: "https://chuanapi.com/v1",
});
const stream = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "用三句话解释 SSE" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}注意 delta.content 可能为 None 或 undefined,拼接前要判空,否则会把 undefined 输出到用户界面上。
用 curl 观察原始流
排查时建议直接看原始流。关键是 -N 关闭缓冲,否则会等到全部结束才一次性输出,看不出增量行为。
curl -N https://chuanapi.com/v1/chat/completions \
-H "Authorization: Bearer $WANCHUAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"stream": true,
"messages": [{"role": "user", "content": "用三句话解释 SSE"}]
}'超时与中断
- 不要用「整体请求超时」约束流式调用。长回答的总时长天然较长, 应该用「两个增量之间的间隔」做空闲超时。
- 反向代理和网关的缓冲会破坏流式体感。自建一层转发时需要关闭响应缓冲。
- 用户主动取消时要真正断开连接,否则生成会继续进行并继续计费。
- 中途断流不会改变已返回的 200 状态码。整体重试会让用户看到重复的前半段, 处理策略见 错误码。
流式下的用量统计
流式增量里通常不携带完整的 token 用量,因此不要依赖最后一个事件来记账。 实际扣费以控制台的调用日志为准;本地若需要统计,应在流结束后按累计文本自行估算, 并明确标注为估算值。换算规则见 计费说明。