Cloudflare で AI エージェント基盤を作る 4 ステップ — Hono → Queues → Workers AI → Agents SDK
結論
Cloudflare 上に AI エージェントの土台を据えるなら、次の順で積む。
- Step 1: Hono で最小の Worker を 1 本書いて
wrangler deployする - Step 2: Webhook を受けて Cloudflare Queues に積み、即 200 を返す「門番 Worker」を作る
- Step 3: Worker から AI Gateway 経由で LLM を呼び、レスポンスをキャッシュする
- Step 4: Agents SDK + Durable Objects で「会話状態を持つエージェント」を 1 体動かす
Wrangler のセットアップとアカウント連携は完了している前提で、各ステップに動くコードと動作確認の叩き方を載せる。1 ステップ 30 分〜2 時間。
⚠️ 着手の前に — 経済合理性チェック(後から読み返す自分へ)
この計画書は学習動機と「将来必要になったときの備忘」として置いている。いま現に自分用に回っている自動化を Cloudflare に移す経済合理性は基本ない。理由は LLM の課金構造だ。
| 経路 | LLM コスト | 月額固定費 |
|---|---|---|
| Claude Code (Max $200/月) | 限度内なら $0 / 呼び出し | $200 |
| Cloudflare AI Gateway → Anthropic Sonnet | 3 / 1M 入力, 15 / 1M 出力 | $5 + 従量 |
| Cloudflare Workers AI (Llama 3.1 8B 等) | 1k token あたり約 $0.001 | $5 |
Claude Code のサブスクは定額で、レート上限内なら追加コストはゼロ。一方 Cloudflare AI Gateway 経由はゼロベースで token 従量課金が積み上がる。
自分用ワークロードをざっくり試算すると次のとおり(Sonnet 相当を AI Gateway 経由で使う前提)。
| ワークロード | 移行した場合 | Claude Code 定額内 |
|---|---|---|
| 毎朝のダイアリ生成 (~50k token) | 約 $9 / 月 | $0 |
| Twitter 下書き (~20k) | 約 $3 / 月 | $0 |
| 決算ビート要約(日次) | 約 $5 / 月 | $0 |
| 月次 ODM / Korea / Japan スクレイピング要約 | 約 $2 / 月 | $0 |
| 合計 | 約 $20 / 月 | $0 |
Claude Max を既に払っているなら、移行した分が丸ごと純損失になる。
この計画書を開いてよいタイミング
次のどちらかが満たされた瞬間にだけ、このページを開く。
- 他人(読者 / クライアント / 家族)がエンドポイントを叩く必要が出た — Claude Code のサブスクは多人数共有不可。Webhook 受口や読者向けチャットは Cloudflare で立てるしかない
- PC を起動していない時刻に外部からトリガーが入る — クライアントが LINE / メールで資料送信、外部 Webhook など。GitHub Actions の cron でカバーできない時間帯依存ジョブも含む
それ以外(自分用の cron 自動化、まとめ、要約、X 下書き、データ取得)は Claude Code + ローカル skill + GitHub Actions cron で完結する。Cloudflare の Cron Trigger Worker に乗せたくなったときは、まず Claude Code の定額枠が本当に詰まっているかを確認すること。
なぜこの順番か
きっかけは yusukebe「AI エージェントはCloudflareに賭けろ」 と、それを引用して「Cloudflare Workers + Queues で Webhook の 3 秒ルールを突破した」という X の運用報告だった。後者は、コールドスタートのある Cloud Run の前段に Workers を置くことで、Zoom Webhook / Slack App の「リクエストから 3 秒以内に 200 を返さないとリトライ扱い」という制約を突破した話だ。
この 2 本を合わせて、AI エージェントに必要な要素を分解するとこうなる。
| 要素 | Cloudflare 上の手段 | 4 ステップ対応 |
|---|---|---|
| HTTP/WS の入口 | Workers | Step 1 |
| 非同期処理・タスクキュー | Queues | Step 2 |
| LLM 呼び出し | Workers AI / AI Gateway | Step 3 |
| 状態保持(セッション・履歴) | Durable Objects | Step 4 |
| エージェントランタイム | Agents SDK | Step 4 |
Agents SDK は上 4 つを抱き合わせたフレームワークなので、いきなり Step 4 から入っても動きはする。ただし Worker の挙動・Queues の動き・LLM の呼び方を体験せずにフレームワークの中身が分かるかというと無理なので、下から積む。
前提
- Wrangler が
wrangler whoamiで自分のアカウントを返す - Workers Paid Plan ($5 / 月) に加入済み(Queues / Durable Objects / Workers AI のほとんどがここから解放される)
- Node.js 20+ / pnpm
未加入なら Step 1 までは Free でも動く。Step 2 以降で Paid Plan が必要になる。
Step 1: Hono で最小の Worker を 1 本書いてデプロイする
目的
「自分の wrangler 環境で、TypeScript で書いた HTTP ハンドラがエッジから返ってくる」状態を作る。以降のステップは全部この上に乗る。
コード
pnpm create hono@latest cf-agent-step1
# テンプレ選択: cloudflare-workers
cd cf-agent-step1
pnpm install
src/index.ts を書き換える。
import { Hono } from 'hono'
type Bindings = {}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => c.text('hello from cf-agent step 1'))
app.get('/ping', (c) =>
c.json({
ok: true,
cf: c.req.raw.cf,
ts: new Date().toISOString(),
})
)
export default app
動作確認
pnpm dev # localhost:8787 で起動
curl http://localhost:8787/ping
pnpm deploy # ⇒ https://cf-agent-step1.<account>.workers.dev/ にデプロイ
curl https://cf-agent-step1.<account>.workers.dev/ping
cf フィールドにエッジで判定された国コード等が入っていれば成功。ここで wrangler tail を別タブで叩いておくと、本番ログがリアルタイムに流れる。これで Step 1 終わり。
Step 2: Webhook + Queues で「3 秒ルール」を突破する門番を作る
目的
外部サービスからの Webhook を受け取って、署名検証だけして即 200 を返す。重い処理は Queue に逃がす。これが yusukebe 記事の引用ポストが言っていた構成そのもの。
構成
[ 外部サービス ] --POST--> [ Producer Worker ] --send--> [ Cloudflare Queue ]
|
└--200 OK (3秒以内)
|
v
[ Consumer Worker ]
|
v
( 外部 API / DB / 通知 )
Queue を作る
wrangler queues create agent-jobs
wrangler.toml
name = "cf-agent-step2"
main = "src/index.ts"
compatibility_date = "2026-06-01"
# Producer 側(Queue にメッセージを送る)
[[queues.producers]]
queue = "agent-jobs"
binding = "AGENT_JOBS"
# Consumer 側(Queue からメッセージを取り出して処理する)
[[queues.consumers]]
queue = "agent-jobs"
max_batch_size = 10
max_batch_timeout = 5
max_retries = 3
dead_letter_queue = "agent-jobs-dlq"
コード
import { Hono } from 'hono'
type Bindings = {
AGENT_JOBS: Queue
WEBHOOK_SECRET: string
}
const app = new Hono<{ Bindings: Bindings }>()
// HMAC-SHA256 で署名検証する小さな関数
const verifySignature = async (body: string, signature: string, secret: string) => {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body))
const expected = Array.from(new Uint8Array(mac))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
return expected === signature
}
app.post('/webhook/:source', async (c) => {
const source = c.req.param('source')
const body = await c.req.text()
const signature = c.req.header('x-signature') ?? ''
if (!(await verifySignature(body, signature, c.env.WEBHOOK_SECRET))) {
return c.json({ error: 'invalid signature' }, 401)
}
await c.env.AGENT_JOBS.send({
source,
payload: JSON.parse(body),
receivedAt: new Date().toISOString(),
})
return c.json({ ok: true }, 200)
})
// Consumer ハンドラ
export default {
fetch: app.fetch,
async queue(batch: MessageBatch, env: Bindings) {
for (const msg of batch.messages) {
try {
const job = msg.body as { source: string; payload: unknown }
console.log('process job', job.source, job.payload)
// ここで時間のかかる処理(外部 API、DB 書き込み、LLM 呼び出し)
msg.ack()
} catch (e) {
msg.retry()
}
}
},
}
動作確認
wrangler secret put WEBHOOK_SECRET # 適当な文字列を入力
pnpm deploy
# 別タブで Consumer のログを流す
wrangler tail
# 署名付きで叩く
BODY='{"hello":"queue"}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "<さっき入れた文字列>" -hex | awk '{print $2}')
curl -X POST https://cf-agent-step2.<account>.workers.dev/webhook/slack \
-H "content-type: application/json" \
-H "x-signature: $SIG" \
-d "$BODY"
wrangler tail に process job slack { hello: 'queue' } が出たら成功。HTTP レスポンスは数十 ms で 200 が返り、重い処理は別レーンで進むという構造が手元で動いた状態になる。
確認しておく観点
- 故意に署名を壊して 401 が返ること
- Consumer 側で
throwしてリトライが効くこと(max_retries: 3で DLQ 行き) - Cloudflare ダッシュボードの Queues タブで「Backlog」「Producer rate」「Consumer rate」のメーターが動くこと
Step 3: AI Gateway 経由で LLM を呼んでキャッシュする
目的
エージェントの「推論」部分を 1 本通す。OpenAI / Anthropic / Workers AI のどれを使っても、AI Gateway を前段に置くとログ・キャッシュ・レート制限・コスト計測が一元化できる。
AI Gateway を作る
Cloudflare ダッシュボード → AI → AI Gateway → Create Gateway。
名前 agent-gateway で作ると、エンドポイントが https://gateway.ai.cloudflare.com/v1/<account_id>/agent-gateway/<provider>/... の形で発行される。
Workers AI 経由で動かす最小コード
wrangler.toml に追記:
[ai]
binding = "AI"
app.post('/chat', async (c) => {
const { prompt } = await c.req.json<{ prompt: string }>()
// Workers AI(Cloudflare 自前のモデル)を AI Gateway 経由で叩く
const result = await c.env.AI.run(
'@cf/meta/llama-3.1-8b-instruct',
{ prompt },
{
gateway: {
id: 'agent-gateway',
cacheTtl: 3600, // 同じ入力なら 1 時間キャッシュ
skipCache: false,
},
}
)
return c.json(result)
})
OpenAI / Anthropic を AI Gateway 経由で叩く場合は、SDK の baseURL を AI Gateway のエンドポイントに差し替えるだけ。
動作確認
curl -X POST https://cf-agent-step2.<account>.workers.dev/chat \
-H "content-type: application/json" \
-d '{"prompt":"give me one sentence haiku about cloudflare"}'
# 同じ prompt を 2 回叩く → 2 回目は AI Gateway のキャッシュヒットでミリ秒応答
AI Gateway のダッシュボードに「Requests」「Cached」「Tokens」「Cost」が積み上がる。この時点で「Webhook → Queue → LLM 呼び出し」のループが Cloudflare 内で完結する。
Step 4: Agents SDK + Durable Objects で「状態を持つエージェント」を 1 体動かす
目的
ここまでの Worker / Queue / LLM 呼び出しに、エージェントごとの永続状態と WebSocket でのリアルタイム同期を足す。Agents SDK は内部で Durable Objects を使ってそれを実現する。
雛形
pnpm create cloudflare@latest cf-agent-step4 -- --template=cloudflare/agents-starter
cd cf-agent-step4
pnpm install
雛形には次のものが含まれる。
src/server.ts— Worker のエントリ。routeAgentRequest()で/agents/<agent-name>/<id>を Durable Object に振り分けるsrc/agent.ts—Agentクラスを継承した自分のエージェント定義wrangler.toml— Durable Objects の bindings と migrations が初期化済み
エージェントを 1 体作る
src/agent.ts:
import { Agent } from 'agents'
type State = {
history: Array<{ role: 'user' | 'assistant'; content: string }>
}
export class ChatAgent extends Agent<Env, State> {
initialState: State = { history: [] }
async onMessage(connection, message: string) {
const userMsg = { role: 'user' as const, content: message }
this.setState({ history: [...this.state.history, userMsg] })
const result = await this.env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: this.state.history,
})
const reply = { role: 'assistant' as const, content: result.response }
this.setState({ history: [...this.state.history, reply] })
connection.send(JSON.stringify(reply))
}
}
動作確認
pnpm dev
# ブラウザのコンソールで
const ws = new WebSocket('ws://localhost:8787/agents/chat-agent/my-session-1')
ws.onmessage = (e) => console.log('reply:', e.data)
ws.onopen = () => ws.send('hello, who are you?')
ブラウザを閉じて開き直しても、同じ my-session-1 に繋ぎ直せば履歴が残っている。これが Durable Objects の「ID ごとに 1 インスタンス」が効いている瞬間。
Step 2 の門番と繋ぐ
Step 2 の Consumer Worker から、
// Consumer 内
const id = env.CHAT_AGENT.idFromName(job.payload.sessionId)
const stub = env.CHAT_AGENT.get(id)
await stub.fetch('https://internal/agents/chat-agent/' + job.payload.sessionId, {
method: 'POST',
body: JSON.stringify({ message: job.payload.text }),
})
のように呼び出せば、「Webhook で受けたメッセージを、Queue 経由で、対応するエージェント Durable Object に渡す」フローが組める。これがエージェント基盤の最小完成形。
条件が満たされた時に着手するチェックリスト(保留中)
冒頭の経済合理性チェックを通過したら、上から順に消化する。
- Step 1 の Worker をデプロイ
- Step 2 の Producer / Consumer を本物の Webhook 送出元(クライアント業務 LINE Bot / 読者フォーム / 外部サービス)で叩く
- Step 3 の AI Gateway のキャッシュヒット率を測る(読者向けチャットなら同じ質問が積もる前提でキャッシュ設計)
- Step 4 の ChatAgent を Step 2 の Consumer に繋ぐ
- エージェントの履歴に長文を入れ続けたときの DO ストレージ消費を観察する
- Code Mode(LLM に TypeScript を書かせて Dynamic Workers で実行する形)を別記事で試す