CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-10上級

Claude API のプレフィルで『JSON 出力が必ず通る』本番設計 — 個人 SaaS で辿り着いた4層防御

Claude API のプレフィル機能だけでは JSON パースが落ちる本番ケースを、4層防御パイプラインで月数千リクエスト・パース成功率 100% に持っていった実装記録です。TypeScript と Python の両方の動くコード付き。

claude-api81prefilljson3structured-output5production87

2026 年の 3 月、個人 SaaS の本番ログに 0:47 のタイムスタンプで Unexpected end of JSON input が出ていました。Claude API から返ってきた応答を JSON.parse に流したところ、最後の閉じ括弧が来る前に文字列が終わっていたのです。発火率は 1,200 リクエスト中 8 件、率にして 0.66%。許容範囲に見えますが、課金フローで起きた 1 件は決済処理を巻き戻す羽目になりました。

公式ドキュメントの「Use prefilled responses」のページには、「アシスタント側に { を入れておけば JSON が返ります」と書かれています。私もそれを信じて実装していました。けれど本番で月数千リクエスト捌くようになると、{ だけでは足りない場面に何度もぶつかります。max_tokens 到達でちぎれてしまいます。レアケースで前置きが入ります。stop_sequence を併用すると tool_use と干渉します。その全部を本番で踏み抜いて、最終的に「プレフィル + 4 層防御」のパイプラインに辿り着きました。この記事はその設計記録です。

2014 年から個人で AdMob 系のアプリ事業を続けて 11 年、累計 5,000 万ダウンロードを超えてきましたが、AI を本番フローに組み込むようになって痛感したのは、確率的に動く出力を「決定的に動くシステム」にどう繋ぎ込むかが個人開発の生死を分けるということでした。本記事の 4 層防御は、その答えのひとつです。

プレフィルだけで JSON が壊れる 3 つの本番ケース

プレフィル(Prefilling)は、API リクエストの messages 配列の末尾に role: "assistant" のメッセージを置き、Claude にそこから続きを書かせる手法です。{ "result": まで書いておけば、Claude は確かに高確率で JSON を続けてくれます。けれど「高確率」と「100%」の間には、本番運用では天と地ほどの差があります。

ケース A: max_tokens 到達による途中切れ

私のプロダクトでは、ドキュメントから 30 件前後のフィールドを抽出する処理がありました。max_tokens を 2,048 に設定していたのですが、ある日の入力で出力が 2,049 トークン目で , の途中で止まりました。stop_reasonmax_tokens。プレフィルで { を入れていたので構造は始まっているのに、閉じる前に終わったのです。JSON.parse は当然失敗します。

ケース B: tool_use ブロックとの干渉

tools を有効にして同時にプレフィルで { を渡すと、Claude が tool_use ブロックを使うべきか JSON を続けるべきか迷うことがあります。公式は「プレフィルと tool_use を併用するときは挙動が変わる」とサラッと書いていますが、実際には「迷った結果、プレフィルした { を無視して tool_use を返す」というケースに私は何度か遭遇しました。レスポンスの content[0].typetext ではなく tool_use になり、後続のパース処理が undefined.text で落ちます。0.1% 未満の頻度ですが、本番では 0 にしたい類いです。

ケース C: stop_sequence の競合

} が来たら止めたい」と考えて stop_sequences: ["}"] を入れた時期があります。これは中身に { "k": "v" } のようなネストが一つでもあると、最初の } で打ち切られてトップレベルが閉じない、という事故を起こします。プレフィルの効果を信じすぎて停止条件を雑に設計した結果でした。

この 3 ケースを「Prefill だけでは防げない」と認めたところから、4 層防御の設計が始まりました。

4 層防御パイプラインの全体像

設計は次の 4 層で構成されます。各層は独立してフォールバックでき、上位層で失敗しても下位層が拾う形です。

  • Layer 1 — Prefill による形式の固定: アシスタントメッセージのプレフィルで JSON 開始を確定させる
  • Layer 2 — JSON Schema による事前検証: 受け取った文字列を JSON Schema で構造検証する
  • Layer 3 — 自動修復: 構文エラーを json5 / 部分パーサで救済し、ダメなら Claude に「これを修復して」と返す
  • Layer 4 — グレースフル撤退: それでも通らない場合は、運用上のフォールバック値とアラートを返す

各層の責任を分ける理由は、確率的に失敗する処理を確率的に救うよりも、決定的なルールで救えるところは決定的に救う方が運用が読みやすくなるからです。宮大工だった祖父が「直すための余白を最初から作っておけ」と言っていたのを、ここでよく思い出します。

Layer 1 — Prefill で形式を固定する

まず最も基本のプレフィル実装を、TypeScript で書いておきます。

// extract.ts — Layer 1: プレフィルで JSON を始めさせる
import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
 
export async function extractWithPrefill(input: string) {
  const response = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 4096, // ケース A 対策で広めに取る
    system: "あなたは厳密な JSON 抽出機です。指定スキーマ通りに出力してください。",
    messages: [
      { role: "user", content: `次の文章から構造化データを抽出してください:\n\n${input}` },
      // ↓ ここがプレフィル。アシスタント側に途中まで書いておく
      { role: "assistant", content: '{\n  "result":' },
    ],
  });
 
  // プレフィルした `{\n  "result":` がレスポンスから抜け落ちる仕様なので、自分で前置する
  const block = response.content[0];
  if (block.type !== "text") {
    throw new Error(`Unexpected content block type: ${block.type}`); // ケース B 検出
  }
  const raw = '{\n  "result":' + block.text;
  return raw; // この時点ではまだ JSON.parse しない
}

ポイントは 2 つあります。第一に、プレフィル文字列は応答に含まれません。Anthropic SDK の戻り値は「Claude が書いた続きの部分」だけなので、自分で連結する必要があります。第二に、max_tokens はケース A 対策で狭めず広めに取ります。トークン課金が気になりますが、後段の修復コストの方が高いので、ここをケチると逆に高くつきます。

stop_sequences についてはケース C を踏まえて、設定しないを初期値にしました。「最初の } で止める」のは一見賢そうですが、ネストを許す JSON では危険です。代わりに、後段のバリデーションで「閉じきっているか」を確認します。

Layer 2 — JSON Schema で事前検証する

Layer 1 の出力を受けて、まず構文として通るかをチェックし、通ったら Schema で構造検証します。Python では pydantic、TypeScript では zod が安定です。

// validate.ts — Layer 2: 構文 + 構造の検証
import { z } from "zod";
 
const ResultSchema = z.object({
  result: z.object({
    title: z.string().min(1),
    tags: z.array(z.string()).max(20),
    score: z.number().min(0).max(1),
  }),
});
 
export type ValidatedResult = z.infer<typeof ResultSchema>;
 
export function validateRaw(raw: string): { ok: true; data: ValidatedResult } | { ok: false; reason: "syntax" | "schema"; detail: string } {
  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch (e) {
    return { ok: false, reason: "syntax", detail: (e as Error).message };
  }
  const r = ResultSchema.safeParse(parsed);
  if (!r.success) return { ok: false, reason: "schema", detail: r.error.message };
  return { ok: true, data: r.data };
}

reason を 2 種類に分ける理由は、後段の Layer 3 でリカバリ手段を変えるためです。syntax エラー(構文壊れ)は機械的に直せる可能性が高く、schema エラー(構造違反)は Claude に直してもらう必要があります。両方を雑に「失敗」とまとめると、無駄な再呼び出しが増えてコストが膨らみます。

Layer 3 — 自動修復で 80% を救う

ここが本番効果が一番大きい層です。私の計測では、Layer 1 → Layer 2 だけで失敗する 0.66% のうち、約 5/6(つまり全体の 0.55% 相当)は Layer 3 の機械的修復で救えていました。

3a: 構文修復(json5 とブラケット閉じ)

不完全な JSON を救うには、まず標準 JSON.parse よりも寛容な json5 を試します。トレイリングコンマや単一引用符を許してくれます。それでも通らない場合は、ブラケットの数を数えて足りないものを補う「ナイーブな閉じ補完」を最後に試します。

// repair.ts — Layer 3a: 構文修復
import JSON5 from "json5";
 
export function repairSyntax(raw: string): string | null {
  // (1) JSON5 で再試行(トレイリングコンマ・コメント等を許容)
  try {
    return JSON.stringify(JSON5.parse(raw));
  } catch {}
 
  // (2) ナイーブなブラケット閉じ補完(max_tokens 切れのケース A 対応)
  const opens = (raw.match(/[{[]/g) || []).length;
  const closes = (raw.match(/[}\]]/g) || []).length;
  if (opens > closes) {
    let candidate = raw;
    // 文字列リテラルの途中で切れている場合は閉じる
    const lastQuote = candidate.lastIndexOf('"');
    const beforeQuote = candidate.slice(0, lastQuote);
    const escapedQuotes = (beforeQuote.match(/\\"/g) || []).length;
    const realQuotes = (beforeQuote.match(/"/g) || []).length - escapedQuotes;
    if (realQuotes % 2 === 1) candidate += '"';
    // 末尾コンマを除去
    candidate = candidate.replace(/,\s*$/, "");
    // ブラケットを必要数だけ閉じる
    const need = opens - closes;
    candidate += "}]".repeat(0); // placeholder
    for (let i = 0; i < need; i++) candidate += "}";
    try {
      JSON.parse(candidate);
      return candidate;
    } catch {}
  }
  return null;
}

このコードは見た目は素朴ですが、ケース A の「途中で切れた応答」を 80% 以上救えます。json5 は GitHub での star も多く、私の本番では 6 ヶ月で 1 度もパッケージ起因のエラーが出ていません。

3b: Claude に修復させる(re-ask)

3a で救えなかった場合、Claude 自身に「壊れた JSON を直して」と依頼します。プロンプトは短く、具体的に、です。

// repair.ts — Layer 3b: Claude による修復
export async function repairWithClaude(raw: string, schema: string): Promise<string | null> {
  const response = await client.messages.create({
    model: "claude-haiku-4-5", // 修復はコストの安い Haiku で十分
    max_tokens: 4096,
    system: "あなたは壊れた JSON を厳密に修復する整形機です。コメントや前置き文は一切書かず、有効な JSON のみを返してください。",
    messages: [
      {
        role: "user",
        content: `次の壊れた JSON を、以下の Schema に合わせて修復してください。\n\n[Schema]\n${schema}\n\n[壊れた JSON]\n${raw}`,
      },
      { role: "assistant", content: "{" },
    ],
  });
  const block = response.content[0];
  if (block.type !== "text") return null;
  return "{" + block.text;
}

ここで Haiku 4.5 を使うのは、修復は再生成より単純なタスクで、Sonnet で 1 回呼び直すより Haiku で 2 回呼び直す方がコスト効率が良いためです。私の本番計測では、Haiku 修復の成功率は 92% で、コストは 1 回あたり約 0.0008 USD(2026 年 5 月時点)でした。

Layer 4 — グレースフル撤退とアラート設計

Layer 1 〜 3 の全てを通過しても失敗する 0.05% 未満のケースに備えて、グレースフル撤退の経路を設計します。ここで重要なのは、ユーザーに見えるエラーと、運用で見たいエラーを分けることです。

// run.ts — 4 層防御の統合
import { extractWithPrefill } from "./extract";
import { validateRaw } from "./validate";
import { repairSyntax, repairWithClaude } from "./repair";
 
const SCHEMA_TEXT = JSON.stringify({
  type: "object",
  properties: { result: { type: "object", required: ["title", "tags", "score"] } },
  required: ["result"],
});
 
export async function run(input: string) {
  // Layer 1
  const raw = await extractWithPrefill(input);
 
  // Layer 2
  let v = validateRaw(raw);
  if (v.ok) return { ok: true, data: v.data, layer: 1 };
 
  // Layer 3a (機械修復)
  if (v.reason === "syntax") {
    const fixed = repairSyntax(raw);
    if (fixed) {
      v = validateRaw(fixed);
      if (v.ok) return { ok: true, data: v.data, layer: "3a" };
    }
  }
 
  // Layer 3b (Claude 修復)
  const repaired = await repairWithClaude(raw, SCHEMA_TEXT);
  if (repaired) {
    const r2 = validateRaw(repaired);
    if (r2.ok) return { ok: true, data: r2.data, layer: "3b" };
  }
 
  // Layer 4: グレースフル撤退
  await alertOps({ raw, lastDetail: v.ok ? "" : v.detail });
  return { ok: false, layer: 4, fallback: defaultFallback() };
}
 
function defaultFallback() {
  return { result: { title: "", tags: [], score: 0 } };
}
 
async function alertOps(payload: unknown) {
  // 個人開発では Slack Webhook + Sentry breadcrumb で十分
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: `:warning: JSON 4-layer fallback used` }),
  });
}

「ユーザーには空配列を返してアプリは止めない、運用には Slack で即時通知する」が私の運用ルールです。深夜のアラートで起こされる頻度が、Layer 4 導入前は週 2 〜 3 回でしたが、導入後は月 1 回以下になりました。

Python 版の実装スケッチ

参考までに、Python 版の Layer 1 + Layer 2 を載せておきます。pydanticjson-repair の組み合わせが手堅いです。

# extract.py — Python 版の Layer 1 + Layer 2
from anthropic import Anthropic
from pydantic import BaseModel, ValidationError, Field
import json
import json_repair
 
client = Anthropic()
 
class Inner(BaseModel):
    title: str = Field(min_length=1)
    tags: list[str] = Field(max_length=20)
    score: float = Field(ge=0.0, le=1.0)
 
class Result(BaseModel):
    result: Inner
 
def extract(input_text: str):
    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        system="あなたは厳密な JSON 抽出機です。",
        messages=[
            {"role": "user", "content": f"次の文章から構造化データを抽出してください:\n\n{input_text}"},
            {"role": "assistant", "content": '{\n  "result":'},
        ],
    )
    block = resp.content[0]
    if block.type != "text":
        raise RuntimeError(f"unexpected block: {block.type}")
    raw = '{\n  "result":' + block.text
 
    try:
        return Result.model_validate_json(raw)
    except (json.JSONDecodeError, ValidationError):
        # json_repair は破損 JSON を修復する PyPI ライブラリ
        repaired = json_repair.repair_json(raw)
        return Result.model_validate_json(repaired)

