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応答から
contentとtool_callsだけを保存している - Pydanticや独自DTOが未定義キーを除去している
- messages再構築時に許可キーを固定している
nullや空文字を共通サニタイズ処理で削除している- ストリーミング断片の結合対象に推論部分を含めていない
DeepSeek公式APIのChat Completion仕様では、assistantメッセージに content、reasoning_content、tool_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
実装上は、reasoning と reasoning_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になります。最低限、次の順序を確認します。
userassistantのtool calltoolの結果- 次の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に加え、auto、required、none などをサポートしていますが、サポート状況はvLLM版とモデルのtool parserに依存します。(vLLMのtool calling仕様) DeepSeekの思考モード側で特定パラメーターが制限される場合もあるため、400の本文、送信先、配備版を揃えて判断します。
最小会話を3経路で再生する
修正後は、同じ失敗会話を3経路で再生します。会話内容は短くし、ツール名、引数、結果はダミー化します。
- DeepSeek公式APIへ直結する
- 対象vLLM版へ直結する
- 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_content、tool_calls、tool_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レンタル環境を使い、アダプター修正前後の実行環境を分けて保持する方が管理しやすいケースがあります。