google.api_core.exceptions.InvalidArgument: 400 Please ensure that multiturn requests alternate between user and model. ── Gemini API でマルチターン会話を組み立てている方なら、一度はこのエラーで足止めされた経験があるのではないでしょうか。私も自前のチャットボットを Vertex AI へ移行する作業で、まさにこのメッセージと半日格闘しました。
ややこしいのは、エラー文だけ見ると「会話が交互になっていない」という意味に読めるのに、実際には別の原因(role の表記ゆれ、Function Calling のレスポンス構造、system_instruction の混入など)で同じ例外が投げられる点です。ここでは私がプロダクトのリリース前に踏んだ落とし穴を中心に、原因別の見分け方と修正パターンを整理しました。
エラーが起きる仕組み — Gemini が要求する 2 つのルール
Gemini API の generateContent エンドポイントは、contents 配列に対して 2 つの厳格なルールを課しています。1 つは「role は user と model の 2 種類のみで、必ず交互に並ぶ」こと。もう 1 つは「最後のターンは必ず user で終わる」ことです。OpenAI 系 API のように assistant や system を contents の中に置くと、Gemini はそれを許容せず即座に 400 を返します。
最初に、最小構成で正しい形を見ておきます。
# 動く最小例(Python / google-genai SDK)
from google import genai
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
contents = [
{"role": "user", "parts": [{"text": "東京の天気は?"}]},
{"role": "model", "parts": [{"text": "東京は晴れです。"}]},
{"role": "user", "parts": [{"text": "明日は?"}]}, # 末尾は必ず user
]
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=contents,
)
print(response.text)この形を「正解」として頭に置いておくと、以降のケース別エラーが読み解きやすくなります。
ケース1: 同じ role が連続している(最頻出パターン)
実装の現場で一番多いのは、ユーザーの発話を受け取る前に履歴へ先回りで model のメッセージを足してしまうケースです。たとえば「初期挨拶」を model ロールで埋め込み、続けて UI から来た最初のユーザー発話を user で push する設計にすると、その後 model の応答が返ってきたときに意図せず user → model → user → model → model の並びになってしまうことがあります。
# NG: 同じ role が連続
contents = [
{"role": "model", "parts": [{"text": "こんにちは。何をお手伝いしましょう?"}]}, # 先頭が model
{"role": "user", "parts": [{"text": "こんにちは"}]},
{"role": "model", "parts": [{"text": "はい"}]},
{"role": "model", "parts": [{"text": "どうされましたか?"}]}, # 連続して model
]修正の方針は単純で、「初期挨拶を system_instruction に逃がす」「連続するターンは 1 つに統合する」のいずれかです。私のプロジェクトでは前者のほうが管理コストが低くおすすめです。
# OK: system_instruction で挨拶を表現し、contents は user 始まり
config = genai.types.GenerateContentConfig(
system_instruction="あなたは親しみやすいアシスタントです。最初の応答で挨拶してください。"
)
contents = [{"role": "user", "parts": [{"text": "こんにちは"}]}]
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=contents,
config=config,
)ケース2: assistant を渡している(OpenAI からの移植で頻出)
OpenAI Chat Completions の感覚で role: "assistant" のまま contents を組むと、Gemini はそれを未知のロールとして弾きます。エラーメッセージは「alternate」と表示されることがあり、原因が掴みにくいのが厄介です。
# NG: OpenAI 互換の role 名
contents = [
{"role": "user", "parts": [{"text": "Hi"}]},
{"role": "assistant", "parts": [{"text": "Hello!"}]}, # ← Gemini では model
]移植する場合は、メッセージ整形の段階で role を写像する小さな関数を 1 つ用意しておくと、以降の混入事故を防げます。
ROLE_MAP = {"user": "user", "assistant": "model", "model": "model"}
def to_gemini_contents(messages):
out = []
for m in messages:
role = ROLE_MAP.get(m["role"])
if role is None:
continue # system は別扱い
out.append({"role": role, "parts": [{"text": m["content"]}]})
return outケース3: Function Calling の応答を間違った role で詰めている
Function Calling を使い始めて最初にぶつかるのが、ツール実行結果の格納方法です。Gemini では「ツールから返ってきた結果」も role: "user" 側に乗せ、functionResponse という part を入れる仕様になっています。role: "function" や role: "tool" を使うと、その時点で例外が出ます。
# OK: functionResponse は user ロールに置く
from google.genai import types
contents = [
{"role": "user", "parts": [{"text": "東京の天気を取得して"}]},
{"role": "model", "parts": [
types.Part(function_call=types.FunctionCall(name="get_weather", args={"city": "Tokyo"}))
]},
{"role": "user", "parts": [
types.Part(function_response=types.FunctionResponse(
name="get_weather",
response={"weather": "sunny", "temp": 22},
))
]},
]「ツール呼び出しなのに user 扱い?」と直感に反するため、ここはつまずきやすい仕様です。Function Calling 全般についてはGemini API で Function Calling が動かないときの調査手順も参考になります。
ケース4: system_instruction を contents に混ぜている
OpenAI 系の習慣で role: "system" を contents の先頭に入れる実装がそのまま残っているケースです。Gemini の場合、システム指示は GenerateContentConfig.system_instruction(または旧 SDK の system_instruction 引数)で渡すのが正解で、contents に入れると「user 始まりではない」と判定されます。
# NG
contents = [
{"role": "system", "parts": [{"text": "あなたは丁寧なサポート担当です"}]}, # ← ここが原因
{"role": "user", "parts": [{"text": "返金したいです"}]},
]
# OK
config = genai.types.GenerateContentConfig(
system_instruction="あなたは丁寧なサポート担当です"
)
contents = [{"role": "user", "parts": [{"text": "返金したいです"}]}]履歴を壊さないためのバリデータを 1 つ持っておく
長期運用で一番効くのは、API へ投げる直前に履歴を検査するバリデータです。私はどのプロジェクトでも下のような 20 行ほどの関数を用意し、テストケースとして「連続 user」「連続 model」「末尾 model」「assistant 混入」をカバーしています。
def validate_contents(contents):
if not contents:
raise ValueError("contents が空です")
if contents[0]["role"] != "user":
raise ValueError("先頭は user である必要があります")
if contents[-1]["role"] != "user":
raise ValueError("末尾は user で終わる必要があります")
for i in range(len(contents) - 1):
if contents[i]["role"] == contents[i + 1]["role"]:
raise ValueError(f"role が連続: index {i} と {i+1} がともに {contents[i]['role']}")
for c in contents:
if c["role"] not in ("user", "model"):
raise ValueError(f"未知の role: {c['role']}")
return TrueCI でこのバリデータを通しておけば、SDK バージョンを上げたときの仕様変更や、コントリビュータの誤実装にも気付きやすくなります。会話履歴の保存設計そのものに不安がある方は、Gemini API のチャット履歴管理エラーまとめも合わせてご覧ください。
本書の「対話モデルの履歴設計」の章は、Gemini に限らず役割設計の基礎として読み応えがあります。
次にすること
このエラーは、慣れてしまえば 1 分で原因を特定できる種類のものです。今日からできる一歩としては、お使いのプロジェクトに先ほどの validate_contents を組み込み、ユニットテストとして「連続ロール」「末尾 model」のケースを 1 件ずつ追加してみてください。エラーの再発防止が驚くほど楽になります。Function Calling を本格的に運用しているなら、functionResponse を含むサンプルもテストに加えておくと盤石です。