この記事は LTS Group(エル・ティー・エス グループ) Advent Calendar 2025 の3日目の記事です。
はじめに
以前、Qiitaの記事をChatGPT APIで要約してX(Twitter)投稿するプログラムをCloud Functions for Firebaseで定時実行する仕組みを開発しましたが、今回そのシステムを Cloudflare Workers ベースで全面リニューアルしました。
この記事では、リニューアルの経緯と新しいアーキテクチャの実装について紹介します。
リニューアルの背景
旧システムの課題
前回作成のシステムを運用する中で、以下の課題が明らかになりました:
| 課題 | 詳細 |
|---|---|
| 投稿ロジックの問題 | 新しい順にN件取得し、日付をNで割った余りで投稿記事を決定する方式だったため、古い記事が完全に対象外に。記事の追加タイミング次第では同じ記事を連続投稿するケースも発生。また、人気記事の再投稿など柔軟な戦略が実装できない |
| 過剰な翻訳処理 | トークン節約と要約精度向上を狙い、記事を英語翻訳(Gemini API)してからAI評価(ChatGPT API)していたが、現在のAI性能では翻訳ステップが不要な上、翻訳APIコストが余計な支出に |
| メトリクス不足 | 投稿後のエンゲージメント追跡がなく、どの記事が反響を得たか分析できない |
| 技術的停滞 | 既存システムで目新しさがなくなり、新しい技術スタックでの学びが得られない |
リニューアルの方針
これらの課題を解決し、より柔軟で効率的なシステムを実現するため、以下の方針で再構築しました:
投稿ロジックの刷新
- 固定的な選定方式を廃止
- メタスコアリング + AI評価による動的な記事選定
- 曜日別戦略で多様な記事を投稿(新着、人気記事、過去の良記事など)
- ベクトル検索による類似記事の連続投稿防止
AI処理の最適化
- 翻訳ステップを完全に削除(日本語のまま処理)
- バッチ評価による効率化
- Claude APIに統一(ChatGPT + Geminiの2段構成を解消)
技術スタック刷新
- 実行環境: Cloud Functions for Firebase → Cloudflare Workers
- ストレージ: なし → KV + D1 + Vectorize
- 新機能: エンゲージメント追跡、重複検出
AI主導の技術選定
今回のリニューアルでは、技術選定の多くをAIに委ねる実験的なアプローチを取りました。ただし完全に任せきりではなく、以下の方針で選択を行いました:
- 未経験技術の優先: 業務でAWS、Google Cloud、Vercelを使用しているため、Cloudflareを選択
- モダンツールの採用: 業務で触れていないBun、Honoを導入
- 開発ツールの多様化: 業務でのDevin + Cursorに対し、今回はOpenAI CodexとClaude Codeを使用。同じ領域のツールだが、それぞれの使い勝手などを知るためにあえて併用
この方針により、個人開発を学びの機会として活用しつつ、AIの提案を最大限に取り入れた技術スタックとなりました。
システム概要
主要機能
-
記事取得と事前フィルタリング
- Qiita APIから組織メンバーの記事を取得
- メタスコア(いいね数、ストック数、鮮度など)で事前スクリーニング
- 過去投稿済み記事と類似記事を除外
-
AI評価と投稿
- Claude AIによる技術的価値・内容品質の評価
- 評価が高い記事を自動選定して投稿文を生成
- X APIで自動投稿
-
メトリクス追跡
- 投稿後のインプレッション、いいね、リツイート、リプライを記録
- 将来的なエンゲージメント学習の基盤
技術スタック
Runtime: Cloudflare Workers
Framework: Hono v4
Language: TypeScript v5.7
Package Mgr: Bun v1.1
AI: Anthropic Claude API
- Claude Sonnet 4: 高品質記事評価
- Claude Haiku: 標準品質記事評価
Validation: Valibot v1.0(Zodより軽量)
Linter: Biome v2.3(ESLintより高速)
Testing: Vitest v4
Storage:
- KV: キャッシュ・実行履歴
- D1: メトリクス・投稿履歴
- Vectorize: 記事ベクトル検索(重複検出)
- Workers AI: 埋め込み生成 (@cf/baai/bge-m3)
APIs:
- Qiita API v2
- X (Twitter) API v2
- Anthropic Claude API
アーキテクチャ
システム構成図
処理フロー
記事投稿処理
実装のポイント
1. 多段階フィルタリング
AI評価の前に機械的なスコアリングで候補を絞り込みます。
メタスコアの構成(最大45点):
- いいね数: 最大10点
- ストック数: 最大10点
- 鮮度: 最大10点(新しいほど高スコア)
- プレミアムタグ: 最大5点(TypeScript, React, AWS等)
- コメント数: 最大5点
- 記事の充実度: 最大5点(本文長、コードブロック数、見出し数)
// src/utils/scoring.ts
export function calculateMetaScore(article: QiitaArticle): number {
return (
calculateLikesScore(article.likes_count) +
calculateStocksScore(article.stocks_count) +
calculateRecencyScore(article.created_at) +
calculateTagsScore(article.tags) +
calculateCommentsScore(article.comments_count) +
calculateBodyScore(article.body)
);
}
2. AIトークン最適化
記事内容を評価に必要な情報のみに圧縮します。
圧縮技術:
- コードブロック: 15行超は最初8行+最後5行のみ抽出
-
画像URL:
→[画像: alt]に変換 - 本文: 重要セクション(最初200文字程度)のみ抽出
// src/utils/tokens.ts
export function compressForEvaluation(
article: QiitaArticle & { metaScore: number }
): string {
const compressedBody = compressArticleBody(article.body);
return `
タイトル: ${article.title}
タグ: ${article.tags.map((t) => t.name).join(', ')}
メタスコア: ${article.metaScore}
内容抜粋:
${compressedBody}
`.trim();
}
3. バッチ評価
複数記事を1回のAPI呼び出しで評価し、API呼び出し回数を削減します。
// src/ai/engine.ts
async evaluateBatch(
articles: Array<QiitaArticle & { metaScore: number }>
): Promise<EvaluationResult[]> {
const compressed = articles.map(compressForEvaluation);
const prompt = `
以下の${articles.length}件の記事を評価してください。
${compressed.map((c, i) => `## 記事${i + 1}\n${c}`).join('\n\n')}
各記事について以下のJSON配列で返してください:
[
{
"article_id": "記事ID",
"total_score": 0-100,
"recommended": true/false,
"reasoning": "評価理由"
}
]
`;
const message = await this.client.messages.create({
model: this.selectModel(articles),
messages: [{ role: 'user', content: prompt }],
});
return this.parseEvaluationResults(message.content);
}
4. 動的モデル選択
記事のメタスコアに応じてClaude AIのモデルを使い分けます。
// src/ai/engine.ts
private selectModel(
articles: Array<QiitaArticle & { metaScore: number }>
): string {
const maxMetaScore = Math.max(...articles.map((a) => a.metaScore));
if (maxMetaScore >= 35) {
return 'claude-sonnet-4-20250514'; // 高品質記事用
}
return 'claude-haiku-4-5-20251001'; // 標準品質記事用
}
5. ベクトル検索による重複検出
Cloudflare Vectorizeを使い、過去投稿と類似する記事を除外します。
// src/services/articleService.ts
async checkRecentSimilarPosts(article: QiitaArticle) {
// 記事の埋め込みを生成
const embedding = await this.generateEmbedding(article);
// 過去3日間の投稿と類似度を計算
const results = await this.vectorize.query(embedding, {
topK: 5,
filter: { posted_within_days: 3 },
});
// 類似度0.8以上の記事があれば重複とみなす
const hasSimilar = results.matches.some((m) => m.score >= 0.8);
return { hasRecentSimilar: hasSimilar };
}
6. 曜日別投稿戦略
曜日ごとに異なる戦略で記事を選定します。
// src/utils/postingStrategy.ts
export const weekdayStrategies: Record<number, PostingStrategy> = {
0: { // 日曜: 最新の記事優先
daysBack: 7,
metaScoreThreshold: 20,
prioritizeRecent: true,
},
1: { // 月曜: 新しめ + 中程度のスコア
daysBack: 14,
metaScoreThreshold: 25,
prioritizeRecent: true,
},
2: { // 火曜: 高スコア記事優先
daysBack: 30,
metaScoreThreshold: 30,
prioritizeRecent: false,
},
// ...
};
7. データベーススキーマ
posts テーブル: 投稿履歴とメトリクス
CREATE TABLE posts (
id TEXT PRIMARY KEY,
qiita_article_id TEXT NOT NULL,
qiita_url TEXT NOT NULL,
tweet_id TEXT,
tweet_url TEXT,
tweet_content TEXT NOT NULL,
ai_score REAL,
meta_score REAL,
impressions INTEGER DEFAULT 0,
likes INTEGER DEFAULT 0,
retweets INTEGER DEFAULT 0,
replies INTEGER DEFAULT 0,
posted_at TEXT NOT NULL,
updated_at TEXT
);
token_usage テーブル: AI使用量の追跡
CREATE TABLE token_usage (
id TEXT PRIMARY KEY,
operation_type TEXT NOT NULL, -- 'evaluation' or 'generation'
model TEXT NOT NULL,
input_tokens INTEGER NOT NULL,
output_tokens INTEGER NOT NULL,
cost_usd REAL NOT NULL,
article_count INTEGER DEFAULT 1,
created_at TEXT NOT NULL
);
リニューアルの成果
技術的改善
| 項目 | 旧版 | 新版 | 改善 |
|---|---|---|---|
| 実行時間制限 | 6分 | 30秒(CPU時間) | 制約緩和 |
| 記事処理能力 | ~50記事/実行 | 200+記事/実行 | 4倍以上 |
| AI評価方式 | 個別評価 | バッチ評価 | 効率化 |
| 重複検出 | 手動管理 | 自動(ベクトル検索) | 自動化 |
| メトリクス | なし | 自動追跡 | 新規追加 |
| AI呼び出し | 記事数回 | 1-2回/バッチ | 大幅削減 |
※性能等の数値は理論値です。今後運用しつつ成果を見ていきます。
運用面の改善
メリット:
- スケーラビリティ: 組織の記事数が増えても処理可能
- 保守性: TypeScript化により型安全性が向上
- 拡張性: サービス層の分離により機能追加が容易
- 可観測性: メトリクス追跡により投稿効果を測定可能
- コスト効率: AI処理の最適化により運用コストを抑制
デメリット・トレードオフ:
- 初期セットアップの複雑さ: Cloudflare Workers、KV、D1、Vectorizeの設定が必要
- ローカル開発環境: Wranglerでの開発に慣れが必要
- 依存関係の増加: より多くのサービスに依存
今後の展望
実装予定の機能
-
エンゲージメント学習
- 蓄積したメトリクスから高エンゲージメントパターンを学習
- AI評価時に学習データを参考情報として活用
-
投稿文のA/Bテスト
- 複数の投稿文候補を生成
- エンゲージメント予測で最適な文面を選択
-
マルチアカウント対応
- 複数のXアカウントへの投稿
- アカウントごとの戦略カスタマイズ
改善の方向性
現在の実装では、記事の評価閾値が高めに設定されているため、投稿頻度が想定より低くなっています。今後、エンゲージメント学習機能の実装により、データドリブンな閾値調整が可能になると考えています。
まとめ
Cloud Functions for Firebaseベースの旧システムをCloudflare Workersで全面リニューアルし、以下を実現しました:
- 実行環境の制約解消: Workers採用により処理能力が向上
- AI処理の効率化: バッチ評価とトークン圧縮により大幅な最適化
- 重複投稿の自動防止: ベクトル検索による類似記事検出
- データドリブンな改善基盤: メトリクス追跡により継続的な改善が可能
今後はエンゲージメント学習機能の実装により、さらなる投稿品質の向上を目指します。
参考リンク
- 前回の記事:Qiitaの記事をChatGPT APIで要約してX(Twitter)投稿するプログラムをCloud Functions for Firebaseで定時実行する
- Cloudflare Workers Documentation
- Anthropic Claude API
- Qiita API v2
- リポジトリ
この記事が、自動投稿システムの構築やCloudflare Workers活用の参考になれば幸いです。