GPT-4でドキュメントデータを基にした質問回答を作る



## 概要

Llamaindexを使用してPineconeに格納されたデータを参照した回答をGPT-4に作成させます。今回は、Slack Botを利用した回答生成(チャットボット)機能・テキストデータ投入機能・テキストデータ取得機能・テキストデータ削除機能を実装しました。

- Llamaindex: LLMアプリケーションがプライベートデータまたはドメイン固有のデータを取り込み、構造化し、アクセスするためのデータフレームワークです。(→GPTの機能を拡張してくれます。)
- Pinecone: ベクトルデータベースです。外部からの質問を受け付けたLlamaindexが回答を生成するためにこのデータベースを参照し、セマンティック検索(=意味検索)を行います。

コードはGitHubで公開しています。

https://github.com/SoichiroSugimoto/vectordb-based_gpt


## 全体構造

全体のアーキテクチャを解説します。
![スクリーンショット 2023-09-10 16.04.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/a63eae13-1ee2-7aec-b6ce-8d9aa1152cdc.png)

- 回答生成(Slack)：
    1. (トリガー)Slack Botへのメンション付きメッセージが送信される
    2. Slackメッセージを含むHTTPリクエストがAPI Gateway経由でLambdaに送信される
    3. HTTPリクエストのpathからAccessibility ID、パラメータからメッセージを取得する
    4. Llamaindexを使用してPineconeからAccessibility IDを基に取得したデータでクエリエンジンを作成する
    5. クエリエンジンの引数としてSlackメッセージを引き渡し、回答を生成する
    6. Slack API経由で回答を送信する
- データ投入
    1. API Gatewayに対してPOSTリクエストが送信される
    2. API Gateway経由でLamabdaがLlamaindexを利用してPineconeにデータを投入する(→返り値としてPineconeのデータIDを返す)
    3. データIDとその他のデータをペアにしてDynamoDBに格納する
- データ取得
    1. API Gatewayに対してGETリクエストが送信される
    2. API Gateway経由でLamabda内の関数が実行されDynamoDBに登録されたIDデータを参照する
    3. 取得したIDデータをもとにPineconeにGETリクエストを送信して詳細データを取得する
- データ削除
    1. API Gatewayに対してDELETEリクエストが送信される
    2. API Gateway経由でLamabda内の関数が実行されDynamoDBに登録されたIDデータを参照する
    3. 取得したIDデータをもとにPineconeにDELETEリクエストを送信して該当データを削除する


## 使用方法
このアプリケーションでは大きく4つの機能を提供しております。4機能ともAPIへのHTTPリクエストで実行可能です。また、一部はローカル実行も可能にしています。
①テキストデータ取得機能
②テキストデータ投入機能
③回答生成(チャットボット)機能
④テキストデータ削除機能
（ここでテキストデータとはチャットボットが回答生成の際に参照するテキストデータのことです。）

以下がHTTPリクエストのパスとパラーメーターです。
| Method | Endpoint |  Description | Required Parameters |
| ---- | ---- | ---- | ---- |
| GET | /vector-data-list | ①テキストデータ取得機能 | |
| POST | /store-article/{accessibilityIds} | ②テキストデータ投入機能 | - title: 投入するテキストデータのタイトル<br>- text: 投入するテキストデータ |
| POST | /chat/{accessibilityIds} | ③回答生成(チャットボット)機能 | - chat_content: クエリする質問文 |
| DELETE | /vector-data | ④テキストデータ削除機能 | - category_id: テキストデータを一意に特定するID |
| POST | /slack-chat | ②テキストデータ投入機能(Slack実行版) | |
| POST | /slack-chat/{accessibilityIds} | ②テキストデータ投入機能(Slack実行版) | |


以下では一部の機能についてもう少し詳細に解説します。

### 回答生成(チャットボット)機能

**HTTPリクエスト(Slack)で実行する場合**

1. Slack Botを作成し、Event SubscriptionsでRequest URL(API Gatewayで生成)を登録する。（「Verified✔️」と表示されれば登録完了です。トリガーとなるイベントを設定しておくことで、特定のイベント発生時に登録したURLにHTTPリクエストが送信されます。）
![スクリーンショット 2023-09-10 14.40.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/fecb160d-a7c3-fffa-647a-1fca90f79439.png)

