Claude API を初めて触るとき、ほぼ全員が一度は遭遇するエラーがあります。それが 400 invalid_request_error の中でも特に多い「messages.X.role must alternate」というメッセージです。私自身、最初に Messages API を叩いたときに同じ場所で詰まりました。ドキュメントには「user と assistant が交互に並ぶ必要があります」とだけ書かれていて、具体的に何がどう違反しているのかが見えにくいのが厄介な点だと思います。
ここではこのエラーが出る代表的な3パターンを、実際に動くコードと「直したあとのコード」をセットで示しながら整理していきます。tool_use / tool_result が絡むケースまで含めて見ていきますので、Function Calling を実装している途中でつまずいた方にも役立つはずです。
このエラーが出る根本理由 — Claude API の「会話の文法」
Claude の Messages API には、人間同士の会話を真似た最低限の文法があります。それは「user と assistant が必ず1ターンずつ交互に並ぶ」というルールです。OpenAI の system メッセージのように途中に挟むことは想定されておらず、system プロンプトは messages 配列の外(トップレベルの system パラメータ)に置く設計になっています。
# ✅ 正しい構造
messages = [
{"role": "user", "content": "こんにちは"},
{"role": "assistant", "content": "こんにちは、お手伝いします"},
{"role": "user", "content": "天気を教えて"},
]このシンプルなルールが守れていない場合、API は配列のどの位置で違反したかをエラーメッセージ内の messages.X の X(インデックス)で教えてくれます。たとえば messages.2.role must alternate なら、配列の2番目(0始まり)以降で交互ルールが崩れているという意味です。
私の経験では、エラーが出た瞬間にまず X の位置を見て、その前後2件のメッセージだけを print する癖をつけておくと、原因の8割はその場で分かります。
パターン1: 同じ role が連続している
最も多いのが、user の発言を続けて2件追加してしまうケースです。会話履歴を自前で管理しているとき、ユーザーの追加質問を「直前のメッセージに append」せず、新しい配列要素として push してしまうと起こります。
# ❌ 失敗例: user が2連続
messages = [
{"role": "user", "content": "こんにちは"},
{"role": "user", "content": "天気を教えて"}, # ここで違反
]
# ✅ 修正例: 連続した user 発言は1つにまとめる
messages = [
{"role": "user", "content": "こんにちは\n\n天気を教えて"},
]履歴を蓄積する関数を書く際は、append する前に「直前のメッセージと role が同じか」を確認するガードを入れておくと再発を防げます。
def append_message(messages, role, content):
"""role が連続しないように吸収するヘルパー"""
if messages and messages[-1]["role"] == role:
# 同じ role なら content を結合する
last = messages[-1]
if isinstance(last["content"], str):
last["content"] = f"{last['content']}\n\n{content}"
else:
# content が list(tool_use 等)の場合は別の戦略を取る
last["content"].append({"type": "text", "text": content})
else:
messages.append({"role": role, "content": content})
return messagesこの小さなヘルパーを履歴管理層に挟むだけで、後述の tool_use 絡みのトラブル以外はほぼ消えます。
パターン2: tool_use と tool_result の対応関係が崩れている
Function Calling を実装し始めると、また別の壁に当たります。Claude が tool_use ブロックを返したら、次の user ターンで対応する tool_result を必ず返さなければならない、というルールです。これを忘れたり、片方だけ送ったりすると同じ「must alternate」系のエラーが返ります。
# ❌ 失敗例: tool_result を返さずに次の user 発言を挟んでいる
messages = [
{"role": "user", "content": "東京の天気を教えて"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_01ABC", "name": "get_weather", "input": {"city": "Tokyo"}}
]},
{"role": "user", "content": "ありがとう"}, # ← tool_result を飛ばしている
]この場合、Claude は「最後の assistant ターンで道具を呼んだのに、結果が返ってこないまま次の話題が来た」と解釈して拒否します。正しくは tool_use の id を tool_use_id として参照する tool_result ブロックを user ターンに含めます。
# ✅ 修正例: tool_use と対応する tool_result を返す
messages = [
{"role": "user", "content": "東京の天気を教えて"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_01ABC", "name": "get_weather", "input": {"city": "Tokyo"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01ABC", "content": "晴れ、24度"}
]},
{"role": "assistant", "content": "東京は今、晴れで24度ですね。"},
]ポイントは「tool_use_id の値が tool_use の id と完全一致していること」です。toolu_01ABC のような自動生成IDをユーザー側で書き換えてしまう実装ミスは意外と多く、私もデバッグで一度詰まりました。
複数の tool が並列で呼ばれた場合は、すべての tool_use に対して tool_result を1つの user ターンに含めて返します。一部だけ返すのも違反になりますので、ループで全件処理する設計にしておくと安全です。
パターン3: 履歴を切り詰めるときに tool_result を取り残す
長いセッションでコンテキストウィンドウを節約するために古いメッセージを削る、というのはよくある実装です。ところが単純に「古い順に N 件削除」を実装すると、tool_use だけ残って tool_result が消える(あるいはその逆)状況が発生します。
# ❌ 失敗例: ナイーブな先頭削除
def trim_history(messages, max_len=20):
return messages[-max_len:]このコードは多くの場合に動きますが、削除境界が tool_use と tool_result の間に来ると壊れます。tool 関連のペアは「assistant が tool_use を含むターン → user が tool_result を含むターン」という2ターン1セットで扱う必要があります。
# ✅ 修正例: tool ペアを尊重した切り詰め
def trim_history_safely(messages, max_len=20):
if len(messages) <= max_len:
return messages
# 末尾から max_len 件を取る候補を作成
start_idx = len(messages) - max_len
# 切断面が tool_use を含む assistant ターンの直後なら、もう1つ前から取る
while start_idx > 0:
msg = messages[start_idx]
if msg["role"] == "user" and isinstance(msg.get("content"), list):
has_tool_result = any(
isinstance(b, dict) and b.get("type") == "tool_result"
for b in msg["content"]
)
if has_tool_result:
# この user ターンは前の tool_use の結果なので、ペアごと取り直す
start_idx -= 1
continue
break
# 必ず user で開始するように調整
while start_idx < len(messages) and messages[start_idx]["role"] != "user":
start_idx += 1
return messages[start_idx:]履歴の先頭は常に user から始まる必要があります(assistant のメッセージから始まる配列は API 側で弾かれます)。切り詰めた後に最初のメッセージが assistant になっていないか必ずチェックしてください。
受信後に挟む小さなバリデーター
ここまで対症療法を見てきましたが、最も効くのは「API に投げる直前で配列を検証する」一手間です。たった30行ほどのバリデーターで、本番に出てから気づくタイプの不整合をほぼゼロにできます。
def validate_messages(messages):
"""Claude Messages API に送る前の最終チェック。
違反を見つけたら詳細なエラーを raise する。"""
if not messages:
raise ValueError("messages が空です")
if messages[0]["role"] != "user":
raise ValueError(f"最初のメッセージは user である必要があります(実際: {messages[0]['role']})")
# 1. 交互ルールのチェック
for i in range(1, len(messages)):
if messages[i]["role"] == messages[i - 1]["role"]:
raise ValueError(
f"messages[{i}] と messages[{i-1}] の role が同じです: '{messages[i]['role']}'"
)
# 2. tool_use と tool_result の対応チェック
for i, msg in enumerate(messages):
if msg["role"] == "assistant" and isinstance(msg.get("content"), list):
tool_use_ids = {
b["id"] for b in msg["content"]
if isinstance(b, dict) and b.get("type") == "tool_use"
}
if not tool_use_ids:
continue
# 直後の user ターンを確認
if i + 1 >= len(messages) or messages[i + 1]["role"] != "user":
raise ValueError(f"messages[{i}] の tool_use に対応する user ターンがありません")
next_content = messages[i + 1].get("content")
if not isinstance(next_content, list):
raise ValueError(f"messages[{i+1}] は tool_result を含む list である必要があります")
result_ids = {
b["tool_use_id"] for b in next_content
if isinstance(b, dict) and b.get("type") == "tool_result"
}
missing = tool_use_ids - result_ids
if missing:
raise ValueError(f"messages[{i}] の tool_use_id {missing} に対応する tool_result が不足しています")
return True
# 使い方
try:
validate_messages(messages)
response = client.messages.create(model="claude-sonnet-4-6", messages=messages, max_tokens=1024)
except ValueError as e:
print(f"[配列の不整合] {e}")このバリデーターは Claude SDK の呼び出し前に挟むだけで、本番環境の不可解な 400 エラーを「ローカルで再現可能な ValueError」に格上げしてくれます。私はこれを書いて以降、Messages API 起因の障害は1度も起こしていません。
会話履歴の管理に関連する話題として、Claude API 認証エラー(401 invalid_key)の対処法 や Claude API の context_window_exceeded エラーの直し方 もあわせて読んでおくと、本番運用で起こりがちな別系統のエラーへの抵抗力が一段上がります。
次の一手 — 今日からできる予防策
エラーが直ったら、再発させない仕組みづくりに10分だけ投資してみてください。具体的には次の3つです。
第一に、メッセージ履歴を扱う層を MessageBuffer のようなクラスに分離して、append・trim・validate の責務を1か所に集めること。生の list を直接いじる箇所をなくすだけで、role 連続のような事故は激減します。
第二に、tool 関連のテストには「最低1往復の tool_use / tool_result が必ず含まれるサンプル会話」を fixture として持たせること。Function Calling のリファクタで壊れる箇所はだいたいここなので、CIで回しておけば未然にキャッチできます。
第三に、上で示した validate_messages を client.messages.create の前に必ず通すこと。30行のコストで本番障害を1件防げれば、それだけで割に合います。
エラー対応を「いつもの作業」にしておけると、Claude API の能力をもっと攻めた使い方に振り向けられます。Messages API の本来の魅力は柔軟性の高さですので、文法を踏み外しにくい土台を整えたうえで、自分のプロダクトに合った会話設計を楽しんでいきたいですね。