API 開發

讓 Claude 邊想邊回:串流回應的原理與實作

串流的做法是在 messages.create 加上 stream=True,或改用 client.messages.stream 的簡化介面。Claude 會把回應拆成一連串事件邊生成邊傳回,你把其中 ContentBlockDelta 的文字片段即時轉發給前端,使用者就能看著答案逐字浮現,不必盯著載入動畫等整段生成完畢。
讓 Claude 邊想邊回:串流回應的原理與實作:文章重點卡

讓 Claude 邊想邊回:串流回應的原理與實作

做聊天應用有一個很實際的體驗問題:Claude 生成一段長回應可能要好幾十秒,這段時間使用者只能盯著載入動畫,不知道系統是當了還是在忙。串流(streaming)就是為這件事而生:讓文字一小塊一小塊即時出現,像有人在對面打字一樣。

這篇帶你看懂串流背後發生了什麼事、六種事件各代表什麼,以及兩種實作寫法怎麼選。

你將學到什麼

為什麼需要串流

長回應可能要等上數十秒,串流讓使用者立刻看到文字開始出現。

六種串流事件

從 MessageStart 到 MessageStop,每種事件各自代表什麼。

兩種實作方式

stream=True 逐事件處理,或用 SDK 的 text_stream 簡化介面。

串流後拿回完整訊息

用 get_final_message 同時滿足即時顯示與資料儲存。

使用者怕的不是等,是不知道在等什麼

用一般方式呼叫 Claude 時,你的伺服器要等整段回應生成完畢才回傳給前端。回應可能要花 10 到 30 秒生成,這段時間畫面上什麼都沒有,使用者很容易以為壞掉了。

這種「先等再一次給」的設計,回應短的時候沒什麼問題;回應一長,體驗就崩了。更麻煩的是,看不到任何進度的使用者常會重新整理或再按一次送出,多出來的重複請求反而加重系統負擔。

開啟串流後劇本完全不同:Claude 一收到請求就先回覆「收到了,開始生成」,接著把文字一小塊一小塊往回送。你的伺服器把這些片段即時轉發給前端,答案就會像打字一樣逐字浮現。重點是:這一切仍然屬於同一個請求,只是回應的形狀從一大包變成一連串小包。

一般回應等待中,畫面沒有任何回饋整段一次到串流回應片段片段片段片段片段完成時間
同樣的總生成時間,串流讓使用者從第一秒就看得到進度。

串流事件有哪六種

開啟串流後,回應不再是一大包,而是一連串各司其職的事件,依序是:

  • MessageStart:一則新訊息開始了
  • ContentBlockStart:一個內容區塊開始,裡面可能是文字、tool use 或其他內容
  • ContentBlockDelta:實際生成的文字片段
  • ContentBlockStop:目前的內容區塊結束
  • MessageDelta:這則訊息即將完成的相關資訊
  • MessageStop:整則訊息結束

其中真正帶著文字的是 ContentBlockDelta,要顯示給使用者的內容就在它身上,其他事件是骨架與訊號。

以一段單純的文字回應為例,事件的節奏會是:MessageStart 開場,ContentBlockStart 宣告文字區塊開始,接著一連串 ContentBlockDelta 把文字一段一段送出,ContentBlockStop 收掉區塊,最後 MessageDelta 與 MessageStop 收尾。先看懂這個節奏,之後遇到 tool use 這類更複雜的回應結構時就不會慌。

基本實作:stream=True

最直接的寫法,是在 messages.create 呼叫裡加上 stream=True

messages = []
add_user_message(messages, "Write a 1 sentence description of a fake database")

stream = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    stream=True
)

for event in stream:
    print(event)

跑起來你會看到事件一個接一個印出。這種寫法讓你完整掌握每一種事件,適合需要精細控制的場景,例如同時處理 tool use 或多個內容區塊。

第一次實作時,我很建議真的把每個事件印出來看一遍:你會對「回應原來是這樣一塊一塊組出來的」有非常具體的感覺,之後除錯看到事件流也能立刻對上號。

簡化介面:text_stream

多數時候你只想要文字,不想自己解析事件。SDK 提供了更省事的串流介面:

with client.messages.stream(
    model=model,
    max_tokens=1000,
    messages=messages
) as stream:
    for text in stream.text_stream:
        print(text, end="")

text_stream 會自動濾掉文字以外的所有事件,只把生成的內容一段一段交給你;print 加上 end="" 就能在終端機做出逐字輸出的效果。在真實應用裡,這個迴圈裡做的事就是把片段轉發給你的前端。

兩種寫法怎麼選?想完整掌控每種事件(例如同時處理 tool use),就用 stream=True 自己解析;只是要把文字端給使用者看,text_stream 乾淨得多,也是多數聊天應用的日常選擇。還沒決定的話,先用簡化介面就好,等需求出現再換。

邊串流邊留一份完整訊息

串流片段適合即時顯示,但存進資料庫或做後續處理時,你需要的是完整訊息。串流結束後呼叫 get_final_message() 就能拿到組裝好的結果:

with client.messages.stream(
    model=model,
    max_tokens=1000,
    messages=messages
) as stream:
    for text in stream.text_stream:
        # 把每個片段轉發給前端
        pass

    # 串流結束後,拿完整訊息存資料庫
    final_message = stream.get_final_message()

一次請求同時滿足兩種需求:使用者看到即時輸出,程式拿到完整物件。這是聊天應用最常見的收尾寫法。

把整條路串起來看:瀏覽器發出請求、你的伺服器帶著金鑰呼叫 Claude 並開啟串流、每收到一個片段就即時轉發回前端、串流結束後把完整訊息存進資料庫。使用者的體感從「等一大段」變成「看著答案長出來」,而你的程式邏輯並沒有因此變得複雜多少。

串流是聊天應用的基本功:它把「模型生成需要時間」這個物理限制,轉化成使用者可以接受、甚至覺得生動的體驗。

什麼時候可以不用串流批次任務、排程工作這種沒有人盯著畫面的場景,直接等完整回應反而簡單。串流是為了「有人在等」的體驗而存在的。另外提醒:就算開了串流,也記得把 get_final_message 的完整訊息存起來,之後要重建對話歷史才有得用。
參考出處本文取材自 Anthropic 官方 Claude Academy 免費課程「Building with the Claude API」,由酒Ann 消化後以自己的視角重新編寫。想看英文原版課程,可到 Claude Academy 修習。

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

串流會多發好幾個請求嗎?
不會。所有事件都屬於同一個請求,只是回應被拆成小塊陸續送達,而不是等全部生成完才一次回傳。
一定要自己解析每種事件嗎?
不用。SDK 提供 client.messages.stream 的簡化介面,text_stream 會自動濾出純文字片段,多數應用直接用它就夠了。
串流時要怎麼把完整回應存進資料庫?
串流結束後呼叫 stream.get_final_message(),就能拿到組裝完成的完整訊息物件,即時顯示與資料儲存兩邊都顧到。
哪種事件裡有實際的文字內容?
ContentBlockDelta。其他事件負責標記訊息與內容區塊的開始與結束,要顯示給使用者的文字都在 ContentBlockDelta 裡。