オープンソースを使ったローカルで動く無料の社内RAGの作成 #2

前回でOpenSearchによるデータベースを作成することができましたので、ここからこれを利用してRAGを作成していきます。

RAGへの適用

RAGAPI用のプログラム

RAGのプログラムをまとめて記載します。

足りないライブラリは適宜インストールしてください。

今回llmの呼び出しにllamacppを使用しますが、pip install llama-cpp-pythonは結構時間がかかります。

今回紹介するRAGですが、私なりに社内RAGとして使うのに汎用性があるように、

少し工夫していて、そのせいでコードが複雑になっております。変な部分は修正して使っていただけると幸いです。

opensearch-pyのコードの変更

また、 rag.py以外にライブラリ内のコードを変更するポイントが一つあります。

以下で記載していますが、AIからの返答に検索のスコアを表示する関係で、opensearch-pyの中身を少し変更します。これをしないとスコアを返答してくれません。

変更するファイルは、

"D:\codes\python\rag-opensearch\.venv\Lib\site-packages\langchain_community\vectorstores\opensearch_vector_search.py"

で、変更部分は750行あたりの similarity_search関数です。vscodeを使っている場合は rag.pyのOpenSearchのクライアント設定の部分の OpenSearchVectorSearchを選択し、F12でジャンプすると対象のファイルに移動できます。

最後のreturnの部分を以下のように変更します。

工夫したところ

社内RAGとして使うのに汎用性があるように工夫した点をまとめておきます。

  • チャット形式にする
    ただ1ラリーで返答するだけでは面白くないので、チャット形式で過去の会話も参照するようにしています。ConversationBufferWindowMemoryの設定で5会話文までメモリーに保存するようになっています。
    小さいLLMモデルを使用しており、コンテキスト長に制限(8192)があるため、とりあえず5会話文にしています。OpenSearchで文章を400文字でチャンクに分けて、追加しているのもそれが理由です。
  • ユーザーごとに分けて会話するようにする
    APIとして利用する場合は、いろんなユーザーが使用することが想定されるため、APIサーバに保存するメモリーはユーザーごとに分ける必要があります。
    そのため、APIリクエストbodyに user_idを入れるようにして、user_idで参照するメモリーを探すようにしています。メモリーが増えるのを防ぐために、時間制限で一定時間でメモリーを削除するようにしています。
  • 参考文献を表示する
    RAGの回答で、AIが参考にした参考文献を表示することで、あとで人間がその資料を見に行きやすくしました。同時にスコアも表示することで、どのくらい類似度があるのかも明確にするようにしました。
  • レスポンスChatGPTみたいにする
    これは自己満足ですが、ChatGPTを使用していると、AIからの返答は文字がちょっとずつ表示されると思います。これはLLMが1トークンずつ予測して出力しているせいだと思いますが、せっかくなので、それを実現したいと思いました。ライブラリの機能として、AIからのすべて回答を待つ前に、少しずつ表示する方法があるみたいなのですが、私のコードではうまくいきませんでした。そこで、なくなくAIからの回答をすべて取得してから、1トークンずつAPIからレスポンスを返すようにしています。参考として次の記事でReactでチャットアプリ作ったときの表示方法についても記載しておきます。

プログラムの全体の流れは基本的には以下のようになると思います。

  1. RAG用APIの起動
  2. LLMモデルの読み込み
  3. OpenSearchの接続
  4. OpenSearchで検索
  5. 検索結果を利用してRAGを実行

RAGの実行と検証

RAGの実行

すべて準備できたら、いよいよRAGを実行します。

まずOpenSearchとRAG用のAPIを起動します。

前回の通りOpenSearchは opensearch-windows-install.batから起動できます。

RAG用APIはコマンドプロンプトなどから、仮想環境に入り、

py rag.pyで起動できます。

RAG用のAPIが立ち上がるのは少し時間がかかりますが、立ち上がると、

INFO: Application startup complete.

と表示されます。

検索の実行は以下のようにjsonを参照することもできますし、そのまま記述することもできます。

直接記述を見ればわかりますが、jsonの中身は以下のようになっています。

user_idはユーザーID、userは質問内容、systemは質問内容以外に会話に含めるものです。

今回 systemには「日本語でおk」という内容を入れています。今使っているモデルが日本語特化のモデルなので、必要ないかもしれませんが、念の為入れています。

以上でRAGが完成しました。

回答例

では、試しに回答例を見てみましょう。

通常の会話

まず、RAGに関係ない普通の質問をしてみます。

  • 質問:熱中症対策を5つ教えて
  • 回答:1. 十分な水分補給をする
       2. 日陰で休息を取る
       3. 熱中症予防グッズ(帽子、サングラスなど)を活用する
       4. 暑さを感じたら早めに病院に行く
       5. こまめに体温を計測し、高熱の場合は適切な治療を受ける

ちゃんと日本語でそれっぽいことを教えてくれます。ローカル環境でここまでできるのは感動しますよね。

会話を覚えているか

次に、過去の会話内容を参照して、チャットができるか確認します。

  • 質問:こんにちは私の名前はあしぺんです。あなたの名前は?
  • 回答:私の名前は、山田です
  • 質問:今日はいい天気です。そちらは?
  • 回答:はい、今日はいい天気ですね。
  • 質問;私の名前を答えてください。
  • 回答:あしぺ

参照ファイル以下は、参考にしたファイルを示しています。

