🤖

Hono on Cloudflare WorkersでAIを動かしたい

に公開

やりたいこと

  • Cloudflare Workersで動かしているHonoで書いたアプリでAIを使いたい

Cloudflare Workers / Honoとは

  • Cloudflare Workers
    • Cloudflare Workers
    • Cloudflareが提供するサーバレスのJavascript実行環境
    • エッジサーバ上で実行され,レスポンスが非常に高速
    • zero cold startを謳っており,Lambdaで悩まされるコールドスタート問題を回避できる.
    • 制約としては,
      • Node.jsと一部互換性はある(正確に言うと互換性フラグをONにすれば使えるようになる)が,動かないモジュールもあるためライブラリ選定には気を使う
      • フリープランではworkerのサイズは3MB,1リクエストあたりのCPU時間は10msまで
  • Hono
    • Hono
    • 高速で軽量なWebフレームワーク
    • 対応環境が多い(Cloudflare Workers/Node.js/Deno/Bun)
      • ポータビリティが高い
      • Web標準に従っているためCloudflare Workers上でも稼働する

Cloudflare Workers × AIで気をつけること

ライブラリの問題

上記の通りNode.js完全互換ではないため,ライブラリによっては動かないものがある.対応しているAPI・していないAPIをキチンと調査してからライブラリを選定しましょう!というのが王道だが,Node.jsの世界はライブラリ間の依存が多すぎて正直何が使われているのかを把握するのがとてもつらい.

そこで基本的にはPoCとして簡単なLLM呼び出しをしてみて確認する,というスタイルにせざるを得ない.

CPU時間の制限

前述の通りCloudflare WorkersのCPU時間は1リクエストあたり10msの制限がある.
あっという間やんけ!と思ったが,以下の通り「実際にCPUが動いている時間」であって,workersプロセスが呼び出すリクエストとその待ち時間はCPU時間に含まれないという.

CPU time is the amount of time the CPU actually spends doing work during a given request. If a Worker's request makes a sub-request and waits for that request to come back before doing additional work, this time spent waiting is not counted towards CPU time.
(https://developers.cloudflare.com/workers/platform/limits/#cpu-time より引用)

現に,公式でもWorkerからAIを呼び出す方法が紹介されており,LLMを呼び出したら時間切れしちゃいました😡,というようなことはどうやら起こらなそうと判断.

ライブラリの選定

上記の通り,Cloudflare Workers AIを使うのが公式ドキュメントもあるので一番安牌な気がするのだが,以下の理由で違う方法を探すことにした.

  • 理由①:せっかくポータビリティの高いHonoを使っているのに,Workers AIを使うとCloudflare Workersとの結合度が上がりそう
    • 他環境からも呼び出せないことはなさそうだったが,バインディングの設定が使えなくなったり(Cloudflare Workersから呼び出す分にはwrangler.tomlに設定すればサクッと使えるようになるが,それができなくなる)しそう
  • 理由②:モデルの制約
    • 提供されているモデルは多いものの,(OSSでない)GPTやClaudeは提供されていない.
    • 今だとgpt-ossを呼び出せるのでノックアウトにはならないかもしれないが,最新のモデルを使うのは難しそう.

Vercel AI SDKを使ってみる

Cloudflare Workers AIの設定例にVercel AI SDKが挙がっていたので,実はCloudflare Workers上でVercel AI SDKを使える説があるのでは?ということで試してみた.

npm install ai @ai-sdk/openai

パッケージをインストールして,

OPENAI_API_KEY="***********"

.dev.varsにOpenAIのAPIキーを設定.

app.get('/poc', async (c) => {
  const prompt =
    'こんにちは!今日はいい天気ですね!今日は何をして過ごすのがおすすめですか?';
  const apiKey = c.env.OPENAI_API_KEY;

  const openaiProvider = createOpenAI({
    apiKey: apiKey,
  });
  const { text } = await generateText({
    model: openaiProvider('gpt-3.5-turbo'),
    prompt,
  });
  console.log(text);

  return c.text(text);
});

あくまでPoCなのでサクッとindex.tsで完結するスクリプトを書いて実行する.
ポイントは,.dev.varsに環境変数としてOPEN_API_KEYを定義するだけではSDKにAPIキーが渡らないので,明示的にコンテキストから取得してcreateOpenAiを呼び出すこと.

すると,

こんにちは!いい天気の日は外で過ごすのがおすすめですね!以下はいくつかのオプションです:

1. ピクニック:公園や庭で友人や家族と楽しいピクニックを楽しんでみてはいかがでしょうか?お気に入りの食べ物や飲み物を持参して、リラックスして楽しむことができます。

2. 散歩やハイキング:自然の中で散歩やハイキングを楽しむのも良い選択肢です。新しい場所を探索することで、リフレッシュされることができます。

3. スポーツやアクティビティ:テニスやバドミントン、サッカーなどのスポーツを友人たちと楽しむのも良い方法です。また、サイクリングや水泳などのアクティビティもおすすめです。

4. カフェでのんびり:カフェでお気に入りの飲み物を楽しみながら本を読んだり、友人とおしゃべりを楽しむのも良い選択肢です。

どんな方法を選んでも、いい天気を存分に楽しんでリフレッシュしてくださいね!

まずローカルで動かしてみると,こんな感じで回答が返ってくる.
APIの実行時間は11秒くら掛かっている.

次にこれをCloudflare Workers上で動かしてみると,これもやはりしっかりと回答が返ってくる.

ということで,どうやらCloudflare Workers上でVercel AI SDKを使っでLLMを呼び出すことができそう!とわかった.

ストリームレスポンスできるのか

現状はgenerateTextを使ってテキストを生成しているので,全文生成完了後にレスポンスとなる.

Vercel AI SDKは当然のことながらstreamTextに対応している.
https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text

HonoのほうもstreamText()がある.
https://hono.dev/docs/helpers/streaming#streamtext

よって,こんな実装をすれば良い.

import { streamText as vercelAiSdkStreamText } from 'ai';
import { streamText as honoStreamText } from 'hono/streaming';

app.get('/poc', async (c) => {
  const prompt =
    'こんにちは!今日はいい天気ですね!今日は何をして過ごすのがおすすめですか?長めに回答してね!';
  const apiKey = c.env.OPENAI_API_KEY;

  const openaiProvider = createOpenAI({
    apiKey: apiKey,
  });
  const { textStream } = await vercelAiSdkStreamText({
    model: openaiProvider('gpt-5-nano'),
    prompt,
  });

  return honoStreamText(c, async (stream) => {
    for await (const textPart of textStream) {
      await stream.write(textPart);
    }
  });
});

これをデプロイすると,こんな感じで回答が返ってくる.

こんにちは!今日は本当にいい天気みたいですね。天気を活かして外で過ごすのがおすすめです。長めにいろいろ案を挙げますので、気分や状況に合わせて選んでください。

1) 屋外でのんびり満喫プラン
- 公園でのんびり散歩+ピクニック
  - 朝ひんやりしている時間帯に公園を散歩して、ベンチで本を1冊読む。昼前くらいに軽いピクニックランチを広げて、日差しを浴びながらリラックス。
  - 持ち物例: 水筒、タオル、日焼け止め、軽いブランケット、スナックやサンドイッチ。
