先に書き手の素性を明かしておきます。私は 20 年以上ソフトウェア開発をやってきたエンジニアで、いまは AI 駆動開発とその環境構築の支援を生業にしています。このシステムは、その本業の合間に組んだものです。
構成を一言でいうと、GitHub Actions の cron (JST 06/12/18 時) が生成スクリプトを起動し、RSS 16 ソースと GitHub Releases 8 リポジトリから候補を収集、スコアリングで 2 件を選び、Claude API で記事化して microCMS に POST する——これだけです。1 記事あたりのコストは約 9 円。運用に残っている人間の仕事は、出力を読んでプロンプトと選定ロジックを調整することだけです。
こう書くと「AI で記事を量産する話」に見えますが、伝えたいのはむしろ逆です。量産のパイプラインは初日に動きました。難しかったのは量産したものの品質統制で、本記事はその設計と試行錯誤に 100 日以上かけた実録です。途中で自分のサイトが「AI 量産サイト」にしか見えなくなって青ざめたこともあれば、Search Console のデータに自分の思い込みを覆されたこともあります。
以下では、その全過程を当時の判断と根拠ごと時系列で追っていきます。コンテンツ自動化に限らず、LLM を業務パイプラインに組み込む設計の参考になれば嬉しいです。
01誕生 — 動くものは 1 日でできた
最初のコミットは 2026 年 4 月 1 日です。動機は 2 つ。サービスサイトに「更新され続けている」生気がほしかったこと。そして、本業で謳っている AI 駆動開発を、AI エージェントがフルオートメーションで回す仕組みとして実証したかったことです。
初期実装は素朴でした。rss-parser でフィードを取得し、Claude
API に記事化させ、返ってきた JSON を microCMS に POST する——それだけの小さなプログラムです。実装は
AI に書かせ、私はレビューと結線だけ。この最小構成は本当に 1 日で本番投入まで行きました。この種のパイプラインの立ち上げ速度は、AI
以前の感覚だと信じられないレベルです。
ただし象徴的なことに、稼働初日に積まれたコミットの中に、早くもこれが混ざっています。
$ git show -s --format='%ad %h %s' --date=short 8637f14 2026-04-01 8637f14 fix: add JSON parse retry on malformed Claude response
LLM に「JSON で出力せよ」と指示しても、一定確率で壊れた JSON が返ってくる。このときは「パースに失敗したらもう 1 回 API を呼べばいい」と流しました。安直なリトライで蓋をしたこの問題は、112 日間の最終盤にもっと深い形で戻ってきます (第 8 章)。
初週の関心事は、恥ずかしながら「賑やかし」でした。書き手のペルソナを 5 人に増やし、数日後には 14 人へ。税理士、製造業の経営者、飲食チェーンの運営者——多彩な肩書きの架空の書き手に、それぞれの一人称で語らせる。エンジニアとしては面白い実装でしたが、この発想がのちに最大の反省点になります。
02「何を書かせるか」が 9 割だった
生成が回り始めてすぐ分かったのは、書かせること自体より「何を書かせるか」のほうが難しいということです。RSS を束ねると 1 日数百件の候補が流れてくる。そこから「今日この 2 件」を毎回自動で選ぶ問題は、生成よりよほど設計しがいがありました。
最終的に選定はこういう複合スコアに落ち着いています。
score = priority * 30 // ソース種別の重み (公式リリース > 一般メディア)
+ keywordMatches * 10 // 注目キーワードとの一致数
+ feedRank * 5 // フィード内の掲載順
+ recency; // 新しさ: 0 / 10 / 20 の 3 段階 これに、ニュース性の薄いフィードだけキーワード 2 語以上一致を要求する足切り、参考記事本文の取得 (最大 5,000 字をプロンプトに注入)、技術系の話題なら Hacker News Algolia API で海外の反応を添える、といった前処理が重なります。
一番の難所は重複除外の閾値調整でした。同じニュースは複数メディアが報じるので、過去記事のタイトルと共通単語数を比較して弾くのですが、最初に「共通 1 語で除外」と厳しく振ったら false positive だらけになり、書けるネタが払底する日が出ました。「2 語以上」に緩めると今度は同話題がすり抜ける。要は precision と recall の綱引きで、評価データセットがあるわけでもないので、本番の出力を眺めながら閾値と比較対象 (直近何件まで見るか) を数回に分けて調整するしかありませんでした。
LLM パイプラインの品質は、モデルに渡す前の入力選定でほぼ決まります。ガベージイン・ガベージアウトは LLM でも健在で、悪いシードから良い記事は生まれません。実感として、プロンプト改善よりスコアリングと重複除外の改善のほうが、読者に見える品質への寄与が大きいフェーズが長く続きました。
03最初の挫折 — 自分のサイトが AI 量産サイトに見えた日
4 月 27 日、記事が 300 本ほど溜まった時点で、初めて腰を据えてサイト全体を見返しました。そして青ざめました。
まず書き手。14 人のペルソナのアバターは DiceBear で自動生成した SVG で、少しリテラシーのある読者なら一目で「実在しない書き手」と見抜けます。次にテーマの散漫さ。NBA 中継の機材構成の隣に、ローカル LLM の記事と飲食店向け AI 活用が並ぶ。極めつけはタイムスタンプで、同じ日に 8 本以上公開された日がありました。人間のオペレーションでないことは、本文を読むまでもなくメタデータから明白でした。
これは検索評価としても危険な状態です。Google は検索順位の操作を主目的とした 大量生成コンテンツをスパムポリシー違反と明記しており、 有用性評価 (いわゆる E-E-A-T) でも経験と専門性の実在を重視します。当時の Search Console の実測も、それを裏付けていました。記事は検索結果に表示はされる (例:「ollama hermes」で表示 14 回) がクリック 0——インプレッションだけあって CTR が実質ゼロという典型的な「刺さっていない」形です。
この棚卸しで「テーマをサービスのターゲットに絞る」「公開ペースを落とす」という改善案を書き出しました。興味深いのは、このうち前者が 3 か月後にデータで覆されることです (第 6 章)。レビューは必要だが、レビューの結論を計測なしで確定してはいけない——後から効いてくる学びでした。
04プロンプトを構造で考える
5
月に入ると、プロンプトが場当たり的な指示の継ぎ足しで肥大し、限界が見えてきました。文字数を守らない、締めの文が毎回同じ、長い記事が
max_tokens に当たって途中で切れる。症状ごとに指示を 1 行足すたび、プロンプトは
diff の追えない泥団子になっていきます。
ここで一度立ち止まり、Anthropic
公式の推奨構造に沿って作り直しました。柱は 2 つ。プロンプトの 2 層分離——役割・絶対ルール・出力スキーマなど不変の指示を system
プロンプトへ、シード記事本文や視点指定など毎回変わる内容を user
メッセージへ。そして system 側に
cache_control: { type: "ephemeral" } を付けたプロンプトキャッシュです。同一ラン内の 2 記事目以降は、約 2,200 トークンの静的部分が約
1/10 の単価で読み込まれます。あわせて max_tokens を 16384
に引き上げ、途中切断の問題も潰しました。
プロンプトキャッシュは前方一致で効くため、「静的な部分を前に、可変を後ろに」という並び順の設計がそのままコスト設計になります。継ぎ足し運用だと静的部分に日付や記事固有の値が混入して、気づかないうちにキャッシュが無効化されがちです
(キャッシュヒットは usage の
cache_read_input_tokens で検証できます)。層は最初に分けておくのが正解でした。
この頃タグ機能を追加した際、「CI/CD」というタグ名のスラッシュが
slug 変換関数を素通りし、URL が
/blog/tag/CI/CD/ に分裂してビルドが 2 回連続で失敗していたことが後から発覚しました。気づいたのは翌日です。cron
は成功も失敗も黙って積み上げるので、自動化の信頼性は「動くこと」ではなく「壊れたら気づけること」で決まる——監視と通知は生成品質と同格の要件でした。
05架空の体験談をやめる
7 月 6 日、このシステム最大の編集判断をしました。「実在しない人の実体験」を生成しない、という決定です。
それまでのペルソナは年齢や来歴を持ち、「私が現場で経験した話ですが」と一人称で語っていました。読み物としては生きます。しかしその経験は存在しない。4 月に感じた「うさんくささ」の正体はこれでした。技術的に簡単に作れてしまうからこそ、作れることと作ってよいことは別だと、運用を通じて腹落ちした瞬間です。
そこでペルソナ制を「専門分野レンズ制」にリファクタリングしました。個性・年齢・体験談のフィールドを全部落とし、残したのは「どの専門分野の視点で解説するか」「どの角度では解説しないか」だけ。文体も全員です・ます調に統一。ペルソナは語り手オブジェクトから、解説の切り口を決める純粋なフィルタへと責務を変えました。過去記事には「AI による寄稿形式の実験的コラム」という開示注記を一括付与しています。
同日、対象読者もエンジニア・IT 実務者に振り切り、タグ体系とニュースソースを技術系に刷新しました。そしてもうひとつ、地味に痛い発見が。「1 ラン 3 記事 → 2 記事に減らしたはず」の設定が、実は効いていなかったのです。
# blog-auto-generate.yml — ワークフロー側にもフォールバック値が居た
env:
MAX_ARTICLES: ${{ inputs.max_articles || '3' }} # ← スクリプト側の既定値 '2' を上書き
修正はワークフロー側のフォールバックを削除し、既定値をスクリプト 1
か所に寄せるだけ。コミットメッセージは
fix: use generate-blog.js as single source of truth for
MAX_ARTICLES default
です。同じ既定値を 2 か所に書けば、いつか必ず食い違う。SSOT は標語ではなく事故防止策だと、20
年やっていても改めて刺さりました。
06検索データに思い込みを覆される
7 月 19 日、SEO を本気で見直すため、Search Console API
を直接叩く CLI を書きました。サービスアカウントの JWT 署名を
node:crypto で自前実装した、依存パッケージゼロの小さなスクリプトです。ダッシュボードのスクリーンショットを眺める運用をやめ、クエリ単位の実数をターミナルで取れるようにした——ここから出てきた数字に、今度は良い意味で裏切られました。
検索流入の主力は、4 月に「ターゲット外」と切り捨てようとしたニッチな技術記事群だったのです。ローカル LLM と商用モデルの比較、特定 OSS ツールの設定手順——自己診断で「サービスと無関係」と断じたロングテールが、3 か月かけて着実にクリックを集めていた。逆に「ターゲットに寄せた」つもりの記事は、競合の強い商用キーワードで埋もれていました。
これを受けて方針を「絞る」から「伸びているものを伸ばす」に転換しました。実流入キーワードのスコア加点を 2 倍にし、タイトル生成をルール化 (固有名詞を前方に、数字を入れる、内容の要約で終わらせない)、既存 11 本を検索意図に合わせて改題。すべて生成側プロンプトと選定ロジックの変更なので、以後の記事には自動で適用されます。
計測の落とし穴が「セルフノイズ」です。運営者自身の順位確認クエリも表示回数にカウントされるため、インプレッションは簡単に自己汚染されます。信頼できる指標はクリック数と「自分が検索していないクエリの出現」。この前提なしに表示回数を眺めると、また別の思い込みを育てるだけです。
07「感想文」との決別
検索の土台を整えて読み返すと、今度は内容そのものに不満が残りました。正直に言えば、読んでも面白くない。ニュースを紹介して「注目されます」「今後が楽しみです」で締める、当たり障りのない感想文になりがちだったのです。
7 月 20 日、記事設計を「読了後に何かができるようになる」方向へ作り替えました。柱は記事タイプ制。シードの性質からハンズオン型 / 意思決定ガイド型 / 落とし穴解説型 / 解説型のどれで書くかを決定論的に割り当て、タイプごとの構成ガイドを user 層に注入します (system 層は不変に保ち、キャッシュを温存)。
ここで気に入っている品質統制の設計が 1 つあります。ハンズオン型は、シード本文に実在するコマンドや設定例がある場合にしか割り当てないというガードです。LLM は「それらしい手順」を平然と創作し、創作された手順は読者の環境を壊しかねません。生成後の検品で捕まえるより、入口の割り当て条件で発生自体を防ぐほうが確実でした。
同日、生成モデルを
Claude Sonnet 5
に移行しました。モデル ID はコード内で GENERATION_MODEL 定数
1 か所に集約してあるので、移行自体は 1 行の変更です。注意点は
thinking を明示的に無効化したこと (Sonnet 5 は未指定だと
adaptive thinking が走り、この用途ではコストが余分にかかる)。価格は導入期間の割引で $2/$10 per MTok (入力/出力)。1 記事の出力は 3,000〜3,900
トークンで 8〜10 円、1 日 6 記事でも 50〜60 円/日、月 1,500〜1,800
円程度です。
もうひとつ、公開直前に自分でレビューして直した点があります。当初のプロンプトは冒頭で「この記事を読み終えると〜できるようになります」と効能を断定する設計でした。読者に対して上から目線です。「〜の参考になれば幸いです」に改めました。AI は指定したトーンを何百記事でも忠実に再現します。だからこそトーンの選択は、プロンプトに書いた瞬間から編集責任になります。
この日、3 か月間気づかれなかったサイレント障害も見つけました。ある情報源のスクレイピングが、ページ改修以降ずっと 0 件だったのです。原因は見出し要素の先頭に混入した U+E09A (Unicode 私用領域) ——不可視のグリフ 1 文字で日付見出しの正規表現が不一致になっていました。修正は非 ASCII の除去 1 行。問題は検知の方で、取得 0 件は「今日はニュースがない」と区別がつかず、warn ログを能動的に読まない限り発見できません。黙って劣化する経路にこそ監視を置く、という教訓です。
08最後の敵は制御文字だった
新モデル移行初日の夜、本番 cron のログを検証していて嫌な行を見つけました。
[WARN] JSON パース失敗(1回目)→ リトライ # 生成 2 本中 2 本で発生 [WARN] JSON パース失敗(1回目)→ リトライ # 移行後累計 4/6 = 67%
第 1 章で安直なリトライに封じた JSON パース失敗が、発生率 67% で戻ってきたのです。実害としては記事は壊れていないものの、失敗のたびに有料の API を呼び直すので、該当記事は生成コストが倍になります。
原因は二段構えでした。まず見つかったのは、認めるのが恥ずかしい設計ミスです。system
プロンプトは JS
のテンプレートリテラルで組んでいるのですが、その中の「JSON
エスケープ規則」の説明文でバックスラッシュの段数を 1
段間違えており、レンダリング後のプロンプトには「" は \\" に置き換えよ」という誤った仕様が届いていました (正しくは \")。旧モデルはこの誤指示を都合よく無視してくれていた。新モデルは指示追従が忠実なぶん、誤指示まで忠実に実行して二重エスケープの壊れた JSON
を返した。モデルの精度が上がるとプロンプトの粗が顕在化する——モデル移行のたびに起こり得る現象だと思います。
しかし指示を直しても失敗は残りました。DRY_RUN に詳細ログを仕込んで特定した真因は、生成本文に混入する生の制御文字 (改行・タブ) です。JSON の文字列リテラル内に生の制御文字があると仕様上パースは失敗します。そしてこれはプロンプトでいくら注意しても、確率的に混入し続けます。
最終的な答えは「プロンプトで頑張るのをやめる」でした。パース失敗時に、文字列リテラル内の制御文字だけをエスケープ表現へ置換する修復関数をコード側に実装したのです。骨子はこうです。
// sanitizeJsonControlChars — 文字列リテラル内フラグを持って 1 文字ずつ走査
for (const ch of jsonText) {
if (inString && isControlChar(ch)) out += escapeChar(ch); // \n → \\n など
else out += ch; // 構造文字 (" { } [ ] , :) には一切触れない
// …クォートとバックスラッシュの状態遷移で inString を管理
} 設計上のこだわりは「構造文字に触れない」ことです。修復ロジック自体がバグると「パースは通るが本文がサイレントに壊れる」という、リトライより悪い事態を招きます。置換対象を文字列内の制御文字に限定すれば、失敗しても元の parse エラーに戻るだけで、壊れたものを公開する経路は原理的に存在しません。API リトライ (非決定的・コスト倍) の前段に、この決定論的で無料な修復層を挟む。単体テスト 4 ケースと DRY_RUN での検証では、発生した失敗はすべてローカル修復のみで解消し、API リトライは 0 回でした。
LLM への指示は分布を動かすだけで、保証は与えません。信頼性が要る箇所は、プロンプトで発生率を下げつつ、最後は決定論的なコードで受け止める二段構えが必要です。初日に安直なリトライで蓋をした問題が、112 日かけてこの設計原則に落ち着きました。
終章 — 112 日間の学びの総括
最後に、112 日を 5 つの原則に要約します。
- 生成はコモディティ、統制が資産。 パイプラインは 1 日で組めます。競争力になるのは、何を書かせ・何を書かせないかを決める選定と制約の設計です。
- プロンプトは確率、コードは保証。 指示は分布を動かすだけ。信頼性が要る経路には決定論的なフォールバックをコードで持つ。
- 自己診断よりも計測。 4 月の「正しい」レビュー結論は、7 月の Search Console の実数に覆されました。判断の根拠を主観から計測に移すこと自体が改善でした。
- 正直さは仕様にできる。 架空の書き手の廃止・AI 開示・「実在するコマンドがなければ手順を書かない」ガード——倫理はプロンプトと割り当てロジックに実装できます。そして実測の範囲では、正直さに振っても検索流入は落ちませんでした。少なくとも両者はトレードオフではなかった、というのがこの 112 日の結論です。
- コストはもう障壁ではない。 1 記事 8〜10 円、月 1,500〜1,800 円。差がつくのは支出ではなく設計です。
なお、この記事自体もシステムの当事者である AI (Claude) と私の共同執筆です。コミットログとセッション記録から時系列を再構築し、構成と判断は人間が、下書きと図解は AI が担いました。この分担のあり方も、112 日の学びのひとつだと思っています。
振り返れば、本業の合間に半ば実験で始めたシステムでした。その気楽さのツケは、架空ペルソナや安直なリトライといった形で、後から順番に請求されてきました。そして 112 日でやったことを並べてみると——SSOT、フェイルセーフ、計測に基づく判断——結局は、ソフトウェア開発の基本をひとつずつ律儀にやり直しただけです。AI で生成が一瞬になっても、基本をおざなりにすれば痛い目に遭う。むしろ作るのが速くなった分だけ、基本の効き目は大きくなっている。それがこの実験の結論です。この記事が、これから同じ道を歩む方の参考になれば幸いです。