🤖

静的サイトに対する検索にWebブラウザで動作する LLM の利用を試す

2024/06/30に公開

はじめに

ライブラリのドキュメントサイトなどはコンテンツが静的であるため、GitHub Pages など安価なサービスを利用して、静的サイトとして実装することが多い。
一方で、利用者が求める情報にいち早く到達するためには、静的コンテンツに対する検索機能が有効だが、これは動的な機能だ。
そこで、pagefind などは、バックエンドサーバを持たずに静的なサイトを検索する機能を提供してくれる。これらはビルドされた静的コンテンツからインデックスデータを生成し、これをサイトと一緒にデプロイすることで、利用者の環境で検索を実行する。
この機能を利用することで、ユーザは自身の興味のある情報が掲載されているページに到達しやすくなる。
ドキュメントサイトではそれぞれのトピックごとにページが分かれているため、複数のトピックに横断した情報を得ることが難しいことがある。
これを解消するためにチュートリアルやデモなど、ユースケースにフォーカスすることで複数のトピックの情報を参照できたり、そのページへのリンクを含んだドキュメントを作成する事がある。
しかし、これはあくまでも限られたユースケースしかカバーできない。
これを解消するために LLM を利用したい。つまり、複数のページから必要な情報を集め、ユーザの求めるシナリオにあった説明を生成させれば良い。WebGPU を用いてブラウザ上で LLM を動かすライブラリがあるので、それを活用することでサーバを持たずにドキュメントの知識を利用した回答を LLM にさせるようなものが作れそうかを試したい。

以降でははじめに実装した結果を示す。その後その結果を元に考えたことなどを示す。次に利用したライブラリや実装についてを記述し、最後にまとめを書く。

結果

以下に実装をした。なお実装には ChatGPT を多く利用している。
https://github.com/sterashima78/local-llm-rag

以下がデモページだが、メモリを多く消費するためページを開く際は気をつけてほしい。
https://sterashima78.github.io/local-llm-rag/

以下のディレクトリにある各ページをサイトのコンテツに見た立てている。
https://github.com/sterashima78/local-llm-rag/tree/main/public/documents

以下のように動作する。

pagefind を使って関連するであろうコンテンツを検索し、そのコンテンツの情報を使って LLM に回答を生成させている。ちなみに、この回答は入力を元にすることはできているが内容は微妙なものだ。

考えたことなど

一般的な RAG と共通するであろうこと

そもそも検索が大事

検索して得られた知識を前提に回答させるのだから、そもそも入力にする知識が適当であることが重要なのは間違いない。今回はコンテンツが非常に少なかったが、コンテンツが増えれば増えるほど検索の重要性が増すのは間違いない。特に今回の実装において検索の改善には2つのポイントがあると考えている。

1つ目は検索エンジンだ。今回は pagefind を使っているが、 RAG では意味的な類似性に基づいて高速に検索することに優れているベクトルデータベースが用いられることが多いらしい。
ブラウザーで動作させることができるベクトルデータベースがあるのかわからないが、あればそのようなものを利用するとよいのかもしれない。

2つ目は検索クエリだ。今回は入力となったプロンプトをそのまま検索のクエリにも利用している。大抵の場合何かしらの加工をするほうが適当だと思う。当初は、入力されたプロンプトを元に、関連コンテンツの検索に適したキーワードの提案も LLM にやらせようとしていたのだが、後述の通りあまり賢いモデルを使っていなかったため、提案されるキーワードの質が低かったことに加えて、加工可能な出力にプロンプトで調整することが難しかった。

当然モデルの賢さが大事

どうしてもメモリの要求が大きいため、今回は要求メモリが小さいものを適当に選択した。回答の賢さもそうだが、入力に使えるトークンが長くなるとより多くの情報が扱えるため、選択するモデルは重要だと思う。実際は、ユーザにいくつかのモデルから選択してもらうのが良いのだろうと思う。

また、Google Chrome では Gemini Nano がフラグ付きで利用できるので、それを利用するのも良いかもしれない。

今回の構成特有のこと

LLM の利用部分は WebWorker にするべき

今回は適当な実装であったため (ChatGPT がそう書いたので) メインスレッドで LLM を用いた回答の生成を行っていたが、比較的長いタスクであるため WebWorker にするべきだと思った。今回利用した web-llm には WebWorker での利用に関するドキュメントが存在しているため、ちゃんと作るときは参考にしようと思った。

入力とするコンテンツの文書構造が大事そう

pagefind から得られる検索にヒットしたコンテンツには文書構造の情報がない。例えば以下のページで、構成要素はリスト形式で3つ並列に記述されているが pagefind から得られるこの情報がない。
https://sterashima78.github.io/local-llm-rag/documents/3.html

前述の動作例では構成要素が2つだけになっていたが、これは、この文書構造がなくなっていたために読み間違えてしまった結果なのかもしれないと思った。pagefind には結果にメタ情報を加えられたと思うので、生のコンテンツを持たせて入力に使えると良いかもしれない。

手段や利用したもの

LLM の利用にはweb-llmを用いた。また、 https://github.com/mlc-ai/web-llm/blob/main/src/config.ts#L299-L1121 に利用できるモデルなどのリストがあるので、使用するRAMのサイズだけをみて、適当なモデルを選択した。

コンテンツの検索には pagefind を用いた。https://github.com/sterashima78/local-llm-rag/blob/main/createIndex.js のスクリプトでインデックスやクライアントスクリプトを生成し、利用するコンポーネントで読み込んで利用した。

主要な実装は以下のコンポーネントにまとまっている。
https://github.com/sterashima78/local-llm-rag/blob/main/src/components/Chat.vue

ユーザからの入力を pagefind のコンテンツ検索に利用し、検索結果から対象ドキュメントのコンテンツ情報を得た。それをプロンプトに埋め込み、前提情報をとし、LLM にはこの前提情報を元に入力となった質問に答えるように指示をした。

おわりに

計算資源が限られるため、少ない資源で良い結果を出す工夫がとりわけ重要であると感じた。
それはモデルの選択もそうだが、比較的重いタスクである LLM の実行以外で前処理などをしてよりよい結果が得られる工夫ができると、モデルが小さくても良くなる事があると思うので、むしろ LLM の利用以外の部分の工夫が重要と感じた。

Discussion