Streaming
逐步接收模型输出,并正确处理事件、增量、完成与断流。
Streaming 让客户端在模型生成完整回答之前持续接收增量内容。它适合聊天界面、长文本生成和需要尽快展示首段结果的场景,但客户端必须正确处理事件边界、内容拼接、完成状态和连接中断。
Streaming 改变返回方式,不改变任务本身
同一个任务通常可以选择非流式或流式返回。先完成非流式请求,再开启 Streaming,更容易区分请求参数问题和流式解析问题。
使用前确认
- 在模型广场确认目标模型支持所选协议和 Streaming。
- 根据协议使用对应端点和事件结构,不要让同一个解析器猜测所有协议。
- 确认客户端、反向代理和网关不会缓冲整个响应后再一次性返回。
完整字段分别由 Chat Completions、Responses、Anthropic Messages 和 Gemini Stream Generate Content Reference 维护。
发起最小流式请求
下面通过 Chat Completions 开启流式输出:
curl --no-buffer 'https://api.agtcloud.cn/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer your-api-key' \
--data '{
"model": "MODEL_ID",
"messages": [
{"role": "user", "content": "分三点解释指数退避。"}
],
"stream": true
}'--no-buffer 让 cURL 及时显示已经收到的数据,不会改变服务端事件结构。
按事件读取,而不是按网络数据包读取
HTTP 连接每次交给客户端的字节数量并不等于一个完整事件。一个 JSON 对象可能被拆成多个数据块,多个事件也可能在同一个数据块中到达。
对于 SSE,客户端应先按空行识别完整事件,再读取其中的 data: 内容:
const response = await fetch('https://api.agtcloud.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-api-key',
},
body: JSON.stringify({
model: 'MODEL_ID',
messages: [{ role: 'user', content: '解释什么是指数退避。' }],
stream: true,
}),
});
if (!response.ok || !response.body) {
throw new Error(`请求失败:${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const event of events) {
const data = event
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trimStart())
.join('\n');
if (!data || data === '[DONE]') continue;
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
}生产代码还应处理取消请求、读取超时、心跳、非 JSON 事件以及流结束时尚未消费的缓冲区。
四种协议的流式结构
Chat 通常返回 Chat Completion Chunk。文本位于 choices[].delta.content,工具参数可能出现在 choices[].delta.tool_calls,结束原因位于 finish_reason;事件流可能以 [DONE] 结束。
协议入口存在不代表所有模型都会返回上述所有内容类型。只实现应用实际使用、并已由目标模型返回的分支。
拼接文本和工具参数
- 文本增量按候选、输出项或 Content Block 的索引分别拼接。
- 工具名称、调用 ID 和参数可能分多次到达,应为每个调用维护独立缓冲区。
- 工具参数只有在完整结束后才能解析为 JSON,不能逐块执行。
- 推理摘要、Thinking Block 和最终回答使用不同缓冲区,不能混成一段文本。
- 如果协议在最后返回 Usage,应在收到完成事件前继续读取连接。
判断成功、失败和中断
| 现象 | 客户端处理 |
|---|---|
| 收到协议规定的完成事件 | 保存完整结果和 Usage,标记请求成功 |
| 收到错误或失败事件 | 保存已展示内容,同时将本次请求标记为失败 |
| 连接提前关闭 | 标记为不完整,不把已有文本冒充完整回答 |
| 用户主动停止 | 取消读取并在界面标记“已停止生成” |
| 长时间没有新事件 | 按应用超时策略取消,保留必要的排障信息 |
流式请求已经向用户展示部分内容后,不应自动无限重放。重试可能产生另一份不同回答,也可能重复触发工具或其他有副作用的行为。
上线前检查
- 页面能够逐步显示文本,而不是等到连接关闭才显示。
- 中文和多字节字符不会因数据块切分而乱码。
- 工具参数按调用 ID 正确拼接,并在执行前完成校验。
- 用户取消、网络断开、超时和服务端错误都有明确状态。
- 日志不会记录 API Key、完整敏感 Prompt 或未经脱敏的工具参数。
下一步可阅读 Tool Calling 和错误与排障。