AIAgent

DeepSeek V4 第二輪400のログ調査

DeepSeek V4 第二輪400のログ調査

1回目のツール呼び出しは成功したのに、ツール結果を返す2回目のリクエストだけが400になります。

最短の解決策は、再試行や拡張ではなく、同じ会話を各処理段階で比較することです。 DeepSeek公式APIでは、思考モードでツール呼び出しを行ったassistantメッセージに reasoning_content を含めて再送する必要があります。一方、現在のvLLM文書では主フィールドが reasoning です。混在環境では、送信先ごとの変換をアダプター層に固定します。(DeepSeek公式の思考モード仕様)

このログ調査が必要な開発者

対象は、多輪のfunction callingを実装し、最初のtool callは通るのに次の呼び出しでDeepSeek V4 第二輪400が発生する開発者です。

APIゲートウェイ、会話データベース、SDKラッパーを管理し、未知フィールドの除去を疑っているバックエンド担当者にも適しています。DeepSeek公式APIとvLLMを切り替える推論基盤では、特にフィールド名の差を分けて記録する必要があります。

最終更新:2026年8月15日。DeepSeek公式の思考モード・ツール呼び出し仕様と、vLLMの現行reasoning出力文書を再確認しています。今後、既定フィールド名や互換方針が変更された場合は再検証が必要です。

1回目成功・2回目失敗なら、まず履歴再送を疑う

この症状では、ツールのJSONスキーマや関数名を最初から疑う必要はありません。1回目のassistant応答がtool callまで生成され、ツール実行も完了しているなら、少なくとも最初のモデル出力とツール定義は、そのリクエストでは受理されています。

優先して確認するのは、次のリクエストに追加された3種類のメッセージです。

  • role: assistant の元応答
  • role: tool の実行結果
  • その後に送信されたmessages全体

DeepSeek公式文書では、思考モードでツール呼び出しを実行したassistantターンの reasoning_content は、後続リクエストへ完全に渡す必要があると説明されています。欠落時には400が返る可能性があります。(DeepSeek公式の思考モード仕様)

最初に保存するログは、全文ではなく次のような脱敏情報で十分です。

{
  "conversation_id": "masked",
  "message_index": 2,
  "role": "assistant",
  "keys": ["content", "reasoning_content", "tool_calls"],
  "content_length": 0,
  "reasoning_length": 128,
  "tool_call_count": 1,
  "tool_call_ids": ["masked"]
}

推論本文、APIキー、実際のツール結果は記録しません。キー名、型、文字数、配列数、IDのハッシュだけを残すと、情報漏えいを避けながら差分を追跡できます。

フィールドが消えた場合と、名前が違う場合を分ける

完全に存在しない場合

最終リクエストのassistantメッセージに推論フィールド自体がない場合、次の実装が候補になります。

  • SDK応答から contenttool_calls だけを保存している
  • Pydanticや独自DTOが未定義キーを除去している
  • messages再構築時に許可キーを固定している
  • null や空文字を共通サニタイズ処理で削除している
  • ストリーミング断片の結合対象に推論部分を含めていない

DeepSeek公式APIのChat Completion仕様では、assistantメッセージに contentreasoning_contenttool_calls が別フィールドとして定義されています。content が空でも、推論やtool callが存在する応答はあります。(DeepSeek公式Chat Completion API)

そのため、次のような保存処理は危険です。

saved = {
    "role": message.role,
    "content": message.content,
    "tool_calls": message.tool_calls,
}

最低限、送信先に応じた内部表現へ変換します。

internal = {
    "role": message.role,
    "content": message.content,
    "reasoning": getattr(message, "reasoning_content", None),
    "tool_calls": message.tool_calls,
}

ここで重要なのは、内部名をそのまま外部へ送らないことです。保存処理と送信処理を分離し、エンドポイント判定後に変換します。

推論内容はあるが、キー名が違う場合

現在のvLLM文書では、推論出力のフィールドは reasoning とされています。文書には、以前は reasoning_content と呼ばれていたため、移行時は置き換えるよう記載されています。(vLLMのreasoning outputs仕様)

一方、DeepSeek公式APIの思考モードでツール呼び出しを含む履歴を再送する場合は、reasoning_content が契約上のキーです。したがって、次の2つは同じ内部オブジェクトを無変換で共有できません。

  • DeepSeek公式API:reasoning_content
  • 現行vLLM出力:reasoning