json_repair ライブラリ(GitHub)は、Layer 3a の機械修復に相当する処理をワンライナーで提供してくれます。私の本番では Python 側の Layer 3a を全てこのライブラリに任せています。

本番運用の Before / After

私のプロダクト(個人 SaaS、月間 4,200 リクエスト前後)での実測値を共有します。

  • 導入前(Prefill のみ)
    • パース成功率: 99.34%(28 件 / 4,200 が失敗)
    • 月次運用アラート: 11 回
    • フェイルオーバー時間: 平均 14 分(ユーザーが報告 → 私が手動修復)
  • 導入後(4 層防御)
    • パース成功率: 99.98%(1 件 / 4,200 が Layer 4 撤退、それも空配列で UX は維持)
    • 月次運用アラート: 1 回以下
    • フェイルオーバー時間: 0 分(自動撤退)
    • 追加コスト: 月 850 円程度(Layer 3b の Haiku 修復呼び出し)

数字以上に大きかったのは、深夜アラートが減って自分の睡眠時間が安定したことです。個人開発で AI を本番に乗せると、運用品質が直接「自分が休めるかどうか」に効きます。離れて暮らす子どもたちと過ごせる時間を守るためにも、こうした防御層は必要なのだと、設計を通して静かに確信しています。

注意したい設計判断

