🍒

Next.js & supabase & LangChainでAIがsakeをおすすめする『sake ai』作ってみた備忘録(後編)

2023/06/14に公開

概要

表題の通りで、日本酒が好きなので勉強がてら作ってみましたという話の後半戦です。

https://github.com/moepyxxx/sake_ai

色々と備忘録を残していたらめちゃくちゃ長くなってたので前後半にわけており、前半はどんな技術を使ったかや作ってみての所感(supabase Auth × Next.js Server Actions, postgreSQLのRLS, tailwindcssのUI周辺など)をまとめました。

https://zenn.dev/moepyxxx/articles/c629c27c45d732

引き続き実装の学びをつらつら書いていきます。後半戦はLLMとNext.jsのServer/Client/Actionsの使い道とかをつらつらします。

実装の学び(続き)

LangChainを活用したLLM体験

LangChainの機能を少し触った身として、感じたことは以下です。

  • LangChainがさまざまな言語モデルとの連携を統一のインターフェースで提供してくれているため、異なる言語モデルを鎖のように繋ぎながらAIから回答を引き出すことが可能。回答から新たに回答を作成する際に、異なる言語モデルを介したい時等に大変便利な気がする
  • プロンプトテンプレートを利用できたりドキュメントを食わせたりもすることができ、GUI上で対話するよりも活用の幅が広がりそう。また、最近はプロンプトエンジニアリングが活性化しているため理解もしやすい
  • 簡単

といいつつpython初心者なので、ドキュメントを漁りながらなんとか2つの回答をChainしながら聞き出すという形の実装を行いました。APIのbodyに詰められているレビューデータを分解してprompt_templateに当てはめて、それをAIに読み込ませる方法です。

# 公式サイトからAPIKeyを発行する必要がある
llm = OpenAI(model_name=“text-davinci-003,openai_api_key=key, max_tokens=-1)
# 別定義してあるjsonファイルをテンプレートとして読み込ませる
recommend_prompt = load_prompt(“recommend_prompt.json”)
analytics_prompt = load_prompt(“analytics_prompt.json”)

class SakeReview(BaseModel):
    # レビューの星1〜5のsake銘柄を分類する形のため
    hoshi_go: str
    hoshi_yon: str
    hoshi_san: str
    hoshi_ni: str
    hoshi_ichi: str

class ConcatenateChain(Chain):
    recommend_chain: LLMChain
    analytics_chain: LLMChain

    @property
    def input_keys(self) -> List[str]:
        all_input_vars = set(self.recommend_chain.input_keys).union(set(self.analytics_chain.input_keys))
        return list(all_input_vars)

    @property
    def output_keys(self) -> List[str]:
        return [‘result’]

    def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
        recommend = self.recommend_chain.run(inputs)
        analytics = self.analytics_chain.run(inputs)
        return {‘result’: {‘recommend’: recommend, ‘analytics’: analytics}}

@app.post(/)
async def answer(review: SakeReview):
    recommend_chain = LLMChain(llm=llm, prompt=recommend_prompt, output_key=“recommend”)
    analytics_chain = LLMChain(llm=llm, prompt=analytics_prompt, output_key=“analytics”)

    concat_chain = ConcatenateChain(recommend_chain=recommend_chain, analytics_chain=analytics_chain, verbose=True)
    result = concat_chain.run({“hoshi_go”:review.hoshi_go, “hoshi_yon”:review.hoshi_yon, “hoshi_san”:review.hoshi_san, “hoshi_ni”:review.hoshi_ni, “hoshi_ichi”:review.hoshi_ichi})
    return result

max_tokens=-1がないと、AIからの回答が永遠に途切れてたので、そのあたりで一生どん詰まりしてました。

こちらでは、sakeのレビューデータからおすすめのsakeや自分のsake嗜好を解析してくれるくんとして作りました。ただこちらなのですが…

prompt

圧倒的に、改善の余地ありです(笑)

私の貧弱なプロンプトエンジニアリングでは、正しいsake情報を返してくれません。今のところあんまり知っていて狙っている銘柄や、飲んで美味しいと思ってはいるけれど登録していないsakeの銘柄等が出てきていないので、精度は全然伸び代しかないかなと思います。特にsake嗜好の方は全く答えになっていない。

プロンプトに改善の余地ありは、それもそうですが、そもそもOpenAIがあまり詳しいsakeのことを知らない気がするので、もっと多くのドキュメントなどをかき集めて独自に読ませる必要があるかもです。

LLM初経験おつぃてはとてもとっつきやすく楽しかったのですが、やはり奥が深いですね。ちょっと触るくらいならサクッとできますが、実運用するとなるとさまざまな調整が必要そうです。

Next.jsのClient/Server/Actionsの使い分け

Next.jsでの実行場所が、コンポーネントごとに分けられるようになった(Client Component/Server Component)とか、そもそも一部だけサーバーで実行できるようになった(Server Actions)とか、色々とありますが、全体的に理解が難しいですし、どれを使うかの判定も難しいなと思ってました。

そこで、sake aiを利用する中で、とにかくいろんなパターンを使ってはコミットし時にはリセットし、消して追加してまた消して…といったことを繰り返し、挙動を見てみました。

その結果、なんとなくそれぞれの機能の特徴や使い道などを、自分なりに咀嚼してみました。まだ坂の途中感ありますが…。

各機能について

おさらいがてら、各機能とNext.jsの公式サイトから読み取れる解釈をまとめます。
基本的に以下の記事がとてもわかりやすいです。