vLLMの旧版には旧フィールドを引き続き受け付ける旨が書かれた文書もありますが、これは全バージョンと全構成に適用される永久契約ではありません。実際に配備しているvLLMの版、チャットテンプレート、推論パーサー、ゲートウェイの挙動を確認します。(vLLM旧版のreasoning outputs仕様)

推奨する変換は次の形です。

def to_outbound_message(message, endpoint):
    result = {
        "role": message["role"],
        "content": message.get("content"),
    }

    if message.get("tool_calls") is not None:
        result["tool_calls"] = message["tool_calls"]

    if endpoint == "deepseek-api":
        result["reasoning_content"] = message.get("reasoning")
    elif endpoint == "vllm":
        result["reasoning"] = message.get("reasoning")

    return result

実装上は、reasoningreasoning_content の両方を常に複製する方法より、出力先ごとの契約を1か所に閉じ込める方法が安全です。二重送信で一時的に通っても、将来のスキーマ検証や中継処理で別の不整合を作る可能性があります。

ゲートウェイ経由と直結を同じログで比較する

フィールドがアプリケーションのメモリには存在するのに、最終リクエストで消える場合は、モデルではなく中継経路を調べます。確認順は固定すると迷いません。

第1段階:SDKの応答直後

レスポンスオブジェクトを辞書へ変換した直後に、キー集合を出します。OpenAI互換SDKでは、属性アクセスとJSONシリアライズで見える項目が異なることがあります。

第2段階:内部メッセージ保存後

データベースのschemaに reasoning または reasoning_content が存在するかを確認します。JSON列を使っていても、ORMの許可リストや更新処理で未知キーが落ちる場合があります。

第3段階:キューの発行後と消費後

キューへ投入する前後で、会話ID、message index、role、キー集合を比較します。オブジェクト再構築型のコンシューマーは、定義されていないフィールドを静かに破棄しやすい箇所です。

第4段階:プロキシ受信後

APIゲートウェイのリクエストログでは、本文全文ではなく、各assistantメッセージのキー一覧を記録します。JSONスキーマ検証、未知フィールド除去、空値削除、サイズ制限を確認します。

第5段階:送信直前

DeepSeek公式APIへ送る場合は reasoning_content、vLLMへ送る場合は reasoning が存在するかを確認します。同じ会話を別エンドポイントへ送る場合、URLだけでなく、モデル名、vLLM版、推論パーサーもログに残します。

第6段階:エラー応答と突合する

400本文にフィールド欠落を示すメッセージがある場合は、その文言と最終payloadの差分を保存します。エラー内容が別のパラメーターを指しているなら、reasoningフィールドだけで結論を出してはいけません。

条件分岐チェックリストで修正場所を決める

次のチェックリストは、障害チケットや回帰テストの判定基準にそのまま使えます。上から順に確認し、該当した分岐で修正場所を固定します。

  • [ ] 最終payloadに推論キーがない
    → 保存層またはmessages再構築層を修正します。モデル設定、リトライ回数、GPUリソースの変更には進みません。

  • [ ] DeepSeek公式APIへ reasoning を送っている
    → 出站アダプターで reasoning_content に変換します。変換前後のキー集合をログに残します。

  • [ ] vLLMへ reasoning_content を送っている
    → 実際のvLLM版とprotocol定義で入力互換性を確認します。互換性が確認できなければ、標準フィールドである reasoning へ移行します。

  • [ ] 推論キーは正しいが、tool_call_id が一致しない
    → ツール実行結果の保存順序とID紐付けを修正します。フィールド名の全体置換は行いません。

  • [ ] 直結では成功し、ゲートウェイ経由だけ失敗する
    → ゲートウェイのschema、空値処理、ボディ再構築、許可キー設定を調査します。

  • [ ] DeepSeek公式APIとvLLMの両方で失敗する
    → 共通の会話履歴、ストリーミング結合、assistantとtoolの順序を優先して確認します。

  • [ ] 400本文がtool_choiceや別パラメーターを示している
    → reasoningフィールドの欠落と断定せず、送信先の仕様、モデル、vLLM版、tool parserを個別に検証します。

判定の優先順位は、フィールド欠落、フィールド名、メッセージ順序、追加パラメーター、端点固有の互換性です。チェックが付かない場合は、3経路の最小会話リプレイへ進みます。

メッセージ順序と追加パラメーターを切り分ける

推論フィールドが正しく残っていても、会話の並びが壊れていれば400になります。最低限、次の順序を確認します。

  1. user
  2. assistant のtool call
  3. tool の結果
  4. 次のassistant生成を求めるリクエスト

