Streaming 與多模態輸入
你用非串流方式呼叫 Claude API,等了十秒才看到完整回應——對程式腳本這沒什麼,但如果你在做一個給人用的介面,使用者盯著轉圈圈等十秒鐘,離開的機率遠比你想的高。解法很直接:Streaming。讓 Claude 產生一個字就推一個字出來,使用者從第一個字就看到畫面在動,等待感直接消失。
同樣讓 API 身價提升一個等級的,是多模態輸入。第 1 到 5 課的 messages 全都是文字,但現實世界的資料很少是純文字:收據是照片、合約是 PDF、截圖是圖片。這堂課讓你把這些東西直接塞進 content 陣列,Claude 就能看到它們、分析它們,不需要你先寫 OCR 轉文字。
這堂學什麼
- Streaming 的 UX 意義:為什麼「看起來快」有時比「真的快」更重要
- SSE 原理:HTTP 層發生了什麼事,SDK 又幫你包了什麼
- Python SDK 的
messages.stream()與 JavaScript SDK 的串流寫法(雙語完整範例) - 圖片輸入:base64 與 URL 兩種方式的差異、支援格式、大小限制
- PDF 文件輸入:用
document類型傳 base64,長文件的 token 計算方式 - 實戰小工具:收據辨識 + 串流輸出的完整 Python 腳本
觀念一:Streaming 的 UX 意義
先說心理學:使用者對「等待」的忍耐度和「感知速度」有關,不是和實際速度有關。研究顯示,看到內容逐漸出現(哪怕總時間一樣),感知等待時間會縮短 30–50%。這就是為什麼 ChatGPT、Claude.ai 的網頁介面全部都是串流——他們在同一台伺服器跑同一個模型,選擇串流純粹是為了 UX。
非串流 vs 串流的時序對比:

串流實作對後端成本沒有任何影響——相同 token 數、相同模型,帳單一模一樣。它唯一做的事是把 Claude 產生的 token 每生一個就推一個到客戶端,而不是等全部產完才回傳。
串流對使用者的實際影響:
- 聊天介面:不串流,使用者盯著「正在輸入...」超過五秒就開始懷疑當掉了;串流讓他們從第一個字就知道 Claude 在認真回答
- 長文生成:一篇 800 字的文章非串流要等 15–20 秒;串流讓使用者邊看邊思考,甚至更早發現方向不對、提早中止,省下繼續燒 token 的費用
- 工具整合:如果你在做 CLI 工具(像這堂課最後的收據辨識器),串流讓命令列用起來像對話,不像「送出請求、盯著畫面、等待結果」
有一個場景不適合串流:你需要在回應完整出來後才能做事的情況——例如你要 Claude 輸出 JSON 格式然後立刻 json.loads() 解析它。串流無法保證 JSON 是在哪個片段結束的;這種場景繼續用非串流,或是等 stream.get_final_message() 拿完整文字後再解析。
觀念二:SSE 是什麼,SDK 又幫你包了什麼
Claude API 的串流協定叫 SSE(Server-Sent Events)——這是一個標準的 HTTP 技術:伺服器保持連線打開,持續往客戶端推送一行一行的 data: {...} 文字,直到一個特殊的 data: [DONE] 事件出現為止。
如果你好奇底層長什麼樣,直接用 curl 呼叫原始 API 就能看到:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data '{
"model": "claude-haiku-4-5",
"max_tokens": 64,
"stream": true,
"messages": [{"role": "user", "content": "說 hi"}]
}'
你會看到類似這樣的原始輸出一行一行往下跑:
event: message_start
data: {"type":"message_start","message":{"id":"msg_01...","type":"message",...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"嗨"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}}
event: message_stop
data: {"type":"message_stop"}

如果你用 stream=True 的低階 API,你需要自己判斷每個事件是 content_block_delta 還是 message_stop。但 Python SDK 的 messages.stream() 方法已經把這些包起來,提供一個高階的 text_stream 迭代器——大多數情況你只需要這個,直接拿到文字片段,事件細節 SDK 幫你過濾掉。
SDK 提供的兩個主要串流介面對比:
| 介面 | 用法 | 適合場景 |
|---|---|---|
messages.stream() + text_stream |
高階,直接拿文字片段 | 大多數場景,最省事 |
messages.create(stream=True) |
低階,需自己處理事件類型 | 要處理 Tool Use 中間事件、需要 streaming 的 token 計數等進階場景 |
第 5 課學過的 Tool Use 也支援串流——input_json_delta 事件會逐字傳回工具的輸入 JSON;但在工具呼叫完成並執行後再繼續生成文字時,串流就特別有用。這個進階用法在第 8 課的綜合實戰會碰到。
觀念三:多模態輸入的 content 陣列結構
第 1 課的 messages 裡 content 是一個字串:
{"role": "user", "content": "這是純文字"}
多模態輸入時,content 改成陣列,每個元素是一個 block,可以是文字、圖片或文件:
{"role": "user", "content": [
{"type": "image", "source": {...}}, # 圖片 block
{"type": "text", "text": "問題文字"} # 文字 block
]}

