気付けば日常でもAIを使った提案や生成テキストを目にすることが多くなりました。
私もChatGPT Plusを契約したりStable Diffusionで遊ぶこともありますが、開発者側としては遅れている感が否めません。そこで、今回はちょっと背伸びしてLLMを駆使したツールを作ってみました。
ところで、右下になんか表示されていますね?今回のテーマです。
何をしたいか
端的に言うと、特定のデータをDBに保存しておき、ユーザーからの質問を起点にLLMが適切な答えを返すようにしたいです。
このブログに既にアップしていますが、これは私自身の情報を少々データ化して自分専用のチャットボットにしました。この仕組みをもっと洗練・汎用化していけば企業の採用窓口やサービス問い合わせ機能を簡単かつ低安価に提供できそうです。
構成
構成は下記の通り。
フロントエンド:Vite React
拘りたいのは、GoogleAnalyticsのように他のWebサイトから簡単に呼び出せるように1ファイルで納めたいという点です。つまりJS/CSSを1ファイルずつ公開し、外部のサイトから読み込むだけで簡単に表示されるような流れが望ましいです。サーバー設定でCSPくらいは必要になるかもしれませんが。
導入先のサイトでiframeを書くのは避けたいところです。はてなブログがそうであるように、headタグ(JSやCSS)なら簡単にいじれるという環境は多いと思います。
Viteはライブラリモードでビルドすれば楽にJS/CSSを出力でき、ReactやVue、あるいはバニラという選択肢を選べるのが利点です。
データベース:sqlite-vss
SQLiteにベクトルDB機能を組み込んだsqlite-vssを利用します。
このDBの素晴らしい点は、別途サーバーを用意する必要がないことです。個人開発においてコストが低いというのは非常に魅力的です。
バックエンド:express
ここは正直なんでもいいです。sqlite-vssがサポートしている言語であれば。
まあでもこの手の実装はNodeJSかPythonがベターかなと思います。
LLM:Gemini
ChatGPTは最近またシステム障害があって不信感を抱いているのでGeminiにしました。
使うAPIは①generateContent(テキスト生成)と②embedContent(エンベディング生成)の2つですが、generateContentはStreamにするかどうか一考の余地があります。後ほど補足します。
バックエンドの実装
まずはデータベースの準備。肝はsqlite-vssです。
ベクトルデータを関するテーブルvss_vectorsを下記のように作成します。
CREATE VIRTUAL TABLE IF NOT EXISTS vss_vectors USING vss0(vec(768));
上記の例では`vec`がベクトルカラムになります。それと別に元のソース情報を管理するテーブルdocumentsを作成します。
CREATE TABLE IF NOT EXISTS documents
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
content
TEXT
NOT
NULL
);
オートインクリメントのidと文字列型のcontent。なぜテーブルを分けるのかと思ったかもしれませんが、どうもベクトルデータを扱うテーブルではTEXTやINTEGERのような従来のカラムを扱えないようでエラーになりました。この辺に書いてあります。
次に問い合わせの回答に必要なデータを用意します。構造は文字列の配列という非常にシンプルなものです。
const sampleData = [
"私は北海道出身です",
"私は珈琲が好きです",
"私は個人事業主です"
];
このsampleDataをdocumentsテーブルに保存、つまり3レコード追加します。
そして各レコードのcontentをベクトル変換し、vss_vectorsテーブルに保存します。変換は前述のGeminiのembedContentを使います。
戻り値は数値の配列(TS的に言うとnumber[])になり、公式ドキュメントを読むとJSONの文字列で良いみたいなのでJSON.stringifyをかましてそのまま保存します。
これで初回のデータ投入はOK。
続いてクライアントの質問を受け取って、LLMが答えを導き出すというAPIを作ります。
最低限の順序としてはこのような流れになります。
1. クライアントのパラメータquestionを受け取る
2. embedContentAPIを実行し、questionをベクトル変換する
3. ベクトル値を条件にvss_vectorsテーブルとdocumentsテーブルから検索してくる(条件によって複数返る)
4. GeminiのgenerateContentに3のデータとquestionを渡し、質問に対し適切な回答を導き出すように指示します。
他にもパラメータのバリデーションや特殊文字のエスケープ、プロンプトインジェクション対策やログ保存など行っていますがここでは省略。
フロントエンドの実装
UIは好みが出るところ。とりあえず一般的なチャットボットみたいな見た目に寄せました。
先ほど少し触れましたが、レスポンスの内容が長文になることが前提であればGeminiのgenerateContentStreamを使った方が良いと思います。
長文の結果に対しgenerateContentを使うと、ユーザーが入力してからレスポンスが完了するまでに時間がかかり、そこから文字列を1文字ずつ表示するような作りにすると非常にストレスです。そこでgenerateContentStreamを使い、なおかつフロント-バックエンド間もServer Sent EventsでやりとりすればGeminiから返る小出しのレスポンスをリアルタイムでユーザーに届けることが出来ます。
私が設定したプロンプトでは「回答の長さを120字程度に収めてください」と明示的に長さを決めており、 そんなに遅くなかったのでgenerateContentを使用してUI側で1文字ずつ表示しています。
まとめ
ところどころ細かい点を省いてしまいましたが、ベクトルデータベース周りはもっと改善の余地がありそうです。元データの文法を調整したり、より精度の高いエンベディングに変えるとか。また、質問&回答の内容から個別の処理を挟むことでよりリッチな体験を提供できそうです。
→例えば企業の採用情報をユーザーが問い合わせする場合、興味がありそうな職種の社員紹介ページや募集要項、会社の実績などを紹介するという機能。
チャットボット自体は何年も前から製品のサポートフォームみたいなWebサイトで存在していましたが、もう少し手軽にコスパ良く導入できるようになると需要が出てくる気がします。
LLMの台頭のせいか、記事に細かいコードを貼っていくモチベーションが下がっています。このくらいならまあいいかな、みたいな。
実装・ツール開発のご相談があればお気軽にご連絡ください( ´Д`)y━・~~