tool メッセージの tool_call_id は、直前のassistantのtool call IDと一致していなければなりません。複数のtool callを並列処理する場合は、結果の紐付けを配列位置だけで管理しないことが重要です。

また、assistantの content をフレームワークが必須文字列として扱い、空値を別の文字列や不正な値へ置き換えていないかも確認します。DeepSeek公式のtool call例では、assistantメッセージにtool callと推論内容が入り、その後にtoolメッセージが追加されます。(DeepSeek公式のtool call仕様)

tool_choice も別軸で確認します。vLLMは現行文書で、名前付きfunction callingに加え、autorequirednone などをサポートしていますが、サポート状況はvLLM版とモデルのtool parserに依存します。(vLLMのtool calling仕様) DeepSeekの思考モード側で特定パラメーターが制限される場合もあるため、400の本文、送信先、配備版を揃えて判断します。

最小会話を3経路で再生する

修正後は、同じ失敗会話を3経路で再生します。会話内容は短くし、ツール名、引数、結果はダミー化します。

  1. DeepSeek公式APIへ直結する
  2. 対象vLLM版へ直結する
  3. SDK、データベース、キュー、ゲートウェイを含む本番相当経路へ送る

各経路で比較する項目は、リクエストのキー集合、assistantとtoolの順序、tool_call_id、推論フィールド名、400本文です。修正記録には、変更前後の差分、送信先、モデル名、vLLM版、チャットテンプレート、推論パーサーを残します。

失敗会話を固定しておけば、次回のSDK更新やvLLM更新でも同じ条件を再現できます。管理画面や実行環境の確認が必要な場合は、ProxyMacのコンソール案内サポート情報を参照し、ログの保存期間とアクセス権限も先に確認します。

よくある切り分けの誤り

  • 400を見て、すぐにリトライ回数を増やす
  • reasoningフィールドを両方送れば互換になると考える
  • データベースに値があることだけを確認し、最終payloadを見ない
  • content が空なのでassistantメッセージ全体を削除する
  • vLLMの別バージョンで動いた結果を現在の配備環境へ一般化する

この問題では、成功した1回目のログと失敗した2回目のログを並べる方が、リトライや拡張より多くの情報を得られます。

FAQ

DeepSeek V4で1回目のツール呼び出しだけ成功する場合、最初に確認する場所はどこですか?

最初に確認するのはツール定義ではなく、1回目のassistantメッセージを次のリクエストへ再利用する処理です。reasoning_contenttool_callstool_call_idを含む応答が、メモリ、保存領域、ゲートウェイ通過後、最終的なmessages配列まで同じ形で残っているかを比較します。

reasoning_contentを保存済みなのに多輪処理が失敗するのはなぜですか?

保存済みでも、次のリクエストでassistantメッセージに戻されていなければ意味がありません。保存先の値だけでなく、再構築後のキー名、メッセージ位置、nullの扱い、tool_call_idとの対応を確認します。ストリーミングの断片結合で内容が空になったケースも分けて調べます。

vLLMのreasoningをDeepSeek公式APIへそのまま送信しても問題ありませんか?

そのまま送る設計は避けるべきです。現在のvLLM文書では推論出力の主フィールドがreasoningで、DeepSeek公式APIの思考モードではツール呼び出しを含むassistant履歴にreasoning_contentが必要です。内部名を統一し、送信先ごとに明示的な変換を行い、実際のvLLM版で入力互換性を検証します。

多輪tool callでフィールドが消えた場所を特定するログの残し方は?

同じ会話IDとメッセージ番号を使い、モデル応答直後、SDKシリアライズ後、ゲートウェイ受信後、データベース読出し後、キュー処理後、最終送信直前の6地点でキー一覧を記録します。推論本文や秘密情報は保存せず、キー名、型、文字数、配列長、ハッシュだけを脱敏ログに残します。

現在の環境が単一の推論端点だけなら、まず直結と業務経路の差分調査で十分です。複数端点の切り替え、長時間稼働するAgent、macOS側の自動化まで必要なら、都度環境を作り直す構成は再現性と権限管理で不利になります。失敗会話を固定して反復検証する用途では、日本向けMacレンタル環境を使い、アダプター修正前後の実行環境を分けて保持する方が管理しやすいケースがあります。

API連携の検証環境をProxyMacで整えませんか

ProxyMacなら、専用のMac mini M4を使ってツール呼び出しや再送処理を実際のmacOS環境で検証できます。
SSHとVNCに対応しているため、端末操作からログ確認まで用途に合わせてスムーズに進められます。