はじめに:「構造化出力=堅牢」という危険な盲信

自律型AIエージェントやLLMワークフローを設計する際、バックエンドエンジニアがほぼ100%の確率で最初に採用するのが「出力フォーマットの厳格なJSON化」です。

OpenAIの Structured Outputs (json_schema) や response_format: { type: "json_object" }、あるいはPydanticやInstructor、LangChainが提供するスキーマ強制機能は、一見すると開発者の夢を叶えてくれたかのように見えます。「自然言語の気まぐれな出力を正規表現でパースして祈る時代は終わった。これからは最初から型定義されたJSONを吐かせれば、後続のプログラムと100%安全に連携できる」と。

「エージェントの意思決定、コード変更プラン、ファイル編集指示はすべてPydanticモデルで定義し、LLMに直接そのスキーマを出力させよう」

しかし、この思想に基づいてコーディングエージェントや複雑なリファクタリングパイプラインを構築したエンジニアは、運用初期に必ず奇妙で深刻な現象に直面します。

型安全性を求めて導入したはずの「JSON強制」が、なぜエージェントの知能をここまで劇的に劣化させるのでしょうか?その根本的な原因は、自己回帰型LLMの計算メカニズムと、文法制約デコーディング(Constrained Decoding)がもたらす構造的な思考の窒息にあります。

自己回帰モデルの計算原理:出力トークンは「思考のワークスペース」である

この問題を根本から理解するには、LLMがどのようにして「推論」を行っているのかという数理的な事実に立ち返る必要があります。