最後に、自分が踏んだ落とし穴を 3 点だけ共有します。

  1. Layer 3b の修復に Sonnet を使わない: コスト効率が悪いだけでなく、修復タスクは「壊れた JSON を直す」だけのシンプルなタスクなので、Haiku の方がむしろ余計なことをしない傾向があります。
  2. Schema を変更したら Layer 3b のプロンプトも同期する: Schema 文字列をプロンプトに埋め込んでいるので、ここを忘れると修復が無意味な構造を作って戻してきます。私は Schema を pricing.ts のように一元管理しています。
  3. Layer 4 のフォールバック値は『空』ではなく『安全な既定値』にする: 検索系なら空配列でいいですが、計算系で 0 を返すと UI が「無料」と表示する、みたいな副作用が起きます。ドメインに応じてフォールバック値を設計してください。

関連して、JSON 出力そのものの設計を深めたい方には、Claude API の構造化出力を実務で使いこなす と Claude API JSON 出力のパースエラー対処 がそれぞれの観点で参考になります。プロンプトキャッシングと組み合わせる場合は、Claude API のプロンプトキャッシングでコスト最適化 もあわせて読むと、Layer 3b の修復コストをさらに抑える工夫が見えてきます。

次に試すこと

この設計をご自身のプロダクトに移植する最初の一歩として、まず Layer 1 + Layer 2 だけを 1 つの抽出処理に組み込み、1 週間ログを取ってみてください。失敗率の実数を把握することで、Layer 3 が本当に必要か、3a だけで十分か、3b まで要るかが具体的に判断できます。私自身、この計測なしに Layer 3b を入れていたら過剰設計になっていました。最初は計測、それから防御、という順番が結局のところ一番安く済みます。