https://nextjs.org/docs/getting-started/react-essentials

Client Component
その名の通り、クライアント(ユーザーのブラウザ)で実行されることになるコンポーネントです。
フックを利用して状態を変えたり、登録しているイベントに基づいて処理をしたりと、馴染みのあるインタラクティブな操作をするものは、基本的にこのコンポーネントを利用します。

使い方は簡単で、コンポーネントファイルの冒頭に"use client";とつけるだけです。

Server Component
その名の通り、サーバー側で実装されるコンポーネントです。そのため、サーバー内でどんな通信をするかという内容は隠蔽されることになります。サーバーで実行することによって、クライアント側で持つJavaScriptのコード量が減って良いとか、パフォーマンスが良くなるとか、そういうことをよく言われています。

また、以下にあるようにServer Componentはコンポーネントそれ自体のasync/awaitに対応しています。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#async-and-await-in-server-components

loading.tsxを利用すると待機中はそちらを自動的に読み込みますので、コンポーネントが持つ責務がわけられてスッキリしますね(公式ドキュメント読まないとやや初見殺しですが)。

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

使い方は簡単で、コンポーネントファイルの冒頭に"use server";とつけるだけです。が、現在、Next.jsのコンポーネントにおいて、コンポーネントのデフォルトとなっています。

Next.jsは「クライアントコンポーネントにする必要のない場合は、サーバーコンポーネントに」とサーバーファーストで考えるべきという姿勢のようです。

Server Actions
その関数だけサーバーで実行、ということができる、という機能です。
2023/06/13現在はアルファ版としてリリースされています。

関数内の冒頭に"use server";とつけるか、"use server";と書かれたファイル内でエクスポートしておいた関数を呼べば、こちらになります。

それだけならばServer Componentとそう変わらないですが、特筆すべき点はClient Componentの中で実行できるという点です。つまり、コンポーネント内において部分的なサーバー処理を行えるということです。

各機能の使い分けイメージ

基本的に公式サイトに書いてある通りですが、実際に自分が行った実装を振り返りながら、使い分けイメージを書いていこうと思います。

認証・認可: Server Actions or Server Component
前回の記事の「supabase × Next.js Server Actionsを利用した認証・認可」でも触れたところなのですが、あまりユーザーに見せたくない処理はサーバー側で行うのが良いと感じました。

クライアント側でAPIを叩くとなると、どうしてもリクエストもレスポンスも丸見えの状態になります。そのような状態が望ましくない場合は、サーバー側に処理を隠蔽できる、という点で、サーバーで処理をできるServer ActionsやServer Componentはとても有用だなと思っています。

sake aiは、認証・認可はすべてServer Actionsで実装しています。

POST/PUT/DELETE: Server Actions
フォームアクションやデータ追加処理などが例に乗ってますが、例の通りこのあたりの「クリックしたら行う」系の処理は、CleintComponent内で利用できるServer Actionsと非常に相性が良いと思いました。

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions

GET レンダリング時に一度だけ行えば良い処理: Server Component or Server Actions
Server Componentは、サーバー側でHTMLを作ってクライアントに渡しています。その性質上、レンダリング時に一度取得すれば良い系のGET系処理は、パフォーマンスが良いという点も鑑みて、Server Componentでの対応が良いかなと思いました。

sake aiがsakeをおすすめするページはまさにそれだったため、Server Actionsで一度だけ取得するようにしています(何度も生成し直す感じでもないので)。

GET 再通信が前提の処理の場合: Client Component のreactQueryやuseEffect

こちらはTwitterのタイムラインのような、リアルタイムに情報を更新する前提の処理を指します。sake aiでいうところの、TOPのsakeレビュー一覧にあたります。sake aiではClient ComponentのuseEffectとServer Actionsで実装しましたが、reactQueryの方がよかったと思います。

他にも、Route Handler + fetchという手もあると思いますが、これもありだと思います。ただ、あくまで意見としてはfetchを使うにしろ何にしろ、Client Componentを利用した方が今は良い気がする、という感じです。

https://nextjs.org/docs/app/building-your-application/routing/router-handlers

この方が良いと思った理由は、以下です。

  • reactQueryのキャッシュ機能がなんだかんだ強力で使いやすいと感じているため
  • Client Componentはイベントが得意という性質のため、Reactへの再実行の伝達が簡単
  • 逆にServer ActionsやServer Componentは、HTMLを最初に生成してから返すという性質上、Reactへの再実行の伝達をする設計が困難

ツールの習熟度の問題だけかもしれませんが、全体的にサーバー側の処理は再実行ややキャッシュのコントロールがかなり難しいと感じました。

以下のようにNext.jsは、fetch関数やそれ以外において再実行をコントロールするような機能を色々とつけてくれているものの、実際に使ってもうまく動かなかったりして、うーん難しいという感じでどん詰まりしてました。

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#data-fetching-without-fetch

とはいえこの辺は、もう少し勉強していくしかないなと思ったところでもあります。。

(全部使いこなしてきちんとパフォーマンス考慮していった場合は、Next.jsが押してくれている通り、fetch関数とかきちんと利用するのが有利なのかなと思っていたりします。今のところ実行単位でキャッシュが付けられるのはこのfetch関数だけ…)

https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating

おわりに

以上、sake好きな自分のための、趣味全開の簡単なアプリケーションでしたが、学びはとても多かったです。

機会があったらもっと改善したいものです。
とくにLLM付近。実際に自分に合うsakeをもっと精度良くおすすめしてほしいものです。

今日も今日とてsake飲みたい。

Discussion