自己回帰型言語モデル(Autoregressive LLM)は、与えられた入力プロンプト $x$ と、これまでに生成されたトークン列 $y_{

P(y_1, y_2, ..., y_T | x) = ∏ [t=1..T] P(y_t | y_{

ここで極めて重要なのは、「LLMには人間のような独立した思考メモリ(隠れ推論空間)がデフォルトでは存在しない」という点です。モデルが複雑な論理関係を紐解き、多角的な検討を行うためのワーキングメモリ(作業領域)は、「直前に自己が出力したトークン系列($y_{なのです。

Chain-of-Thought(思考の連鎖)がなぜあれほど絶大な精度向上をもたらすのか。それは「思考のステップをトークンとして空間上に吐き出すことによって、次のトークンを生成するためのアテンション・コンテキストが豊かになるから」に他なりません。人間が難問を解くときに紙に図や数式を書き殴りながら考えるのと全く同じです。

文法制約デコーディング(Constrained Decoding)の冷酷な代償

現在、APIやフレームワークで提供されている「Structured Outputs」の多くは、単なるプロンプトによるお願いではなく、デコーディング時に**文法制約(CFG: Context-Free Grammar)やロジットマスキング(Logit Masking)**を適用しています。

つまり、語彙辞書(Vocabulary)に含まれる数万〜十数万のトークンのうち、「その位置でJSONスキーマの構文を満たすトークン」以外のロジット(Logit)を機械的にマイナス無限大にマスクし、サンプリング候補から強制排除するアルゴリズムです。

この機構は構文エラー(パースエラー)を100%防ぐという点では完璧です。しかし、その代償としてモデルが本来探索すべきだった「意味論的に最も確からしい推論パス」を力づくでへし折ることになります。

モデルは「このコードの依存関係を解き明かすために、まずはあのインターフェースの振る舞いを言語化したい」と思っても、文法チェッカーから「いま許されているトークンは "action" または "{" だけだ」と首根っこを掴まれます。結果として、豊かな推論空間は消失し、モデルは極小の語彙制約の中で窒息死するのです。

Prefix Biasの罠:キーの順序が思考を逆転させる

推論拘束の被害を最も直接的に受けるのが、スキーマ内の「キーの定義順序(Key Ordering)」です。これを現場ではPrefix Bias(プレフィックス・バイアス)と呼びます。

多くのエンジニアが素朴に設計してしまう、典型的なコーディングエージェントのPydanticモデルを見てみましょう。

# 典型的な失敗スキーマ(人間のバックエンド目線)
class CodeModificationPlan(BaseModel):
    action: Literal["create", "update", "delete", "skip"]
    target_file: str
    target_lines: list[int]
    reasoning: str
    new_code: str

このスキーマをAPIに渡し、モデルに一発で生成させようとすると何が起きるでしょうか?自己回帰モデルは先頭から1トークンずつ順番に出力しなければなりません。

{
  "action": "update",
  "target_file": "src/services/billing.ts",
  "target_lines": [42, 43],
  "reasoning": "決済手数料の計算ロジックが誤っているため..."
}

この生成シーケンスにおける致命的な欠陥にお気づきでしょうか?

モデルが "action": "update" や "target_file": "src/services/billing.ts" というトークンを決定した瞬間、モデルはまだ reasoning(理由や論理)を1行も考えていません。思考のトークンがコンテキスト上にゼロの状態で、勘と確率のゆらぎだけでいきなり「アクションと対象ファイル」を確定させてしまっているのです。

思考の逆転と後付けの自己正当化(Rationalization)

一度 "action": "update" と出力してしまった以上、自己回帰モデルのアテンション機構は、それ以降に生成するすべてのトークンを「最初に出力したアクション」に適合するように引きずられます。

もし本来の正しい判断が「そもそもこのモジュールは責務過多だから分割すべき(refactor)」や「既存の別関数を再利用できるから修正不要(skip)」だったとしても、最初に出したトークンをモデル自身が否定することは確率論的に極めて困難です。その結果、reasoning フィールドには次のような哀しい自己正当化の言い訳が出力されます。

「とりあえずこのファイルの42行目を修正するのが最も手軽であり、依存関係への影響を最小限に抑えられると判断したため、この変更を実施します(大嘘)」

人間で言えば、「まず当てずっぽうで結論の判子を押し、あとから帳尻を合わせるために適当な稟議書をでっち上げている」状態です。これでは知的なエージェントとは到底呼べません。

「じゃあ reason を先頭に置けばいい」の罠

この問題に気づいたエンジニアは、次のようにスキーマを変更します。

class CodeModificationPlan(BaseModel):
    thinking_process: str  # ← 先頭に思考領域を配置!
    action: Literal["create", "update", "delete", "skip"]
    target_file: str
    ...

「これで先に考えさせてからアクションを決めさせれば解決だ」と思うかもしれません。しかし、これでも問題の半分しか解決しません。なぜなら、JSON文字列(String Literal)の内部で思考させること自体が、モデルにとって極めて不自然で負荷の高い行為だからです。

  • エスケープと構文オーバーヘッド: JSONの文字列内では、改行は \n に、ダブルクォートは \" にエスケープしなければなりません。モデルは思考の論理展開に集中すべき瞬間に、「JSONの文字列を意図せず閉じてしまわないか」「エスケープが崩れていないか」という構文維持にアテンション計算リソースを浪費します。
  • 表現の矮小化: Markdownの箇条書きやインデント、コードブロックを用いた複雑な比較表などをJSON文字列の中に押し込めようとすると、モデルの出力トークン確率分布が歪み、思考の深度がプレーンテキスト時と比べて劇的に浅くなります。

現場の泥臭い地雷:JSON強制が引き起こす3大崩壊

開発現場でこのアンチパターンを踏むと、具体的にどのような悲劇が起きるのか。生々しい3つの典型地雷を紹介します。

地雷1:ValidationErrorリトライ地獄と「論理の蒸発」

エージェントシステムでよく見られるのが、Pydanticのパース失敗時に、バリデーションエラーをそのままプロンプトに送り返して再試行させるループです。

# 現場でよく見る「愚直なリトライループ」
for attempt in range(max_retries):
    response = call_llm(prompt, schema=MyComplexSchema)
    try:
        return MyComplexSchema.model_validate_json(response)
    except ValidationError as e:
        prompt += f"\nValidation Error: {e.json()}\n修正して再出力してください。"

このループが回った瞬間、モデルはパニックに陥ります。「なぜスキーマを満たせなかったのか」を修正しようとするあまり、モデルの意識は「フィールドの型を合わせること」だけに100%吸い取られます。

その結果、エラーが起きたフィールドの値を空文字列 "" や空配列 [] に置き換えるなど、「構文エラーは通るが、中身のビジネスロジックや修正コードが綺麗サッパリ蒸発したゴミデータ」を出力して満足するようになります。型エラーを解消するためにコードの意味を捨てるという本末転倒の極致です。

地雷2:思考モデル(Reasoning Models)の利点を完全抹殺

近年登場した推論特化型モデル(OpenAI o1/o3、Claude 3.7 Sonnet の Extended Thinking、Gemini 2.0 Flash Thinking 等)は、ユーザーへの出力前に隠れた思考トークンを大量に生成することで高い知能を発揮します。

しかし、エージェント基盤側で「最初からStructured Outputs(JSON強制)」をAPIリクエストに設定すると、モデルによっては内部のThinkingプロセスが制約されたり、思考の展開が窮屈になったり、最悪の場合はAPIレベルでエラー(Structured OutputsとThinkingの併用制限)になります。

モデルが本来持っている「深く自問自答し、仮説を棄却し、最適な設計を練り直す」という最強の武器を、こちらの受け取り側の都合(JSONで受け取りたい)だけで封じ込めてしまっているのです。

地雷3:AST・ファイル境界を無視した「パッチ貼り付け症候群」

コード修正において、JSON Schemaで {"target_lines": [10, 20], "new_code": "..."} のような行番号ベースの構造化出力を強制すると、モデルは抽象構文木(AST)や言語の文脈を無視し、機械的に指定行を文字列置換することだけに拘束されます。

自然言語であれば「この変更により関数のシグネチャが変わるため、呼び出し元のXファイルとYファイルも併せて修正する必要がある」と気付けるはずのモデルが、単一のJSONオブジェクトの枠組みに閉じ込められることで、スコープ外の破壊的変更を平気で見落とすようになります。

解決策:思考と抽出を完全分離する「二相パイプライン」

では、後続のプログラムで扱える「型安全で厳密なデータ」を手に入れつつ、LLMの推論能力を1ミリも損なわせないためにはどうすればいいのか?

その答えが、「非構造化自由推論(Phase 1)」と「構造化射影(Phase 2)」を物理的に分離する【二相パイプライン(Two-Phase Decoupled Pipeline)】です。

二相パイプラインのアーキテクチャ概要

  • Phase 1: Free-form Reasoning(非構造化推論フェーズ)
    JSON Schemaや文法制約を一切かけない。プレーンなMarkdownと思考タグ(<thinking>)を用い、モデルにエンジニアとしての思考、コード設計、エッジケース検証を全力で実行させる。
  • Phase 2: Constrained Projection(構造化射影フェーズ)
    Phase 1で出力された完全な思考ログと設計結果をコンテキストに入力し、極めて軽量なモデル(または同一モデルの決定論的プロンプト)を用いて、指定のJSON Schemaへと一気に「射影(Projection / Extraction)」する。

このアプローチの最大の利点は、「推論」と「シリアライズ」という全く異なる2つの責務を分離できる点です。Phase 1では思考の帯域を100%解放し、Phase 2ではすでに確定した思考結果を型に流し込むだけの単純作業に専念させます。

実践実装例:TypeScriptによるクリーンルーム設計

以下に、実戦で耐えうる二相パイプラインの最小限かつ堅牢な実装パターンを示します。

import { generateText } from "ai";
import { z } from "zod";

// 最終的にシステムが要求する厳密な型定義
const RefactorPlanSchema = z.object({
  action: z.enum(["modify", "extract_module", "skip"]),
  targetFilePath: z.string(),
  rationale: z.string(),
  changes: z.array(
    z.object({
      description: z.string(),
      replacementCode: z.string(),
    })
  ),
});

type RefactorPlan = z.infer;

export async function planCodeRefactoring(
  sourceCode: string,
  taskDescription: string
): Promise {
  // =========================================================================
  // Phase 1: Free-form Scratchpad (非構造化推論)
  // JSONを一切意識させず、最高性能のモデルに思考を全開放させる
  // =========================================================================
  const reasoningPrompt = `
あなたはシニアソフトウェアエンジニアです。
以下のコードに対するリファクタリング計画を検討してください。

【要件】
${taskDescription}

【対象コード】
\`\`\`typescript
${sourceCode}
\`\`\`

思考プロセスを余すところなく  タグ内に記述してください。
1. 現在のコードの構造的課題・依存関係の分析
2. 考えられる複数のアプローチとそれぞれのトレードオフ
3. 採用する方針の決定と、具体的なコード差分の策定
思考が完了したら、決定内容のサマリーをプレーンテキストで提示してください。
`;

  const phase1Result = await generateText({
    model: "claude-3-7-sonnet-20250219", // または gemini-2.0-flash / o3-mini
    prompt: reasoningPrompt,
    // ここでは schema も jsonMode も絶対に指定しない!
  });

  const rawReasoningOutput = phase1Result.text;

  // =========================================================================
  // Phase 2: Constrained Projection (構造化射影)
  // 確定済みの思考結果を入力とし、スキーマへの流し込みに専念させる
  // =========================================================================
  const extractionPrompt = `
以下は、シニアエンジニアが検討したリファクタリングの推論プロセスと決定内容です。
この内容を正確に読み取り、指定のJSONスキーマに忠実にマッピングしてください。
新たな推論や変更を加えてはいけません。

【検討ログ】
${rawReasoningOutput}
`;

  const phase2Result = await generateText({
    model: "claude-3-5-haiku-20241022", // 射影は軽量・安価なモデルで十分
    prompt: extractionPrompt,
    // ここで初めて厳格なスキーマ制約を適用する
    output: {
      schema: RefactorPlanSchema,
    },
  });

  return phase2Result.output;
}

二相パイプラインがもたらす圧倒的なメリット

  1. IQ劣化の完全な阻止: Phase 1でモデルは構文エラーの恐怖から解放され、自然言語とコードブロックを駆使して極めて深い推論とエッジケースの検証を行えます。
  2. コストとレイテンシの最適化: 最も知能を要する「Phase 1の推論」にはフラッグシップモデルを使い、すでに答えが出ている「Phase 2の射影」には軽量で高速なモデル(HaikuやFlash系)を割り当てることで、全体のトークンコストを最小化できます。
  3. デバッグ可能性の劇的な向上: エージェントが誤った判断を下した際、Phase 1の <thinking> ログを読めば「どの前提条件の解釈で間違えたのか」が一目瞭然になります。単一のJSON出力では得られなかった完全なトレーサビリティが手に入ります。

まとめ:LLMはデータベースではなく「思考プロセッサ」である

なぜ私たちはエージェントの出力を無理やりJSONで縛ろうとしてしまったのでしょうか?

それは、私たちが普段書いているWeb APIやRDBMSのメンタルモデル——「システム間の通信はすべて厳密な型定義とスキーマに従うべきである」というソフトウェアエンジニアリングの常識を、確率的推論エンジンであるLLMにも無批判に適用してしまったからです。

しかし、LLMは構造化データを溜め込むデータベースでもなければ、静的なRPCエンドポイントでもありません。次のトークンを生成しながら文脈を構築し、思考を飛躍させる「プロセッサ」なのです。

プログラマがプログラムを動かすためにCPUのレジスタ操作やメモリアドレスの自由度を奪わないのと同様に、システムの都合(JSONパースの容易さ)のためにLLMの思考帯域を奪ってはならない。

エージェントを本番運用する上で目指すべきは、推論の自由を奪うことではなく、「思考の場(コア)」には無限の自由を与え、「システムの境界(バウンダリ)」においてのみ厳格に型付けを行うアーキテクチャです。

もしあなたのエージェントが最近妙に頭が悪くなったり、雑な修正を繰り返したりしているなら、まず疑うべきはプロンプトの質ではなく、その出力を縛っている response_format: { type: "json_object" } かもしれません。一度その手錠を外し、自由に思考させてみてください。エージェントは本来の知性を驚くほど鮮やかに取り戻すはずです。