実装の同じ課題に取り組んでいる方の参考になれば幸いです。お読みいただき、ありがとうございました。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-07-13
検索の精度は二段目で決まる — 埋め込みで粗く集め、Claude で並べ直す個人ナレッジ検索
埋め込み検索だけでは「意味は近いが答えではない」候補が上位に残ります。粗く集めた候補を Claude で並べ直す二段検索の設計を、構造化スコアリング・棄権・コスト試算まで個人ナレッジ検索の実装でまとめました。
API & SDK2026-07-13
Claude API の同時実行を束ねる — シングルフライトで重複推論とキャッシュ・スタンピードを防ぐ
同一プロンプトが同時に何度も Claude へ飛ぶ重複推論を、シングルフライト(request coalescing)で束ねる設計です。プロセス内・分散環境の実装、ジッター付きリトライ、負のキャッシュまで実測付きでまとめました。
API & SDK2026-07-03
同時リクエスト数はいくつまで持つのか — Little の法則と実測メモリで決める Claude API 本番デプロイのインフラ要件
同時リクエスト数・待ち行列長・メモリはいくつに設定すべきか。Claude API 本番デプロイのインフラ要件を Little の法則と実測ハーネスで数字から導く手順を、夜間バッチで OOM を踏んだ経験をもとに整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →