跳至主要內容
技術

Messages API 深度解析:對話的基本單位

Messages API 深度解析:對話的基本單位
Claude API & Agent SDK 完全指南 第 2 / 15 篇

本篇是「Claude API & Agent SDK 完全指南」系列的第 2 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。

你已經發出了第一個 API 呼叫。

但那只是「打個招呼」——傳一句話,收一句回答,連線結束。真正的 AI 應用比這複雜得多:使用者跟 Claude 來回對話好幾輪;你需要給 Claude 一個「角色設定」;你需要控制回應的風格和長度。

這一章,我們來把 Messages API 徹底搞清楚。

對話即陣列:Messages API 的核心設計哲學

Messages API 的設計非常直白:對話是一個 messages 陣列,每個元素代表一輪對話

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 1024,
  "messages": [
    { "role": "user", "content": "台北有什麼好吃的?" },
    { "role": "assistant", "content": "台北的美食非常豐富..." },
    { "role": "user", "content": "你剛說的那個,有推薦的店嗎?" }
  ]
}

注意這裡最關鍵的設計決策:Claude API 是無狀態的(stateless)

每次你呼叫 API,你需要把完整的對話歷史傳過去。API 不會幫你「記住」之前的對話。這跟 Claude.ai 的使用體驗不同——Claude.ai 的 Projects 功能會幫你保存記憶,但那是前端應用自己做的,底層的 API 每次都是全新的。

這個設計的優點是簡單、可預測,而且讓你完全掌控對話狀態。缺點是你需要自己管理對話歷史,而且隨著對話越來越長,每次呼叫的成本也越來越高(因為 input tokens 包含了所有歷史)。

三種 Roles:system、user、assistant

Messages API 有三種 role,每種有不同的用途和限制。

system(系統指示)

system 不是 messages 陣列的一部分,而是獨立的頂層參數。它用來給 Claude 設定「背景」:角色、能力範圍、回應格式、行為準則。

client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    system="""你是一位專業的 TypeScript 程式碼審查員。

你的職責:
- 找出潛在的 bug 和型別錯誤
- 指出效能問題
- 建議更符合 TypeScript 慣例的寫法

你不應該:
- 重寫整段程式碼(除非被要求)
- 討論與程式碼無關的話題

回應格式:
1. 問題摘要(條列式)
2. 具體建議(帶程式碼範例)
3. 嚴重程度評估(高/中/低)""",
    messages=[
        {"role": "user", "content": "請幫我審查這段程式碼:\n```typescript\n...\n```"}
    ]
)

我有一個強烈的觀點:system prompt 是你的 AI 應用最重要的工程工件,值得你花很多時間打磨它。

一個好的 system prompt 應該:

  1. 明確說明 Claude 是誰,而不是「你是一個 AI 助理」這種廢話
  2. 清楚界定能做什麼、不能做什麼(比只說「能做什麼」更重要)
  3. 定義輸出格式:如果你要解析 Claude 的回應,請在 system prompt 裡明確說
  4. 提供必要的背景知識:你的產品是什麼、使用者是誰、常見的問題類型

一個我常犯的錯誤是 system prompt 寫太短。「你是一個客服機器人,回答用戶問題。」這種 prompt 在開發階段看起來 work,但在生產環境會有各種奇怪的邊際情況。

user(使用者輸入)

user role 代表你的使用者(或你的應用)發出的訊息。messages 陣列必須從 user 開始,而且 user 和 assistant 要交替出現。

messages=[
    {"role": "user", "content": "第一個問題"},
    {"role": "assistant", "content": "第一個回答"},
    {"role": "user", "content": "第二個問題"},
    # 下一個一定要是 assistant,然後才能再 user
]

content 可以是字串,也可以是陣列(用於多媒體訊息,例如上傳圖片)。目前我們先處理純文字的情況:

# 簡單字串
{"role": "user", "content": "你好"}

# 或者明確的 content array(兩種寫法等效)
{"role": "user", "content": [{"type": "text", "text": "你好"}]}