- 写真散策とカフェ休憩
  - 気になる街角や自然の風景を写真に収めつつ、途中でお気に入りのカフェに寄って休憩。スマホのカメラ機能を使って、光の入り方を探すのも楽しいですよ。

2) アクティブに楽しむプラン
- 自転車で近場を探検
  - 近所の川沿い・海沿い・丘陵地帯など、行きたいスポットを2〜3カ所回るコースを組むと充実感大。ルートは事前に地図アプリでチェックして、途中のベンチや展望ポイントも押さえるといいです。
- 体を動かすゲーム感覚
  - 友人とバドミントン、 frisbee、キャッチボール、フリスビーなどを楽しむのもおすすめ。天気が良いと体も軽く動きやすいですよ。

3) 文化・知的に楽しむプラン
- 路地歩きと写真+ミニ美術鑑賞
  - 街の路地を散策して、気になるお店の看板やアートを写真に収める。路地の小さなギャラリーやイベントがあれば立ち寄ってみるのも良いです。
- 屋外イベントや図書館・カフェのワークショップ
  - 近場で屋外ワークショップやミニ講座が開催されていれば参加してみる。知的好奇心を満たしつつ、自然と触れ合える良い機会になります。

4) ファミリー・友人・一人旅、それぞれのおすすめ
- 一人旅・自分時間派
  - 好きな音楽を聴きながらのんびり散歩→お気に入りのカフェで読書や日記を書く。自分のペースでリフレッシュできます。
- 友人と
  - 市場やパン屋で朝活してから、公園でピクニック、夕方はみんなで写真を見返して語り合う。短いミニセルフイベントのようで楽しいです。
- 家族で
  - 子どもと一緒に公園で遊んだ後、ファミリーフレンドリーレストランやアイスクリーム店でクールダウン。安全に気をつけつつ、みんなで楽しめるルートを選ぶと良いです。

5) 1日の例(天気が良い日向け・ゆったり版)
- 8:30 - 朝のコーヒーと窓の外の風景を楽しむ
- 9:00 - 近所の公園へ出発、軽い散歩と写真
- 11:00 - 地元の市場で新鮮な材料を見て回る
- 12:00 - ピクニックランチ(サンドイッチ+果物+水分補給)
- 13:00 - 公園内のベンチで本を読んだり、空を眺めたり
- 15:00 - カフェでひと休み、ジェラートやアイスコーヒー
- 16:00 - 自転車で周辺の新スポットを探索
- 18:00 - 夕日を眺めながら、それぞれの帰路へ
- 19:30 - 家で軽い夕食・片付け・今日の写真を振り返り

