カスタムChatGPTを自作する３つの方法

# はじめに
2023年11月6日、OpenAIは初の開発者向けイベント「OpenAI DevDay」を開催しました。

その中でも私が注目したのは、誰でも簡単にカスタムChatGPTを作成できるようになる「GPTs」と「Assistants」という二つの新機能です。

これらの新機能で作れるカスタムChatGPTはシンプルですが、社内や自社サービスのドキュメントを基に、ユーザのQ&Aに回答するようなカスタムChatGPTは実現できるかと思います。
（社内情報を用いる際にはプライバシーとセキュリティの面で十分な検討が必要ですが）

以上を踏まえ、この記事ではカスタムChatGPTを作るための主要な3つの方法―コーディング、GPTs、Assistants―を整理します。

# １.コーディング
コーディングでのカスタムChatGPTの作成は、LLM（Large Language Model）用のアプリケーション開発ライブラリである`LangChain`の使用が一般的なようです。

今回は、指定したWebサイトからデータをスクレイピングし、
抽出したデータをベクトルDB（データを数値配列で管理するDB）に保存した上で、
そのデータを利用して質問に答えてくれるカスタムChatGPTを作ってみます。

ちなみに、Webサイトからのデータ収集を選んだ理由は、
社内情報や機密情報などの非公開情報を用いるリスクを回避するためです。

- データ収集
![url_scraping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/d08c4cae-9e28-77cf-c7b6-c7c4a680ac9e.png)

- 質問
![ask_from_scraped_data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/e7b6bae2-c6e4-0204-8769-8584193c4047.png)

## 作り方
カスタムChatGPTに関連する主要な処理2点にフォーカスし、次のセクションで紹介します。
1. データをベクトルDBに保存（ChatGPTの前提知識となるデータ）
2. ベクトルDBのデータを利用してChatGPTに質問

### ①ベクトルDBを操作するクラス
こちらの `QdrantManager` を用いてベクトルDBを操作します。
```python:app/client/qdrant_manager.py
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
from langchain.vectorstores import Qdrant
from langchain.embeddings.openai import OpenAIEmbeddings

# Qdrantの設定
QDRANT_HOST = "qdrant"
QDRANT_PORT = 6333
COLLECTION_NAME = "my_collection"

class QdrantManager:
    def __init__(self):
        self.client = self._load_qdrant_client()
        self.qdrantNLP = Qdrant(
            client=self.client,
            collection_name=COLLECTION_NAME,
            embeddings=OpenAIEmbeddings()
        )

    def _load_qdrant_client(self):
        client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
        collections = client.get_collections().collections
        collection_names = [collection.name for collection in collections]
        if COLLECTION_NAME not in collection_names:
            client.create_collection(
                collection_name=COLLECTION_NAME,
                vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
            )
        return client
```
**1. self.client**
Qdrantの基本的な機能にアクセスし、コレクションの作成、更新、削除、文書の挿入、検索などができます。
既にベクトル表現を持っている場合や、低レベルの制御が必要な場合に適しています。

**２. self.qdrantNLP**
OpenAIの埋め込みを活用して、自然言語のクエリに基づいて文書を検索するなど、より高度な機能にアクセスできます。
テキストデータを扱い、それに対する埋め込みを生成して検索する場合に適しています。

### ②ベクトルDBに保存する処理
Webサイトから収集したテキストデータをベクトルDBに保存します。
```python:capp/modules/url_scraping.py
    def _build_vector_store(self, url_text):
        text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
            model_name=st.session_state.emb_model_name,
            chunk_size=250,
            chunk_overlap=0
        )
        split_text = text_splitter.split_text(url_text)
        for text in split_text:
            st.write(f"Processing text: {text[:50]}...")
            self.qdrant_manager.qdrantNLP.add_texts([text])
```

**1. データをチャンクに分割する**
`RecursiveCharacterTextSplitter.from_tiktoken_encoder` と、
`text_splitter.split_text(url_text)` メソッドで、
大きなテキストデータを小さなテキストチャンクに分割します。

