0
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 AI 編 (Text Generation) 1/2

Posted at

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 を追加します。

wrangler.jsonc
{
	"$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 を作りましょう。

src/index.ts
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 を開きます。

すると、少し待ってレスポンスが返ってきます。

image.png

なんかいかにもな文章ですね。

せっかくなので翻訳してみた

原文

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 っぽく、文章がぶわーって出てくる感じにしたい

ストリームを使って、出力されている文章をどんどん表示するようにしましょう。

src/index.ts
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>;

で、アクセスしてみると

image.png

あーあ…全部のデータがそのまま返って来ちゃいました…。

それはそうで、Response は ReadableStream が返って来ています。

なので、Llama が投げたレスポンスは全部そのまま返って来てしまうわけですね。

ってことで、細工をします。

まず、今返って来てるデータは SSE(Server-Sent Events) って形式で、まあ WebSocket の単方向バージョンみたいな感じで思っておきます。

で、画像を見ると data: ${JSON Object} って感じで、JSON Object の response に一句一句大事に詰まっていますよね。

ってことで、これを取り出して Stream に載せます。

そして出来上がったコードがこちらです。

src/index.ts
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-Typetext/plain にしていないのは圧縮が効いてストリームレスポンスが返って来なくなるからです。

text/plain じゃないと嫌だなあという方は、代わりに Headers に Content-Encofing: identity をつけて圧縮しないでって伝えることで、圧縮せずにストリームがブワーって返ってきます。

いい感じ!

前編 おわり

さて、ここまでで AI をストリームで呼ぶことに成功しました。

次の記事は、

  • Durable Objects で質問を記憶させる
  • AI Gateway を使う

ってことをやっていこうと思います。

0
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
0
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?