AI Agent 開發者必學的 Streaming 漸進式輸出技巧
近年來「AI Agent」的開發已經非常普遍,不管是做客服機器人、知識檢索,還是線上客服小幫手,大家常用的API介面幾乎都是 OpenAI 的 Chat Completions API。
在大多數情境下,我們會用 非串流(non-streaming) 的方式呼叫,也就是送出一個請求,等伺服器思考完再一次回傳完整的回答。這樣的模式在 LINE、Teams、Slack 這類即時通訊平台的 Chat Bot UI 中沒什麼問題,因為使用者本來就習慣機器人一次回覆一整段文字。
但其實還有一種像是ChatGPT 官網那樣的輸出方式。
漸進式輸出的好處
但如果你用過 ChatGPT 官方網站,就會發現它的回覆方式完全不同:文字會像打字機一樣一個字一個字冒出來。這種「漸進式輸出」的體驗有幾個好處:
-
回應更即時
使用者不需要等模型完整算完才能看到內容,能先讀到前幾個字,就有「系統已經在思考」的感覺。 -
降低等待焦慮
在 UX 上,空白畫面最容易讓人懷疑「是不是壞掉了」。逐字輸出則能持續回饋,讓人安心。 -
模擬自然對話
人類聊天的過程就是一邊想一邊講,這種輸出方式更貼近自然互動。 -
更有戲劇感
對 Demo、教學、產品展示特別有用,可以讓使用者感覺「AI 正在思考」。
因此,雖然很多 Chat Bot 不一定需要這樣的 UI,但如果你的產品本身能夠提供這種 漸進式對話,以「Streaming 方式回覆」可以讓用戶體驗好很多。
Streaming 是怎麼實作的?
那麼,ChatGPT 網站的打字機效果是怎麼做的?
關鍵就在於 OpenAI 提供的 stream: true
參數。
當你在呼叫 /v1/chat/completions
API 時,如果設定 stream: true
(底下第二行),伺服器就不會一次把整個 JSON 給你,而是會用 Server-Sent Events (SSE) 協議,持續推送「事件」。
{
"model": "gpt-4o-mini",
"stream": true,
"messages": [
{ "role": "system", "content": "你是一個友善的助教。" },
{ "role": "user", "content": "請說明迴圈的用途" }
]
}
當這樣呼叫時,回傳時候會以漸進方式輸出,每一行會以 data:
開頭,裡面包的是 JSON 片段,例如:
data: {"id":"...","choices":[{"delta":{"content":"哈"}}]}
data: {"id":"...","choices":[{"delta":{"content":"囉"}}]}
data: [DONE]
每個片段(delta)可能只有幾個字,Client端只要不斷讀取並即時顯示,就能拼湊成完整的回答。
最後一行 data: [DONE]
代表輸出結束。
用 C# 呼叫 API,實現 Streaming
以下是一個簡單的 C# Console 範例,示範如何用 HttpClient
呼叫 OpenAI API,並逐字輸出回覆內容。
// 建立一個 HttpClient 實例,用於發送 HTTP 請求
using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
var body = new
{
model = "gpt-4o-mini",
// 啟用串流模式,讓回應可以逐步傳回
stream = true,
messages = new object[]
{
new { role = "system", content = "你是一個友善的助教。" },
new { role = "user", content = question }
}
};
// 將請求主體序列化為 JSON 字串
string json = JsonSerializer.Serialize(body);
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
// 發送請求並等待回應
using var resp = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
// 以串流方式取得回應的內容
await using var stream = await resp.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
Console.WriteLine("\n回答:\n");
// 逐行讀取回應內容
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
// 忽略空行和非數據行
if (line is null || !line.StartsWith("data: ")) continue;
// 取得有效的 JSON 資料,並且檢查是否為結束標記
var payload = line.Substring("data: ".Length);
if (payload == "[DONE]") break;
try
{
// 解析 JSON 資料
using var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
// 取得 choices 陣列
var choices = root.GetProperty("choices");
// 取得第一個選項的 delta 屬性
var delta = choices[0].GetProperty("delta");
// 取得內容
if (delta.TryGetProperty("content", out var contentElem))
{
// 取得內容
Console.Write(contentElem.GetString());
}
}
catch
{
// 解析錯誤...略
}
}
Console.WriteLine("\n\n--- 完成 ---");
這個程式跑起來後,當用戶問一個問題,就會看到 AI 的回覆逐字出現在 Console 裡,效果跟 ChatGPT 網頁很像。
這段程式碼有幾個關鍵:-
設定
stream: true
這告訴伺服器要用 SSE 傳輸,而不是一次給完整 JSON。 -
使用
HttpCompletionOption.ResponseHeadersRead
這樣HttpClient
才會在讀到 Response Header 後立刻開始讀取,不會等整個 Body 收完。 -
逐行讀取
StreamReader.ReadLineAsync()
SSE 的特徵是每行以data:
開頭,客戶端要逐行處理。 -
解析 JSON 片段
伺服器推來的每段 JSON 都很小,真正的文字藏在:choices[0].delta.content
-
即時輸出
在 Console 直接Console.Write()
就能模擬「打字機」的效果。
結語
很多人用 Chat Completions API 時,直覺會用一次請求、一次回覆的模式,這當然也能滿足大多數 Bot 的需求。
但如果你想在 Web UI 或 Desktop App上做出 更接近 ChatGPT 網站的互動體驗效果,那麼 Streaming 輸出就是關鍵。
而實作其實並不複雜,只要在呼叫 API 時加上 stream: true
,搭配一點 SSE 的處理邏輯,就能完成。
希望這篇文章能幫助你在下次開發 AI Agent 時,能帶給使用者更流暢、更人性化的對話體驗。
ref repo:
https://github.com/isdaviddong/ex-openai-chat-stream-console.git
留言