6) 予算や気分で選べるコツ
- 予算0円〜1,000円程度
  - 散歩・写真・公園・自炊ベースのピクニックが中心。お気に入りのコーヒーを持参して外で味わうのも良いです。
- 1,000円〜3,000円程度
  - カフェで休憩を挟んだり、近場の市場で食材を買って簡単なランチを作る。週末のイベントや路地の小さなギャラリーを探してみるのも◎
- 3,000円以上
  - 自転車レンタル、短距離の日帰り遠出、屋外イベントの体験やワークショップ参加など、体験型のプランも組みやすいです。

7) 天気を最大限に活かすコツ
- 日焼け止め・帽子・水分を忘れずに
- 暑い日には早朝や夕方の涼しい時間帯を選ぶ
- 水分はこまめに、こまめな休憩を挟む
- 荷物は軽めに、使うものだけを持って出発

もしよろしければ、住んでいる場所の近さ(公園の有無、海・川の距離)、一緒に楽しみたい相手(ひとり、友人、家族)、予算や好きなアクティビティ(写真、散歩、スポーツ、料理など)を教えてください。天気・気温・風の感じなどの情報が分かれば、さらにぴったりの1日プランを具体的に作成します。どう過ごしたいか、教えてくださいね。

LangSmithでトレースできるのか

ここまで書いた通り,Hono on Cloudflare WorkersでLLMを呼び出して生成されたテキストをストリームとしてレスポンスすることができた.

ここまでは,まあできるんだろうな,という感じ.趣味で遊ぶ分にはここまでで全然良いのだが,一歩踏み込んでオブザーバビリティはどうだろう?というところを見てみる.

https://ai-sdk.dev/providers/observability/langsmith

ただ,結論から言うとHono on Cloudflare WorkersでLangSmithとの連携はうまく行かなかった.

https://nekoze.xyz/posts/introduce-langsmith-to-vercel-ai-sdk-otel-on-hono-cloudflare-workers

こちらを参考にしてローカルで起動した時は,LLMへのinput(プロンプト)はLangSmithで確認できるようになったがoutput(出力)は確認できず,Cloudflare Workersにデプロイしたアプリはinputすら表示されず.

LangChain.jsを使ってみる

Vercel AI SDKはLLMの呼び出しは特に引っかかりなくできたもののLangSmithとの連携で詰まってしまったので,LangSmithとの連携がしやすそうなLangChain.jsを使ってみたらどうだろう?と思い立った.

GitHubを確認するとどうやらCloudflare Workersをサポートしているらしい.
https://github.com/langchain-ai/langchainjs

ということで以下の通りライブラリをインストール.

npm install langchain @langchain/core @langchain/openai

.dev.varsには以下の通りOPENAIのAPIKEYとLANGCHAIN関連の設定を追加

OPENAI_API_KEY="***********"
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY="***********"
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
LANGCHAIN_PROJECT="***********"

処理は以下のように.

app.get('/poc', async (c) => {
  process.env.LANGCHAIN_API_KEY = c.env.LANGCHAIN_API_KEY
  process.env.LANGCHAIN_TRACING_V2 = c.env.LANGCHAIN_TRACING_V2
  process.env.LANGCHAIN_PROJECT = c.env.LANGCHAIN_PROJECT
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";
  const model = new ChatOpenAI({
    model: 'gpt-5-nano',
    apiKey: c.env.OPENAI_API_KEY,
  });
  const messages = [
    new SystemMessage('You are helpful assistant.'),
    new HumanMessage('ハロー!!今日は非常にいい天気ですね.どこに出かけたら良いかアドバイスしてくれませんか?'),
  ];
  const textStream = await model.stream(messages);

  return streamText(c, async (stream) => {
    for await (const chunk of textStream) {
      const content = String(chunk.content);
      if (content) {
        await stream.write(content);
      }
    }
  });
});

ポイントは

process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

これで,これがあることでtraceがサーバレス環境で確実に送信されるようになるという(DeepWikiに教えてもらった).
実際,これが無いとLangSmith上でLLMのoutputが出力されなかった.
Vercel AI SDKで遭遇したoutput出力がない問題もこれが原因かもしれない?と思い試してみたが改善せずで,結局Vercel AI SDKをうまくトレースできない問題は分からずじまい.

おわりに

結局LangChainだなあ,という感じ.
Python版ほどメジャーではないが,情報の多さやLangSmithとの連携のやりやすさなど,かゆいところに手が届くという意味でLangChain.jsに軍配があがる印象.

Cloudflare WorkersでLLMを呼び出すやり方を整理できたので,今後は自作のWEBアプリにもAI機能を組み込んでいきたい.

Discussion