🚨

ChatGPT API にコードを書かせて eval する際のエラーハンドリング・プラクティス

2023/03/08に公開

最近 ChatGPT API にコードを書かせてそれを eval するという仕組みを作りました。
その際に考えたエラーハンドリングのプラクティスが、ChatGPT API ならではで個人的に面白かったので記事に残しておきます。

📝 tl;dr

  • ChatGPT はコードを書くことを拒否する場合がある。考慮したリトライの仕組みを作る
  • ChatGPT の書いたコードで発生したエラーは ChatGPT に解決してもらう

💬 何をやろうとした?

以前の記事にて Minecraft 上で ChatGPT に作りたいものを伝えると魔法みたいに実現してくれるコマンドというものを紹介しました。

https://zenn.dev/ryo_kawamata/articles/5980d30972ff29


豪華(?)な家を作ってもらう例

このコマンドの実装はとてもシンプルで、ユーザーの指示(prompt)から ChatGPT API にコードを書かせて、それを eval しているだけです。

def think_code(prompt: str):
  # ChatGPT API にコードを書かせてその結果を返却する

def get_code_blocks(text: str):
  # ChatGPT の結果からコードブロックを抽出する

def main:
  generated_text = think_code(prompt)
  code = get_code_block(generated_text)

  # コードを実行する
  exec(code)

しかし、なかなか正確なコードを書いてもらえないことも多く当初はエラーが頻発しました。

🚨 どんなエラーが発生する?

おもに 2 つのタイプのエラーが発生しました

1. ChatGPT がコードを書いてくれない

複雑な造形を依頼した場合に発生することが多いのですが、ChatGPT が「私は AI なので〜を作ることができません」という返答のみを返してくることがあります。
これではコードがないので eval できません。

🤖 実際の返答例

申し訳ありませんが、私たちはMinecraftとPythonを組み合わせた業務には従事しておりません。
また、MinecraftからPythonにアクセスするには、特別なサーバーやツールが必要になる場合があります。
あなたが探しているのがMinecraftとPythonのAPIインターフェースの場合、Minecraft ForgeやRaspberryJuiceを調べてみてください。

2. ChatGPT が書いたコードでエラーが発生する

ChatGPT がコードを書いてくれる場合でも、なんらかの理由でエラーになることがあります。
よくあるのは引数の数が合わなかったり、末 import のライブラリを使っている場合などです。

🤖 返答コード例

def hoge_func(arg1, arg2):

def main():
  hoge_func(1, 2, 3) #引数の数が合わずevalで失敗する

main()

🤔 どうハンドリングする?

それぞれの対応方針です。
基本的には最大のリトライ回数を指定した上で、 Chat GPT にエラーを伝えて修正してもらいます。

1. ChatGPT がコードを書いてくれない場合の対応

コードを書いてくれない場合は、コードを書いてくれるように再度依頼します。

まず、ChatGPT を呼ぶ関数で追加のメッセージを配列で受け取れるようにします。

def think_code(prompt: str, extra_messages=[]):
  order = f"${prompt} 初回のプロンプト..."

  completion = openai.ChatCompletion.create(
      model="gpt-3.5-turbo",
      messages=[
          {"role": "system", "content": "ロール定義のprompt"},
          {"role": "user", "content": order},
          *extra_messages, # ここに追加のメッセージを入れる
      ]
  )
  return (completion.choices[0].message.content, False)

そして、ChatGPT がコードを書いてくれなかった場合に追加のメッセージを送ります。

def main():
    # 略

    retry_count = 0
    max_retry_count = 8
    extra_messages = []

    # 成功するか最大のリトライ数に達するまで繰り返す
    while retry_count < max_retry_count:
        # chat completion APIでコード生成
        generated_text = think_code(
            prompt=prompt,
            extra_messages=extra_messages
        )

        # APIの出力結果からコード部分をテキストで抽出
        code = get_code_blocks(generated_text)

        # コード部分が空の場合、ChatGPTに修正してもらう
        if code == "":
            retry_count += 1
            extra_messages.append({
                "role": "assistant",
                "content": generated_text
            })
            extra_messages.append({
                "role": "user",
                "content": "Minecraft上で実行できるPythonコードをコードブロックに書いてください。"
            })
            continue
    # 略

ポイントは、code が空の場合に、extra_messages に ChatGPT の出力テキストと、「Minecraft上で実行できるPythonコードをコードブロックに書いてください。」というメッセージを追加してからリトライしている点です。

2 回目に送るリクエストのメッセージの配列はこうなります。

[
  {"role": "system", "content": "ロール定義のprompt"},
  {"role": "user", "content": "ユーザーの入力したprompt"},
  {"role": "assistant", "content": "ChatGPTの出力テキスト(ここにコードがない)"},
  {"role": "user", "content": "Minecraft上で実行できるPythonコードをコードブロックに書いてください。"}
]

こうすれば、ChatGPT は意図を理解して、次のレスポンスではコードを返してくれるはずです。

2. ChatGPT が書いたコードでエラーが発生する場合の対応

こちらの場合は、ChatGPT にエラー内容をそのまま伝えて修正してもらいます。