陣列裡可以同時有多張圖、多份文件、多段文字,Claude 會按順序理解它們。問題文字放最後是常見的最佳實踐:先讓 Claude「看到」素材,再看到問題。
三種 block 類型的完整 source 結構快速對照:
# 圖片 block — base64 方式
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg", # jpeg / png / gif / webp
"data": "<base64 字串>",
}
}
# 圖片 block — URL 方式
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/image.jpg",
}
}
# 文件 block(PDF)
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "<base64 字串>",
}
}
# 文字 block
{
"type": "text",
"text": "你的問題"
}
Claude 4 系列(Opus 4.8、Sonnet 4.6、Haiku 4.5)全部支援圖片和 PDF 輸入。圖片的最大尺寸是 8,000 × 8,000 pixels,超過的會被自動縮放;單一圖片 block 上限 5 MB(base64 方式)。
手把手實戰
以下所有範例假設你已完成第 1 課的環境設定:.env 裡有 ANTHROPIC_API_KEY、已安裝 anthropic 和 python-dotenv。
Step 1:最小串流範例(Python)
建立 stream_basic.py:
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
print("Claude 說:", end=" ", flush=True)
with client.messages.stream(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": "用三個重點解釋什麼是 Streaming API,每點一句話。"
}]
) as stream:
for text in stream.text_stream: # text_stream 直接給你文字片段
print(text, end="", flush=True)
print() # 最後換行
# 取最終完整的 message 物件(stream 結束後才能拿)
final_msg = stream.get_final_message()
print(f"\n輸入 token: {final_msg.usage.input_tokens}")
print(f"輸出 token: {final_msg.usage.output_tokens}")
幾個重點:
client.messages.stream()用withcontext manager 包起來,結束後自動關閉連線stream.text_stream是一個 generator,每次迭代給一小段文字(可能只有一兩個字)print(text, end="", flush=True)的end=""不換行、flush=True立刻推到 terminal,不然 Python 的 output buffer 會讓你感覺不到串流效果stream.get_final_message()要在with區塊結束後才呼叫,拿到完整的Message物件
執行:
python stream_basic.py
你應該會看到文字逐字出現在 terminal,而不是等一下子跳出一大段。
Step 2:串流範例(JavaScript)
建立 stream_basic.mjs:
import Anthropic from "@anthropic-ai/sdk";
import "dotenv/config";
const client = new Anthropic();
process.stdout.write("Claude 說: ");
// 高階寫法:用 stream() + 事件監聽
const stream = client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 512,
messages: [{
role: "user",
content: "用三個重點解釋什麼是 Streaming API,每點一句話。"
}]
});
// 每有新文字片段就觸發
stream.on("text", (text) => {
process.stdout.write(text); // 不換行、不加 buffer
});
// 等 stream 全部完成,取得 final message
const finalMessage = await stream.finalMessage();
console.log("\n");
console.log(`輸入 token: ${finalMessage.usage.input_tokens}`);
console.log(`輸出 token: ${finalMessage.usage.output_tokens}`);
執行:
node stream_basic.mjs
JS SDK 的 .on("text", handler) 是事件驅動式;Python SDK 的 for text in stream.text_stream 是迭代式。兩種風格都是官方支援的寫法,效果相同。
Step 3:圖片輸入——base64 vs URL
圖片有兩種傳法,各有適合場景:
| 方式 | 適合 | 限制 |
|---|---|---|
| base64 | 本機圖片、私有圖片、不確定 URL 能否連到 | 每個 image block 上限 5 MB;要自己編碼 |
| URL | 公開圖片、CDN 上的圖片 | URL 必須公開可存取;Anthropic 伺服器會去抓圖 |
支援的圖片格式:JPEG、PNG、GIF、WebP。不支援 SVG、BMP 或 HEIC。

