LLMに任せた自動化はプロンプトでなくゲートで動かす—validate-frontmatter.sh実装ログ

LLM に自動化を任せていると、いつかこの壁にぶつかります。「プロンプトに正確なフォーマットを書いたのに、生成物が微妙にずれていて、下流のシステムが壊れる」——。
この記事は、そのパターンで Qiita の CI が10日・14回連続失敗した実体験から導いた設計判断の記録です。対策として実装した validate-frontmatter.sh の中身と、「テキスト指示をやめてゲートを置く」という設計転換の話をします。
結論
LLM が生成する構造化データ(frontmatter, JSON, CSV など)の品質を「より良いプロンプト」で担保しようとするのは設計ミスです。代わりに、生成直後・push直前に決定論的な検証スクリプトを噛ませ、NGなら処理を止める構造にする。プロンプトは「何を書くか」を指示するが、フォーマットの正確さを保証する責務はゲートが持つ——この分担が、自動化を安定稼働させる設計原則です。
この記事の前提
- masatoman.net の記事(MDX)を content-executor というルーティンで毎日自動生成・push している
- Qiita / Zenn への adapted 記事も同ルーティンの Step5 で自動生成・push
- LLM は Claude (claude-sonnet-4-6)、フォーマット指定はプロンプト内のテキスト指示として渡していた
- 自動化のコードは GitHub Actions で動く、スケジュール実行
読者のよくある詰まり
「プロンプトにフォーマット例を貼ったのに、たまに違う形式で出てくる」——これは LLM 自動化あるあるです。
よくある対応は「プロンプトをもっと詳しく書く」「Few-shot 例を増やす」。しかしそれは壊れる確率を下げるだけで、ゼロにはなりません。自動化ルーティンは数十・数百回と実行されます。低確率のミスでも蓄積すると、あるとき突然「CI が全部落ちた」という状態になります。
そのとき「なぜ今日だけ壊れたのか」を追う時間コストは、ゲートを最初から実装するコストより大きいです。
実際に起きたこと
2026-05-07 から 2026-05-17 の10日間、Qiita の publish CI が連続失敗し続けました。
直接原因は2つ:
updated_at/idフィールドの欠落: Qiita CLI はupdated_at: 'ISO8601'(シングルクォート必須)とid: '...'(新規は空文字列)を必須とするが、LLM が生成した frontmatter でこれらが抜けていた。3記事で欠落が発生し、CI の--allモードが全件対象のため全体停止した。published_atフィールドの残存: 旧 Qiita CLI のフィールドで、v0.5.0 以降は非認識。LLM がテンプレ例として学習したらしいこのフィールドを混入させ続けた。
14回失敗して気づいたのは、自分が「プロンプトを直せば直る」と思い込んで、毎回テキスト指示を書き直していたことです。3回書き直した段階で、これは指示の問題ではなく設計の問題だと判断しました。
CI 復旧後(2026-05-17)の恒久修正として選んだのは、プロンプトの修正ではなく、push 前の決定論的検証の追加でした。
原因分析
テキスト指示で LLM を縛る設計が脆い理由は3点あります。
1. 生成確率はゼロにならない: LLM は確率モデルです。指示が正確でも、低確率で指示から外れた出力を生成します。ルーティンが繰り返されるほど、この低確率が現実の障害になります。
2. 指示の伝達ロスが起きやすい: プロンプトが長くなると、細かいフォーマット指定が中盤に埋もれます。LLM はコンテキスト全体を均等に参照するわけではなく、末尾や強調マーカーに影響されます。
3. 失敗の原因が見えにくい: テキスト指示だけの設計では、失敗時に「今回何が違ったか」を特定しにくいです。ゲートがあれば「このフィールドが欠落していた」と即座に分かります。
判断基準
自分の自動化フローのどこに検証ゲートを入れるべきか、以下の3点で判断します。
1. 下流が構造依存かどうか: push 先の CLI・API・パーサーが特定のフォーマットを前提とするなら、生成→消費の境界にゲートが必要です。Qiita CLI は frontmatter の型・フィールド名を厳密にチェックするため、ここが境界でした。
2. 失敗のコストが広がるかどうか: 1件の失敗が全件停止を引き起こす設計(--all モードなど)は、ゲートの優先度を上げます。1件だけ落ちてそれだけ skip される設計なら、ゲートの緊急性は下がります。
3. 修正ループに人が入るかどうか: 自動化ルーティンは「人がいない時間に動く」前提です。失敗しても誰も気づかない・直せない状況なら、人が介入する前に問題を止めるゲートが必要です。
今回の Qiita CI はこの3点すべてに該当していたため、最優先でゲートを実装しました。
実装:validate-frontmatter.sh
実装した検証スクリプトは以下の項目をチェックします(Qiita 記事を対象):
titleの存在と非空updated_atの存在とシングルクォートフォーマットidの存在(新規は空文字列'')id: nullの自動修正(→id: '')published_at/publishedの混入禁止organization_url_nameの存在slideの boolean 値tagsの件数(1〜5個)
これを content-executor の Qiita push 前に実行し、NG なら push をスキップする設計にしました。push 前に止まるため、CI は落ちません。失敗はスクリプトのエラーメッセージで即時特定できます。
# push 前に実行(NG なら exit 1 で push をスキップ)
bash scripts/validate-frontmatter.sh public/${slug}.md || {
echo "frontmatter NG: push をスキップします"
exit 0
}
key point: ゲートは push の前に置く。CI に届いてから失敗するより、ローカルで(あるいは自動化スクリプト内で)止まるほうがフィードバックループが短くコストが低いです。
今日やること
-
自分の自動化フローで「LLM が生成して下流のシステムに渡す」ステップを列挙する: frontmatter 以外にも、JSON API レスポンスのパース、CSV 形式の生成など、構造依存のデータ受け渡しがあれば候補です。
-
そのステップの「失敗コストの広がり」を確認する: 1件の失敗が全体停止を引き起こすなら、ゲートの優先度は高い。
-
小さな検証スクリプトを1つ書いてみる: 必須フィールドの存在チェックだけでも、最初のゲートとして機能します。完璧を目指すより、まず動くゲートを1つ持つことが先です。