DeepSeek公式ドキュメントでは、思考モードでツール呼び出しを行ったターンの reasoning_content を後続リクエストへ完全に戻さない場合、APIがHTTP 400を返すと明記されています。DeepSeek公式の思考モード仕様でも確認できます。
症状: 初回の function calling は成功するのに、ツール結果を追加した次の送信だけが400になります。
最短解決策: 公式API用とvLLM用に、別々の原始HTTP再現チェーンを作り、公式APIでは reasoning_content、vLLMでは現在の reasoning を端点の境界で一方向に変換します。
この手順は、SDKやAgentフレームワークを外して再現証拠を作りたい開発者向けです。クライアント、ゲートウェイ、推論サーバーのどこで履歴が欠落したかを確認したいバックエンド担当者にも適しています。公式APIと自前vLLMの両方を回帰環境に入れるプラットフォームチームも対象です。
まず端点を固定する:モデル名ではなくbase URLで分ける
最初に記録するのはモデル名ではありません。実際に送信した base_url、HTTPメソッド、パス、デプロイ済みvLLMのバージョン、推論パーサー設定です。同じモデル名を使っていても、公式APIとvLLMでは入力契約が同じとは限りません。
vLLMの現行ドキュメントでは、推論出力のフィールド名は reasoning とされ、以前の名称が reasoning_content だったと説明されています。vLLMのReasoning Outputsは、単純な文字列置換ではなく、現在の出力仕様として確認してください。
| 確認対象 | DeepSeek公式API | vLLM自前端点 |
|---|---|---|
| 実際に固定するもの | 公式APIのbase URLとAPI仕様 | 自分のbase URL、vLLM版、モデル設定 |
| 推論出力の記録名 | reasoning_content |
現行文書では reasoning |
| ツール呼び出し | tool_calls と tool_call_id |
パーサー設定とモデル形式に依存 |
| 先に確認する資料 | 公式APIスキーマ | vLLMの推論出力仕様 |
| 400の判断 | ツール呼び出し履歴の推論内容が欠落していないか | 入力契約と対象バージョンの互換性を確認 |
ここで公式APIのレスポンスを reasoning に改名したり、vLLMのレスポンスを無条件に reasoning_content として保存したりしないでください。内部モデルは共通化しても、送信直前の出口変換は端点ごとに分けます。
首輪は同じにする:単一ツールで初回のfunction callingを発生させる
最小リクエストでは、外部状態を変更しない単一ツールを使います。たとえば日付や固定値を返す関数です。ツール定義、ユーザー入力、思考モード設定、ヘッダーを固定し、フレームワークが自動追加するメタデータを除外します。
以下は構造確認用の例です。実際のキーは環境変数から読み込み、ログには認証情報を残しません。
curl -sS "$BASE_URL/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-pro",
"messages": [
{
"role": "user",
"content": "現在の日付を確認してください。"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_date",
"description": "固定の日付を返す",
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false
}
}
}
],
"tool_choice": "auto",
"thinking": {
"type": "enabled"
}
}'
公式APIでは、レスポンスから少なくとも content、reasoning_content、tool_calls、finish_reason、呼び出しIDを別々に保存します。DeepSeekのAPIスキーマでも、assistantメッセージの中に content、reasoning_content、tool_calls が定義されています。Create Chat Completionの公式スキーマを基準にしてください。
vLLM側では、DeepSeek V4のモデル形式に合う推論パーサーとツールパーサーが必要です。現行のvLLMレシピでは、--reasoning-parser deepseek_v4、--tool-call-parser deepseek_v4、--enable-auto-tool-choice を使う構成が示されています。DeepSeek-V4-Flash向けvLLMレシピと、DeepSeek V4パーサーの仕様を、実際の起動引数と照合します。
初回成功後に壊れる理由:assistant履歴を省略しない
初回レスポンスにツール呼び出しが含まれたら、次のリクエストへassistantメッセージを戻します。公式APIでは、ツール呼び出しを実行したターンの reasoning_content が必須です。content だけ、または tool_calls だけを残すと、初回成功後の次の送信で400になる条件に入ります。
完全な思考本文を記事やチケットへ貼る必要はありません。保存用の実データでは保持し、共有用の再現資料では <REDACTED_REASONING> のように置き換えます。重要なのは、キーが存在すること、assistantメッセージの順序が崩れていないこと、ツール呼び出しIDが一致していることです。
{
"role": "assistant",
"content": "<REDACTED_CONTENT>",
"reasoning_content": "<REDACTED_REASONING>",
"tool_calls": [
{
"id": "call_redacted_001",
"type": "function",
"function": {
"name": "get_date",
"arguments": "{}"
}
}
]
}
次の3点を、シリアライズ後にも確認します。
reasoning_contentがキーごと消えていないかtool_callsの配列と呼び出しIDが残っているかcontentがnullの場合も、勝手に空文字へ変換して契約を変えていないか
ログの脱敏処理、オブジェクトから辞書への変換、ミドルウェアのフィルターは、端点の契約とは別の証拠です。まず原始レスポンス、次に保存後のメッセージ、最後に送信直前のJSONを比較します。
失敗例と成功例を並べる:ツール結果の後に再送する
assistant履歴を追加したら、対応する tool_call_id を持つ最小のツール結果を続けます。実際の外部データではなく、固定文字列を返すだけで十分です。こうすれば、失敗原因をツール実装やネットワーク処理から切り離せます。
| ケース | assistant履歴 | ツール結果 | 期待する観察 |
|---|---|---|---|
| 失敗サンプル | reasoning_content を削除 |
tool_call_id は一致 |
公式APIでは400になる条件を確認 |
| 成功サンプル | reasoning_content を保持 |
tool_call_id は一致 |
同じ会話骨格で次の応答を確認 |
| 混用サンプル | vLLMの reasoning をそのまま送信 |
IDは一致 | 公式APIの契約に自動適合すると仮定しない |
| vLLM確認 | 配備版の入力仕様に合わせる | パーサー設定と整合 | バージョン固有の挙動として記録 |
公式API用の失敗サンプルは、次のようにassistantメッセージから推論フィールドだけを外します。
{
"messages": [
{
"role": "user",
"content": "現在の日付を確認してください。"
},
{
"role": "assistant",
"content": "<REDACTED_CONTENT>",
"tool_calls": [
{
"id": "call_redacted_001",
"type": "function",
"function": {
"name": "get_date",
"arguments": "{}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_redacted_001",
"content": "2026-08-18"
}
]
}
成功サンプルでは、このassistantメッセージに元の reasoning_content を戻します。DeepSeek公式ガイドのサンプルも、レスポンスのassistantメッセージをそのまま履歴へ追加し、ツール結果を続ける構成です。公式のツール呼び出し例と同じ順序で比較してください。
vLLMから reasoning が返った場合、それをそのまま公式APIへ返せるとは判断できません。vLLMの文書が示す名称変更は、vLLM側の出力仕様に関する説明です。公式APIが要求する入力名を変更したという意味ではありません。
注意:
reasoningとreasoning_contentは、名前だけを全履歴で置換する対象ではありません。内部メッセージモデルに両方を保持し、送信先ごとのアダプターで一方だけを選ぶ設計にしてください。
公式APIとvLLMを同じ会話で比べない:出口で単方向変換する
比較テストでは、同じツール定義とユーザー入力を使います。ただし、会話履歴を片方の端点からもう片方へそのまま移しません。公式APIで得たassistant履歴をvLLMへ流す場合も、vLLMの配備版が受け付ける形式へ変換します。
vLLMのDeepSeek V4実装は、<think> ブロックとDSML形式のツール呼び出しを解析する構造を持ちます。vLLMのDeepSeek V4エンコード仕様では、推論、本文、ツール呼び出しを別要素として解析する処理が説明されています。一方で、対象バージョンの入力互換性まで現行文書だけで断定することはできません。
切り分けでは、次の順で見ます。
- 送信先のbase URLが想定どおりか確認します。
- 初回レスポンスの実フィールド名を保存します。
- 保存後のassistantメッセージを比較します。
- ツール結果のID対応を確認します。
- 送信直前JSONと400のエラー本文を保存します。
- vLLMでは起動引数、モデル形式、対象バージョンのプロトコル定義を照合します。
「公式APIでもvLLMでも同じJSONが通る」という前提を捨てると、クライアントとゲートウェイの責任範囲が見えます。ゲートウェイを挟む場合は、受信JSON、内部表現、出口JSONの3点を別ログにし、推論本文は脱敏したハッシュや存在フラグで管理します。
修正を一度の成功で終わらせない:回帰用の最小チェーンにする
最小リクエストは、その場の障害確認だけで終わらせません。次のチェック項目をCI、手動リリース前確認、端点切り替え時の検証へ組み込みます。
- [ ] 公式APIとvLLMのbase URLを別設定として記録した
- [ ] ツールなしの通常応答を確認した
- [ ] 単一ツールの初回呼び出しを確認した
- [ ] ツール結果を返した後の継続呼び出しを確認した
- [ ] 連続したツール呼び出しでassistant履歴を保持した
- [ ] 公式APIでは
reasoning_contentの存在と再送を確認した - [ ] vLLMでは
reasoningと実際の配備版仕様を確認した - [ ]
tool_call_idと結果メッセージの対応を検査した - [ ] 状態コードだけでなく、送信JSONとエラー本文を保存した
- [ ] 端点切り替え後に別のアダプターを通して再実行した
修正完了の条件は、成功レスポンスが1回返ることではありません。公式APIとvLLMの両方で連続ツール呼び出しが通り、reasoning_content を欠落させた公式API用サンプルが安定して失敗し、エラー本文と送信履歴が再現資料として残ることです。
ローカル環境だけでこの確認を続けると、依存パッケージの更新、環境変数の混在、別プロセスの常駐、GPU側の設定差が証拠を曖昧にします。MacHTMLのコンソール環境や利用ガイドを使って、同じスクリプトを隔離した作業環境へ持ち込み、フレームワーク更新前後とロールバック後を同じ手順で比較する方法もあります。
ローカル開発機は、短時間の修正や単一端点の開発には十分です。しかし、公式APIと自前vLLMを長期間並行保持する用途では、端点の切り替え忘れ、権限設定の差、履歴キャッシュの残留、共有リソースによる再現性低下が起きやすくなります。長期の高負荷推論や物理インターフェースが必要なら自前設備が向きますが、短期の多輪400調査や回帰確認なら、隔離したMac環境をレンタルして同一スクリプトを再実行するほうが、原因比較の条件を揃えやすい選択です。
MacHTMLで検証環境を整えましょう
遠隔のMac環境を利用して、APIの最小リクエストやマルチターン処理の再現テストを進められます。 手元の環境構築にかかる負担を抑え、ツール呼び出しや推論設定の違いを効率よく切り分けられます。 必要な期間だけMacを利用できるため、400エラーの再現確認から回帰テストまで柔軟に対応できます。 開発に適したMac環境をMacHTMLで用意し、安定した検証フローを整えてみてください。