def main():
    # 略

    retry_count = 0
    max_retry_count = 8
    extra_messages = []

    # 成功するか最大のリトライ数に達するまで繰り返す
    while retry_count < max_retry_count:
        # chat completion APIでコード生成
        generated_text = think_code(
            prompt=prompt,
            extra_messages=extra_messages
        )

        # APIの出力結果からコード部分をテキストで抽出
        code = get_code_blocks(generated_text)

        # 中略

        try:
            # 出力されたコードをeval
            exec(code, globals())
            break
        except Exception as e:
            retry_count += 1
            # evalでエラーが発生した場合はエラー内容をChatGPTのパラメーターに追加してリトライ
            extra_messages.append({
                "role": "assistant",
                "content": generated_text
            })
            extra_messages.append({
                "role": "user",
                "content": f"実行したところ {e} というエラーが発生しました。修正してください。"
            })
            continue
    # 略

ポイントは、exec でエラーが発生した場合に、Try Catch でエラーを捕捉し、extra_messages に「実行したところ {e} というエラーが発生しました。修正してください。」 と exec で発生したエラー内容をそのまま追加している点です。

2 回目に送るリクエストのメッセージの配列はこうなります。

[
  {"role": "system", "content": "ロール定義のprompt"},
  {"role": "user", "content": "ユーザーの入力したprompt"},
  {"role": "assistant", "content": "ChatGPTの出力テキスト(ここのコードでエラーが発生する)"},
  {"role": "user", "content": "実行したところ hoge というエラーが発生しました。修正してください。"}
]

こうすれば、ChatGPT は、意図を理解して、次のレスポンスではエラーを修正したコードを返してくれるはずです。

🤖 実際に修正してくれた例

申し訳ありません、書籍で紹介されているコードが古いバージョンのmcpiに対応したものでした。最新のmcpiでは `mcpi.vec3.Vec3` を使用する必要があります。

以下が修正済みのコードとなります。確認してみてください。

...

最終的な全体のコードはこちらです

エラーハンドリングを考慮した Minecraft x ChatGPT の実装
import openai
import sys
from mcpi import minecraft
from mistletoe import Document
from mistletoe.block_token import CodeFence
import logging

logging.basicConfig(filename='magic_log.txt', level=logging.INFO)

mc = minecraft.Minecraft.create()


def get_code_blocks(text: str):
    doc = Document(text)

    code_blocks = []
    for token in doc.children:
        if isinstance(token, CodeFence):
            code_blocks.append(token.children[0].content)

    return "\n\n".join(code_blocks)


def think_code(prompt: str, extra_messages=[]):
    order = f"""
「{prompt}」という指示をMinecraft上で実現するためのPythonコードを教えてください。
MinecraftとPythonの連携には、mcpiというライブラリを使用します。
コードを出力する際には、以下のルールを守ってください。

- 可能な限り細部まで精巧な構築物を作るコードを出力する
- 完成したら構築物の全体を見通せる位置にプレイヤーを移動させる
- コードはMarkdownのコードブロックで囲む
- Pythonのexec関数で実行できる形式にする

"""

    openai.api_key = "YOUR API KEY"
    try:
        completion = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": "あなたは優秀なPythonプログラマーです。"},
                {"role": "user", "content": order},
                *extra_messages,
            ]
        )
        return (completion.choices[0].message.content, False)
    except Exception:
        return ("", True)


def main():
    # 引数からpromptを取得
    prompt = sys.argv[1]
    mc.postToChat(f"Prompt: {prompt}")

    retry_count = 0
    max_retry_count = 8
    extra_messages = []

    # 一度でうまくいかない場合もあるので指定回数リトライする
    while retry_count < max_retry_count:
        # chat completion APIでコード生成
        generated_text, has_error = think_code(
            prompt=prompt,
            extra_messages=extra_messages
        )

        if has_error:
            retry_count += 1
            continue

        # APIの出力結果からコード部分をテキストで抽出
        code = get_code_blocks(generated_text)

        # コード部分が空の場合
        if code == "":
            retry_count += 1
            extra_messages.append({
                "role": "assistant",
                "content": generated_text
            })
            extra_messages.append({
                "role": "user",
                "content": "Minecraft上で実行できるPythonコードをコードブロックに書いてください。"
            })
            continue

        try:
            # 出力されたコードをeval
            exec(code, globals())
            mc.postToChat("成功しました!")
            break
        except Exception as e:
            retry_count += 1
            # evalでエラーが発生した場合はエラー内容をChatGPTのパラメーターに追加してリトライ
            extra_messages.append({
                "role": "assistant",
                "content": generated_text
            })
            extra_messages.append({
                "role": "user",
                "content": f"実行したところ {e} というエラーが発生しました。修正してください。"
            })
            continue

    if not retry_count < max_retry_count:
        mc.postToChat("Error: 実行に失敗しました。")


main()

おわりに

今回紹介したエラーハンドリングを組み込んだところ、前回の記事の Minecraft のコマンドはかなり安定しました。

Chat GPT API という文脈を考慮して、返答を返してくれる API ならではのエラーハンドリングで個人的に面白かったです。何かの参考になれば嬉しいです。

Discussion