メインコンテンツへスキップ
← 記事一覧に戻る
·運用·10 min read

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

自動化CI/CDClaude Code設計個人開発
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つ:

  1. updated_at / id フィールドの欠落: Qiita CLI は updated_at: 'ISO8601'(シングルクォート必須)と id: '...'(新規は空文字列)を必須とするが、LLM が生成した frontmatter でこれらが抜けていた。3記事で欠落が発生し、CI の --all モードが全件対象のため全体停止した。
  2. 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 に届いてから失敗するより、ローカルで(あるいは自動化スクリプト内で)止まるほうがフィードバックループが短くコストが低いです。

今日やること

  1. 自分の自動化フローで「LLM が生成して下流のシステムに渡す」ステップを列挙する: frontmatter 以外にも、JSON API レスポンスのパース、CSV 形式の生成など、構造依存のデータ受け渡しがあれば候補です。

  2. そのステップの「失敗コストの広がり」を確認する: 1件の失敗が全体停止を引き起こすなら、ゲートの優先度は高い。

  3. 小さな検証スクリプトを1つ書いてみる: 必須フィールドの存在チェックだけでも、最初のゲートとして機能します。完璧を目指すより、まず動くゲートを1つ持つことが先です。

シェア

コメント

投稿にはログインが必要です(メールアドレスのみ・パスワード不要)。入力内容は保持され、ログイン後そのまま投稿されます。