はじめに
個人開発で電気料金比較サービス エネジェント を運営しています。記事数が95本を超えたあたりで「全記事の品質を揃えたい」という課題に直面し、Gemini API を使って並列レビュー+修正スプリントを回した話を書きます。
最終的に以下を達成しました:
- 平均スコア: 38.4 → 45.2 / 50(公開可判定の閾値を超えた記事の比率が 18/95 → 82/95)
- 要修正記事(40点未満): 17本 → 1本
- 所要時間: 延べ5日のスプリント(うち自動化部分は1時間程度)
「LLMで記事を書く」話はよく見かけますが、本記事は 「LLMで品質保証する」 実例です。個人開発でメディアを運営している方の参考になれば。
きっかけ
サービスが育ってきて記事が増える一方、以下の問題が目に見え始めました:
- 古い日付(「2024年時点」等)が残っている記事がある
- 同じテーマで 2 記事書いていて内容が矛盾している
- 初期に書いた記事と最新記事で品質差が大きい
- どこを直せば良いか、手動レビューでは網羅できない
全記事を人力で読み返すのは現実的ではない。かといって「LLMで書き直してもらう」と既存のファクトが壊れる。「採点だけ」にLLMを使うのがバランスが良さそうでした。
スプリント全体像
ざっくり以下の流れです:
Phase 1: 採点エンジン構築(Gemini API 並列呼び出し)
Phase 2: 全95記事を一括採点 → スコア分布を把握
Phase 3: 低スコア記事をLLMベースで個別修正(サブエージェント並列)
Phase 4: 再採点で改善確認 → 残差に再度スプリントを回す
ポイントは「採点と修正を分離する」こと。採点は Flash 系の速いモデル、修正は Opus/Sonnet 系の高精度モデル という組み合わせで、コスト効率を両立させています。
Phase 1: 採点プロンプトの設計
採点プロンプトは次のような形にしました。5 観点を明示し、必ず JSON で返させるのが肝です。
REVIEW_PROMPT = """以下は電気料金比較サービスのSEO記事です。
5観点で厳しく評価してください。
# 評価観点(各10点、合計50点)
1. **SEO適合性**: タイトル・メタ・H2でキーワードが自然
2. **ユーザー価値**: 検索者の疑問に具体的に応えている
3. **事実正確性**: 数値・単価・仕組みの記述が正しい
4. **CTA・内部リンク**: シミュレーター誘導が文脈に合う
5. **安全性**: 景表法抵触・誤認表現なし
# 出力形式(JSONのみ)
```json
{{"slug":"{slug}","seo":X,"user":X,"fact":X,"cta":X,"safety":X,
"total":XX,"verdict":"公開可|軽微修正|要修正",
"suggest":"最重要改善点を1行で"}}
verdict は total>=45=公開可 / 40-44=軽微修正 / <40=要修正 で判定。
記事: {slug}
{content}
"""
### 記事本文は全文ではなく「抜粋」を渡す
page.tsx 丸ごとだと JSX のマークアップで採点が揺らぐので、本文の要点だけを正規表現で抽出して渡します。
```python
def extract_key_sections(content: str, max_chars: int = 8000) -> str:
# メタデータ
meta = re.search(r"export const metadata[^}]+}", content, re.DOTALL)
# FAQ
faq = re.search(r"faqs=\{[^}]+\}\]", content, re.DOTALL)
# 本文(JSX の > ... < 間のテキスト)
text_parts = re.findall(r'>([^<>{}\n]{10,})<', content)
body = " / ".join(text_parts[:60])
combined = f"【メタ】{meta}\n【FAQ】{faq}\n【本文】{body}"
return combined[:max_chars]
>テキスト< を拾う正規表現でマークアップを剥がすのがコツ。これで採点の安定度が一段上がりました。
ThreadPoolExecutor で並列化
記事1本の採点に Flash で平均 4 秒ほど。95 本を逐次実行すると 6 分。ThreadPoolExecutor で 10 並列にして 40 秒に短縮しました。
from concurrent.futures import ThreadPoolExecutor, as_completed
import google.generativeai as genai
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
model = genai.GenerativeModel("gemini-flash-latest")
def score_one(slug: str) -> dict:
path = ARTICLES_DIR / slug / "page.tsx"
content = path.read_text(encoding="utf-8")
prompt = REVIEW_PROMPT.format(slug=slug, content=extract_key_sections(content))
try:
res = model.generate_content(prompt)
m = re.search(r"```json\s*(\{.+?\})\s*```", res.text, re.DOTALL)
return json.loads(m.group(1))
except Exception as e:
return {"slug": slug, "error": str(e)}
results = []
with ThreadPoolExecutor(max_workers=10) as ex:
futures = {ex.submit(score_one, s): s for s in all_slugs}
for f in as_completed(futures):
results.append(f.result())
モデル選定は gemini-flash-latest が現時点では価格性能比でベスト。精度が必要な箇所は gemini-pro-latest に切り替える設計にしています(別の記事で書いた話)。
Phase 2: スコア分布を把握する
95本一気に採点した結果がこうでした:
=== 集計 ===
公開可(45+): 18 / 軽微修正(40-44): 60 / 要修正(<40): 17
平均: 42.4/50
「公開可は 1/5 しかない」という事実は痛かった。ただこの数字があるから、次の打ち手を決められます。
採点の揺らぎ問題
同じ記事を時間を空けて再採点すると、±2〜3点のブレが普通に出ます。これは避けられない問題で、個別記事の絶対スコアより、相対順位とコメントを見る運用にしました。
# 低スコアの記事をソートして眺める
scored = [r for r in results if "total" in r]
scored.sort(key=lambda r: r["total"])
for r in scored[:20]:
print(f'{r["slug"]:40s} {r["total"]}/50 → {r["suggest"]}')
suggest フィールドに LLM が「最重要改善点を1行で」書いてくれるので、ここが次のフェーズの指示書になります。
Phase 3: サブエージェントで個別修正
ここが今回いちばん効いた工程です。Claude Code(Agent SDK)のサブエージェント並列で、低スコア記事を一本ずつ「別エージェントに直させる」運用にしました。
指示の設計
各エージェントに渡す指示はテンプレート化:
エネジェント記事「{slug}」を2-3点押し上げる軽量修正(45+/50狙い)。
## 対象
- 記事パス: {absolute_path}
- 現状スコア: {total}/50
- 採点者の指摘: {suggest}
## 方針
- 採点者の指摘をピンポイントで反映(大改造は避ける)
- 事実整合性を優先(既存の数値を壊さない)
- 禁止ワード遵守(特定ブランド名指し推奨は避ける)
## 実施
- Editツールで該当箇所のみ修正
- 型チェックが通ること
- 完了後に修正差分の要点を報告
1 エージェント 1 記事に絞り、同時に 10 本並列で走らせるのがポイント。1 本あたり 2-3 分なので、10 本バッチで 3 分程度で片付きます。
実際の修正例
スコア 34 だった 1K一人暮らしの電気代記事。採点者の指摘は:
削減額の訴求『年1.5〜2.5万円』は1Kの実態から乖離している
総務省家計調査で単身世帯の月平均電気代は約6,700円。新電力への切替による節約効果も、世帯規模が小さいほど小さくなります(基本料金・従量料金の絶対額が小さいため)。元の訴求額は単身世帯の実態と合っていませんでした。
サブエージェントは以下のように直しました:
- 「年間1.5万〜2.5万円」→「月300〜700円(年間約4,000〜8,000円)」に全箇所訂正
- 3つの節約方法(プラン切替・アンペア変更・使い方)ごとに現実的な効果額を併記
- リード直後にシミュレーターCTAを1つ追加(個別試算に誘導)
結果、再採点で 43/50 まで上がりました。「小さく、ピンポイントに、早く」 が成功パターン。
ここでのポイントは「削減額を盛らない」こと。世帯人数の小さいユーザーに対して過大な期待を煽ると、再来訪の信頼を失います。
「大改造しない」という縛り
LLMに修正を任せると、指示していない箇所まで直したがります。これはプロンプトで明示的に抑えるのが大事:
方針: 採点者の指摘をピンポイントで反映(大改造は避ける)
Edit ツールしか使わせない(Write を禁止)のも効きます。記事全体を書き直されるリスクが消えます。
Phase 4: 再採点とループ
Phase 3 でバッチ 1 の 10 本を直したら、そのバッチだけ再採点します。全 95 本再採点は時間もコストもかかるので、差分だけ確認:
# targeted_slugs だけ再採点
TARGETS = ["1k-denki", "2026-natsu-denki-yoso", ...]
results = [score_one(s) for s in TARGETS]
改善が確認できたらバッチ 2 に進み、これを 7 バッチ繰り返しました(計 77 本)。
最終的な全件再採点
全工程後にもう一度全 95 本を採点:
=== 最終採点結果 ===
公開可(45+): 82 / 軽微修正(40-44): 12 / 要修正(<40): 1
平均: 45.2/50
スコアの揺らぎはあるので「完全達成」とは言えませんが、分布が明確に右に寄りました。
副次的に見つかった問題
一番良かったのは、「採点」プロセスで想定していなかったバグが見つかったこと:
計算ミス
電気毛布の電気代記事 の「月電気代 約37円」という記述を LLM が「1日あたりと月あたりを混同している」と指摘。確認すると 1 日単価でした。正しくは約 370 円/月。
事実の古さ
「2024年の改定で…」という表現が十数本の記事に残っていました。最新版に全件更新。
カテゴリ違いのコンテンツ
スラグが dorai-shitsu-denki(エアコンのドライ運転の電気代)なのに、中身はドラム式乾燥機の話になっていた記事が 1 本。スラグと内容が不整合。
「採点者」という第三者視点を低コストで導入すると、こういう地雷が次々と出てきます。
振り返り
効いたこと
- 採点と修正の分離: Flash で採点 → 必要な箇所だけ高精度モデルで修正
- 並列実行: ThreadPoolExecutor(採点)+ サブエージェント(修正)
- ピンポイント修正の強制: 「Edit のみ」「大改造禁止」を指示に明記
-
suggestフィールド: LLM に次の一手を書かせる → そのまま次フェーズの指示になる
ハマったこと
- Gemini の出力が壊れる: JSON 取り出しで parse 失敗がたまに出る → 正規表現で 2 段階に救済
- 採点の揺らぎ: 再採点で ±3 点ブレる → 絶対スコアより分布で判断
- サブエージェントの暴走: 「ついでに全部リファクタ」を防ぐのに指示の書き方を工夫
次やること
- 月次メンテナンスに組み込む(料金データ更新とセットで再採点)
- 採点結果を Git にコミットし、スコア推移を可視化
- 「要修正が3回連続で出る記事」は人間レビューに回す運用
まとめ
- LLM 採点は「書く」より「読む」方に使うと便利
- 並列化と分割実行で、95記事レビューは 1 時間以内に収まる
- スコアの揺らぎは避けられないので、絶対値より分布と指摘内容を見る
- 「採点者」という視点を足すと、想定外のバグが面白いほど見つかる
個人開発メディアでの品質保証としてはかなり実用的だと感じました。
参考リンク
- 運営サービス: エネジェント(電気料金比較シミュレーター)
- 運営方針・技術検証の方針: エネジェントについて
- Google AI Studio(Gemini API): https://aistudio.google.com/
- Anthropic Claude Agent SDK: https://docs.anthropic.com/
読んでいただきありがとうございました。個人開発で似たような課題に取り組んでいる方がいれば、コメントで教えてもらえると嬉しいです。