OCIでRAGチャットボットデモを作ってみた

## はじめに 

RAGを使って生成AIチャットボットのデモを作ってみました。

![ragmovie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/a7df3b2b-f9d8-06bf-b5c6-5ea112a0b18e.gif)

サーバ側のバックエンドはOCI上のVM一本で構築しています。
チャットボットだとやはりUIも揃えたいので、
フロントエンドはこちらのreactベースのUIを参考にしています。

https://github.com/mckaywrigley/chatbot-ui

https://github.com/mahm/custom_chatbot_server




## 構成概要

OCI上で構築するにあたり、下記の要領で作ってみました。

- LLMはopenaiのgpt3.5をpythonで使用
- チャットボットで回答させたいデータは事前にPDFとして作成
- ベクトルストアはchromaを使用し、VMのブロックボリュームにPDFデータを永続化して保存
- langchainを使用してLLM+chromaの連携
- 上記をfastapi+uvicornでwebapi化
- (これをreactのデモアプリからrestで呼んでいます)

分かりやすく絵にすると下記のような構成になります。



![ragdemo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/dc51a415-3aaa-c219-5587-4dd6dcdc3c53.jpeg)


## OCI構築方法

構築方法です。
まずはpipがない場合は下記を入力します。
```shell:
wget https://bootstrap.pypa.io/get-pip.py
python get-pip.py
```

openai等をインストールします。
```shell:
pip install openai langchain chroma fastapi uvicorn
```

chromaを動かすにあたって下記も追加でインストールします。
```shell:
sudo yum install gcc-c++
sudo yum install python3-devel
```

残りのフロントエンド部分は、今回使用したgithubページにあるreadmeに従って構築してください。


fastapiで使用するlangchainの実装は下記です。
embedding時に作成したフォルダ(ここではmanual)を指定します。
```python:simple_conversation_chat.py
(略)
def llm_thread(self, g, user_message):
    try:
        llm = ChatOpenAI(
            verbose=True,
            streaming=True,
            callback_manager=CallbackManager([ChainStreamHandler(g)]),
            temperature=0.7,
        )
        embedding = OpenAIEmbeddings()
        db_dir = 'manual'
        loaded_db = Chroma(persist_directory=db_dir,    # データベースの読込先
                           embedding_function=embedding  # Embeddings関数
                           )
        retriever = loaded_db.as_retriever()
        QA = RetrievalQA.from_chain_type(
            llm=llm,                # LLM
            chain_type="stuff",     # チェーン種類
            retriever=retriever,   # retrieverオブジェクト
        )
        QA.run(user_message)
    finally:
        g.close()
```
以上です。


## チャットボットに回答させるデータ

例えば、社内システムの問い合わせ用チャットボットを想定して、
「交通費精算」と「稟議申請」のやり方を記載した、PDFデータを1ページ1内容で作成してみました。
※gptに問い合わせればサンプルを作るのは簡単ですね！

```pdf:サンプル
交通費精算の手順は以下です。
ステップ 1:
    グループウェアへのアクセス
    Web ブラウザを開き、会社が提供するグループウェアにアクセスしてください。
    グループウェアアドレス: https://gw.company-name.com
    あなたのユーザーID とパスワードを使用してログインしてください。
ステップ 2:
    申請ページへの移動
    メインページのナビゲーションバーから、「経費精算」または「交通費申請」のセクションを探し、クリックして
    ください。
    「新規申請」ボタンを選択して、交通費精算フォームを開いてください。
ステップ 3:
    精算情報の入力
    申請日、名前、部署などの必要な個人情報を入力してください。
    使用した交通手段、日付、出発地点と到着地点、目的、使用金額などの交通費詳細を記入してください。
ステップ 4:
(以下略)
```

あとはこのPDFデータをブロックボリュームに保存しておき、
以下のopenaiのembedding関数を呼んで、ブロックボリュームに保存します。

```python: samve_chroma.py
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.indexes import VectorstoreIndexCreator
from langchain.document_loaders import PyPDFDirectoryLoader
from langchain.text_splitter import CharacterTextSplitter


import os
os.environ["OPENAI_API_KEY"] = "sk-***"

loader = PyPDFLoader("./manual.pdf")
docs = loader.load()

text_splitter = CharacterTextSplitter(
    separator="\n\n",     # テキスト分割の基準を指定
    chunk_size=1000,      # チャンクの最大サイズ
    chunk_overlap=50,     # チャンク間のオーバーラップ。チャンク間の連続性の維持に有効
    length_function=len,  # チャンクの長さの計算方法。デフォルト(len)は単純な文字数カウント
)
documents = text_splitter.split_documents(docs)
embeddings = OpenAIEmbeddings()
db_dir = 'manual'  # 保存先のディレクトリ
db = Chroma.from_documents(
    documents=documents,       # テキスト分割済データ
    embedding=embeddings,      # Embeddings関数
    persist_directory=db_dir,  # データベースの保存先
)
db.persist()
db = None
```


今回は1VMなのでブロックボリュームに保存していますが、
データの更新が割と定期的にあったり、あるいはvmをスケールアウトさせてLBに複数台持たせる場合はNFSマウントさせるFile Storageがよいでしょう。

https://speakerdeck.com/ocise/ociji-shu-zi-liao-huairusutorezi-gai-yao




## チャットボットで色々質問してみる
早速できたので触ってみましょう。
デモアプリでは、
- gpt3.5のみ
- gpt3.5+RAG　←今回作ったもの！
をプルダウンで切り替えるようにして違いを簡単にわかるようにしてみました。

![top.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/1ceb0356-133a-3d81-6fdc-600db5042496.jpeg)


まずは何が回答できるか聞いてみます。
RAGの方はベクトル化したデータのみ回答できると言っています。

![q1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/39b4a84e-5aff-6762-a1f4-6706cb5ea58a.jpeg)

次に交通費精算について聞いてみます。
こちらも素のgpt3.5は一般的な手順についての回答に対し、
RAGは取り込んだデータをもとに回答できています。


![q2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/a0c15195-bd66-86d2-e47d-73645b1d2161.jpeg)


ベクトルデータにない社内コピー機の使い方について聞いてみます。
RAGはわかりませんと回答し、別の問い合わせ先に誘導しています。

![q3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846649/60856f50-5e31-f440-d46b-10348e3f3b06.jpeg)


素のgpt3.5とRAGを比較しましたが、違いは明確ですね。
バックオフィス系ツールのマニュアルや、FAQなどをバシバシ取り込んで簡単にセマンティック検索できるのがRAGのいいところですね。


## おわりに
RAGを使用した生成AIチャットボットデモを構築してみました。

直近のopenaiの発表を見ると、GPTsのように簡単にRAGチャットボットを構築できるサービスが出てきています。
便利でありますが、やはりRAG連携するデータはsensitiveなので自前で管理したいですし、既存SaaSやLINE,Slackなど様々なプラットフォームで使うとなると、セキュリティを十分に担保したうえでAPI化したいかなと思いました。


チャットボットは生成AIを用いたユースケースの一例ですが、RAGを使用したサービスを構築するにあたって、クラウドのPaaS/SaaSだったりOSSだったり、今後様々な機能が登場するので、適宜上手く最適なものを組み合わせてアップデートしていきたいところです。