はじめに:AI量産と品質・コンプライアンスのジレンマ
AI(Claude Code)を使うと、記事の初稿を短時間で大量に生成できる。しかしそこには見えにくい落とし穴がある。
生成AIは「それらしい文章を書く」のが得意な分、次のような問題を混入させやすい。
- 「必ず稼げる」「誰でも月10万」といった誇張・断定表現
- 公式確認が取れていない料金・統計の断定
- 運営者の実体験が無いのに、あるかのように書かれた体験談
- 情報の根拠(出典・確認日)が抜けたまま公開
お金・副業のテーマはYMYL(Your Money or Your Life)領域に該当し、検索品質評価で特に厳しく見られる。加えて、誇張表現や根拠なし比較は景品表示法(以下、景表法)違反のリスクを生む。
人手のレビューは疲弊するし、確認漏れが起きる。「書かせる量が増えれば増えるほど、人手チェックの精度は下がる」という構造的な問題がある。
方針:検出できるものは lint で機械化して CI ゲートにする
コンプライアンス上のリスクを全部人間が拾おうとするのには限界がある。一方、次のような項目はパターンマッチで機械的に検出できる。
-
[要確認]マーカーが消えずに残っている -
<Experience>コンポーネントの中身が空のままか「実体験を記入」というプレースホルダのまま - 誇張・断定語の辞書ヒット
- 出典の記載がない
- 確認日(
reviewedDate)が未設定
これらを npm run review の一発コマンドで全記事に走らせ、警告がゼロになるまで draft:false(公開)にしない運用にした。
技術スタックはNode.js(ESM)のみ。外部依存ゼロで、scripts/check-articles.mjs 単体で動く。
チェッカー(check-articles.mjs)が実際に検出する7項目
実装に存在するチェック項目を、なぜそれが必要かの景表法・YMYL観点とあわせて列挙する。
1. 未確定マーカーの残存
// 検出対象(抜粋)
const PLACEHOLDERS = [
'[要確認',
'要確認]',
'TODO',
'(見出し',
'(まとめ',
'(ここに実体験',
'ここに実体験を記入',
// ...
];
AI 生成時に「ここは後で確認」という意味で挿入したマーカーが消えずに公開されるケース。数値や料金に [要確認] が付いていれば、まだ事実確認が済んでいないサインだ。このまま公開すると、誤情報を確定情報として提示することになり、景表法上のリスクがある。
TODO は開発者向けメモだが、記事本文に混入して公開されてしまうことがある。
2. 実体験(<Experience>)の欠落・未記入
const expRequired = ['comparison', 'experience'].includes(type);
const exp = body.match(/<Experience\b[^>]*>([\s\S]*?)<\/Experience>/);
const hasEditorialView = /##\s*編集部の見解/.test(body);
if (!exp) {
if (expRequired && !hasEditorialView) {
issues.push('<Experience>(実体験)も「## 編集部の見解」も無し...');
}
} else {
const inner = exp[1].replace(/\{\/\*[\s\S]*?\*\/\}/g, '').trim();
if (inner.length < 30 || inner.includes('実体験を記入')) {
issues.push('<Experience>の中身が未記入/プレースホルダのままです');
}
}
Google の品質評価ガイドラインが重視する E-E-A-T の "Experience(経験)" に対応する。比較記事・体験記事(articleType: comparison / experience)は、一次情報(使ってみた・試してみた)が価値の核であり、それが無ければ薄い情報記事と判定される。
重要なのは、実体験が無い状態で「あたかも体験したように」AIに書かせてはいけないという点だ(景表法のNG事例5:嘘の体験談)。そのため、実体験が無い場合の代替として「編集部の見解」セクションを置くことも許容している。どちらもなければブロックする。
3. 確認日(reviewedDate)の未設定
if (!fm.reviewedDate) issues.push('reviewedDate(確認日)が未設定です');
料金・キャンペーン情報は変動する。確認日が記録されていなければ「いつ時点の情報か」が読者に伝わらず、古くなったときに気づく手段もなくなる。景表法NG事例7(キャンペーン終了時の情報更新義務)への対応として、全記事に確認日の記載を求める。
4. 出典の記載なし(比較・体験・データ系記事)
if (['comparison', 'experience', 'pillar'].includes(type)) {
if (!body.includes('出典') && !body.includes('source=') && !body.includes('source:')) {
issues.push('出典の記載が見つかりません(比較/体験/データ記事は根拠の明示が必須)');
}
}
ランキング・比較・データを扱う記事で根拠を示さないのは、景表法NG事例2(根拠のないランキング)・NG事例3(不当な比較)に該当しうる。コンポーネントの source= 属性か、本文中の「出典」文字列のどちらかがあればパスとする。
5. 誇張・断定表現の検出
const HYPE = [
'絶対',
'必ず稼',
'必ず儲',
'誰でも稼',
'誰でも月',
'100%',
'簡単に大金',
'確実に稼',
'すぐ稼げる',
'楽に稼げる',
'一攫千金',
];
YMYL 領域(お金・副業)で「誰でも月10万」「必ず稼げる」は景表法に抵触する優良誤認にあたりうる。語彙辞書をチェッカー側で管理することで、AIが文脈を変えながら似た表現を使うパターンも検出しやすくなる。
「悪い例」として引用符の中に入れてある場合の誤検知を防ぐため、「」『』 内のテキスト(40文字以内)を除外してから照合する。
// 実装(抜粋)
const bodyNoQuotes = body.replace(/[「『][^」』]{0,40}[」』]/g, '');
for (const w of HYPE) {
if (bodyNoQuotes.includes(w)) issues.push(`誇張・断定の疑い「${w}」`);
}
6. PR・広告表記の欠落(NG事例1)
アフィリエイトリンクを含む(relatedASP が設定されている)のに、<CTA> コンポーネントも PR/広告/プロモーション 表記も本文にない場合に警告する。消費者庁の景表法ガイドライン(ステルスマーケティング規制)への対応。
なお、テンプレート側でページ全体に広告開示バナーを描画している設計のため、<CTA> か <AffiliateLink> が使われていれば自動で PR 表記が補完される。
7. サムネイル画像の実在確認
thumbnail: /images/xxx.png をフロントマターに指定しているのに、対応ファイルが public/ に存在しない場合を検出する。本番デプロイ後に画像が割れるのを防ぐ。
if (thumb && thumb.startsWith('/')) {
if (!existsSync(join(PUBLIC, thumb.replace(/^\//, '')))) {
issues.push(`thumbnail の画像が見つかりません(${thumb})`);
}
}
実装の要点
ファイルの読み込みとフロントマターのパース
Node.js 標準の fs/promises のみを使い、外部依存をゼロにした。YAML パーサーを使わないシンプルな正規表現パースで draft / articleType / reviewedDate などのフィールドを取得する。
// 実装(抜粋)
function parseFrontmatter(raw) {
raw = raw.replace(/\r\n/g, '\n'); // Windows 環境の CRLF に対応
const m = raw.match(/^---\n([\s\S]*?)\n---/);
const fm = {};
if (!m) return { fm, body: raw };
for (const line of m[1].split('\n')) {
const mm = line.match(/^(\w+):\s*(.*)$/);
if (mm) fm[mm[1]] = mm[2].trim();
}
return { fm, body: raw.slice(m[0].length) };
}
draft 状態による出力の切り分け
公開記事(draft:false)に問題があれば終了コード1でプロセスを止め、デプロイを阻止する。下書き(draft:true)は情報として記録するが、ブロックしない。「今すぐ直す必要がある」と「次に直す」を分けることで、開発者の負担を過度に増やさずに公開ゲートだけを厳しくできる。
[NG] [公開] some-article.mdx
- reviewedDate(確認日)が未設定です
- 誇張・断定の疑い「必ず稼」(YMYL/NG#6)
[OK] [公開] another-article.mdx
────────────────────────────────────────
公開記事(draft:false)で要修正: 1 件
[STOP] 公開記事に問題があります。修正するか draft:true に戻してください。
下書きの詳細を確認したい場合は npm run review -- --all を実行する。
ワークフローへの組み込み
npm run review の構成
package.json の review スクリプトは2つのチェッカーを連結している。
"review": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs"
-
check-emoji.mjs: 記事・コンポーネント・ページ内の装飾絵文字を検出(絵文字は記事の方針として使用禁止) -
check-articles.mjs: コンプライアンス・品質チェック(本稿のテーマ)
&& でつないでいるので、絵文字チェックに引っかかった時点でコンプライアンスチェックは実行されない。絵文字を先に排除してからコンプライアンスチェックに進む設計。
preflight はさらに check-freshness.mjs(情報鮮度チェック)も連結しており、デプロイ直前の最終チェックとして使う。
"preflight": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs && node scripts/check-freshness.mjs"
情報鮮度チェック(check-freshness.mjs)の補足
check-freshness.mjs は reviewedDate が設定日から90日(デフォルト)を超えた公開記事を一覧する。比較記事や relatedASP が設定されている価格敏感な記事を優先して上位に表示する。
npm run check:freshness # 90日超の記事を一覧
npm run check:freshness -- --days 60 # しきい値を変更
npm run check:freshness -- --strict # 古い公開記事があれば終了コード1
キャンペーン終了・価格変更時に古い情報が残り続けるのを防ぐための定期メンテナンス支援ツールとして機能する。
公開前の必須ゲートとしての位置づけ
運用上のルールはシンプルにした。
- 記事を書く(
draft:trueのまま) -
npm run reviewを実行し、警告がゼロになるまで修正する - 警告ゼロを確認してから
draft:falseにする -
npm run preflightを通過したらデプロイする
npm run review が警告を出している記事は draft:false にしない。これをルールとして明文化し、CLAUDE.md(Claude Code への指示ファイル)に記載することで、AIにコード生成を依頼しているセッションでも同じルールが自動適用される。
まとめ
AI による記事量産の本当のボトルネックは「生成速度」ではなく「品質・コンプライアンスの確認コスト」だと感じている。特に YMYL 領域では、誇張表現ひとつで景表法の問題になりうるし、出典のない数値ひとつで読者の信頼を損なう。
今回実装したアプローチをまとめると次のとおりだ。
| 観点 | 対応 |
|---|---|
| 景表法 NG#1(PR 表記) | CTA/AffiliateLink コンポーネントか PR 表記の有無を確認 |
| 景表法 NG#6(誇張表現) | 語彙辞書によるパターンマッチ |
| YMYL(数値・根拠) |
[要確認] マーカー・出典・確認日の有無をチェック |
| E-E-A-T(経験) |
<Experience> または編集部の見解セクションの存在確認 |
| 情報鮮度(NG#7) |
reviewedDate + 90日超アラート |
「人間が確認すればよい」という運用は、量が増えると必ず崩壊する。検出できるリスクは機械に任せ、人間は「実体験を書く」「根拠を調べる」という機械には代替できない部分に集中する、という分業が AI 時代の品質管理のあり方だと考えている。
本記事で紹介した実装は、Astro + Node.js 構成の副業・アフィリエイトメディアで実際に使用しているものを元にしている。景表法・YMYL 対応の lint の実例として参考になれば幸い。
関連記事
同じく「AIに任せた作業の品質を機械で担保する」テーマで書いた記事です。