この記事は ひとりCloudflareを使い倒す Advent Calendar 2025 の8日目です
Cloudflare には Workers AI と呼ばれる、要は AI モデルを使えるサービスがあります。
今回はシンプルに Text Generation のモデルを使って、ChatGPT っぽいことをやってみましょう。
なお、ローカロリー化のため例によって UI はなく、ヘッドレスな構成としています (すなわち、ほぼチュートリアルをなぞっています)
で、内容が結構膨れてきたので 2 部構成にしています。
この記事では、Workers AI をストリームで出せるってとこまでやってみています。
この記事を読み進めるために必要なこと
- TypeScript を使って Web アプリを作ったことがある
- Cloudflare のアカウントを持っている
Workers AI ってなんぞ
AI モデルを、ローカルのサーバーも GPU も使わずに動かすことができるサービスです。
無料プランであっても、10,000 Neurons / day までは無料で使えます。(課金すると、無料枠を超えて従量制で使えます)
使えるモデルは基本的にオープンソースモデルですが、オープンソースだからといって侮ることなかれ。
テキスト生成にとどまらず、音声生成や文字起こし、画像生成、翻訳やベクトル化 (RAG とかに使うやつ) も使えます。
また、Workers の名前を冠していますが、OpenAI の API と互換性があるので、Workers 以外からも呼べます。
AI Gateway ってなんぞ
Workers AI を使うなら、一緒につかうべきということで紹介です。
Workers AI はモデルの提供、AI Gateway はプロンプトのキャッシュや外部 AI Provider への接続など、Workers AI だけではできないいい感じのことをやってくれます。
あとは DLP や Guardrail など、安全系の機能がくっついてるのも特徴的です。
まあ今回そんなに大事なことじゃないので、プロンプトキャッシュできるんだなくらいの理解度で使います。
なお、この記事では使いません。ざんねん。
触るぞ、Workers AI
ただチュートリアルやるのもあんまおもんないので、Durable Objects で会話履歴を保存して毎度ユーザープロンプトだけを送れば、Workers で会話履歴を組み合わせて送ってくれる仕組みを 2 記事構成で作ります。
環境構築
今回も今回とて Workers を使います。ということでお決まりの。
pnpm create cloudflare@latest hello-ai
Worker Only テンプレートを使います。
pnpm run cf-typegen で型を作って、pnpm run dev で動作したら環境構築は終わりです。
Workers AI を使う
AI Gateway だの Durable Objects だの言いましたが、とりあえず Workers AI が叩けなければ何も始まりません
ってことで、シンプルに Workers AI を叩きます。
まずは Workers を叩くために、wrangler.jsonc に Bindings を追加します。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "hello-ai",
"main": "src/index.ts",
"compatibility_date": "2025-12-12",
"observability": {
"enabled": true
- }
+ },
+ "ai": {
+ "binding": "AI"
+ }
}
で、pnpm run cf-typegen で型を作ったら準備 OK です。
AI リソースを叩くために、wrangler login でログインしておいてください。
そしたら、src/index.ts をいじって、アクセスしたら llama-3-8b-instruct を叩いて Cloudflare Workers についてシェイクスピアっぽい詩を返してくれる Workers を作りましょう。
export default {
async fetch(request, env, ctx): Promise<Response> {
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Write a two-paragraph poem about Cloudflare Workers in the style of Shakespeare.',
})
return new Response(response.response)
},
} satisfies ExportedHandler<Env>;
で、pnpm run dev で立ち上げましたらば、localhost:8787 を開きます。
すると、少し待ってレスポンスが返ってきます。
なんかいかにもな文章ですね。
せっかくなので翻訳してみた
原文
Fair Cloudflare Workers, thou dost weave
A tapestry of code, a marvel to perceive.
A layer 'mong the layers of the digital sea,
Thou dost inspect each packet, as 'twere a curious spree.
With scripts of might, thou dost transform and guide,
The flow of data, as a gentle brook doth glide.
Thy Workers, swift and silent as a summer's breeze,
Do intercept and redirect, with ease and expertise.And when the threats of cyber-space do assail,
Thou dost defend, a steadfast sentinel, without fail.
Thy firewall strong, a bulwark 'gainst the foe's assault,
Thou dost protect the gates, where data doth make its halt.
And when the requests doth come, in rapid, ceaseless stream,
Thou dost respond, a courteous host, with speed and gleam.
Oh, Cloudflare Workers, thy virtues we do extol,
A marvel of the digital age, a wonder to behold.
DeepL 訳
公正なるクラウドフレア・ワーカーよ、汝は織りなす
コードのタペストリー、見る者を驚嘆させる
デジタルの海の層の層の間に
汝は各パケットを検査する、好奇心に駆られたように
力あるスクリプトで、汝は変容させ導く
データの流れを、穏やかな小川が流れるように。
汝のワーカーは、夏のそよ風のように速く静かに
遮断し、迂回させる、容易さと熟練をもって。そして仮想空間の脅威が襲いかかる時
汝は守り抜く、揺るぎない見張りとして、決して失敗せず。
汝の堅固なるファイアウォールは敵の攻撃に対する防壁
データが停滞する門を汝は守る
そして要求が絶え間なく急速な流れで押し寄せるとき
汝は礼儀正しいホストとして、速さと輝きをもって応える
おお、Cloudflare Workersよ、汝の美徳を我らは称える
デジタル時代の驚異、見る者を魅了する奇跡
いい感じの文章が出てきました。
ChatGPT っぽく、文章がぶわーって出てくる感じにしたい
ストリームを使って、出力されている文章をどんどん表示するようにしましょう。
export default {
async fetch(request, env, ctx): Promise<Response> {
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Write a two-paragraph poem about Cloudflare Workers in the style of Shakespeare.',
+ stream: true
})
return new Response(response, {
+ headers: { 'Content-Type': 'text/event-stream' }
})
},
} satisfies ExportedHandler<Env>;
で、アクセスしてみると
あーあ…全部のデータがそのまま返って来ちゃいました…。
それはそうで、Response は ReadableStream が返って来ています。
なので、Llama が投げたレスポンスは全部そのまま返って来てしまうわけですね。
ってことで、細工をします。
まず、今返って来てるデータは SSE(Server-Sent Events) って形式で、まあ WebSocket の単方向バージョンみたいな感じで思っておきます。
で、画像を見ると data: ${JSON Object} って感じで、JSON Object の response に一句一句大事に詰まっていますよね。
ってことで、これを取り出して Stream に載せます。
そして出来上がったコードがこちらです。
export default {
async fetch(request, env, ctx): Promise<Response> {
const aiStream = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Write a two-paragraph poem about Cloudflare Workers in the style of Shakespeare.',
stream: true
})
const body = new ReadableStream({
async start(controller) {
const reader = aiStream.getReader();
const dec = new TextDecoder();
const enc = new TextEncoder();
let buf = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
buf = buf.replaceAll('\r\n', '\n');
let cut;
while ((cut = buf.indexOf('\n\n')) !== -1) {
const block = buf.slice(0, cut);
buf = buf.slice(cut + 2);
const data = block
.split('\n')
.filter((l) => l.startsWith('data:'))
.map((l) => l.slice(5).trimStart())
.join('\n');
if (!data) continue;
if (data === '[DONE]') return;
const obj = JSON.parse(data);
const token = obj?.response ?? '';
if (token) controller.enqueue(enc.encode(token));
}
}
} finally {
controller.close();
}
},
});
return new Response(body, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
},
} satisfies ExportedHandler<Env>;
Content-Type を text/plain にしていないのは圧縮が効いてストリームレスポンスが返って来なくなるからです。
text/plain じゃないと嫌だなあという方は、代わりに Headers に Content-Encofing: identity をつけて圧縮しないでって伝えることで、圧縮せずにストリームがブワーって返ってきます。
いい感じ!
前編 おわり
さて、ここまでで AI をストリームで呼ぶことに成功しました。
次の記事は、
- Durable Objects で質問を記憶させる
- AI Gateway を使う
ってことをやっていこうと思います。

