ChatGPT APIを利用したシステム構築 後編③ エンドツーエンドシステムの構築とアウトプットの評価

[【UiPath×OpenAI】開発者向けのプロンプトエンジニアリング](https://qiita.com/utanesuke/items/9d8590fbdfa1b2159148)の前編・中編・後編の記事では、ChatGPTの単なる一つのプロンプトや一つのモデルを利用して、プロンプト文を書く際の原則とテクニックを解説しました。

この記事では、LLMを利用して複雑なアプリケーションを構築する際のベストプラクティスを共有します。カスタマーサービスアシスタントの構築を例に、異なるPromptを用いて言語モデルを連鎖的に呼び出します。具体的なPromptの選択は前回の呼び出しの出力結果に依存し、時には外部ソースから情報を検索する必要があります。

このテーマに沿って、アプリケーション内部の構築手順を段階的に学びながら、長期的な視点でのシステム評価と継続的改善に関するベストプラクティスを共有します。

本記事は以下の講座を纏めた内容です。

https://www.deeplearning.ai/short-courses/building-systems-with-chatgpt/

- Build an End-to-End System
- Evaluation part I
- Evaluation Part II


# 環境設定(共通)
この記事では、OpenAIが提供するChatGPT APIを使用しています。したがって、最初にChatGPTのAPIキーを取得する必要があります（または公式ウェブサイトでオンラインテストを実施することもできます）。次に、openaiとlangchainのライブラリをインストールする必要があります。
```bash
pip install openai
```
```bash
pip install langchain
```
```bash
pip install --upgrade tiktoken
```
ライブラリのインポートと自分のAPIキーを設定します。
```bash
import os
import openai
import sys
sys.path.append('../..')
# 日本語のPromptライブラリ
import utils_jp

import panel as pn  # UI画面
pn.extension()

openai.api_key='自身のOpenAI キー'
```
これから、OpenAIが提供するChatCompletion APIを利用します。ここでは、それを関数にラップしてみます。

```python
# 「Prompt」を受け取り、それに対応する結果を返す関数
def get_completion_from_messages(messages, 
                                 model="gpt-3.5-turbo", 
                                 temperature=0, 
                                 max_tokens=500):
    '''

    パラメータ: 
    messages: メッセージのリスト、一つのメッセージが辞書型の一つ、role(ロール）とcontent(内容)。ロールが'system'、'user' 或 'assistant’，内容がロールのメッセージです。
    model: 利用するモデル、デフォルトはgpt-3.5-turbo(ChatGPT)
    temperature: 生成されたテキストの多様性や予測のランダム性を調整するためのパラメータ
    max_tokens: このモデルの最大Token数。
    '''
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, 
        max_tokens=max_tokens,
    )
    return response.choices[0].message["content"]
```

以下の三つのファイルも必要ですので、[**こちら**](https://drive.google.com/drive/folders/1urT4Pnc8VbAOE8qkLiEh8KqBlbpSZjAu?usp=drive_link)をご参照ください。
- utils_jp.py
- products.json
- categories.json

# エンドツーエンドシステムの構築

こちらで、評価機能付きのエンドツーエンドの質問応答システムを構築します。以下の既に学習済テーマを含んだステップの通りで作られています。
1. 入力をチェックし、それが監査APIの審査を通過するかどうかを確認します。
1. 審査に合格した場合、製品リストを検索します。
1. 製品が見つかった場合、それに関連する情報を検索しようとします。
1. モデルを使用して、ユーザーが提出した質問に回答します。
1. 生成された答えを監査APIで審査します。

有害とマークされていない場合、その答えをユーザーに返します。

## エンドツーエンドの関数

```python 
'''
注意：モデルの日本語理解力が弱いため、日本語プロンプトがランダムに失敗することがあります。
'''
def process_user_message_jp(user_input, all_messages, debug=True):
    """
    ユーザインプットに対して事前処理
    
    パラメータ:
    user_input : ユーザインプット
    all_messages : 履歴データ
    debug : デバッグモードかどうか、デフォルトがON
    """
    # 区切り
    delimiter = "```"
    
    # ステップ1: OpenAIのモデレーションAPIを使用して、ユーザー入力のコンプライアンスまたは注入されたプロンプトをチェックします。
    response = openai.Moderation.create(input=user_input)
    moderation_output = response["results"][0]

    # Moderation API を使って、ユーザインプットが無害かどうかを確認
    if moderation_output["flagged"]:
        print("ステップ1：Moderationがあなたのインプットを拒否しました")
        return "あなたのインプットはコンプライアンス違反です"

    # もしDEBUGモードが有効なら、リアルタイムの進捗を表示する。
    if debug: print("ステップ1：入力はモデレーションチェックを通過しました")
    
    # ステップ2：前のコースの方法と同様に、商品と対応するカタログをラッパーで抽出する。
    category_and_product_response = utils_jp.find_category_and_product_only(user_input, utils_jp.get_products_and_category())
    #print(category_and_product_response)
    # 抽出した文字列をリストに変換する。
    category_and_product_list = utils_jp.read_string_to_list(category_and_product_response)
    #print(category_and_product_list)

    if debug: print("ステップ2：商品リストの抽出")

    # ステップ3：抽出された製品情報を見つける
    product_information = utils_jp.generate_output_string(category_and_product_list)
    if debug: print("ステップ3：抽出された製品情報を見つける")

  # ステップ4：メッセージに基づいて返信を生成する
    system_message = f"""
        あなたは大型電気店の接客係です。 \
        親しみやすく親切な口調で、簡潔明瞭にお答えください。 \
        ユーザーに関連するフォローアップの質問をするようにしてください。
    """
    
    # メッセージ追加
    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': f"{delimiter}{user_input}{delimiter}"},
        {'role': 'assistant', 'content': f"関連商品情報:\n{product_information}"}
    ]
    # GPT3.5から回答を得る
    # all_messagesを追加して複数回の対話を行う
    final_response = get_completion_from_messages(all_messages + messages)
    if debug:print("ステップ4：ユーザー・レスポンスの生成")
    # メッセージの履歴にラウンドを追加する
    all_messages = all_messages + messages[1:]

    # ステップ5：モデレーションAPIに基づく出力のコンプライアンスチェック
    response = openai.Moderation.create(input=final_response)
    moderation_output = response["results"][0]

    # アウトプットにコンプライアンス違反な内容が含まれている
    if moderation_output["flagged"]:
        if debug: print("ステップ5：モデレーションによって拒否された出力")
        return "申し訳ありませんが、その情報は提供できません"

    if debug: print("ステップ5: モデレーションによってチェックされた出力")

  # ステップ6：モデルはユーザーの質問にうまく答えられたかどうかをチェックする
    user_message = f"""
    ユーザメッセージ: {delimiter}{user_input}{delimiter}。
    エージェントのレスポンス：{delimiter}{final_response}{delimiter}。

    応答が質問に答えるのに十分かどうか
    はいの場合、Yと答える
    そうでなければNと答える
    上記の文字だけを答える
    """
    # print(final_response)
    messages = [
        {'role': 'system', 'content': system_message},
        {'role': 'user', 'content': user_message}
    ]
    # 回答にはモデル評価が必要
    evaluation_response = get_completion_from_messages(messages)
    # print(evaluation_response)
    if debug: print("ステップ6：モデルは応答を評価する")

   # ステップ7: 評価がYの場合、回答を出力する; 評価がNの場合、フィードバックは回答の手動修正となる
    if "Y" in evaluation_response: # モデルがYesを生成することを避けるためにinを使う
        if debug: print("ステップ7: モデルは回答に同意しました。")
        return final_response, all_messages
    else:
        if debug: print("ステップ7: モデルはレスポンスに同意しました。")
        if debug: print("ステップ7: モデルは回答に同意しません。")
        neg_str = "申し訳ありませんが、要求された情報を提供できません。 カスタマーサービス担当者におつなぎします。"
        return neg_str, all_messages

user_input = "smartx pro phoneとfotosnap cameraについて教えてください。 また、テレビについても教えてください。"
response,_ = process_user_message_jp(user_input,[])
print(response)
```

結果：

```python 
ステップ1：入力はモデレーションチェックを通過しました
ステップ2：商品リストの抽出
ステップ3：抽出された製品情報を見つける
ステップ4：ユーザー・レスポンスの生成
ステップ5: モデレーションによってチェックされた出力
ステップ6：モデルは応答を評価する
ステップ7: モデルは回答に同意しました。
「SmartX ProPhone」は、SmartXブランドのパワフルなスマートフォンです。6.1インチのディスプレイ、128GBのストレージ、12MPのデュアルカメラ、5G対応などの特徴があります。価格は899.99ドルです。

「FotoSnap Camera」にはいくつかのモデルがありますが、一つ例を挙げると「FotoSnap DSLR Camera」は、24.2MPのセンサー、1080pのビデオ撮影、3インチの液晶ディスプレイ、交換可能なレンズなどの特徴があります。価格は599.99ドルです。

テレビに関しては、いくつかのモデルがありますが、一つ例を挙げると「CineView 4K TV」は、55インチのディスプレイ、4K解像度、HDR、スマートTV機能などの特徴があります。価格は599.99ドルです。

それぞれの製品には1年間の保証が付いています。他にも関連するアクセサリーやオプションもありますので、何か特定の質問や関心があればお知らせください。
```

上記のように、各ステップごとのDebug情報も出力されました。

# アウトプットの評価

前の記事では、LLMを利用してアプリケーションを作りました。このアプリケーションでは、インプットのチェック、インプットの処理、及びユーザへ見せる前に最終チェックもしました。

こちらで、LLMのアウトプットを評価するベストプラクティスを共有します。

まず製品とカテゴリのリストを取得します。

```python 
products_and_category = utils_jp.get_products_and_category()
products_and_category
```
出力は：
```python 
{'Computers and Laptops': ['TechPro Ultrabook',
  'BlueWave Gaming Laptop',
  'PowerLite Convertible',
  'TechPro Desktop',
  'BlueWave Chromebook'],
 'Smartphones and Accessories': ['SmartX ProPhone',
  'MobiTech PowerCase',
  'SmartX MiniPhone',
  'MobiTech Wireless Charger',
  'SmartX EarBuds'],
 'Televisions and Home Theater Systems': ['CineView 4K TV',
  'SoundMax Home Theater',
  'CineView 8K TV',
  'SoundMax Soundbar',
  'CineView OLED TV'],
 'Gaming Consoles and Accessories': ['GameSphere X',
  'ProGamer Controller',
  'GameSphere Y',
  'ProGamer Racing Wheel',
  'GameSphere VR Headset'],
 'Audio Equipment': ['AudioPhonic Noise-Canceling Headphones',
  'WaveSound Bluetooth Speaker',
  'AudioPhonic True Wireless Earbuds',
  'WaveSound Soundbar',
  'AudioPhonic Turntable'],
 'Cameras and Camcorders': ['FotoSnap DSLR Camera',
  'ActionCam 4K',
  'FotoSnap Mirrorless Camera',
  'ZoomMaster Camcorder',
  'FotoSnap Instant Camera']}
```

:::note info

現在はアウトプットの評価に対して、そこまで評価しなくても良いので、残りの内容を一旦省略します。必要がある際に、また参照します。

:::

# 最後に

このコースでは、LLMの動作原理について詳しく説明しており、トークナイザーの詳細、ユーザー入力の品質と安全性の評価方法、プロンプトとしての思考チェーンの使用、連鎖的なプロンプトでのタスクの分割、およびユーザーに返す前の出力のチェックなどが含まれています。

このコースでは、システムの長期的な性能を評価し、パフォーマンスを監視し改善する方法についても紹介しています。