🤪

LLMをWebフレームワークにしたら、未来が見えた #2024

2024/05/04に公開

最近、LLMにWeb Backendを書かせて遊ぶ、Hanabiというサービスを作っています。その開発過程で、前に試したLLMをAPIとして振る舞わせるアプローチを再検討したので、記事としてまとめました。

一年ちょっと前、私はChatGPTをWebフレームワークにしようと試みました...が、残念ながら全く実用的ではありませんでした。しかし、あれから一年、LLMは目覚ましい進歩で進化を遂げました。価格は下がり、速度も上がり、記憶容量の増加やRAGの発展など、もはや別物レベルで進化しています。
いまならもうちょっと実用的なヤツが作れるんじゃねってことで、色々な手法を面白がった再検討したまとめです。
余談ですが、一年前はLLM=ChatGPTという状況でしたね...懐かしい。ちょうどvicuna13Bが出た頃ですかね?

↓去年の記事(できれば読んでほしい)↓
https://zenn.dev/inaridiy/articles/4c5a0da5b0b421

出来たもの

全部プロンプトに入れちゃえ版
SQLを頑張って使え版

https://github.com/inaridiy/llm-as-api

今回は、APIの呼び出し毎に状態の変化を記録させ、それを全部プロンプトに突っ込むやつ(通称:全部プロンプトに入れちゃえ版)と、ToolアプローチでSQLの実行もできるようにしたやつ(通称:SQLを頑張って使え版)の二種類試作しました。それぞれモデルもバラバラで、あまり比較としては適切ではありませんが、LLMの進化と実用度を観察して面白がるには十分だと思います。

それぞれ上記のリンクから試すことができますが、運が良ければいい感じにデータの読み書きができます。つまり、驚くべきことにフロントエンドとバックエンド間の連携が取れています!!
特に、前者の物は8割ぐらいで上手いこと動きます。これは地味ですが、昨年のデモが毎回適当こいてた事を考えると、劇的な進化です。あとUIがマシになったり、データが永続化してたり、色々実用化に近づきました。

続いて、各バージョンの比較と簡単な解説をします。

全部プロンプトに入れちゃえ版

https://llm-as-api.inaridiy.workers.dev
このバージョンは、状態の変化を申告させて記録し、全部プロンプトに突っ込めば、データの永続化ができるのでは?という仮説を元に作成されました。
この手法自体は昨年の記事でも触れてましたが、Inputトークン数が全く足りず、頓挫していました。信じられないことですが、当時のGPT3.5のトークン数はたったの4096トークンです。

しかし、2024年の今、Claude3 Haikuという神モデルが登場しました。 なんとContextWindowが200Kトークンもあり、しかもかなり安く、入力が増えても割と捌いてくれます。

んで、そのHaikuを組み込んだ、HonoのRoute Handerを作成して、それを用いてTODOアプリを作ったのが次のコードです。
https://github.com/inaridiy/llm-as-api/blob/d63d1036ca3316ef289dc07b93ce065d13e9bc8d/all-prompt/src/index.ts#L1-L25
このコード自体に特に着目する点はないですが、HTMLの生成プロンプトにAPIを参照するように含めており、実際に生成されるコードもそれに従ってAPI経由でTODOを取得してくれます(8割ぐらいの確率で)。

そして、肝心のllmRouteハンドラー内のプロンプトと記憶処理部分は、次のとおりです。
汎用的なプロンプトとコードのみで構成されているため、TODOに限定しない使い方ができるはずです。
https://github.com/inaridiy/llm-as-api/blob/d63d1036ca3316ef289dc07b93ce065d13e9bc8d/all-prompt/src/llmRoute.ts#L15-L20
https://github.com/inaridiy/llm-as-api/blob/d63d1036ca3316ef289dc07b93ce065d13e9bc8d/all-prompt/src/llmRoute.ts#L38-L43

まず、Claude3系のモデルは、Markdownではなく、XML的な構文で文章の構造化やアウトプットの構造を指示することが推奨されています。
プロンプトでは、APIのモックとして振る舞い、<response_body>タグで囲んでレスポンスを返すこと、状態の変化を<state_diff>タグで囲んで出力することを指示しています。
そして、出力から雑な正規表現でそれらを取り出し、HTTPリクエストとしてブラウザに返却しています。

記憶部分は、LLMが出力した<state_diff>内部の自然言語を、そのままCloudflare KVに突っ込んでいます。そして、LLMを呼び出す前に、直近20件の状態変化(コスト削減のため)をKVから取り出して、プロンプトに突っ込むことで、過去の状態変化を踏まえた返答をさせます。

たったこれだけで、結構いい感じに記憶してくれていました。いやはやHaikuは凄いですね。

余談ですが、今回Cloudflare KVを初めて使ったのですが、かなり面白い書き心地でした。Firestoreほどの重たさはなく、RDBのようにテーブルを作る必要もないので、サクッと作る場合はメインの記憶先としても使える気がします。

まとめですが、結構面白いものができたと思います。特に、今回はデモの都合上注目されずらいですが、API側はかなりの精度で動作していたので、フロントエンドをOpusなどの上位モデルで生成した上でキャッシュして、バックエンドのみ動的にさせれば、簡単なSNSなども作れる感触を得ました。

/api/todos?ids=1,4
上記のように、実装や指示すらしていないのにも関わらず、Queryパラメータを元にそれっぽくレスポンスを返してくれるので、面白いですね。

