自前のチャットボットを「LlamaIndex+GPT4+gradio」で爆速構築してみよう!  

# <font color=royalblue> 0.　はじめに </font>
本稿は、<font color=royalblue>LLMを使ったチャットボットを作ってみたいけど、、、</font>、どう作ったらいいの？という人向けに、GPT4とGoogle Colabを使って簡単に自前のチャットボットを構築する手順を整理したものです（本格導入に向けたものではありません）。簡単に作れるので是非一度試してみてください。また、筆者が構築するにあたって苦労したこともヒントとして記載していますので参考にして頂けたらと思います。
また、記載に誤り等があればご指摘をお願いします。随時アップデートします。

©︎2023 NPO法人AI開発推進協会

# <font color=royalblue> 1.　仕組みの説明 </font>
コードに入る前に簡単に仕組みの説明をします。

今回作成するチャットボットは、過去の問い合わせと回答のデータを使ったQ＆Aシステムを作る前提で話を進めます。  この場合、いきなりGPT４に過去の問い合わせを行っても、GPT４ではローカルサービスのQ&Aのようなファクトデータでの学習は行っていないため、（おそらく）答えることができないか、間違えた答え（Hallucination）を返してしまうと思います。  

では、どうように対応するのでしょうか？　1つは目はLLMをファインチューニングする、２つ目はプロンプトエンジニアリングでプロンプトを工夫する、そしてもう一つは「質問」に応じて外部の情報から「事実」を検索して、プロンプトに追記してLLMに問い合わせを行う`RAG（Retrieval-Augmented Generation）/In-Context Learning`を用いるパターン等があります。ファインチューニングはハードルが高く効果も不明なため（※１）、今回はRAGを用いいる実装を説明します。。
※1:[Fine tuning is for form, not facts](https://www.anyscale.com/blog/fine-tuning-is-for-form-not-facts?ref=blog.langchain.dev)では、ファインチューニングは「事実」を学習するタスクはLLMは不得意と説明されています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413278/981fcf5a-0fed-3235-09ca-7c41823a3d63.png)


RAGを実現するOSSライブラリィとして<font color=royalblue>LlamaIndex</font>があります（他にもLangChain等があります）。詳細は様々な記事があると思いますのでそちらにお任せしますが、おおまかな流れは以下のようになります。[LlamaIndexの公式ドキュメント](https://gpt-index.readthedocs.io/en/latest/getting_started/concepts.html)を抜粋し説明します。

【全体構成】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413278/ad82912a-d169-b2ac-82d8-0decd1b15b54.png)

- [ ] 1. LlamaIndexで検索する元データ(Data Source)を用意する（ex. Q＆Aの一覧や関連ドキュメント等）
- [ ] 2. Data SourceをLlamaIndexのDocumentsとして保存する
Documentsは、PDF、API 出力、データベースなど、あらゆる「データソース」の汎用コンテナです。LlamaIndex内のデータの単位はNode/チャンクと呼ばれます。これはテキスト、画像、または他のデータの小さなセグメントです
- [ ] 3. Documentsをindexing化(ベクトル化)しKowledge Baseとして保存する

【Knowledge Base(indexファイル）生成の流れ】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413278/3a795465-4469-20c2-a83f-2a1b114945ce.png)

これで準備はおしまいです。実際の運用で質問して回答を得る流れは以下になります。

- [ ] 4. ユーザが質問を入力、この時の質問文と各Knowledge Base上の文章（ノード/チャンク）との相関を取り、近い文章を取得します（Retriever）。
- [ ] 5. LLMにリクエスト際に上記をコンテキストとして質問文と一緒に埋め込ます(Node PostProcessor）。
- [ ] 6. LLMの応答を受け取り最終的な質問に対する回答を生成します。このときコンテキストの情報も使って出力文を生成することでLLMが知らなかった情報を使って回答を生成することができるようになります(Response Synthesizer)。

【検索時の流れ】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413278/ee48f6c5-6d10-6bb2-fbe9-f43e837f3f0c.png)


また、これだけではバッチ的なシステムになってしまうのでチャット用のWeb画面も必要になります。このWeb画面を簡単に生成することができるライブラリィが <font color=royalblue>gradio</font> になります。gradioではチャットボットをほぼノンコードで作れるハイレベルなインタフェースも持っています。下記の流れでチャットボットが出来上がります（セッションの保持等は除く）。
- [ ] ユーザがgradioで作ったチャットボット画面に質問を入力する
- [ ] その入力を先ほど作成したLlamaIndexのコードに渡して、LLMから回答を取得する
- [ ] LLMからの回答を質問の回答として画面に表示する


#  <font color=royalblue> ２.　実装コード </font>

ここからはGoogleコラボ(Proで検証しました)上でチャットボットアプリのコードを実装していきます。リソースとして`A100`が望ましいと思いますが、`V100` でも動きました。

::: note info
Googleコラボ
Googleコラボの使い方、ファイルアップロードの仕方がわからない場合はこちらの記事を参考にしてください
[これから始める人向け Google Colaboratoryの操作方法](https://qiita.com/DeepTama/items/02e299b2c616a603fb4a)
:::

+ Llamaindexのインストール
``` python
# LlmaIndexパッケージのインストール (今回は0.8.0で検証) 
!pip install llama-index==0.8.0
```

+ ログの設定
``` python
# ログレベルの設定
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True)
```

::: note info
ヒント  
`level=logging.DEBUG`とすることで、
+ API の実行回数
+ API の処理時間
+ API に送った Token 数
+ 検索時のクエリーとindexドキュメントのコサイン類似度
等が取得できます。  
なお、ログをファイルに出力したい場合は、`stream=sys.stdout`を`filename='ファイル名'`に置き換えてください。
上記が不要な場合は、`level=logging.INFO`としてください。
:::

+ OpenAIのAPIキーを設定
OpenAIのAPIキーを設定します（まだ未取得の方は取得してください）。

``` python
%env OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxx
```

+ LLMの設定
必要に応じてデバックトレースを設定します。
``` python
from llama_index import GPTVectorStoreIndex, ServiceContext
from llama_index.llms import OpenAI
from llama_index.callbacks import CallbackManager, LlamaDebugHandler

# Open AIのGPT4をLLMとして指定しServiceContextに設定する
llm = OpenAI(model='gpt-4-0314', temperature=0.5, max_tokens=1024)

# デバックトレース 不要な場合は削除
llama_debug_handler = LlamaDebugHandler()
callback_manager = CallbackManager([llama_debug_handler])

# サービスコンテキストの設定
service_context = ServiceContext.from_defaults(llm=llm, callback_manager=callback_manager)
```

::: note info
ヒント  
デバックトレースを指定することで、LlamaIndexの各イベント情報を見ることができます。詳細は[公式ドキュメント](https://gpt-index.readthedocs.io/en/latest/core_modules/supporting_modules/callbacks/root.html)を参照してください。
+ CHUNKING : テキストの分割前と分割後のログ
+ NODE_PARSING : ドキュメントとそれらが解析されたノード
+ EMBEDDING : 埋め込まれたテキストの数
+ LLM : LLM呼び出しのテンプレートと応答
+ QUERY : 各クエリの開始と終了
+ RETRIEVE : クエリのために取得されたノード
+ SYNTHESIZE : 合成呼び出しの結果
+ TREE : 生成された要約と要約のレベル
+ SUB_QUESTION :生成されたサブクエスチョンとその回答
:::

+ LlamaIndexに読ませるデータをGoogle Colabでアップロードする
pdfやexcel、wordでも読めますが、評価用にはテキストファイルに落としてから読ませるのが良いかと思います。 
※適当なサンプルが手元にない場合、普段使っているWebサービスのQAを検証用に使うこともありかと思います

また、あとでチャットボットで使うアイコンの画像も適当な場所にアップロードしておきます

``` python
# 場所は任意ですが、今回は `sample_data` の下に、 `data`ディレクトを作成しその下にドキュメントをアップロード
%cd sample_data
%mkdir data
```

+ アップロードしたドキュメントを読み込みます

``` python
# ドキュメントの読み込み
from llama_index import SimpleDirectoryReader

# Data Source -> Documents化を行うStep
documents = SimpleDirectoryReader(
    input_dir="/content/sample_data/data"
).load_data()
```

+ QAテンプレートの準備
QAのテンプレートを準備します。以下に簡単なサンプル例を記載します（個々のユースケースにおいて生成の精度を上げるためには別途チューニングが必要と思われます）。

``` python
from llama_index.prompts.prompts import QuestionAnswerPrompt

# QAテンプレートの準備
qa_template = QuestionAnswerPrompt("""

 

あなたはヘルプセンターのオペレターです。質問に対して親切に答えください。


== 以下にコンテキスト情報を提供します。
{context_str}
== 質問
{query_str}

 

""")
```

+ Knowledge Base(index)の作成

``` python
# Document -> Knowledge Base生成を行うステップ
index = GPTVectorStoreIndex.from_documents(documents, service_context=service_context)
```

これで準備完了です。　
チャットボットを生成する前に動作確認をしてみてください。下記の`Query`に適当な質問文を埋め込んで実行しみましょう。

``` python
# LLMへの問い合わせ
# この時indexを参照し、問い合わせに近い情報（チャンク）を取得し、それをプロンプトに組み込む
# 幾つのチャンクを組み込むのかを `similarity_top_k` で指定する
query_engine = index.as_query_engine(
    similarity_top_k=3,
    text_qa_template=qa_template,
)

query = "質問文？"

response = query_engine.query(query)
print(response)
```

#  <font color=royalblue> ３.　チャットボットの作成 </font>
チャットボットの画面を生成します。　今回は `gradio` を使用します。

+ gradioのインストール

``` python
!pip install gradio
```

::: note warn
最新のGoogleコラボでgradioのインポートでエラーになる事象が発生しました。この場合、fastapiのバージョンを下げてください。
`!pip install fastapi==0.100.0`
:::


+ gradioの[サンプル](https://www.gradio.app/guides/creating-a-custom-chatbot-with-blocks)をベースにチャットボットを作成
今回はBlocksを使用しています。 `avatar_images`には先ほどアップロードした画像を指定します。

``` python
import gradio as gr
import os

def add_text(history, text):
    history = history + [(text, None)]
    return history, gr.Textbox(value="", interactive=False)

def bot(history):
    /* クエリに前回のデータを引き継ぐ場合は個別に編集が必要です */
    query = history[-1][0]
    #response = query_engine.query(query).response
    response = str(query_engine.query(query))
    history[-1][1] = ""
    for character in response:
        history[-1][1] += character
        yield history

with gr.Blocks(show_error=True) as demo:
    chatbot = gr.Chatbot([],
                         element_id="chatbot",
                         bubble_full_width=False,
                         avatar_images=(None, "XXXXX.png"),
                         )
    with gr.Row():
        txt = gr.Textbox(
            scale=4,
            show_label = False,
            container = False
        )
    clear = gr.Button("Clear") 

    txt_msg = txt.submit(add_text, [chatbot, txt], [chatbot, txt], queue = False).then(bot, chatbot, chatbot)
    txt_msg.then(lambda: gr.Textbox(interactive = True), None, [txt], queue = False)
    clear.click(lambda: None, None, chatbot, queue=False)

demo.queue()    
demo.launch()
```

下記のようなチャットボットの画面が生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1413278/396a7611-ee39-2f7b-b568-69705bcd3580.png)



::: note info
ヒント
+ `response = query_engine.query(query)` が 質問文をRAGを通してLLMに問い合わせる箇所になります。結果取得後 `gradio` で結果を表示するには文字列である必要があるため `.response`で文字列を取り出しています。
+ Google Colaba以外（別のPCやスマホを含む）からチャットボットへは、`Running on public URL: https://xxxxxxxxxxxxx`に表示されたURLからアクセスすることができます
+ gradioの詳細については、 [公式ドキュメント](https://www.gradio.app/guides/quickstart)を参照してください
:::

また、`gradio` ではより簡単にチャットボットのアプリが作れるインタフェース（ChatInterface）を提供しています。コードを参考に記載します（当たり前ですが自由にできる反面、カスタマイズできる範囲も狭くなります）。

``` python:sample
import gradio as gr

def echo(message, history):
    message = query_engine.query(message).response
    return message

demo = gr.ChatInterface(fn=echo, examples=["hello", "hola", "merhaba"], title="Echo Bot")
demo.launch()
```

#  <font color=royalblue> ４.　おわりに </font>
上記のように簡単にGPT4(LLM)を使ったチャットボットの作成ができます。
なお、上手に回答が生成されるようにするためにはプロンプトテンプレートや準備するドキュメントの分割や記載にもコツが必要と思われます。この辺りはデバックトレースを入れながらチューニングしていくことになると思います。可能であれば実績を積んで今後記事にして行けたらと思います。

チャットボットだけでなく、GPTの中身ってどうなっているのかより興味を持たれた方は、以下の記事も参考にしてください。（GPT3をベースに記載しています）

https://qiita.com/DeepTama/items/63cbc1221662f9e86ac0