はじめに
個人ブログの記事を自動生成するシステムをPython + Gemini APIで運用しています。
LINE Webhookからトリガーし、コンセプト生成 → 本文生成 → ファクトチェック → レビュー → 校正 → アフィリエイト付与 → はてなブログ投稿、という15ステップ以上のパイプラインを組みました。
エンジニアリング的には「完璧な分業」のはずでした。
ところが、ワンショット(1回のプロンプト)で生成した記事のほうが圧倒的に品質が高いことに気づきました。
この記事では、なぜそうなるのかを構造的に分析し、得られた教訓をまとめます。
品質の差を見てみる
ワンショットで生成した記事(85/100点)
まいどです。
最近、ほんまに時間が経つのが早うて困りますね。
「あかん、もうこんな時間や…」って、時計見てため息つくこと、ありませんか?
実は私も、ちょっと油断するとすぐネットサーフィンしてしもたり、
ピーナッツ食べ過ぎて後悔したり(笑)、なかなか集中できひんことが多かったんです。
...(中略)...
▼過去の関連記事はこちら
昔の自分に教えてやりたいですわ…。
[ライフハッキング - 楽天アフェリエイト 今年のブログの...](https://lifehacking1919...)
[ライフハッキング - ピーナッツを食いすぎて・・・](https://lifehacking1919...)
パイプラインで生成した記事(60/100点)
<h2>効率化の手法</h2>
<p>時間管理において重要な手法として、ポモドーロ・テクニックが知られています。</p>
<p>この手法は25分の集中と5分の休憩を繰り返すものです。</p>
...(中略)...
## 🔗 あわせて読みたい
<ul>
<li><a href='...'>ピーナッツを食いすぎて・・・</a></li>
<li><a href='...'>楽天アフェリエイト 今年の...</a></li>
</ul>
差は歴然です。
ワンショット版は「関西弁のおっちゃんが友人に語りかけている」感じ。過去記事も本文の流れの中で自然に引用されています。
パイプライン版は「教科書を読んでいる」感じ。過去記事は末尾に機械的にリストアップされ、記事全体から浮いています。
なぜこうなるのか?
ワンショットの情報フロー
LLMは全情報を同時に見ているから、「ピーナッツの話をしたあのエントリ、ちょうど怠けグセの文脈で使えるな」 と判断できる。
パイプラインの情報フロー
Step 2(本文生成)の時点で、LLMは過去記事の存在すら知らない。
だから本文中に自然に織り込めない。Step 6で末尾に「あわせて読みたい」として追加するしかない。
具体的な「分断」のコード
問題1: 過去記事が本文生成に渡されていない
# ArticleContentGeneratorTask._generate_article_content()
prompt = (
f"あなたは『{blog_name}』のライターです。\n"
f"【コンセプト】\n{article_concept}\n\n"
f"【参考情報】\n{source_content[:2500]}\n\n" # ← 2500文字で切断!
# ← similar_articles がここにない!
)
一方、FinalArticleEnricher ではこう使われている:
# FinalArticleEnricherTask._get_past_articles_html()
def _get_past_articles_html(self, articles):
html = "<ul>\n"
for art in articles[:3]:
html += f" <li><a href='{art.get('url')}'>{art.get('title')}</a></li>\n"
html += "</ul>"
return html # ← 末尾にリスト追加するだけ
問題2: ペルソナが過度に抑制されている
persona_instructions = (
f"【ブロガーのペルソナ(参考)】\n{persona_text}\n\n"
"上記のペルソナの「口調」と「視点」だけを参考にしてください。\n" # ← 「参考」止まり
"- ペルソナの感情や内面の語りを加える必要はない。\n" # ← 個性を殺す指示
"- 哲学的な自己省察、人生訓、過去の自分への語りかけは禁止\n"
)
禁止事項が多すぎて、LLMは安全策を取って無個性な文体を選択する。
問題3: ソースコンテンツの切断
f"【参考情報】\n{source_content[:2500]}\n\n"
ワンショット版では、過去記事10本分のフルテキスト(数千〜数万トークン)がLLMに渡されている。
パイプライン版は2500文字で打ち切り。文脈の豊かさが段違い。
修正してみた
Fix 1: 過去記事をコンテンツ生成プロンプトに注入
# similar_articles をプロンプトに追加
past_articles_text = ""
if similar_articles:
for art in similar_articles[:5]:
past_articles_text += f"- [{art['title']}]({art['url']})\n"
prompt += (
"【過去記事(本文中に自然に織り込むこと)】\n"
f"{past_articles_text}\n"
"上記の記事タイトルとURLを、本文の適切な文脈で自然に引用してください。\n"
"「あわせて読みたい」のような末尾リストではなく、文の流れの中に組み込んでください。\n\n"
)
Fix 2: ペルソナの自由度を上げる
- "上記のペルソナの「口調」と「視点」だけを参考にしてください。\n"
- "- ペルソナの感情や内面の語りを加える必要はない。\n"
+ "上記のペルソナになりきって記事を書いてください。\n"
+ "- テーマに関係する体験・感想は積極的に語ること。\n"
+ "- ただしテーマと無関係な哲学的内省や人生訓は避けること。\n"
Fix 3: ソースコンテンツの上限を緩和
- f"【参考情報】\n{source_content[:2500]}\n\n"
+ f"【参考情報】\n{source_content[:5000]}\n\n"
得られた教訓
1. 「美しい分業」はLLMには逆効果なことがある
ソフトウェアエンジニアリングの常識:単一責任の原則(SRP) — 各モジュールは1つのことだけをやる。
LLMの現実:文脈こそが品質。情報を分断すると、各ステップが「見えないものは使えない」。
2. パイプラインが有効なケースもある
全部ワンショットにすべきかというと、そうではない。
| タスク | ワンショット向き | パイプライン向き |
|---|---|---|
| クリエイティブ生成 | ✅ 文脈が品質に直結 | |
| 構造化チェック | ✅ ルールベースで正確 | |
| 外部API呼び出し | ✅ LLMの幻覚を避ける | |
| 品質ゲート | ✅ 機械的判定が確実 |
ファクトチェック(外部検索)やアフィリエイト商品取得(Rakuten API)は、パイプラインのほうが正確です。LLMはURLを幻覚するから。
3. 理想形は「太いコンテキストの1本パス + 後段の機械的補強」
前段はLLMの強み(文脈理解・創造性)を最大化し、後段は機械の強み(正確性・ルール準拠)で補強する。 この2層構造が現時点での最適解だと考えています。
まとめ
| ワンショット | 15ステップ パイプライン | |
|---|---|---|
| 品質スコア | 85/100 | 60/100 |
| 過去記事の扱い | 本文中に自然に引用 | 末尾にリスト追加 |
| 個性・口調 | ペルソナが活きている | 無個性・教科書調 |
| 見出し | 具体的・面白い | 短すぎて味気ない |
| 開発の手間 | 低い | 高い |
15ステップの分業制を組んで「アーキテクチャ的に正しい」と満足していましたが、肝心のLLMが必要な情報を見れていなかったというオチです。
「エンジニアリングの美学」と「LLMの実効性能」は別物だと痛感しました。
参考
- Prompt Engineering Guide
- LangChain vs 直接API呼び出し — フレームワークの抽象化がかえって邪魔になるケースの議論