2. 作成したSlack Botにメンション付きメッセージを送信する。
3. Slack Botからの返答が得られます。
![スクリーンショット 2023-09-17 18.39.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/6c9c48c5-5760-5a11-9587-0d7095f51354.png)


**ローカル環境で実行する場合**

1. Dockerを起動する
    
    ```bash
    $ docker compose up -d --build
    $ docker compose exec app bash
    ```
    
2. mainを実行する
    
    ```bash
    $ python main.py {accessibility_id}
    ```
3. CLI上でChatbotが使用可能になります。
    

### テキストデータ投入機能

**HTTPリクエストで実行する場合**

```bash
$ curl -X POST 
    -H "Content-Type: application/json" 
    -d '{"title": "ハプスブルク家", "text": "中世以来、神聖ローマ皇帝位を継承した有力な家系。スイスの地方領主から出発し、オーストリアに侵出、ドイツ王の地位を兼ね、ネーデルラント、ブルゴーニュ、スペイン、ボヘミア、ハンガリーなどヨーロッパの広大な領土の他、新大陸にも支配地を持った。"}' 
    https://xxxxx/store-article/{accessibilityIds}
```

**ローカル環境で実行する場合**

1. Dockerを起動する
    
    ```bash
    $ docker compose up -d --build
    $ docker compose exec app bash
    ```
    
2. file_loaderを実行する
    
    ```bash
    $ python file_loader.py {accessibilityId}
    ```
3. ```root/data```の配下に設置されているテキストファイルが読み込まれます。
    

### データ取得

**HTTPリクエストで実行する場合**

```bash
$ curl -X POST 
    -H "Content-Type: application/json" 
    https://xxxxx/vector-data-list
```



## デプロイ
このアプリケーションはAWS・OpenAI API・Pinecone・Slack APIを使用しています。それぞれの設定方法について解説します。


### AWS

- **ECR**
    : Docker imageを保存するサービスです。保存されたDocker imageはLambdaから立ち上げられます。
    1. [Amazon ECR] > [リポジトリ] > [リポジトリを作成]
    2. 「可視性設定」はプライベートをチェックしてください
    3. 「リポジトリ名」は任意の名前で問題ありません
        ![スクリーンショット 2023-09-17 18.53.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/9feb942e-66db-74de-ac16-a0b426597de4.png)
    4. [リポジトリを作成]
    5. 【リポジトリ名をクリック】> [プッシュコマンドの表示]
        ![スクリーンショット 2023-09-17 18.59.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/3e60c087-d093-ca49-ede1-ac15b18c4edb.png)
    6. 表示に従ってDocker imageをプッシュする
- **Lambda**
    : 特定の条件をもとに関数を実行することのできるサービスです。今回はAPI Gatewayによって実行されます。
    1. [Lambda] > [関数] > [関数の作成]
    2. 「コンテナイメージ」を選択します
    3. 「関数名」は任意の名前で問題ありません。
    4. 「コンテナイメージ URI」はECR上で表示されるものを入力してください。
    5. [関数の作成]
        ![スクリーンショット 2023-09-17 19.04.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/280dcf30-5074-1a68-16bd-01a90c46174a.png)

- **API Gateway**
    : URLを発行し、そのURLへのアクセスを処理することのできるサービスです。
    1. [API Gateway] > [API] > [APIを作成] > REST API[構築]
    2. 「API名」は任意の名前で問題ありません。
    3. [APIの作成]
    4. 【API名をクリック】> [アクション] > [メソッドの作成] > [ANY]
        ![スクリーンショット 2023-09-17 19.22.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/18fd75d9-e6bd-6e09-0754-99350074007f.png)
    5. 【”ANY”をクリック】> [アクション] > [リソースの作成] > 【”{proxy+}”を入力】
        ![スクリーンショット 2023-09-17 19.22.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/3dbcde12-2b1a-8aff-dc64-4f15534e8b82.png)
    6. [アクション] > [APIのデプロイ]
- **DynamoDB**
    : キーバリューストアを提供するサービスです。
    
    ※`vectordb-based_gpt/vectordb-based_gpt/article/article_model.py`を実行することでDynamoDB上に必要なテーブルを作成することが可能です。
    

### OpenAI
https://platform.openai.com/account/api-keys
[Create new secret key] > 【任意のNameを入力】> [Create secret key]
![スクリーンショット 2023-09-10 16.48.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/e77265f7-b8a3-0bf2-737d-d495f9e909b4.png)


