GPT-4V x LINE Bot を Cloudflare Workers で実現するためにやったこと・やらなかったこと

この記事では OpenAI が提供する Vision API (GPT-4V) を使用して、LINE に投稿した画像に反応する Bot を作成した際にやったこと・やらなかったことを書いています。 Bot の実装を細かく解説はしていないので、それを知りたい方は「ChatGPT LINE」などでググると参考になる良い記事が沢山でてくるのでそちらを参照してください!

モチベーション

LINE Bot は昔実装したものがありグループ LINE で身の回りのあれこれを通知する君になっていました。機能としては通知のみだったので何か反応してほしいなーと思ったのが最初のきっかけでした。冬休みで時間もあったので OpenAI 周りのプロダクトを整理するためにドキュメントを一通り見てその中から Vison API を使えば画像にも反応できる Bot にもなり面白そうなことがわかったのでガッと実装することにしました。

作ったもの

今回完成したものになります。

アーキテクチャ概要

もともとある LINE Bot は Next.js on Vercel で実装されており、LINE Webhook の受け取りやメッセージ送信はすべてこの Next.js で行われます。LINE とのやりとりに関しては引き続きこちらを利用しました。

そのため Vision API を扱う部分が今回の実装範囲になります。そのまま Next.js を使うことを考えたのですが、最近 X で目にする Hono を使ってみたかったのと今回のユースケースだと Cloudflare D1, R2 も使用できそうだったので勉強もかねて Hono のデプロイ先に Cloudflare Workers を選定しました。つまり興味ドリブン開発です。

下記はここまでを図でまとめたものです。

やったこと

Vision API への画像連携

Vision API に画像を送るためには下記のいずれかの方法を選択します。

  1. URL
  2. Base64エンコード

Vision API のドキュメントには For long running conversations, we suggest passing images via URL's instead of base64. と記載されています。ここでいう long running conversations がどれくらいかは分からないですが、今回はオススメに従って URL 指定を選択しました。

URL 指定をする場合は OpenAI 側からアクセス可能なエンドポイントを用意する必要があります。R2 は Public Bucket として公開できるので、手軽に OpenAI 側からアクセス可能なエンドポイントを用意できます。しかし今回は Hono で R2 から画像を取得して返す下記のような実装を行い Workers を通して画像を返すようにしました。

app.get('/image/:id', (c) => {
  const id = c.req.param('id')
  const image = await c.env.R2.get(id)
  if (!image) {
    return c.text("image not found", 404)
  }
  image.writeHttpMetadata(c.res.headers);

  return c.body(image.body)
})

ローカルでの開発ときには Cloudflare Tunnel を利用することで、ローカルサーバーを公開して OpenAI からのリクエストを受け付けるようにしました。Cloudflare Tunnel の設定方法に関しては以前記載した下記の記事をご覧ください。

hatappi.blog

この構成によりローカルでも本番と同じ構成で開発を行うことができるようになりました。

AI Gateway の使用

OpenAI API のリクエストには微量ながらもお金がかかります。開発段階では試行錯誤が続くので、このコストは気になるところです。モック API サーバーをたてることも考えましたが、面倒なので別の案を探していたところ Cloudflare AI Gateway にたどり着きました。

Cloudflare AI Gateway は私達と OpenAI などの AI 関連の API の間の Proxy となる Cloudlfare のプロダクトの1つで、現状 Caching, Rate Limiting, real-time logs などを提供します。Caching はリクエストの Body が完全一致?した時に AI Gateway がキャッシュを返してくれるようです。そのためローカルで試行錯誤する際は Body を固定することでキャッシュを活用できるためコストを気にせずに開発を進めることができます。

使い方はとても簡単で管理画面上から作成したエンドポイントをいつものエンドポイントの代わりに使用するだけです。例えば OpenAI の場合はこちらです。

何より現状 Cloudflare AI Gateway は無料なのが嬉しいですね!

R2 の Object Lifecycle の設定

今回使用する画像は長期で保存する必要はなく1週間くらい残っていれば良いので、Object lifecycles を設定して7日後にファイルを削除することにしました。

Object Lifecycle を Terraform 経由で設定したい場合は Cloudflare Provider ではなく AWS provider を使用する必要があります。

developers.cloudflare.com

今回は 7日後にファイルを削除するので下記のようなリソースを定義します。

resource "aws_s3_bucket_lifecycle_configuration" "r2" {
  bucket = [bucket id]

  rule {
    id     = "delete-uploaded-objects"
    status = "Enabled"
    expiration {
      days = 7
    }
  }
}

drizzle を使ったスキーマと型情報の連携

D1 を使う際に検討したことの1つとして DB のスキーマから Typescript の型をどのように推論してアプリケーション上で使用するかでした。D1 では Migration の仕組みを Wrangler を通して提供しています。しかしマイグレーションのみなので、自分がやりたかった型情報は得られません。

D1 ではコミュニティのプロジェクトを紹介している Community projects · Cloudflare D1 docs というページがありそこを眺めていたところ Drizzle ORM が自分のやりたいことを実現してくれそうでした。Drizzle ORM では Typescript でスキーマを定義して、そこから migration ファイルを生成してくれます。また Typescript で定義したスキーマをそのまま型情報として使用できます。これによりスムーズに D1 とのつなぎ込みを実装することができました。

やらなかったこと

Cloudflare Queues の使用

元々は Cloudflare Workers に月 $5 の課金をして Cloudflare Queues を使用することで OpenAI との連携する重ための処理を非同期で処理する予定でした。しかし Workers には context.waitUntil() と呼ばれる Promise を渡すことでクライアントには Response を返しつつも Workers としての処理はこの Promise が終わるまで待ってくれる機能があります。Hono では executionCtx で使うようでした。この機能で LINE Webhook に対してはすぐにレスポンスを返しつつも Vision API のリクエストなどの重ための処理は waitUntil 側で行うように実装できました。これにより $5 も払う必要がなくなり、結果としては今回の実装では Workers は無料で済んでしまいました。

画像のアクセスを OpenAI からのみにする

今回 R2 に格納している画像は OpenAI からのリクエストを想定しているので、アクセス制限を行うことを検討しました。 OpenAI では ChatGPT Plugin 用にアクセスしてくる IP Ranges を公開はしているのですが、イメージ用の記載は見つかりませんでした。アクセスログを見てると Microsoft の ASN だったのでこれらの情報でアクセス制限は可能そうですが、今回の画像はそこまで秘匿性の高い画像でもないのと URL も推測しずらいため、直接リクエストされることに対するリスクはあまり考慮しなくても良さそうと判断して今回は対応しませんでした。

公開を求める issue は作成されていたので、どこかで公開はされるかもしれません。

github.com

まとめ

今回は Hono, D1 やR2 など自分の中で新しい要素が多くいろいろ学べました。Hono は公式のドキュメントにやりたいことがすべて書かれていたのと D1, R2 は Wrangler や Cloudflare Tunnel を使うことで本番との差異をほぼなくすことでローカルでの開発からデプロイまでをスムーズに行えて開発体験も良く終始楽しんで開発することができました!