assistant(模型回應)

assistant role 代表 Claude 的回應。在多輪對話中,你會把 Claude 之前的回應加入 messages 陣列,讓它在下一輪有上下文。

⚠️ 注意(2026 更新):assistant prefill(在 messages 結尾放一個 assistant 訊息讓 Claude 接續)在 Claude 4.6 之後的模型(Opus 4.6/4.7/4.8、Sonnet 4.6、Fable 5)已不支援,會回傳 400 錯誤。要強制結構化輸出,請改用 output_config.format(structured outputs)或用 system prompt 指示格式。以下 Prefilling 技巧僅適用於舊版模型(3.x):

有一個進階技巧叫做 Prefilling:你可以在最後一個 messages 元素放一個 assistant role(只包含部分文字),讓 Claude 從那個地方繼續往下說:

messages=[
    {"role": "user", "content": "請用 JSON 格式回傳一個使用者物件"},
    {"role": "assistant", "content": "{"}  # Prefilling:強制 Claude 從 { 開始
]

這個技巧對於強制結構化輸出很有用,但要小心:如果你 prefill 了一個 {,Claude 幾乎一定會繼續輸出 JSON,但不保證它一定是合法的 JSON。

關鍵參數詳解

max_tokens(必填)

max_tokens=1024  # 最多回傳 1024 tokens

max_tokens 是必填的,而且它決定了這次呼叫可能產生的最大 output tokens 數

幾個常見的設定策略:

  • 聊天機器人:512-2048,視你的應用允許多長的回答
  • 文件摘要:2048-4096
  • 程式碼生成:2048-8192(程式碼可能很長)
  • 分析報告:4096+

注意:max_tokens 不是「我希望它說這麼長」,而是「最多不要超過這麼長」。Claude 可能更早結束(stop_reason: "end_turn")。

如果 Claude 的回應因為達到 max_tokens 而被截斷,stop_reason 會是 "max_tokens" 而不是 "end_turn"。生產環境一定要處理這種情況。

temperature(創意度控制)

temperature=0.7  # 範例值(API 預設為 1.0),範圍 0.0 到 1.0

temperature 控制模型回應的「隨機性」:

  • 0.0:非常確定性,每次相同的輸入幾乎會得到相同的輸出。適合需要一致性的任務(程式碼生成、資料提取、分類)
  • 0.7:適度的多樣性,適合大多數對話場景
  • 1.0(API 預設):最大隨機性,適合創意寫作、頭腦風暴

我的個人規則:API 預設是 1.0(偏隨機)。多數對話場景我會設成 0.7 左右;需要確定性輸出時設 0.0 或 0.1。

top_p 和 top_k

top_p=0.9   # nucleus sampling
top_k=50    # top-k sampling

這兩個參數跟 temperature 一樣是控制「採樣策略」的。在實際使用時,Anthropic 建議你只調整 temperature 或 top_p 其中一個,不要同時調整兩個。

坦白說,在大多數應用場景,你根本不需要動這兩個參數。只有在你有非常特殊的需求,而且你理解採樣策略的數學原理時,才值得去調整。

stop_sequences

stop_sequences=["</answer>", "Human:", "---"]

stop_sequences 讓你定義「遇到這些字串就停止生成」。這在結構化輸出時很有用:

# 讓 Claude 生成 XML 風格的分析,但遇到 </analysis> 就停止
client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    system="請將你的分析包在 <analysis> 和 </analysis> 之間。",
    messages=[{"role": "user", "content": "分析這篇文章..."}],
    stop_sequences=["</analysis>"]
)

另一個常用場景是在 few-shot prompting 時,避免 Claude 繼續生成下一個「示例」:

stop_sequences=["\nHuman:", "\nAssistant:"]

多輪對話管理

這是大多數 AI 應用最重要的工程問題之一。

基本的多輪對話實作

import anthropic

client = anthropic.Anthropic()
conversation_history = []

