こんにちは、JX通信社で CTO をしている小笠原(@yamitzky)です。
JX 通信社の Slack では、プレスリリースへの反響や、弊社の情勢調査事業に関する発信や、ニュースアプリの「NewsDigest(ニュースダイジェスト)」についての言及を監視するチャンネルがあります。皆さんの会社でも、広報部門が自社の反響を見ているのではないでしょうか。

当該 Slack チャンネルでは、かなり単純なキーワードマッチによる定期監視をしていたのですが、
- 「NewsDigest(ニュースダイジェスト)」は一般名詞としても使われるため、似たサービスや無関係のドメインの記事を誤検知する
- 記事本文は無関係なのにサイドバーに自社記事が表示されているだけで検知される
といった課題がありました。 1週間分のメッセージを分析したところ、61% が誤検知でした。これだと、読むべき記事が埋もれてしまい、チャンネルが形骸化してしまいます。
そこで Cloudflare Workers を使って、LLM でニュース記事の関連性を判定してから投稿するボット を作りました。「Slack 上で動く LLM を活用したボット」という要件に必要な機能が全部 Cloudflare に揃っていて開発体験が良かったので共有します。
「Slack 上で動く LLM を活用したボット」に必要な要件
今回の要件は「定期的に RSS を監視し、必要なものだけに絞り込み、Slack に投稿する」という非常にシンプルなボットです。しかし、ある程度使い勝手の良い AI エージェントを作ろうと思うと、求められる要件は意外と多いです。
- 定期実行できる基盤
- 一度通知した内容を記憶しておくこと
- 投稿内容が自社に関係するか判断する LLM
- Slack とやりとりするためのHTTPサーバー
- 監視設定が、会話を通じてできること/設定が永続化されていること
- これらが安く、メンテナンスフリーで実現できること
つまり、安価で、サーバーレスで、ストレージと、Cronと、HTTP サーバーと、LLM が必要なわけですが... なんと Cloudflare なら全部揃っています!
全体アーキテクチャ
作ったボットの全体像です。

