昨天分享的模型回應與平常大家用聊天介面有個不一樣的地方:程式是等模型「全部生成」完回應,才將內容印出來,而平常在使用聊天機器人時,介面都是一個字一個字或一段一段輸出。這個並不是由前端做的效果,而是 API 本身自帶的機制——streaming(串流)。
LLM 生成回應的方式,本質上是一次產生一個 token,再根據前面已經產生的內容繼續生成,一路生成到結束。也就是說,「整段回應」其實是逐步組裝出來的,只是不用串流的話,程式會等到全部組裝完成才一次拿到。
兩者的差別呈現在使用體驗上。假設一次回應需要 8 秒才能完整生成:
這個概念叫做 perceived latency(感知延遲)——實際花的時間沒有變,但讓使用者「感覺」系統有在動。任何會產生較長回應、或是面向使用者即時互動的場景(聊天機器人、客服回應),串流幾乎是必要的。
串流的實作,會由 SDK 包裝掉底層細節(常見的做法是用 Server-Sent Events 這類逐段推送的協定),不需要自己處理協定層。
因此我們需要了解,串流底下拿到的回應,不再是「一個完整的 response 物件」了,而是「一連串的事件(chunk / event)」,每個事件帶著一小塊內容,需要透過組裝來獲得完整的回應內容。
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
full_text = ""
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "用三句話介紹台灣夜市文化"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
full_text += text
print("\n---完整內容組裝完成---")
這裡用了 SDK 提供的 text_stream,是一個逐段吐出文字的產生器(generator),用 for 迴圈把每一小段接起來,同時印出來,也累積成完整字串,方便之後還需要用到整段內容(例如記錄到資料庫、算 token 用量)。
延續昨天的觀察,串流這件事在不同廠商手上,資料的取用方式也不同。以 OpenAI 為例:
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
full_text = ""
stream = client.chat.completions.create(
model="gpt-4.1",
max_tokens=1024,
messages=[{"role": "user", "content": "用三句話介紹台灣夜市文化"}],
stream=True,
)
for event in stream:
delta = event.choices[0].delta.content or ""
print(delta, end="", flush=True)
full_text += delta
print("\n---完整內容組裝完成---")
client.messages.stream() 代表要 stream 回應,並透過 context manager(with ... as stream)搭配 text_stream 產生器逐段取值拼湊完整回應。client.chat.completions.create(),並透過設置參數 stream=True 來指定串流形式,再透過 for 迴圈從 delta.content 取值。這也代表:如果你的程式同時要支援多家模型的串流輸出,「把串流資料轉成統一格式」這件事本身也是一塊需要獨立處理的邏輯。
Streaming 的回應可以用來提升使用者體感上的問題,在資料處理上並不複雜。
雖然 Streaming 可以讓使用者閱讀與使用的更流暢,但若要讓模型的回覆在程式中更有效地被應用,該如何讓這一段一段的文字變成可以被解析的結構化資料呢?
明天會針對模型回應的處理做分享!