### Pinecone
https://app.pinecone.io/
[Indexes] > [+ Create Index] > 【任意のNameを入力】> 【Dimensionsに”1536”を入力】> [Create Index]
![スクリーンショット 2023-09-10 16.52.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/c55ca891-72e7-dfab-3cff-7abe8b897435.png)


### Slack
https://api.slack.com/
[slack api] > [Create New App] > [From an app manifest]

で以下のYAMLからBotを作成する

```YAML
display_information:
  name: Pinecone-Based GPT-4
  description: I am Pinecone-Based GPT-4👋
  background_color: "#184700"
features:
  bot_user:
    display_name: Pinecone-Based GPT-4
    always_online: false
oauth_config:
  scopes:
    user:
      - channels:history
      - channels:read
      - channels:write
      - chat:write
      - im:read
    bot:
      - commands
      - app_mentions:read
      - channels:history
      - channels:join
      - channels:manage
      - chat:write
      - channels:read
      - im:history
      - im:read
      - im:write
      - incoming-webhook
settings:
  event_subscriptions:
    request_url: {API Gatewayで発行したURL}
    bot_events:
      - app_mention
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
```

### 環境変数

実行環境としてローカル(Docker)とAWSを使用した環境を選択することが可能です。それぞれの実行時には環境変数として以下のものが必要になります。

- ローカル(.envに登録)
    - OPENAI_API_KEY
    - PINECONE_API_KEY
    - PINECONE_ENVIRONMENT
    - PINECONE_INDEX_NAME
    - AWS_ACCESS_KEY_ID
    - AWS_SECRET_ACCESS_KEY
- AWS(Lambdaに登録)
    - OPENAI_API_KEY
    - PINECONE_API_KEY
    - PINECONE_ENVIRONMENT
    - PINECONE_INDEX_NAME
    - SLACK_BOT_TOKEN
    - TRANSFORMERS_CACHE: /tmp/home/sbx_user1051/.cache/huggingface/hubを登録


## まとめ（+ 今後の課題）
今回はLlamaindexを使用してDBに保存したドキュメントデータを基に回答を生成するチャットボット及びチャットボットの参照データ管理機能を開発しました。また、業務で利用することを想定し、チャットボットの回答内容をコントロールできる様にAccessibilityIDというものを実装し、チャットボットが回答生成時に参照するデータ範囲を管理できる様にしました。
下図(例として銀行業務を使用しています。)のように多くの業務現場ではユーザーによって回答内容を変える必要があるためです。
![スクリーンショット 2023-09-17 20.09.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2611951/53d11549-9971-f4e3-6ff0-6ad36f065d8d.png)
今回の実装ではAccessibilityIDをPinecone上でnamespaceとして扱うことで、データを分割しています。ただ、データの仕切りとしてはPineconeのmetadata_filterを使うのが適切だったなの感じています。今回はLlamaindex内にその様な機能がなかったので実装できませんでした。
また、全体を通して「[Using LlamaIndex with Pinecone](https://gpt-index.readthedocs.io/en/latest/examples/composable_indices/city_analysis/PineconeDemo-CityAnalysis.html)」を参照しましたが、こちらのページではComposableGraphの使用が紹介されていました。ComposableGraphはクエリエンジンを作成するためのインデックスを複合的に利用することができる(ComposableGraphを使用しない場合は1クエリエンジンに対して使用できるインデックスが1つ)というものです。参照データの範囲をコントロールするという観点では有用に感じられましたが日本語だとやや回答精度が落ちるので使用はしませんでした。
今後はLlamaindexのようなライブラリを自分でアレンジしてもっと効率的な検索方法を実装したいと考えています。また、業務で使用する観点で実装をしたものの現時点での活用は一部にとどまっているので、業務での運用観点で更なる改善を進めていきたいです。


**最後までお読みいただきありがとうございます。🙏**


## 参照

- [Llamaindex](https://www.llamaindex.ai/)
- [Pinecone](https://www.pinecone.io/product/)
- [Semantic Search in Confluence Wiki With LlamaIndex and Pinecone](https://betterprogramming.pub/semantic-search-in-confluence-wiki-with-llamaindex-and-pinecone-eb39c1d8c004)
- [Initializing the GPTSimpleVectorIndex from a file on aws lambda results in a read-only file system error](https://github.com/jerryjliu/llama_index/issues/1292)