def chat(user_message: str) -> str:
    # 把使用者訊息加入歷史
    conversation_history.append({
        "role": "user",
        "content": user_message
    })

    # 呼叫 API(傳入完整歷史)
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        system="你是一位友善的繁體中文助理。",
        messages=conversation_history
    )

    # 取出回應文字
    assistant_message = response.content[0].text

    # 把 Claude 的回應也加入歷史
    conversation_history.append({
        "role": "assistant",
        "content": assistant_message
    })

    return assistant_message

# 使用範例
print(chat("我叫小明,是一個 Python 開發者"))
print(chat("你知道我叫什麼名字嗎?"))  # Claude 會記住「小明」
print(chat("我剛說我是做什麼的?"))     # Claude 會記住「Python 開發者」

Context Window 管理:截斷策略

隨著對話進行,conversation_history 越來越長,成本也越來越高,最終可能超過 context window 上限(200K tokens)。你需要截斷策略。

策略一:固定視窗(最簡單)

MAX_HISTORY_TURNS = 20  # 保留最近 20 輪

def chat(user_message: str) -> str:
    conversation_history.append({"role": "user", "content": user_message})

    # 只取最近 N 輪,但確保從 user 開始
    recent_history = conversation_history[-MAX_HISTORY_TURNS * 2:]
    # 確保第一個是 user(API 要求)
    while recent_history and recent_history[0]["role"] != "user":
        recent_history = recent_history[1:]

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        messages=recent_history
    )

    assistant_message = response.content[0].text
    conversation_history.append({"role": "assistant", "content": assistant_message})

    return assistant_message

策略二:Token 預算

import anthropic

def count_tokens_estimate(messages: list) -> int:
    """粗略估算 token 數:每個字符約 1.5 tokens"""
    total_chars = sum(len(str(m.get("content", ""))) for m in messages)
    return int(total_chars * 1.5)

MAX_INPUT_TOKENS = 150_000  # 留 50K 給輸出

def trim_history(history: list) -> list:
    while count_tokens_estimate(history) > MAX_INPUT_TOKENS and len(history) > 2:
        # 刪掉最舊的一輪(user + assistant 各一條)
        history = history[2:]
        # 確保從 user 開始
        while history and history[0]["role"] != "user":
            history = history[1:]
    return history

策略三:摘要壓縮(最聰明但最複雜)

對於真正的長對話,你可以讓 Claude 定期把舊的對話內容摘要成一段文字,然後把那段摘要放在 system prompt 裡,同時清空 messages 歷史:

async def compress_conversation(history: list) -> str:
    """把一段對話歷史壓縮成摘要"""
    summary_request = client.messages.create(
        model="claude-haiku-4-5",  # 用便宜的模型做摘要
        max_tokens=1024,
        system="請將以下對話摘要成一段簡潔的重點,保留重要資訊。",
        messages=[{
            "role": "user",
            "content": f"對話歷史:\n{format_history(history)}"
        }]
    )
    return summary_request.content[0].text

回應物件的完整結構

理解 API 回應物件的結構很重要,特別是你需要處理各種邊際情況時。

response = client.messages.create(...)

# 基本屬性
response.id            # 唯一的請求 ID,例如 "msg_01XFDUDYJgAACzvnptvVoYEL"
response.type          # 永遠是 "message"
response.role          # 永遠是 "assistant"
response.model         # 實際使用的模型,例如 "claude-sonnet-4-6-20251101"

# 回應內容
response.content       # List[ContentBlock]
response.content[0].type  # "text" 或 "tool_use"
response.content[0].text  # 如果 type == "text"

# 停止原因(非常重要)
response.stop_reason   # "end_turn" | "max_tokens" | "stop_sequence" | "tool_use"

# Token 使用量(付費依據)
response.usage.input_tokens   # 這次請求消耗的 input tokens
response.usage.output_tokens  # 這次請求產生的 output tokens

# 如果有啟用 Prompt Caching
response.usage.cache_creation_input_tokens  # 新建快取消耗的 tokens
response.usage.cache_read_input_tokens      # 從快取讀取的 tokens(便宜很多)

