Messages API 深度解析:對話的基本單位
本篇是「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 應該:
- 明確說明 Claude 是誰,而不是「你是一個 AI 助理」這種廢話
- 清楚界定能做什麼、不能做什麼(比只說「能做什麼」更重要)
- 定義輸出格式:如果你要解析 Claude 的回應,請在 system prompt 裡明確說
- 提供必要的背景知識:你的產品是什麼、使用者是誰、常見的問題類型
一個我常犯的錯誤是 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「即時打字」的體驗,把感知等待時間從幾秒降到幾乎為零。