外部サービスへの依存は RSS と Slack だけです。それ以外は Cloudflare で完結しています。
Cloudflare のここが便利
Workers AI で LLM が binding 一つで使える
Cloudflare Workers AI を使えば、wrangler.toml に 2行書くだけで LLM が使えます。
[ai] binding = "AI"
あとはコードから env.AI.run() を呼ぶだけです。
const result = await env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", { messages: [ { role: "system", content: "あなたはメディアクリッピングの関連性判定アシスタントです。..." }, { role: "user", content: "以下の記事の関連性を判定してください。..." }, ], });
API キーの管理も SDK のインストールも不要です。
wrangler.toml に binding を書くだけで、TypeScript のコードからそのまま呼び出せます。
KV での永続化/キャッシュ
Cloudflare KV は Workers から使えるキーバリューストアです。 用途に応じて3種類のデータを管理しています。
[[kv_namespaces]] binding = "KV" id = "xxxxx"
| キー | 用途 | TTL |
|---|---|---|
feeds |
RSS フィード URL の配列 | なし(永続) |
rules |
フィルタリングルールの配列 | なし(永続) |
seen:{entryId} |
既読エントリの重複排除 | 7日 |
seen:* キーの設計がポイントです。
エントリ1件ごとに1つのキーを作り、7日間の TTL を設定しています。
const SEEN_TTL = 604800; // 7日 export async function markSeen(kv: KVNamespace, entryId: string): Promise<void> { await kv.put(`seen:${entryId}`, "1", { expirationTtl: SEEN_TTL }); }
Cloudflare には D1(SQL)や R2(オブジェクトストレージ)など用途の異なるストレージがいくつかありますが、今回は単純なキーバリュー操作であり、結果整合性でも問題なかったため KV を選びました。
Cron Triggers での定期実行
15分ごとの RSS ポーリングは、Cron Triggers で実現しています。
[triggers] crons = ["*/15 * * * *"]
export default { fetch: handler.fetch, async scheduled(_c: ScheduledController, env: Env, ctx: ExecutionContext) { ctx.waitUntil( getAgentByName(env.ClippingAgent, "clipping").then((a) => a.poll()) ); }, };
wrangler.toml に cron 式を書いて、scheduled ハンドラを export するだけです。
外部のスケジューラや GitHub Actions を用意する必要はありません。
HTTP ハンドラ(Slack Events API の受信)と定期実行が同じ Worker の中に同居できるのも Cloudflare Workers の良いところです。
HTMLRewriter で外部ライブラリなしに記事本文を取得
今回のボットは、検知した記事が自社に関係するかどうかを LLM で判断します。しかし RSS にはスニペット(本文冒頭)しか含まれず、判定材料としては足りないケースがあります。
実データの分析で、スニペットにキーワードが含まれないのに実際は関連記事だったケースがいくつか見つかりました。
この対策として、タイトルとスニペットに監視対象語が含まれない記事だけ、本文を取得して判定材料に加えています。 ここで使うのが Workers ランタイムに組み込みの HTMLRewriter です。
const cleaned = await new HTMLRewriter() .on("script, style, nav, header, footer, aside", { element(el) { el.remove(); }, }) .transform(new Response(html)) .text();
jsdom や cheerio といった外部ライブラリを入れなくても、Workers ランタイムの標準機能だけで HTML からテキストを抽出できます。
依存が増えないのでバンドルサイズも小さく保てます。
Agents SDK を使った Slack ボット
Cloudflare には Agents SDK という、Durable Objects ベースのステートフルなエージェントを構築するためのフレームワークがあります。
公式に Slack Agent のサンプルが用意されており、Slack の署名検証やイベントルーティングといった定型処理を SlackAgent 基底クラスとして提供してくれます。
今回のボットでは、この SlackAgent を継承して独自のエージェントを実装しています。
[[durable_objects.bindings]] name = "ClippingAgent" class_name = "ClippingAgent" [[migrations]] tag = "v1" new_sqlite_classes = ["ClippingAgent"]
import { SlackAgent } from "./slack-agent"; // 公式サンプルをベースにした基底クラス export class ClippingAgent extends SlackAgent<Env> { // Slack メンションで呼ばれる対話処理 async onSlackEvent(event) { // → LLM の tool calling でフィード管理・ルール管理を実行 } // Cron で呼ばれる定期クリッピング処理 async poll() { // → RSS 取得 → フィルタ → LLM で判定 → Slack 投稿 } }
継承元の SlackAgent が署名検証・ACK・イベントルーティングを担い、 ClippingAgent の実装はビジネスロジック(対話管理と定期クリッピング)に集中できます。
Agent は Durable Object として動くため、Worker の fetch / scheduled いずれのハンドラからも getAgentByName() で同一インスタンスにアクセスできます。
Slack メンションによる自然言語オペレーション
今回のボットの利用者は、エンジニア(ボットの開発をする人)だけではありません。むしろ広報部や、マネジメント系の人の方が、よく利用するでしょう。そこで、ボットのチューニングや設定変更はエンジニアだけではなく、他の職種の人でも Slack 上で簡単にできることを目指しました。
@clippingbot example.com は除外して → ✅ ドメイン除外ルールを追加しました: example.com @clippingbot 今のルール教えて → 現在のフィルタリングルール: 1. [exclude_domain] example.com 2. [exclude_keyword] ほげほげダイジェスト @clippingbot サイドバーに載ってるだけの記事は無視して → ✅ AI判定基準を追加しました: サイドバーに載ってるだけの記事は無視して