**2. チャンクをベクトルDBに保存する**
`self.qdrant_manager.qdrantNLP.add_texts([text])` メソッドで、
チャンクをベクトル化してベクトルDBに保存します。
ベクトル化することで、テキストの内容を効率的に検索できます。

### ③ChatGPTに質問する処理
ベクトルDBのデータを用いてChatGPT APIに質問し、回答を得ます。
```python:app/modules/ask_from_scraped_data.py
    def _build_qa_model(self, llm):
        retriever = self.qdrant_manager.qdrantNLP.as_retriever(search_type="similarity", search_kwargs={"k": 10})
        return RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=retriever,
            return_source_documents=True,
            verbose=True
        )

    def _ask(self, qa, query):
        with get_openai_callback() as cb:
            answer = qa(query)
        return answer, cb.total_cost
```

**1.ベクトルDBからデータ取得**
`self.qdrant_manager.qdrantNLP.as_retriever(search_type="similarity", search_kwargs={"k": 10})` メソッドで、
ベクトルDB内のデータから類似度に基づき、最も関連性の高いテキストデータを取得します。
ここでの`k=10`は、検索結果の上位10件を取得することを意味します。

**2.質問応答モデルを生成**
`RetrievalQA.from_chain_type` メソッドで、質問応答モデル（RetrievalQA）を構築します。
この際、ベクトルDBとの連携が設定されます。
retrieverオブジェクトは、ベクトルDBから関連性の高いテキストを検索するために使用されます。
この段階では、まだChatGPT APIを使用した質問は行われておらず、
単に質問応答モデルを準備するための設定を行っています。

**3.ChatGPT APIに質問**
`with get_openai_callback() as cb:とanswer = qa(query)` を実行し、ChatGPT APIに質問します。
ChatGPT APIは、ユーザーからの質問（query）と、質問応答モデル（qaオブジェクト:RetrievalQA）から取得した情報と組み合わせ、適切な回答を生成します。
また、`get_openai_callback`は、このプロセスで発生したコストを計算します。

## ソースコード全体
ソースコード全体はGitHubに公開してます。
https://github.com/rabitt1ove/custom-chat-gpt

