7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare Workers × Claude AIでQiita記事のX投稿システムをリニューアルした話

7
Last updated at Posted at 2025-12-03

この記事は 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を選択
  • モダンツールの採用: 業務で触れていないBunHonoを導入
  • 開発ツールの多様化: 業務でのDevin + Cursorに対し、今回はOpenAI CodexClaude Codeを使用。同じ領域のツールだが、それぞれの使い勝手などを知るためにあえて併用

この方針により、個人開発を学びの機会として活用しつつ、AIの提案を最大限に取り入れた技術スタックとなりました。

システム概要

主要機能

  1. 記事取得と事前フィルタリング

    • Qiita APIから組織メンバーの記事を取得
    • メタスコア(いいね数、ストック数、鮮度など)で事前スクリーニング
    • 過去投稿済み記事と類似記事を除外
  2. AI評価と投稿

    • Claude AIによる技術的価値・内容品質の評価
    • 評価が高い記事を自動選定して投稿文を生成
    • X APIで自動投稿
  3. メトリクス追跡

    • 投稿後のインプレッション、いいね、リツイート、リプライを記録
    • 将来的なエンゲージメント学習の基盤

技術スタック

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](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での開発に慣れが必要
  • 依存関係の増加: より多くのサービスに依存

今後の展望

実装予定の機能

  1. エンゲージメント学習

    • 蓄積したメトリクスから高エンゲージメントパターンを学習
    • AI評価時に学習データを参考情報として活用
  2. 投稿文のA/Bテスト

    • 複数の投稿文候補を生成
    • エンゲージメント予測で最適な文面を選択
  3. マルチアカウント対応

    • 複数のXアカウントへの投稿
    • アカウントごとの戦略カスタマイズ

改善の方向性

現在の実装では、記事の評価閾値が高めに設定されているため、投稿頻度が想定より低くなっています。今後、エンゲージメント学習機能の実装により、データドリブンな閾値調整が可能になると考えています。

まとめ

Cloud Functions for Firebaseベースの旧システムをCloudflare Workersで全面リニューアルし、以下を実現しました:

  • 実行環境の制約解消: Workers採用により処理能力が向上
  • AI処理の効率化: バッチ評価とトークン圧縮により大幅な最適化
  • 重複投稿の自動防止: ベクトル検索による類似記事検出
  • データドリブンな改善基盤: メトリクス追跡により継続的な改善が可能

今後はエンゲージメント学習機能の実装により、さらなる投稿品質の向上を目指します。

参考リンク


この記事が、自動投稿システムの構築やCloudflare Workers活用の参考になれば幸いです。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?