裏側では Workers AI の Function calling 機能を使っています。add_feed、remove_feed、add_rule、list_rules など、TypeScript で作ったツールの定義を LLM に渡し、ユーザーの自然言語メッセージに対して LLM がどのツールをどんな引数で呼ぶべきかを判断します。
const result = await env.AI.run(MODEL, { messages: [ { role: "system", content: "管理アシスタントとして、適切なツールを呼び出してください。" }, { role: "user", content: userMessage }, ], tools: [ { name: "add_feed", description: "RSSフィードを追加する", parameters: { ... } }, { name: "add_rule", description: "フィルタルールを追加する", parameters: { ... } }, // ... ], }); // result.tool_calls に LLM が選んだツールと引数が入る // 選ばれたツールごとに実行される、実際の処理を用意しておく private async executeTool(name: string, args: Record<string, string>): Promise<string> { const kv = this.env.KV; switch (name) { case "add_feed": { const feeds = await getFeeds(kv); if (feeds.includes(args.url)) return `${args.url} は既に登録されています。`; feeds.push(args.url); await setFeeds(kv, feeds); return `フィードを追加しました: ${args.url}`; } case "remove_feed": { } // ... } }
Workers AI の無料枠
これらの仕組みは Cloudflare の無料プランだけでも運用できます。AI を使っていても、一定ラインまでは無料です。
| リソース | 無料枠 |
|---|---|
| Workers AI | 10,000 Neurons/日 |
| KV 読み取り | 100,000回/日 |
| Workers リクエスト | 100,000回/日 |
「Neurons」という見慣れない単位がありますが、 Cloudflare Workers AI のプライシングページに、トークン数と Neurons の関係性が記載されています。ユースケースにもよりますが、1日数百メッセージ程度であれば収まるのではないかと思います。
Cloudflare を使ってみて良かったところ
これらの要素技術自体は、Google Cloud や Amazon Web Services などのパブリッククラウドにも備わっているものが多いです。
Cloudflare の場合、デプロイが圧倒的に速いのが驚きでした。 10 秒くらいで完了します。そのため、作る→試す→作る→試すのトライアンドエラーがとてもやりやすかったです。
そして AI の実行にも無料枠があるので、気軽に AI つき Slack ボットが作れます。
注意点1:LLM の精度と速度
Cloudflare Workers AI にはたくさんのモデルが用意されていますが、精度、速度、コストなどの特性・トレードオフがあります。また「精度」もツール呼び出しの精度(壊れづらさ)と、実施したいタスクの正解率があります。
当初は @cf/meta/llama-3.3-70b-instruct-fp8-fast を使っていましたが、ツール呼び出しの精度が悪く @cf/openai/gpt-oss-120b に移行しました。以下は、ツール呼び出しの精度を簡易的に検証したものです(正確ではない可能性があるので、ご自身での検証をおすすめします!)。
| モデル | 合格率 | 中央値レイテンシ | 実際の判断 |
|---|---|---|---|
| llama-3.3-70b | 13% | — | 精度が悪く、本番で使い物にならなかった |
| gpt-oss-120b | 87% | 2.2s | 採用。致命的失敗モードなし |
| qwen3-30b | 91% | — | 不採用。質問→勝手に変更の致命的な失敗が1回 |
| kimi-k2.6 | 95% | 11.7s | 不採用。高価($0.95/$4.00 per 1M) |
| gemma-4-26b-a4b-it | 23/23 (100%) | 15.4s(最大125s) | 不採用。メンション対話には遅すぎる |
注意点2:セキュリティ
このようなボットを作るうえで、一点、注意いただきたいことがあります。それはプロンプトインジェクションの危険性です。RSS など信頼できない情報は、攻撃的なプロンプト指示を混ぜ込まれる危険性があります。

そこで、
- Slack 上の社員からのメッセージ起点にした自動化 → ある程度信頼できるため、幅広にツールを呼び出せるようにする
- RSS など信頼できない情報を起点にした自動化 → 信頼できないので、ツールは無効化した状態で LLM を呼び出し、サニタイズする(LLM は true / false を返すだけ)
としています。LLM を使った業務自動化をする際は「信頼できないメッセージ × 権限の強いツール呼び出し」をしないように注意してください。メールやカレンダーを活用した自動化などでも同じ問題があります。
参考記事
Cloudflare の Developer Advocate の yusukebe さんの記事がとても参考になりました。ありがとうございます。
公式ドキュメントには、上記記事に載ってないようなプラクティスも大量に乗っているのでぜひ参考にしてみてください。
まとめ
Cloudflare Workers は、AI ボットに必要な機能が一通り揃ったプラットフォームです。 Workers AI で LLM を呼び、KV でステートを管理し、Cron Triggers で定期実行し、HTMLRewriter で HTML を処理し、Agents SDK で Slack ボットにする。これらが全部簡単に設定できることをご紹介しました。
ちなみに、元々 61% が誤検知だったところ、今回の Slack bot によってほぼ誤検知がなくなりました。もっと早く作っておけばよかったです。
このような社内のちょっとした困りごとを AI で解決したいけれどインフラの構築が面倒、という方はぜひ Cloudflare Workers を試してみてください。