まあ、私の名前に一文字足りない気がしますが、ちゃんと私のことをおぼえているようです!このときのプロンプトはどうなっているか見てみます。

上図のように、プロンプトに以前の会話の内容が含まれているため、私の名前を答えられたわけですね。

コンテキスト情報にはOpenSearchで検索した結果が入っています。最後の質問「私の名前を答えてください」に対する検索結果だと思いますが、回答では無視されています。つまり、たぶんファイルを参考にしつつ、ファイルの内容は関係ないと判断して、私と会話していることになります。すごいですね!

このあたりの調整は opensearch_client.as_retrieverscore_threshold値を調整することで、必要ない情報まで取得するかどうか変更できるかもしれません。

しかし、あまり高くしすぎると、RAGで必要になる情報まで取得しなくなるので、難しいところです。うまくいかないようであれば、OpenSearchに保存されているデータ自体に問題があるかもしれません。

この会話に、どのくらいのレスポンス時間がかかるか動画を載せておきます。

1レスポンスあたり十数秒くらいでしょうか、GPUを使ってこれなので、正直遅いと言われたら遅いですが、モデルも比較的軽量のものなので、速くするにはスペックを上げるしかないですかね。

RAGの有無による違い

次に、OpenSearchを使ったRAGについて、RAGを使用しない場合と、使用した場合でどのくらい違うのか見てみます。

太宰治の走れメロスについての質問です。

  • 質問:メロスが激怒した理由は?
  • RAGなしの回答:メロスが激怒した理由は、アフロディーテに、自分の父親であるゼウスを貶める言葉をかけているからです。
  • RAGありの回答:メロスが激怒した理由は、暴君ディオニスが「人を信ずる事が出来ぬ」と言ったこと。

RAGありのほうが、作品の内容を引用し、それっぽい回答をしていると思います。

アフロディーテはメロス島という島で出土した女神像らしいです。RAGがない場合は、作品の走れメロスについてはおそらく知らない?ので、一般的な知識から回答を作成したみたいです。

RAGありについて、追加で質問しました。

  • 追加質問:暴君ディオニスが人を信じられない理由は?
  • RAGありの回答:暴君ディオニスは、メロスが「人の心を疑うのは、最も恥ずべき悪徳だ」と言ったことを理由に人への不信感を抱いている。

作品中の言葉は使っているけどなんとちがう?

RAGありについて、別の回答例を見てみます。宮沢賢治の銀河鉄道の夜についての質問です。

  • 質問:カムパネルラは誰を助けようとしましたか?
  • 回答:カムパネルラはザネリを助けようとしました。
  • 追加質問:その後ザネリとカムパネルラはどうなりましたか?
  • 回答:ザネリは舟の上から鳥うりのあかりを水の流れる方へ押して、舟がゆれたことで水へ落ちてしまった。

時系列が間違っているように思います。やはり、400文字ごとにチャンクを分けたのが影響して、文章ごとの順番が理解できていないのでしょうか。

一応チャンク分けの際にオーバーラップはさせているので、多少の時系列はわかると思いますが、あまりうまく行っていないかもしれません。

というわけで、今回作成したRAGの回答例は以上です。

まとめ

0円でRAGを実現する方法を紹介しました。私のPCのスペックの制限で、今回使ったgemma2-2b程度でやっとでしたが、

オープンソースには、Metaのllama3.1を初めとしてもっと精度が良いものがたくさんあります。ローカルで動かす限界があると思いますが、そういうものを使えば精度は上がるのかもしれません。

また、RAGにつかうデータベースですが、今回使うOpenSearchだけでなく、類似度の計算ができるものであれば、他にも色々あると思います。

私はOpenSearch以外にもFaissを試しましたが、なぜか重かったし、OpenSearchはそれ単体でも検索エンジンとして十分優れていると思ったので、今回はOpenSearchを使いました。

RAGの開発には、LangChainというライブラリを使っており、ここに書いてあるものであれば色々使えると思いますので、試してみるのもいいかもしれません。

LLMモデルを使うにあたって、今回はllama_cppを使用しましたが、他にもOllamaも試しました。しかし、これも重かったので、今回はllama_cppにしました(使い方間違ってたかもしれません)。

Ollamaはすごく使いやすく、プログラムを書かなくても簡単にローカルでLLMを体験できるので、試してみると面白いと思います。

あと、やはり検索データそのものに問題がある場合は、精度が落ちると思います。今回はもともと、青空文庫で整形されてあったデータだったため、

そのまま使用してもそれなりの検索結果だったと思います。しかし、これでも文章の初めの記号の説明や文末の作品と関係のない文章はノイズになっている可能性はあります(下図のような)。

ノイズになり得る部分その1(文章と関係のない説明文)
ノイズになり得る部分その2(文章と関係のない説明文)

本来であればこういった情報を整理したうえで除外し、データベースに追加する必要があると思います。

社内のデータであればデータの整理はより難しく思います。

テキスト形式であればまだ整形しやすいですが、エクセル形式のファイルなどは整形するのにかなりの工夫が必要であるように思います。

いくらモデルの精度が良くても検索データがしょぼければ、生成される情報もとんちんかんになる可能性があります。

他にも注意点はあるかもしれませんが、今回はこのあたりにしたいと思います。

別の記事でこのRAGAPIを使ったチャットアプリの作成について少し触れたいと思います。

参考サイト

コメント

タイトルとURLをコピーしました