精華筆記

· @aihub.tw

Claude API 開發實戰

Streaming 與多模態輸入

Streaming 與多模態輸入

你用非串流方式呼叫 Claude API,等了十秒才看到完整回應——對程式腳本這沒什麼,但如果你在做一個給人用的介面,使用者盯著轉圈圈等十秒鐘,離開的機率遠比你想的高。解法很直接:Streaming。讓 Claude 產生一個字就推一個字出來,使用者從第一個字就看到畫面在動,等待感直接消失。

同樣讓 API 身價提升一個等級的,是多模態輸入。第 1 到 5 課的 messages 全都是文字,但現實世界的資料很少是純文字:收據是照片、合約是 PDF、截圖是圖片。這堂課讓你把這些東西直接塞進 content 陣列,Claude 就能看到它們、分析它們,不需要你先寫 OCR 轉文字。

這堂課適合誰 適合:已經能用 Claude API 送文字訊息、想讓回應更即時或要處理圖片/文件資料的開發者(本課程屬工程師專區)。需要基礎:Python 或 JavaScript 基礎、會讀 async/await。前置課:第 1 課(API 呼叫)+ 第 3 課(系統 prompt 與多輪對話)。

這堂學什麼

  • 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 串流的時序對比:

非串流 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"}

SSE 串流資料流圖

如果你用 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
]}

多模態 content 陣列結構圖

陣列裡可以同時有多張圖、多份文件、多段文字,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、已安裝 anthropicpython-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()with context 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 與 URL 兩種傳法對照圖

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 長文件策略決策圖

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:串流到一半就斷——APIConnectionErrorstream 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 是掃描版且品質不好:先用 pdfplumberPyMuPDF 把頁面轉成高解析度 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()  # 正確

作業

  1. 跑通 stream_basic.py,確認文字是逐字出現(不是一次跳出),並印出 token 用量
  2. 找一張你手邊的收據照片(或用截圖也行),跑 receipt_ocr.py,看辨識準不準
  3. 進階:把 receipt_ocr.py 改成同時接受多張圖片路徑(用 sys.argv[1:]),對每張圖都跑一次串流分析。提示:在迴圈裡每次建立新的 stream 即可
  4. 嘗試傳一份你有的 PDF 文件,觀察 token 消耗量和辨識品質

下一課預告

這堂課你學到了讓 API 更好用的兩個技能——串流和多模態。但有一個問題你可能已經發現:每次呼叫都要把完整的文件或 System Prompt 重新傳一次,token 費用不斷疊加。**第 7 課「Prompt Caching 與 Batch:成本砍半」**就是來解這個問題的。Prompt Caching 讓你快取重複的 System Prompt 和文件,後續呼叫的快取部分只收 10% 費用;Batch API 則讓你把大量非同步任務打折 50% 跑完。兩個技術搭配使用,高用量場景的成本可以壓到原來的 1/4 以下。

#Claude API#Streaming#SSE#多模態#圖片辨識#PDF#Python#JavaScript

← 回所有文章