[Docker](https://www.docker.com/products/docker-desktop/) と [OpenAI API Key](https://qiita.com/melty_go/items/a6929b0a341e75d24f01#2-openai-api%E3%81%AE%E3%82%AD%E3%83%BC%E3%82%92%E7%99%BA%E8%A1%8C) があれば１コマンドで簡単に動くようにしてあります。
(Python未経験なのでパッケージ構成やソースがベストプラクティスになってないかもです。マサカリいただけると幸いです)

# ２.GPTs
GPTsはChatGPTの有料プランに含まれる機能で、以下の特徴があります。
・UI付きのカスタムChatGPTを自然言語で作成
・開発したツールを他者に共有
・外部APIと連携
・GPT Storeに一般公開することができ、利用した人数に応じて収益を得られる！

今回は、UI付きのカスタムChatGPTを作成してみます。
自然言語で「どのようなカスタムChatGPTを作成したいか」を伝え、
必要に応じて知識ベースとなるファイルをアップロードし、ウェブ検索、画像作成、データ分析などの機能を選択することで、
簡単にChatGPTを搭載したカスタムツールを作成できます。
コーディングの知識は必要ありません。

## 作り方
### ①GPTsの作成画面にアクセスする
有料版ChatGPTの左側メニュー`Explore`から`Create a GPT`を押下し、
GPTsの作成画面にアクセスしてください！

![gs1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/e9d24c4a-1507-5418-1c10-994ec90ccc91.png)

### ②作りたいカスタムChatGPTを伝える
画面に表示されているGPT Builderに作りたいカスタムChatGPTを日本語で伝えます。

現時点では複雑なものは作れなさそうですし、今回はお試しなので簡単な要件にしてみます。

```text:プロンプト:簡単
まず前提として、以降は日本語でコミュニケーションしましょう。

＜機能＞
添付のテキストファイルを前提知識とし、
そのテキストデータを利用して質問に答えるカスタムChatGPTを作ってください。
```

```text:添付したテキストファイル(melty_go.txt)
ざっくりSIer→メガベンチャー×2→某事業会社のノーマルエンジニア。
Java/JavaScript/C/Rails/Goが得意で、現在はGo/gRPC/GCPとかをやってます。 
最近は主に業務自動化を推進してまして、仕事をなくす仕事が得意かもしれないです。
```

![gs2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/4d350e82-cef7-d128-f588-25379044f226.png)

---

余談ですが、色々な要件を盛り込んでみたら、後続のステップでタイムアウトしました。

```text:プロンプト:複雑
まず前提として、以降は日本語でコミュニケーションしましょう。

作成希望のカスタムChatGPTについて、以下の概要をご検討ください。
もし、何か実現が困難な点がありましたら、その理由も併せて連絡いただけると幸いです。

＜機能＞
指定したWebサイトのURLからテキストデータをスクレイピングし、
そのデータを利用して質問に答えるカスタムChatGPT。

＜使い方＞
1. 準備
・ユーザーはカスタムChatGPTにスクレイピングするWebサイトのURLを入力します
・カスタムChatGPTは入力されたURLのテキストデータをスクレイピングし、それを学習します
・過去に学習済みのURLが再度入力された場合、カスタムChatGPTは学習データを更新します
・学習が完了すると、カスタムChatGPTは「学習完了しました」とユーザーに通知します

2. 質問回答
・ユーザーが質問を入力します（URLを除く全ての入力は質問として扱われます）
・カスタムChatGPTは、事前に学習したデータを基にして質問に回答します
・回答は、スクレイピングしたデータに基づいて、具体的かつ情報豊富なものになります

＜制約＞
１.　プライバシー
・スクレイピング対象は、公開されているWebサイトのテキストデータのみとする
・ログインが必要なページやプライベートな情報へのアクセスは避ける
・各Webサイトの利用規約やロボット排除標準（robots.txt）を遵守する
・個人情報（名前、住所、メールアドレスなど）は学習しない

2.　アクセス制御
・スクレイピングにはユーザーエージェント「MyScrapingTool/1.0」を使用する
・サーバーへのアクセス頻度を適切に制限する

3. エラーハンドリング
・ユーザーが入力したURLがHTTPS/HTTP形式でない場合は入力エラーとする
・Webサイトが存在しない、リンク切れやリダイレクトがある場合は適切なエラーメッセージを返す
・5秒以内にスクレイピングできない場合は「Webサイトを読み取れませんでした」と回答する
```

### ③名称とアイコンを設定する
これから作成するカスタムChatGPTの名称とアイコンを提案してくれるので、回答します。

![スクリーンショット 2023-12-15 22.41.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/c7ebad17-6f24-6906-3b56-59b5d4235b5b.png)

### ④質問の種類を伝える
カスタムChatGPTに対して、どのような種類の質問に答えることを期待するのか伝えます。

今回は機能がシンプルだからか、GPT Builderが自動設定してくれました。

![gs４.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/6d1b7e4f-98b6-be9f-e455-3bd1ef1d21ef.png)

### ⑤回答における注意点を伝える
情報の提供方法や応答のスタイルなど、特に注意すべき点について伝えます。

今回は `質問に対して知らない情報があっても、検索せずに「不明です」と回答してください。`
と入力します。

![gs５.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/cff4ed5b-5c24-1742-30fa-eb61a9a86d83.png)

### ⑥回答のスタイルを伝える
カスタムChatGPTが質問に答える際の口調や情報量を伝えます。

今回は `カジュアルなトーンで詳細な回答をしてください` と入力します。

![gs6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/0c7cf0fe-7737-8929-0211-030e0354d191.png)

### ⑦動作確認をする
GPT Builaderが設定完了したら、
右側のプレイグラウンドでカスタムChatGPTの動作確認をします。

`melty_goのプロフィールを教えて` と質問したら、良い感じに回答が返ってきました。

![gs7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/75b87364-2793-b52f-2a8d-317fc67b1ca6.png)

### ⑧保存する
動作確認した結果、問題なければ右上の「Save」ボタンを押下して保存します。
その際、公開方法も選択可能で、リンクで公開したり無条件に公開したりできます。
今回はお試しでの作成なので「Only me」を選択しておきます。

# ３.Assistants
Assistantsは、GPTsと類似した設定で作成できるカスタムChatGPTですが、
APIとして利用することができます。
また、ブラウザでOpenAIのPlaygroundからでも利用できます。

AssistantsはGPTsと異なり、有料版のChatGPTではなく、
無料のOpenAIアカウントとAPI使用枠を持っていれば利用開始できます。

OpenAIアカウント作成時、無料でAPI使用枠が提供されます。
無料の使用枠を使い切った後は、更にAPIを利用するためには課金が必要ですが、
10$ほどでも結構遊べます。

## 作り方

### ①作成画面にアクセスする
https://platform.openai.com/assistants
で `Create`ボタンを押下し、
Assistantの作成画面を表示します。
![as1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/94b5641e-6eb3-1273-08ea-657141882bec.png)

### ②設定する
以下の設定を入力し、`Save`ボタンを押下します。
その後、「asst_」で始まるIDが付与されるのでメモしておきます。

- Name: Assistantの名称
- Instructions: Assistantの役割、何をしてもらうのか
- Models: ChatGPTのベースモデルを選択
- Tools: Assistants APIが対応している連携ツール
  - [Functions](https://platform.openai.com/docs/assistants/tools/function-calling): Assistantに対してカスタムFunctionsを指定
  - [Code Interpreter](https://platform.openai.com/docs/assistants/tools/code-interpreter): Pythonコードを作成して実行
  - Retrieval: あらかじめアップロードしたファイルを前提知識として回答
  - Files: 前提知識となるファイル

今回アップロードしたテキストファイル
```text:melty_go.txt
ざっくりSIer→メガベンチャー×2→某事業会社のノーマルエンジニア。
Java/JavaScript/C/Rails/Goが得意で、現在はGo/gRPC/GCPとかをやってます。 
最近は主に業務自動化を推進してまして、仕事をなくす仕事が得意かもしれないです。
```

![as2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/8ad626ec-822c-1404-97a9-dc861ae911db.png)

### ③APIで会話する
**1. 会話用スレッドを作成**
まず最初に会話用スレッドを作成し、スレッドのmessageとして質問を設定します。
今回はMacのターミナルからcurlでAPIを実行します。

```zsh:zsh
curl https://api.openai.com/v1/threads \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {OpenAI API KEY}" \
-H "OpenAI-Beta: assistants=v1" \
-d '{
    "messages": [{
      "role": "user",
      "content": "melty_goのプロフィールを教えて"
    }]
  }'
```

実行するとスレッドが作成され、そのIDが表示されます。

![as3-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406684/41a9dc6f-c49c-799c-2771-ccb7c863e46d.png)

**２. 会話用スレッドにメッセージを格納**
作成した会話用スレッドにメッセージを格納します。

先ほど表示されたスレッドID `thread_xxx` と
「②Assistantを設定する」でメモしたAssistantID `asst_xxx` を指定し、
以下のAPIを実行します。

```zsh:zsh
curl https://api.openai.com/v1/threads/{THREAD ID}/runs \
-H "Authorization: Bearer {OpenAI API KEY}" \
-H "Content-Type: application/json" \
-H "OpenAI-Beta: assistants=v1" \
-d '{
    "assistant_id": "{ASSISTANT ID}"
}'
```

**3. 会話用スレッドからメッセージを取り出す**
最後に、会話用スレッドからメッセージを取り出すAPIを実行し、回答を取得します。

```zsh:zsh
curl https://api.openai.com/v1/threads/{THREAD ID}/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {OpenAI API KEY}" \
-H "OpenAI-Beta: assistants=v1"
```

# おわりに
今回は、コーディング、GPTs、Assistantsという３つの異なるアプローチを使ってカスタムChatGPTの作成方法を整理してみました。

コーディングでは、技術的な深さとカスタマイズの自由度が高い一方で、
GPTsとAssistantsは、迅速な作成と使いやすさに魅力がありました。

どの方法を選ぶかはプロジェクトの目的やスキルセットによるところが大きいかもしれませんが、
どれを選んでもカスタムChatGPTは価値を提供できそうですね。