ブログのタグ付けをAI任せで自動化した話
タグ付けは面倒
ブログ記事にタグを付けるのは地味に面倒です。
- 既存タグとの整合性を考えないといけない
- 新しいタグを作るとき粒度(ジャンル?テーマ?具体的な技術名?)を揃えたい
- 言語ごとにラベルをどうするか決める必要がある
- 記事が増えるほど過去のタグとの名寄せが難しくなる
「ルールに従った分類」は AI が得意な領域です。Gemini API を使ってタグ付けを自動化するスクリプトを作り、さらに GitHub Actions のパイプラインに組み込んで完全自動化しました。
tags.ts: AI タグ付けスクリプト
まずローカルで動くスクリプトを作りました。やっていることはシンプルで、日本語記事の本文と既存のタグ一覧を Gemini に渡して、適切なタグを提案・適用してもらいます。 ただ、AI に自由にタグを作らせると破綻するので、いくつかルールを設けています。
タグの粒度ルール
タグには3段階の粒度を設けて、AI にはこの体系ごと渡しています。
| 粒度 | 意味 | 例 |
|---|---|---|
large | ジャンル・カテゴリ | blog, tech, entertainment |
medium | 分野・テーマ | ai, web-development, anime |
small | 具体的な技術・作品名 | nextjs, vercel, supabase |
1記事あたり最低2個、最大10個。各粒度に1つ以上は努力目標にしています(ハードルールにすると短い記事でタグの水増しが起きるので)。
名寄せ(重複防止)
AI にタグを自由に作らせると "ai" と "machine-learning" のような重複が生まれがちです。プロンプトで「既存タグと重複する概念は作るな」と明示し、新タグは本当に新しいトピックのときだけ生成させています。
日本語ラベルの表記
日本語の技術コミュニティで実際に使われている表記に従います。英語のまま使われる用語を無理にカタカナ化しません。
- java → "Java"(○)、"ジャバ"(×)
- embedding → "Embedding"(○)、"エンベディング"(△)
- blog → "ブログ"(○)
既存タグの一覧 tags.json を手動で修正すれば、AI は次回以降それを正として尊重します。人間の判断が常に優先される設計です。
LLM 出力のブレ対策
LLM の出力は毎回微妙にブレるので、タグ数やタグ ID の存在チェックなどのバリデーションを入れて、違反時はリトライする設計にしています。ルールをハードルールと努力目標に分けて、厳しくしすぎないのがコツです。
モデルの使い分け
無料枠の制約があるので、用途に応じてモデルを切り替えています。タグ付けには RPD 500 で大量実行に向いている軽量モデル(gemini-3.1-flash-lite-preview)を使用。タグ体系全体の品質レビュー(重複・粒度ミス・不自然なラベルのチェック)は手動で別途実行しますが、こちらは俯瞰的な判断が必要なので賢いモデル(gemini-2.5-flash)を使っています。
プロンプト調整の効果
実際に同じ記事(AIで翻訳品質を自動検証する仕組み)で、プロンプトの違いによるタグ付け結果を比較してみます。
シンプルなプロンプト
「この記事に適切なタグを付けてください」とだけ指示した場合:
ai, llm, translation, backtranslation, embedding,
vectorsearch, pgvector, postgresql, nlp, automation
問題だらけです。
- 重複: "ai" と "llm" と "nlp" は概念が被っている
- 重複: "pgvector" と "postgresql" も粒度が違うだけで同じもの
- 既存タグ無視: "backtranslation" は既存の "translation" と別に作ってしまっている
- 粒度バラバラ: ジャンルタグも具体名タグも区別なく羅列
- 上限ギリギリ: 10個全部使い切っている
ルール込みのプロンプト
既存タグ一覧・粒度体系・名寄せルールを渡した場合:
[medium] ai, translation
[small] postgresql, embedding, gemini, supabase
6個に絞られ、重複もなし。既存タグを優先し、新タグは本当に必要なときだけ提案されています。
この差が出るのは、AI に「自由に分類して」と頼むのと「このルール体系の中で分類して」と頼むのでは、タスクの性質が全然違うからです。後者のほうが制約がある分、安定した結果が得られます。
GitHub Actions への組み込み
tags.ts がローカルで動くようになったので、次は GitHub Actions の翻訳パイプラインに組み込みます。
設計: タグ付け → 翻訳 → まとめて1コミット
このブログでは、記事を push すると自動で翻訳が走る仕組みがすでにありました(翻訳の仕組みについてはこちら)。タグ付けはその前段に追加しました。
posts/ja/ に push
→ 変更された記事の slug を検出
→ AI タグ付け(tags.ts apply) ← 追加
→ 翻訳(translate.ts translate)
→ ベスト翻訳選択(translate.ts best)
→ まとめて git commit & push
ポイントは タグ変更と翻訳変更を1回のコミットにまとめている こと。posts/ja/(frontmatter のタグ更新)、posts/en/(英訳生成)、content/tags.json(新タグ追加)をすべて git add してから1回だけ commit します。コミットが分散すると履歴が散らかるので、まとめるのが大事です。
この記事自体がテスト
この記事は tags: [](タグなし)の状態で push しています。パイプラインが正しく動けば、AI がこの記事を読んで適切なタグを自動生成し、frontmatter に書き込んでくれるはずです。
この記事にタグが付いていれば、自動タグ付けパイプラインは正常に動作しています。
ついでに meta description も自動生成する
タグ付けの仕組みが安定してきたので、ついでに HTML の <meta name="description"> も同じタイミングで自動生成するようにしました。
meta description とは
検索結果やSNSでシェアしたときに表示される記事の要約文です。SEO 的にも重要で、Google の検索結果では日本語で約120文字まで表示されます。
普通は記事を書くたびに手動で書くか、本文の先頭を機械的に切り出すかのどちらかですが、どちらもイマイチです。手動は面倒だし、先頭切り出しは「## はじめに」のような見出しが含まれたり、文が途中で切れたりします。
1回の API コールでタグと description を同時に生成
やったことはシンプルで、タグ付けのプロンプトに「ついでに120文字以内の要約も作って」と追加しただけです。
{
"tags": ["tech", "ai", "gemini", "github-actions"],
"new_tags": {},
"description": "ブログのタグ付けにおける「粒度」や「名寄せ」の...",
"reasoning": "..."
}
API コールの回数は増えません。もともとタグ提案のために記事全文を渡しているので、要約を追加しても追加コストはゼロです。無料枠の RPD も消費しません。
手動の description を尊重する
frontmatter に description がすでに書かれている場合は上書きしません。自分で書いた要約の方がいいケースもあるので、AI は空欄のときだけ補完する設計です。
まとめ
- Gemini API を使った AI タグ付けスクリプト(tags.ts)を作成
- 粒度ルール・名寄せ・日英ラベル生成・バリデーション+リトライを実装
- GitHub Actions の翻訳パイプラインの前段に組み込み、1回のコミットに統合
- タグ付けと同じ API コールで meta description も自動生成(追加コストゼロ)