SQLを頑張って使え版

https://with-sql.inaridiy.workers.dev
先ほどの「全部プロンプトに入れちゃえ版」は、上々な成果を挙げましたが、Claude3のコンテキスト長に依存しており、他モデルで同等のことができるのはGemini1.5だけで、それもコスト的に厳しいでしょう。
そこで、軽量OSSなLLMで最強のllama3 8bを使って、実現可能な代替アプローチを探しました。
そうして出来たのが、Tool的なアプローチでLLMにSQLを実行可能にし、複数回LLMとやりとりしてレスポンスを返すアプローチです。

また、llama3 8bだとCloudflare Workers AIでサポートされているので、CFスタックで完結できるのも旨味でした。しかし、これがのちの苦労につながります

こちらもHonoのRoute Handerを作成して、同じ流れでTODOアプリを作成しました。
https://github.com/inaridiy/llm-as-api/blob/d63d1036ca3316ef289dc07b93ce065d13e9bc8d/with-sql/src/index.ts#L1-L50
プロンプトはGPT系と同じくMarkdown的に構成していて、SQLを扱うのでデータベースのスキーマをcreate table文を使ってLLMに示しています。

こちらの記憶部分は次のように実装しています。こちらも同じく汎用的なプロンプトとコードの組み合わせで、データは最終的にCloudlfare D1に格納しています。
https://github.com/inaridiy/llm-as-api/blob/d63d1036ca3316ef289dc07b93ce065d13e9bc8d/with-sql/src/prompt.ts#L1-L19
https://github.com/inaridiy/llm-as-api/blob/2c17ec7edc7766207a00bde10b9891e0eee81b8c/with-sql/src/llmRoute.ts#L27-L66
コードが汚くなっていますが、ご容赦ください。 まず、プロンプトはシンプルで、SQLかResponseBodyをMarkdownで返却するように指示しています。
ちょっとした工夫として、LLMからの出力を一つ目のcode blockで切り取っています。というのも、llama3系ではどうプロンプトを調整しても、SQLの実行結果まで勝手に推測してしまい、SQLを実行する前に、レスポンスを返してしまっていました。そこで、SQLを叩こうとした段階でLLMの出力を強制的に停止させて、無理やり実行結果を渡しています。

本来はstopトークンで指示するべきですが、Workers AIのインターフェースでstopトークンを指定できる箇所がなさげだったので、止むを得ずの処置です。

また、temperatureが指定できない、出力トークン数がモデルの最大値よりも少ない、レスポンスが遅いなどの問題もあったので、Groqというサービスを使った実装も行いました。これについては、後述します。

肝心の出力精度についてですが、フロントエンドの安定感は先の手法に比べて明らかに下がりました。しかしこれは、モデルそのものの出力精度によるものが大きいです。やはりHaikuは凄いですね。
そして、バックエンド側の安定感は、先の手法に比べて安定している印象です。やはり、きちんとSQLを通してデータのやり取りをするので、SQLさえ書いてくれれば、よしなに動きます。

また、レートリミットの関係で今回のデモでは使用していませんが、LLMサービスをGroqに置き換えると、少し遅いWebサイトぐらいの速度で動作しました。これはマジでびっくりするレベルで、1~2秒でページが表示されました。実装自体はすでに存在するので、お手持ちの環境で試してみてください。

まとめると、SQLを使ったアプローチはバックエンドに限れば、軽量なOSSモデルでも十分動作しました。しかし、精度を求めるなら、GPT3.5 Function CallingやそれこそHaikuを使った方がいいと思います。8Bでもそこそこ動作している時点で、llama3の性能が高いのは疑う余地はないですが。
そして、groqを使うと、モックとして使えるレベルでは?と思う速度が出ます。多分、APIの仕様書を読ませれば、かなりそれっぽく動くはずです。これはマジで凄いです(小並感)。

余談と宣伝

ここまでこの記事を読んでくださってありがとうございます。久しぶりに血湧き肉躍る感じでLLMを触ることができて楽しかったとともに、少し懐かしかったです。
今となっては昔話ですが、1年前はGPT3.5のAPIが公開され、Langchainが流行り、まだRAGという言葉がなかった時期です。しかし、LLMに無限の可能性を感じ、アドレナリンが出まくって開発にのめり込んでいました。
あれから一年、LLMに対する肌感がおおよそ固まり、昨年ほどアドレナリンが出ることは少ないと思います。

そんな退屈を消しとばすような、ワクワクで満ちたサービスを開発中で、現在開発の最終段階を迎えています。
冒頭にチラッと触れましたが、そのサービスはHanabiという名前で、ユーザーのプロンプトからWeb Backend APIを生成して、ブラウザ上で実行したり、そのままCloudflare Workerにデプロイすることができます。まぁ、V0のAPI版って感じです。

https://hanabi.rest/applications/rhGlHQWdZDp

このリンクは、そのHanabiを使ってTwitter風のSNSを作ってもらった例です。 よかったら軽くみてみてください。
Hanabiは、現在初期のウェイトリストユーザーのみでβテストを行なっていますが、数日以内に一般公開をして皆様に使っていただけるようにする予定です。
それまで、Twitterなどをフォローしてお待ちしていただけると幸いです!!

https://twitter.com/hanabi_rest

hanabi.rest

Discussion