base64 方式:
import base64
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
# 讀取本機圖片並轉 base64
with open("receipt.jpg", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg", # 要和實際格式一致
"data": image_data,
}
},
{
"type": "text",
"text": "請列出這張收據的店名、日期和總金額。"
}
]
}]
)
print(response.content[0].text)
URL 方式:
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/receipt.jpg", # 必須公開可存取
}
},
{
"type": "text",
"text": "請列出這張收據的店名、日期和總金額。"
}
]
}]
)
print(response.content[0].text)
多張圖片同時傳就在 content 陣列裡加多個 image block,最多可以傳 100 張。不過 token 消耗會等比增加——每張圖大約消耗 1,000–4,000 token,視解析度而定(高解析度圖片會被自動縮到 1568px 以內再計算 token)。
Step 4:PDF 文件輸入
PDF 用 document 類型的 block,目前只支援 base64 方式(不支援 URL 直傳 PDF)。Claude 4 系列的所有模型都支援 PDF 輸入。
import base64
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
# 讀 PDF 並轉 base64
with open("contract.pdf", "rb") as f:
pdf_data = base64.standard_b64encode(f.read()).decode("utf-8")
# PDF 分析建議用 Sonnet,複雜文件用 Opus
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf", # 固定這個值
"data": pdf_data,
}
},
{
"type": "text",
"text": "請摘要這份文件的重點,條列式,不超過十點。"
}
]
}]
)
print(response.content[0].text)
**PDF 的 token 計算要注意:**每頁大約消耗 1,500–3,000 token(依內容密度而定),一份 50 頁的合約可能就要 75,000–150,000 token——這很快就會逼近 max_tokens 上限,或讓帳單超出預期。
長文件策略建議:
- 20 頁以內:直接整份丟進去,讓 Claude 自己讀
- 20–100 頁:先跑一次「目錄摘要」(只傳前幾頁或標題結構),再針對需要的章節拆段傳
- 100 頁以上:考慮用 Files API 上傳後取得 file_id,用 file_id 傳比每次重新 base64 更有效率;或結合第 7 課的 Prompt Caching(同一份文件重複問不同問題時,快取文件 token 可省 90% 費用)

