CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-04-30中級

Claude API の『messages.X.role must alternate』を1分で直す — 400 invalid_request_error の典型パターン

Claude API で頻発する『messages.X.role must alternate』エラーの原因と直し方を、user/assistant の交互ルール・tool_use と tool_result の対応関係・履歴管理の落とし穴に分けて、実例コード付きで整理します。

troubleshooting61api38messages-apierror-handling10

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 には、人間同士の会話を真似た最低限の文法があります。それは「userassistant が必ず1ターンずつ交互に並ぶ」というルールです。OpenAI の system メッセージのように途中に挟むことは想定されておらず、system プロンプトは messages 配列の外(トップレベルの system パラメータ)に置く設計になっています。

# ✅ 正しい構造
messages = [
    {"role": "user", "content": "こんにちは"},
    {"role": "assistant", "content": "こんにちは、お手伝いします"},
    {"role": "user", "content": "天気を教えて"},
]

このシンプルなルールが守れていない場合、API は配列のどの位置で違反したかをエラーメッセージ内の messages.XX(インデックス)で教えてくれます。たとえば 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 の idtool_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_useid と完全一致していること」です。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_usetool_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 のようなクラスに分離して、appendtrimvalidate の責務を1か所に集めること。生の list を直接いじる箇所をなくすだけで、role 連続のような事故は激減します。

第二に、tool 関連のテストには「最低1往復の tool_use / tool_result が必ず含まれるサンプル会話」を fixture として持たせること。Function Calling のリファクタで壊れる箇所はだいたいここなので、CIで回しておけば未然にキャッチできます。

第三に、上で示した validate_messagesclient.messages.create の前に必ず通すこと。30行のコストで本番障害を1件防げれば、それだけで割に合います。

エラー対応を「いつもの作業」にしておけると、Claude API の能力をもっと攻めた使い方に振り向けられます。Messages API の本来の魅力は柔軟性の高さですので、文法を踏み外しにくい土台を整えたうえで、自分のプロダクトに合った会話設計を楽しんでいきたいですね。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-05-16
Claude API の Tool Use でスキーマ定義ミスが出たとき:実際に遭遇した3つのパターンと診断手順
Claude API の Tool Use でよく起きるスキーマ定義ミス・invalid_tool_use・Claude がツールを呼ばない問題を、実際のアプリ開発で遭遇したケースをもとに診断手順と修正パターンで解説します。
API & SDK2026-05-22
Claude の『このモデルは現在利用できません』が出る時に効くモデル選択フォールバックの設計
Claude API で時折出る『このモデルは現在利用できません(this model is currently unavailable)』は、529 Overloaded ともレート制限とも違う性質のエラーです。個人開発で運営している6サイトの自動投稿パイプラインで実際に観測した発生条件と、リクエスト単位で動的に切り替えるフォールバック実装をまとめました。
API & SDK2026-05-04
Claude API の stop_sequences が効かないときに確認すべき5つのポイント
Claude API の stop_sequences パラメータが期待通りに止まらない問題の原因を、トークン境界・空白・Tool Use・ストリーミングの観点から実例コード付きで切り分ける手順をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →