外觀
串流輸出
串流輸出讓模型在產生內容的同時,就把已完成的片段回傳給客戶端,適合聊天介面等需要「逐字顯示」的情境。
啟用方式
在請求主體加上 "stream": true:
json
{
"model": "MODEL_ID",
"messages": [{ "role": "user", "content": "請用 200 字介紹串流輸出。" }],
"stream": true
}啟用後,回應的 Content-Type 會變成 text/event-stream,內容以 SSE(Server-Sent Events) 格式逐段送出。
事件格式
每一段資料都是一行 data:,後面接一個 JSON 物件;資料流結束時會送出 data: [DONE]:
text
data: {"id":"chatcmpl-xxxxxxxx","object":"chat.completion.chunk","created":1735689600,"model":"MODEL_ID","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-xxxxxxxx","object":"chat.completion.chunk","created":1735689600,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":"串流"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxxxxxxx","object":"chat.completion.chunk","created":1735689600,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":"輸出"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxxxxxxx","object":"chat.completion.chunk","created":1735689600,"model":"MODEL_ID","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]待補充
以上為 OpenAI 相容格式的示意內容(id、時間戳與 Token 數皆為假值)。瓜瓜AI 實際送出的 chunk 欄位、是否附帶 usage、以及結束標記是否為 [DONE],請以服務端實際回應為準。
chunk 欄位
| 欄位 | 說明 |
|---|---|
object | 串流時通常為 chat.completion.chunk |
choices[].delta | 本次新增的內容片段(非完整內容) |
choices[].delta.role | 第一個 chunk 通常會帶 assistant |
choices[].delta.content | 本次新增的文字,需自行累加 |
choices[].finish_reason | 最後一個 chunk 會帶結束原因,其餘為 null |
解析要點
| 重點 | 說明 |
|---|---|
| 逐段累加 | delta.content 是片段,必須自行串接成完整字串 |
| 空片段 | 第一個與最後一個 chunk 的 content 常為空,需防護 |
| 結束判斷 | 收到 [DONE] 或 finish_reason 非 null 時結束 |
| 緩衝處理 | 網路封包可能一次包含多行 data:,或一行被切成兩段,需以行緩衝解析 |
| 不要重試整段 | 串流中斷時,已顯示的內容通常無法續傳,建議重新發起請求 |
程式範例
bash
curl -N https://api.guagua5487.xyz/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "MODEL_ID",
"messages": [{ "role": "user", "content": "請用 200 字介紹串流輸出。" }],
"stream": true
}'python
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.guagua5487.xyz/v1",
)
stream = client.chat.completions.create(
model="MODEL_ID",
messages=[{"role": "user", "content": "請用 200 字介紹串流輸出。"}],
stream=True,
)
for chunk in stream:
if not chunk.choices:
continue
piece = chunk.choices[0].delta.content or ""
print(piece, end="", flush=True)javascript
import OpenAI from 'openai'
const client = new OpenAI({
apiKey: process.env.GUAGUA_API_KEY,
baseURL: 'https://api.guagua5487.xyz/v1'
})
const stream = await client.chat.completions.create({
model: 'MODEL_ID',
messages: [{ role: 'user', content: '請用 200 字介紹串流輸出。' }],
stream: true
})
for await (const chunk of stream) {
const piece = chunk.choices?.[0]?.delta?.content ?? ''
process.stdout.write(piece)
}javascript
const res = await fetch('/api/chat', { // 由自家後端代理,勿在前端放 API Key
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: '請用 200 字介紹串流輸出。' })
})
const reader = res.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 lines = buffer.split('\n')
buffer = lines.pop() ?? '' // 保留最後一行未完成的內容
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed.startsWith('data:')) continue
const payload = trimmed.slice(5).trim()
if (payload === '[DONE]') return
try {
const json = JSON.parse(payload)
const piece = json.choices?.[0]?.delta?.content ?? ''
if (piece) process.stdout.write(piece)
} catch {
// 忽略不完整的 JSON 片段,等下一輪緩衝補齊
}
}
}注意事項
- 代理與閘道可能緩衝回應:Nginx 等反向代理預設可能等回應結束才轉發,導致串流變成「一次出現」。請關閉對該路徑的緩衝(例如 Nginx 的
proxy_buffering off;)。 - CDN 與 Serverless 逾時:長時間串流可能超過平台請求逾時,需調整設定。
curl請加-N:否則輸出同樣可能被緩衝。- HTTP/2 與壓縮:部分中介層會對 SSE 造成額外延遲,若出現異常可先繞過測試。
- 前端務必經由後端代理:把 API Key 放在瀏覽器端會導致金鑰外洩。
待補充
以下項目尚未確認,請以服務端實際行為為準:
- 串流是否支援
stream_options(例如回傳usage) - 單一連線的最長持續時間與閒置逾時
- 是否會發送 keep-alive 註解行(
: ping)