397問の構造化解説を自動生成したら品質が壊滅的だった
ある資格試験対策のiOSアプリを個人開発しています。問題数は約400問、8カテゴリ。内容についてはいろいろあるので伏せますが、この記事ではグラップラー刃牙の問題だと思って読んでください。
間隔反復(FSRS)で問題を出すだけのアプリでしたが、解説UIを強化したくなりました。単なるテキスト解説ではなく、4層構造の「構造化解説」を実装することにしました。
public struct EnhancedExplanation: Codable, Equatable, Sendable {
public let correctSummary: String // Layer 1: 正解の要約(≤200文字)
public let contrastTable: [ContrastEntry] // Layer 2: 選択肢ごとの判定・解説
public let keyPhrases: [String] // Layer 3: 3-6個の技術用語
public let relatedConcepts: String? // Layer 4: 関連概念(optional)
}
まず3問だけ手作りでサンプルを作りました。Claude Codeと対話しながら、correctSummaryの書き方、contrastTableの粒度、keyPhrasesの選び方を詰めました。この3問は品質が高かったです。
残り397問をどうするか。Claude Codeに相談したところ、Pythonスクリプト(正規表現)で既存の解説テキストから構造化データを一括抽出する方針になりました。
結果はこうでした。
| 問題カテゴリ | 具体例 |
|---|---|
| OCRスペース残存 |
"地 上 最 強", "消 力(シャオリー)"
|
| マーカー除去失敗 |
"に範馬勇次郎、に花山薫" → 意味不明 |
| 図表テキスト混入 | "ッッ <KO> <KO> (地下闘技場) underground)" |
| correctSummary品質 | 接続詞で開始、文途中切断、要約になっていない |
| 選択肢解説の重複 | A/B/D の解説が全く同じテキスト |
| 文の途中切断 | 200文字制限で単語途中でぶった切り |
シミュレータで確認した瞬間、「これは全部やり直しだ」と思いました。
そしてふと気づきました。手作りした3問だけが段違いに良い。それはClaude Codeが直接書いたものでした。
Claude Codeが提案した3つの選択肢と、見落とされた選択肢D
品質の問題を伝えると、Claude Codeは3つの改善案を提示してきました。
- A: 正規表現の改善 — OCRスペースの正規化パターンを追加し、抽出精度を上げる
- B: Claude APIで1問ずつ生成 — API経由でSonnetに構造化データを生成させる(推定$2-5)
- C: ハイブリッド — 正規表現で8割抽出し、品質の低い問題だけAPIで再生成
妥当な提案に見えます。ですが、何かが引っかかりました。
手作りの3問は、目の前のClaude Code自身が書いたものです。 同じ品質で残り397問を書ける能力が、まさにこの会話の中にあります。なぜそれを選択肢に入れないのでしょうか?
「Maxプラン内で、お前自身が直接生成すればいいのでは?」
そう伝えて初めて、Claude Codeは「確かにその方法が最適です」と認めました。これが選択肢D: Claude Code自身による直接生成でした。
なぜClaude Codeは自分の能力を提案しないのか
この出来事は面白い問いを投げかけます。
Claude Codeは高機能なLLMそのものです。テキスト生成、構造化データの作成、文脈理解 — すべてが得意分野のはずです。なのに、データ生成タスクでは「正規表現 → API」という外部手段を提案し、自分自身を選択肢に含めませんでした。
いくつかの仮説を考えました。
「ツール使用者」としてのセルフイメージ。 Claude Codeはコード実行・ファイル操作が主な仕事だと認識しています。「397問のJSONを生成して」と言われたとき、まずコードを書こうとするのは自然な反応です。自分自身がデータ生成エンジンになるという発想は、その自己認識の外にあります。
「API呼び出し=外部サービス」という暗黙の前提。 Claude APIとClaude Codeの中で動いているモデルは同じ系統です。しかしClaude Codeにとって、APIは「呼び出すもの」であり、自分自身は「呼び出す側」という区別があります。
バッチ処理パターンの不在。 「20問ずつ読んで構造化JSONを生成する」というワークフローは、Claude Codeの定型パターンにありません。コードを書いてループ処理する、あるいはAPIを叩いてループ処理する — そういうパターンは知っています。しかし「自分自身がループの中で生成し続ける」というパターンは、提案の引き出しに入っていません。
結論として、ユーザーの「待って、それ自分でできるよね?」という介入が最適解を引き出しました。
実践: Maxプランでのバッチ生成ワークフロー
方針が決まれば、実行は速かったです。
準備: バッチ入力ファイルの生成
まずPythonスクリプトで、397問を20問ずつのバッチ入力ファイルに分割しました。
397問(手作り3問を除外)
↓ prepare_enhanced_batches.py
21個のバッチ入力ファイル (batch_01.txt 〜 batch_21.txt)
各バッチ入力ファイルは、問題文・選択肢・正解・既存解説をプレーンテキストで並べたものです。
# Batch 01 (20 questions)
=== category01_q004 ===
問題文: 範馬勇次郎が使用する技の中で、全身の筋肉を
鬼の顔のように隆起させる現象を表す用語として、
最も適切なものを選べ。
選択肢:
A: 消力(シャオリー)
B: 紐切り
C: マッハ突き
D: 鬼の背中
正解: D
解説: 範馬勇次郎の「鬼の背中」についての理解を問う問題です。...
生成: Claude Codeによる直接生成
Claude Codeにバッチ入力ファイルを読ませ、手作りサンプル3問を品質基準として提示し、同じフォーマットのJSONを生成させました。
品質基準は明確にしました。
-
correctSummary: 「正解は{X}です。」で開始、2-3文、200文字以内、OCRアーティファクトなし -
contrastTable: 各選択肢に固有の解説、判定ラベル付き、150文字以内 -
keyPhrases: 3-6個の技術用語、試験に関連するものだけ -
relatedConcepts: 学習ヒントまたはnull
21バッチを数セッションに分けて処理し、完了まで約5.5時間。
後処理: 正規化とバリデーション
Claude Codeが出力するJSONは、セッションをまたぐと微妙にフォーマットが揺れました。contrastTableのキー名不統一が96問で発生し、別途修正が必要でした。また選択肢の破損が7問。これらをバリデーションスクリプトで検出・修正しました。
最終的に、Pythonテスト52件・Swiftテスト542件がすべて通過しました。
数値まとめ
| 項目 | 値 |
|---|---|
| 再生成対象 | 397問 |
| バッチ数 | 21(20問/バッチ、最終バッチは8問) |
| questions.jsonの変更量 | +33,073行 / -439行 |
| contrastTable正規化 | 96問 |
| 選択肢破損修正 | 7問 |
| 追加コスト | $0(Maxプラン定額内) |
Before / After
正規表現抽出とClaude Code直接生成で、品質がどう変わったかを示します。具体的な問題内容は伏せますが、構造の違いを見てください。
Before(正規表現抽出):
{
"correctSummary": "正解はCです。一方、 「消力(シャオリー)」とは、特定の格闘技...",
"contrastTable": [
{
"choice": "A",
"judgment": "不適切",
"point": "現時点では、地下闘技場で稼動して いる選手はすべて..."
}
],
"keyPhrases": ["消力", "鬼の背中", "アメリカ", "範馬勇次郎"]
}
OCRスペース(稼動して いる)が残り、keyPhrasesに試験と無関係な「アメリカ」が混入しています。correctSummaryは文が途中で途切れています。
After(Claude Code直接生成):
{
"correctSummary": "正解はCです。消力(シャオリー)は全身の関節を脱力させることで打撃の衝撃を無力化する中国武術の技法です。",
"contrastTable": [
{
"choice": "A",
"judgment": "不適切",
"point": "握力の強さは消力と鬼の背中の分類基準ではありません。"
}
],
"keyPhrases": ["消力", "鬼の背中", "脱力", "中国武術", "範馬勇次郎"]
}
OCRアーティファクトが消え、各選択肢に固有の教育的な解説がつき、keyPhrasesは試験に直結するものだけになりました。
もう1問の例です。
Before:
correctSummary: "正解はDです。それは無限にある可能性からの探索であり、地上最強の生物が同様の ことを行うのは非常に困難です。"
After:
correctSummary: "正解はDです。ドリアンの脱獄とは、あらゆる拘束手段を無効化し脱出することの本質的困難さを示す概念です。"
Beforeでは「それは」が何を指すのか不明で、OCRスペースも残っています。Afterでは概念の定義から入り、1文で正確に説明しています。
「まず自分で試す」フェーズの重要性
今回の体験から、LLMを使ったデータ生成の実践的なワークフローが見えてきました。
いきなりAPIスクリプトを書かないでください。まずClaude Codeで試作しましょう。
Claude Code(またはChatGPTのようなチャットインターフェース)で数十件のデータを試作すると、以下が確立されます。
- 品質基準 — 「良いデータ」がどういうものか、具体例ができる
- プロンプトパターン — どういう指示を出せば期待する品質が出るか
- エッジケースの把握 — 図表参照問題、穴埋め問題など、パターン別の対応策
これらが手元にある状態でAPIスクリプトを書くのと、ない状態で書くのでは、プロンプトの品質がまったく違います。
そしてもうひとつ。個人プロジェクトなら、試作フェーズでそのまま完走できることがあります。
| Claude Code 直接生成 | API スクリプト | |
|---|---|---|
| 対象 | 個人・1回限り | チーム・再実行・CI/CD |
| 再現性 | 低い(会話依存) | 高い(コードとして残る) |
| コスト | Maxプラン定額内 | 従量課金 |
| プロンプト管理 | 暗黙的 | バージョン管理可能 |
| 品質検証 | 即座にフィードバック | スクリプト実行後に確認 |
| 立ち上げ速度 | 速い(対話で開始) | 遅い(コード実装が必要) |
個人プロジェクトで1回限りの生成なら、Claude Code直接生成で十分です。チームで再利用する、CI/CDに組み込む、バージョン管理したい — そういう要件があるなら、試作フェーズで得た品質基準とプロンプトをAPIスクリプトに移植すればよいでしょう。
「直接生成 vs API」は二者択一ではありません。常に試作フェーズから始めましょう。
まとめ: LLMとの協働で「人間の直感」が果たす役割
今回の一連の流れを振り返ります。
- 正規表現で397問を一括生成 → 品質崩壊
- Claude Codeが3つの改善案を提示 → すべて「外部手段」
- 人間が「お前自身で生成しろ」と介入 → 最適解に到達
- 追加コスト$0、5.5時間で完了
Claude Codeは強力ですが、自分の能力の使い方を知らないことがあります。「正規表現 → API」という思考パターンに沿って提案するため、「自分自身がデータ生成エンジンになる」という選択肢が抜け落ちます。
AIツールを使いこなすとは、ツールの盲点を見抜くことです。Claude Codeが提案しない選択肢の中に、最適解が眠っていることがあります。「待って、それ自分でできるよね?」と問いかける直感は、まだ人間の側にあります。
タイムライン(2026-02-11)
- 08:51 — 構造化解説UIの初期実装(手作りサンプル3問)
- 09:14 — 正規表現スクリプトで全400問にデータ搭載
- 11:14 — Claude Codeで397問を再生成し品質大幅改善
- 14:28 — contrastTableのキー名不統一を正規化(96問修正)
EnhancedExplanation データモデル(Swift)
/// 選択肢ごとの判定・解説エントリ
public struct ContrastEntry: Codable, Equatable, Sendable {
public let choice: String // "A", "B", "C", "D"
public let judgment: String // "適切", "不適切", "正解", "不正解"
public let point: String // 解説テキスト(≤150文字)
}
/// 構造化された解説データ - 4層構造
public struct EnhancedExplanation: Codable, Equatable, Sendable {
public let correctSummary: String // Layer 1: 正解の要約(≤200文字)
public let contrastTable: [ContrastEntry] // Layer 2: 選択肢ごとの判定・解説
public let keyPhrases: [String] // Layer 3: 3-6個の技術用語
public let relatedConcepts: String? // Layer 4: 関連概念(optional)
}
バッチ生成ワークフロー全体像
397問(手作り3問を除外)
↓ prepare_enhanced_batches.py
21個のバッチ入力ファイル (batch_01.txt 〜 batch_21.txt)
↓ Claude Code が20問ずつ読み → JSON生成
21個のバッチ出力ファイル (batch_01_output.json 〜 batch_21_output.json)
↓ 3種類のJSON形式を正規化・マージ
questions.json に統合
↓ validate_enhanced.py
品質バリデーション(4層チェック)→ 全テスト通過