一定要檢查 stop_reason。如果是 "max_tokens",代表回應被截斷了——你的使用者會看到一個不完整的回答。常見的處理方式:

if response.stop_reason == "max_tokens":
    # 方案一:繼續生成(continuation)
    # 方案二:通知使用者回答被截斷
    # 方案三:增大 max_tokens 重試
    raise ValueError("Response was truncated. Consider increasing max_tokens.")

錯誤處理

生產環境一定會遇到這些錯誤,提前準備好:

import anthropic
import time
from anthropic import APIStatusError, APIConnectionError, RateLimitError

def call_with_retry(client, max_retries=3, **kwargs):
    for attempt in range(max_retries):
        try:
            return client.messages.create(**kwargs)

        except RateLimitError as e:
            if attempt == max_retries - 1:
                raise
            # 指數退避
            wait_time = (2 ** attempt) * 1 + 0.1
            print(f"Rate limited. Waiting {wait_time:.1f}s...")
            time.sleep(wait_time)

        except APIStatusError as e:
            if e.status_code == 401:
                raise ValueError("Invalid API key") from e
            elif e.status_code == 400:
                raise ValueError(f"Bad request: {e.message}") from e
            elif e.status_code >= 500:
                # 伺服器錯誤,可以重試
                if attempt == max_retries - 1:
                    raise
                time.sleep(2 ** attempt)
            else:
                raise

        except APIConnectionError:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)

常見的錯誤狀態碼:

  • 400 Bad Request:通常是你的 request 格式有問題,例如 messages 不是以 user 開始、max_tokens 超過模型上限
  • 401 Unauthorized:API Key 無效或已被 revoke
  • 403 Forbidden:帳號問題(可能欠費停用)
  • 429 Too Many Requests:超過 Rate Limit,需要等待
  • 500 Internal Server Error:Anthropic 伺服器問題,可以重試
  • 529 Overloaded:Anthropic 系統過載(高峰期可能發生),可以重試

System Prompt 設計的實戰心得

讓我分享幾個我在生產環境學到的 system prompt 技巧。

技巧一:先說「不要做什麼」

大多數人的 system prompt 都在說「請做 X、Y、Z」,但更有效的做法是同時說清楚「不要做 A、B、C」。

你是一個 XX 公司的客服助理。

你應該:
- 回答關於我們產品的問題
- 協助用戶排解常見問題
- 提供退換貨流程說明

你不應該:
- 透露公司的定價策略或利潤資訊
- 對還未正式宣布的功能做出承諾
- 在沒有確認身份的情況下修改用戶帳號設定
- 討論競爭對手的產品

技巧二:定義「不知道」的處理方式

如果你不確定某個問題的答案,請明確說「我不確定這個問題的答案,建議您聯繫我們的客服團隊(service@example.com)。」不要猜測或提供可能不準確的資訊。

技巧三:指定輸出格式(尤其是需要解析的場景)

每次回應,請使用以下格式:

<answer>
[你的主要回答]
</answer>

<confidence>
[high/medium/low]
</confidence>

<sources>
[如果有參考特定資訊,列出來源]
</sources>

技巧四:提供「人格」範例而不只是描述

你是 Aria,一個親切、有點幽默但很專業的 AI 助理。

你的說話風格範例:
- 「好問題!讓我想想...」(輕鬆但不浮誇)
- 「這個需要多說幾句,因為背後有個有趣的原因」
- 不用「當然!很高興為您服務!」這種過於熱情的開場

下一步

你現在對 Messages API 有了深入的了解:messages 的結構、三種 roles、各種參數的用途,以及如何在生產環境管理對話狀態。

但我們的範例有一個問題:你發出請求,然後等 Claude 把整個回應生成完再傳回來。對短回應來說還好,但如果 Claude 需要生成 2000 字的文章,使用者就要等好幾秒什麼都看不到。

下一章,我們來解決這個問題:Streaming。讓使用者看到 Claude「即時打字」的體驗,把感知等待時間從幾秒降到幾乎為零。

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →