DeepSeekマルチターンで欠落した reasoning_content を常に送信するよう修正

crmne/ruby_llm

DeepSeek の思考対応モデルがマルチターンリクエストで reasoning_content が欠如するとエラーになる問題を、プロバイダー側のフォーマットロジックで常にキーを出力するように修正しました。

背景

DeepSeek の thinking‑capable モデル(deepseek-reasonerdeepseek-v4-flashdeepseek-v4-pro など)は、次回リクエスト時に reasoning_content キーが必須であることが API 仕様で求められています。従来の OpenAI 互換実装 format_thinking は、thinking.text が空の場合に reasoning_content を省略し、結果としてマルチターンで直前のアシスタントメッセージに思考テキストが無いと API がリクエストを拒否していました。この挙動は DeepSeek 固有のバリデーションに起因し、他のプロバイダーでは問題になりませんでした。

この欠落は、ツール呼び出しだけを返したターンや思考テキストがキャプチャされなかったケースで顕在化し、実運用でのマルチターン対話が失敗する根本原因となっていました。そこで、DeepSeek 用プロバイダーに限定した修正を加えることで、全モデルで一貫した動作を保証する方針が取られました。

技術的な変更

RubyLLM::Providers::DeepSeek::Chatformat_thinking メソッドが新たに実装され、アシスタントメッセージで常に reasoning_content キーを出力します。テキストが空でも空文字列を返し、reasoningreasoning_signature は従来通り条件付きで追加されます。実装は以下の通りです。

def format_thinking(msg)
  return {} unless msg.role == :assistant

  thinking = msg.thinking
  text = thinking&.text.to_s
  payload = { reasoning_content: text }
  payload[:reasoning] = text unless text.empty?
  payload[:reasoning_signature] = thinking.signature if thinking&.signature
  payload
end

この変更により、thinkingnil でも reasoning_content: "" が必ず含まれ、DeepSeek のバリデーションを通過します。reasoningreasoning_signature は元のロジックと同様に、テキストが非空の場合またはシグネチャが存在する場合にのみ付与されます。結果として、非思考モデル(例: deepseek-chat)は空キーを無視し、既存の動作に影響を与えません。

設計判断

本修正は プロバイダー単位でのオーバーライド という選択肢を取っています。OpenAI 互換実装はそのまま残し、DeepSeek 用にだけ format_thinking を再定義することで、共通コードの侵食を防ぎつつ後方互換性を保ちました。モデル ID に依存しない実装とすることで、将来追加される DeepSeek 系列モデルすべてに自動的に適用される設計となっています。

代替案としては reasoning_content 用の新規設定キーを導入する案が議論されましたが、既存の API 仕様に沿うためにはキー名自体を必須にする必要があり、設定散在のリスクが高まります。そのため、既存キー reasoning_content を拡張し、空文字列でも必ず送出するシンプルなアプローチが選ばれました。

結果的に、最小限のコード変更で DeepSeek のマルチターン制御を安定化させ、他プロバイダーへの影響を回避するというトレードオフが実現されています。

まとめ

DeepSeek 系列の思考対応モデルが求める reasoning_content を必ず送信するよう format_thinking をオーバーライドし、空文字列でもキーを出力することでマルチターンリクエストの失敗を防止しました。この変更はプロバイダー単位の限定的な実装で、既存コードとの互換性と将来のモデル拡張性を両立させています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
78004009

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文が明確に要旨を伝え、背景・技術的変更・設計判断・まとめの各セクションが揃っており、総論→各論→結論の構成が成立しています。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式です。GitHubリンクは [PR #749](URL) となっており、PR番号の記法が「[#749](URL)」でない点が仕様とやや異なります。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的な Ruby/Large Language Model の知識を前提した記述で、初心者向けの過度な説明はありません。対象読者に適しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションの冒頭が総論パラグラフとなり、続く段落が具体的事実・コードで裏付け、最後にまとめがある。トピックセンテンスが先頭に位置し、1段落1トピック・長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと完全に一致しており、ファイル名・内容ともに正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語は PR と一致しており、誤用は見られません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明は PR の概要・目的・実装内容と合致しており、因果関係も正しく示されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張は PR のタイトル・説明・Diff で裏付けられており、根拠のない推測や外部情報は含まれていません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #749 は正しく記載されています。数値や固有名詞の誤りはありません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(欠落した reasoning_content の修正)を正確に日本語化して表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

LTS・EOL・リリース日程 といった PR に無い外部知識は記載されていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致しています。