PDF 上限:單一請求 32 MB;頁數上限依模型 context 而定——200K context 模型約 100 頁,1M context 的大模型最多 600 頁。超過就先拆分或改用 Files API。
Step 5:實戰小工具——收據辨識 + 串流輸出
把前面兩個技能組合起來:讀本機圖片(收據照片),傳給 Claude,用串流即時輸出辨識結果。
建立 receipt_ocr.py:
"""
收據辨識工具:接收圖片路徑,串流輸出結構化資訊
用法: python receipt_ocr.py <圖片路徑>
python receipt_ocr.py receipt.jpg
"""
import sys
import base64
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
# 副檔名 → MIME type 對照
MEDIA_TYPES = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"webp": "image/webp",
"gif": "image/gif",
}
def analyze_receipt(image_path: str) -> None:
# 判斷 media type
ext = image_path.rsplit(".", 1)[-1].lower()
media_type = MEDIA_TYPES.get(ext)
if not media_type:
print(f"不支援的格式:{ext}。請使用 jpg/png/webp/gif。")
sys.exit(1)
# 讀圖片並 base64 編碼
with open(image_path, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
print(f"正在分析 {image_path}...\n")
print("=" * 40)
# 串流呼叫
with client.messages.stream(
model="claude-haiku-4-5", # 收據辨識 Haiku 夠用,最省錢
max_tokens=512,
system=(
"你是一位收據分析助理。用繁體中文條列式回答,"
"格式整齊、數字精確。若圖片模糊或資訊不清楚,如實說明。"
),
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_data,
}
},
{
"type": "text",
"text": (
"請從這張收據中提取以下資訊:\n"
"1. 店名 / 商家名稱\n"
"2. 日期與時間\n"
"3. 品項與各自金額\n"
"4. 小計 / 稅額 / 總金額\n"
"5. 付款方式(如有顯示)\n\n"
"若某項資訊在圖片中找不到,寫「未顯示」即可。"
)
}
]
}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print("\n" + "=" * 40)
# 顯示用量
final = stream.get_final_message()
print(f"token 消耗:輸入 {final.usage.input_tokens} / 輸出 {final.usage.output_tokens}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python receipt_ocr.py <圖片路徑>")
sys.exit(1)
analyze_receipt(sys.argv[1])
執行範例:
python receipt_ocr.py receipt.jpg
預期輸出(以實際收據為準):
正在分析 receipt.jpg...
========================================
1. 店名:全家便利商店 FamilyMart 信義店
2. 日期與時間:2026/07/04 14:32
3. 品項與金額:
- 雞蛋沙拉三明治 $45
- 茶裏王無糖綠茶 600ml $25
- 關東煮(米血) $15
4. 小計:$85 / 稅額:內含 / 總金額:$85
5. 付款方式:悠遊卡
========================================
token 消耗:輸入 1823 / 輸出 147
這個小工具已經可以直接整合進費用報帳系統、記帳 App 或內部審核流程——只要把 print 換成寫進資料庫,或改成串流回傳 HTTP Response,就是一個可以上線的 API。
常見坑
坑 1:串流到一半就斷——APIConnectionError 或 stream closed
anthropic.APIConnectionError: Connection error.
最常見的情況是 max_tokens 設太小,Claude 話說到一半就被截斷,SSE 連線跟著收掉。症狀:串流輸出到某個地方突然停,或拋出連線錯誤。
檢查方式:看 final_msg.stop_reason,如果是 max_tokens 就代表確實截斷了——把 max_tokens 調大即可。另一個情況是網路問題(手機熱點、公司代理),串流比非串流更容易被中間設備截斷,這時候用非串流先確認基本呼叫能通,再切回串流。
坑 2:圖片傳進去但 Claude 說「看不到圖片」
出現這類回應:「對不起,我無法看到任何附件或圖片。」通常不是模型能力問題,而是 content 結構錯了。最常見的兩種錯誤:
# 錯誤寫法一:content 還是字串(不是陣列)
messages=[{"role": "user", "content": "請分析這張圖"}] # 根本沒傳圖
# 錯誤寫法二:陣列順序怪、type 拼錯
{"type": "Image", ...} # 大寫 I 是錯的,應該是小寫 "image"
{"type": "img", ...} # 也是錯的
正確結構:content 必須是陣列,圖片 block 的 type 必須是小寫的 "image",source.type 必須是 "base64" 或 "url" 其中之一。
另一個踩法:URL 方式傳的圖片 URL 需要公開可存取——你自己電腦上 localhost 的圖、需要登入才能看的 S3 圖、Notion 的附件圖,Anthropic 伺服器都連不到。這種情況用 base64 方式。
坑 3:PDF 傳進去但回應品質差,或說「無法讀取文件」
PDF 辨識依賴 Claude 的視覺能力,因此掃描版 PDF(整頁是圖片的 PDF)和文字版 PDF 效果差很多:
- 文字版 PDF(可以在 PDF 裡選取文字的那種):Claude 能直接讀取文字內容,準確率高
- 掃描版 PDF(整頁是圖片):Claude 用 OCR 模式處理,準確率取決於掃描品質,模糊或角度偏斜的頁面可能認錯
如果你的 PDF 是掃描版且品質不好:先用 pdfplumber 或 PyMuPDF 把頁面轉成高解析度 PNG,再用圖片 block 傳入,效果通常比直接傳 PDF 好。
import fitz # PyMuPDF
import base64
doc = fitz.open("scanned.pdf")
for page_num in range(len(doc)):
page = doc[page_num]
mat = fitz.Matrix(2.0, 2.0) # 2x 解析度
pix = page.get_pixmap(matrix=mat)
img_bytes = pix.tobytes("png")
page_b64 = base64.standard_b64encode(img_bytes).decode("utf-8")
# 接著把 page_b64 用 image block 傳入
坑 4:stream.get_final_message() 拋出錯誤
RuntimeError: stream is not done
get_final_message() 必須在串流全部跑完後才能呼叫。如果你在 with 區塊內部、還在迭代 text_stream 的時候就試圖呼叫它,就會拋這個錯誤。把它移到 with 區塊外面:
# 錯誤:在 with 區塊內
with client.messages.stream(...) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # 錯!串流還沒結束
# 正確:在 with 區塊外
with client.messages.stream(...) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# with 結束後,stream 已完成
final = stream.get_final_message() # 正確
作業
- 跑通
stream_basic.py,確認文字是逐字出現(不是一次跳出),並印出 token 用量 - 找一張你手邊的收據照片(或用截圖也行),跑
receipt_ocr.py,看辨識準不準 - 進階:把
receipt_ocr.py改成同時接受多張圖片路徑(用sys.argv[1:]),對每張圖都跑一次串流分析。提示:在迴圈裡每次建立新的stream即可 - 嘗試傳一份你有的 PDF 文件,觀察 token 消耗量和辨識品質
下一課預告
這堂課你學到了讓 API 更好用的兩個技能——串流和多模態。但有一個問題你可能已經發現:每次呼叫都要把完整的文件或 System Prompt 重新傳一次,token 費用不斷疊加。**第 7 課「Prompt Caching 與 Batch:成本砍半」**就是來解這個問題的。Prompt Caching 讓你快取重複的 System Prompt 和文件,後續呼叫的快取部分只收 10% 費用;Batch API 則讓你把大量非同步任務打折 50% 跑完。兩個技術搭配使用,高用量場景的成本可以壓到原來的 1/4 以下。