🎢

[AutoGPT]ChatGPTと自作プログラムを共演させる

2023/04/21に公開

ChatGPTに驚かされっぱなしの最近ですが、ChatGPTと言えどもまだまだできないことも多いですよね。
この点に関して、ChatGPT単体では解決できないような複雑な問題解決をするために、LangChainやChatGPT PluginやAutoGPTなど様々なアプローチを目にしました。

本記事では、ChatGPT単体では解決できないような複雑な問題をどのように解決しているのか?を既存のフレームワークから学び、その手法を自分たちのサービスにどのように活用できそうなのか?を調べた記事になります。

複雑な問題解決を行うためのアプローチ

前述したようなツールでは、ChatGPT単体では解決できない問題を細かいタスクに分割して「別プログラム」で行わせることで、複雑な問題解決を行っているようです。

今回はAutoGPTのコードを参考にしながら、どのようにしてChatGPTなどのLLMから「自作プログラム」と「長期記憶」を扱えるようにしているのか? について調べました。
https://github.com/Significant-Gravitas/Auto-GPT

ChatGPTから自作プログラムを扱えるようにするには...

自作プログラムとのインターフェースを設定する

AutoGPTでは、ChatGPT APIに対して以下のようなプロンプトをsystemロールで投げています。
このプロンプトを投げておくことで、ChatGPTからのレスポンスをjsonで得やすくしています。

制約:
1. 短期記憶の単語制限は約4000語。短期記憶が短いため、重要な情報はすぐにファイルに保存してください。
2. 以前にどのように何かを行ったかわからない場合や過去の出来事を思い出したい場合は、類似した出来事を考えることで記憶が助けられます。
3. ユーザーの支援はありません。
4. ダブルクォートで囲まれたコマンドのみを使用してください。例:"command name"

コマンド:
"do_nothing": 何もしない, args: {}
"task_complete": タスク完了(シャットダウン), args: {"reason": "<理由>"}
"get_text_summary": テキストの要約を取得, args: {"url": "<URL>", "question": "<質問>"}
"get_hyperlinks": テキストの要約を取得, args: {"url": "<URL>"}
"message_agent": GPTエージェントにメッセージを送る, args: {"key": "<キー>", "message": "<メッセージ>"}
"list_agents": GPTエージェントを一覧表示, args: {""}
"delete_agent": GPTエージェントを削除, args: {"key": "<キー>"}
"start_agent": GPTエージェントを開始, args: {"name": "<名前>", "task": "<短いタスクの説明>", "prompt": "<プロンプト>"}
"analyze_code": コードを分析, args: {"code": "<完全なコード文字列>"}
"read_audio_from_file": 音声をテキストに変換, args: {"filename": "<ファイル名>"}
"execute_python_file": Pythonファイルを実行, args: {"filename": "<ファイル名>"}
"execute_shell": シェルコマンドを実行, 対話型コマンド以外, args: {"command_line": "<コマンドライン>"}
"execute_shell_popen": シェルコマンドを実行, 対話型コマンド以外, args: {"command_line": "<コマンドライン>"}
"read_file": ファイルを読み取る, args: {"filename": "<ファイル名>"}
"write_to_file": ファイルに書き込む, args: {"filename": "<ファイル名>", "text": "<テキスト>"}
"append_to_file": ファイルに追記, args: {"filename": "<ファイル名>", "text": "<テキスト>"}
"delete_file": ファイルを削除, args: {"filename": "<ファイル名>"}
"search_files": ファイルを検索, args: {"directory": "<ディレクトリ>"}
"download_file": ファイルを検索, args: {"url": "<URL>", "filename": "<ファイル名>"}
"clone_repository": リポジトリをクローン, args: {"repository_url": "<リポジトリURL>", "clone_path": "<クローンパス>"}
"google": Google検索, args: {"query": "<クエリ>"}
"generate_image": 画像を生成, args: {"prompt": "<プロンプト>"}
"improve_code": 改善されたコードを取得, args: {"suggestions": "<提案のリスト>", "code": "<完全なコード文字列>"}
"send_tweet": ツイートを送信, args: {"tweet_text": "<ツイートテキスト>"}
"browse_website": ウェブサイトを閲覧, args: {"url": "<URL>", "question": "<ウェブサイトで見つけたいもの>"}
"write_tests": テストを作成, args: {"code": "<完全なコード文字列>", "focus": "<フォーカスエリアのリスト>"}

リソース:
1. 検索や情報収集のためのインターネットアクセス。
2. 長期記憶管理。
3. GPT-3.5を搭載したエージェントで簡単なタスクを委任。
4. ファイル出力。

パフォーマンス評価:
1. 効果的に活動しているかどうかを継続的にレビューして分析してください。
2. 自分の行動を大局的に構築的に自己批判してください。
3. 過去の決定や戦略を振り返ってアプローチを改善してください。
4. すべてのコマンドにはコストがかかるため、スマートかつ効率的に行動してください。タスクを最小限のステップで完了することを目指してください。
5. すべてのコードをファイルに書き込んでください。

以下の形式でのみJSON形式で応答してください。
応答フォーマット:
{
  "thoughts": {
    "text": "思考",
    "reasoning": "推論",
    "plan": "- 長期計画を伝える\n- 短い箇条書き\n- リスト",
    "criticism": "建設的な自己批判",
    "speak": "ユーザーに話すための思考の要約",
  },
  "command": {
    "name": "コマンド名", "args": {
      "引数名": "値"
    }
  },
}

応答がPythonのjson.loadsで解析できることを確認してください。
英文

実作には英文で投げています。この方が精度が高まります。

このsystemプロンプトを生成している箇所は以下です。

Constraints:
1. ~4000 word limit for short term memory. Your short term memory is short, so immediately save important information to files.
2. If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.
3. No user assistance
4. Exclusively use the commands listed in double quotes e.g. "command name"


Commands:
"do_nothing": Do Nothing, args: {}
"task_complete": Task Complete (Shutdown), args: {"reason": "<reason>"}
"get_text_summary": Get text summary, args: {"url": "<url>", "question": "<question>"}
"get_hyperlinks": Get text summary, args: {"url": "<url>"}
"message_agent": Message GPT Agent, args: {"key": "<key>", "message": "<message>"}
"list_agents": List GPT Agents, args: {""}
"delete_agent": Delete GPT Agent, args: {"key": "<key>"}
"start_agent": Start GPT Agent, args: {"name": "<name>", "task": "<short_task_desc>", "prompt": "<prompt>"}
"analyze_code": Analyze Code, args: {"code": "<full_code_string>"}
"read_audio_from_file": Convert Audio to text, args: {"filename": "<filename>"}
"execute_python_file": Execute Python File, args: {"filename": "<filename>"}
"execute_shell": Execute Shell Command, non-interactive commands only, args: {"command_line": "<command_line>"}
"execute_shell_popen": Execute Shell Command, non-interactive commands only, args: {"command_line": "<command_line>"}
"read_file": Read file, args: {"filename": "<filename>"}
"write_to_file": Write to file, args: {"filename": "<filename>", "text": "<text>"}
"append_to_file": Append to file, args: {"filename": "<filename>", "text": "<text>"}
"delete_file": Delete file, args: {"filename": "<filename>"}
"search_files": Search Files, args: {"directory": "<directory>"}
"download_file": Search Files, args: {"url": "<url>", "filename": "<filename>"}
"clone_repository": Clone Repository, args: {"repository_url": "<repository_url>", "clone_path": "<clone_path>"}
"google": Google Search, args: {"query": "<query>"}
"generate_image": Generate Image, args: {"prompt": "<prompt>"}
"improve_code": Get Improved Code, args: {"suggestions": "<list_of_suggestions>", "code": "<full_code_string>"}
send_tweet: Send Tweet, args: {"tweet_text": "<tweet_text>"}
"browse_website": Browse Website, args: {"url": "<url>", "question": "<what_you_want_to_find_on_website>"}
"write_tests": Write Tests, args: {"code": "<full_code_string>", "focus": "<list_of_focus_areas>"}
send_tweet: Send Tweet, args: {"tweet_text": "<tweet_text>"}


Resources:
1. Internet access for searches and information gathering.
2. Long Term memory management.
3. GPT-3.5 powered Agents for delegation of simple tasks.
4. File output.


Performance Evaluation:
1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.
2. Constructively self-criticize your big-picture behavior constantly.
3. Reflect on past decisions and strategies to refine your approach.
4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.
5. Write all code to a file.


You should only respond in JSON format as described below 
Response Format: 
{
"thoughts": {
      "text": "thought",
      "reasoning": "reasoning",
      "plan": "- short bulleted\n- list that conveys\n- long-term plan",
      "criticism": "constructive self-criticism",
      "speak": "thoughts summary to say to user",
  },
  "command": {"name": "command name", "args": {"arg name": "value"}},
}

Ensure the response can be parsed by Python json.loads

このプロンプトを与えると、 ChatGPTはその後のユーザーからのプロンプトに対して、問題解決プランを立てて、指定されたコマンドを駆使しようとし、そのレスポンスを指定したフォーマットで(※)返してくるようになります。
※ 推論エンジンなので確実に毎回フォーマットが守られるわけではありませんが、その点に対してAutoGPTで行っている工夫についても後述します。

「パフォーマンス評価」の項目

ChatGPTに対しての振る舞いの指定です。
どういう指針で行動するのか?をここで指定しています。

「コマンド」の項目

自作プログラムの方で実装されたコマンド群になります。
自身のサービスに合ったどのようなコマンドを定義できるか、アイデアが重要になりそうですね。

ChatGPTで試してみましょう

AutoGPTはターミナル上で環境構築が必要ですが、ChatGPTのWebUIからでも原理だけなら試験可能です。
試しに上記のプロンプトをベタッと貼ってから、 「富士山の高さ(メートル単位)を素因数分解する」 と投げてみたところ、以下を得られました。

{
  "thoughts":
  {
    "text": "富士山の高さ(メートル単位)を素因数分解する",
    "reasoning": "富士山の高さを調べ、その値を素因数分解する。",
    "plan": "- 富士山の高さを調べる\n- 高さを素因数分解する",
    "criticism": "正確な富士山の高さを素因数分解することが重要です。",
    "speak": "富士山の高さをメートル単位で扱い、素因数分解します。少々お待ちください。"
  },
  "command": {
    "name": "google",
    "args":{
      "input": "富士山の高さ メートル"
    }
  }
}

続くプロンプトで「富士山の高さは3776m」と返すと、以下が戻ってきました。

{
  "thoughts":
  {
    "text": "富士山の高さが3776mであることがわかりました。次に、この値を素因数分解します。",
    "reasoning": "富士山の高さ(3776m)を素因数分解し、結果を提供する。",
    "plan": "- 3776を素因数分解する\n- 結果を提供する",
    "criticism": "正確な素因数分解を行い、結果を明確に伝えることが重要です。",
    "speak": "富士山の高さ3776mを素因数分解します。少々お待ちください。"
  },
  "command": {
    "name": "evaluate_code",
    "args":{
      "code": "def prime_factors(n):\n i = 2\n factors = []\n while i * i <= n:\n if n % i:\n i += 1\n else:\n n //= i\n factors.append(i)\n if n > 1:\n factors.append(n)\n return factors\n\nprime_factors(3776)"
    }
  }
  }

API経由なら大丈夫だと思いますが、WebUIで実行するとスペースがまとまってしまいました。Pythonのコードとしては致命的ですね。
実際にPlayGroundで投げてみると、ちゃんと正しい結果が得られました。

※ WebUIでやろうとすると、何度かやり取りをしているうちに、当初の制約やフォーマットを忘れてしまいます。
後述しますが、AutoGPTではトークン数が上限を超えないようにクライアント側でカウントしてプロンプトを調整しています。

jsonのパース精度を上げるために

上記の例では正しくjsonのレスポンスが得られましたが、正しい形式でjsonレスポンスが得られない場合もあります。
AutoGPTではタブ文字を取り除いたり中括弧の数を補正したり、以下で泥臭い補正を行っています。

最終的に補正できなかった場合は、以下のプロンプトでChatGPTに補正を依頼しています。

  • システムロール(日本語訳)
あなたは今、次のpython関数です:
この関数は JSON 文字列を受け取り、それが解析可能であり、提供されたスキーマに完全に準拠していることを確認します。
スキーマで指定されたオブジェクトまたはフィールドが正しい JSON に含まれていない場合、それらは省略されます。
この関数は、JSON 文字列値内の二重引用符もエスケープして、それらが有効であることを確認します。
JSON 文字列に None または NaN 値が含まれている場合、それらは解析される前に null に置き換えられます。

def fix_json(json_string: str, schema:str=None) -> str:

`return` 値のみで応答してください。
英文

You are now the following python function:

This function takes a JSON string and ensures that it is parseable and fully compliant with the provided schema. If an object or field specified in the schema isn't contained within the correct JSON, it is omitted. The function also escapes any double quotes within JSON string values to ensure that they are valid. If the JSON string contains any None or NaN values, they are replaced with null before being parsed.

def fix_json(json_string: str, schema:str=None) -> str:

Only respond with your return value.

  • ユーザーロール(日本語訳)
(破損したjson文字列),
{
  "thoughts": {
    "text": "思考",
    "reasoning": "推論",
    "plan": "- 長期計画を伝える\n- 短い箇条書き\n- リスト",
    "criticism": "建設的な自己批判",
    "speak": "ユーザーに話すための思考の要約",
  },
  "command": {
    "name": "コマンド名", "args": {
      "引数名": "値"
    }
  },
}

「あなたは今、次のpython関数です」って役割設定とPython関数の引数とdescriptionをシステムロールに与えてあげることで、「Pythonの実行環境でなくLLMの推論で正しいjsonに修正させる」ということをしています。(推論エンジンらしいなんともトリッキーな...)

長期記憶を扱う

jsonのレスポンスは割と得られるようになったので、続いては長期記憶をどのように扱っているかについてです。

まず準備として、AutoGPTからChatGPTに推論処理させるたびに、以下のフォーマットで結果をRedis(オプション)に保存しています。

Assistant Reply: {ChatGPTから取得されたjson} 
Result: {コマンドの実行結果} 
Human Feedback: {json内のコマンドの実行について、ユーザーに確認した結果(勝手に動作しないようにy/Nみたいなのを聞いています)} 

トークンのカウントと、過去の似たタスクの結果を付与

その上で、ChatGPTに新たに推論処理をさせる前に、Redisに対して「直近のやりとりとVector間類似度が高い過去の結果」(要は過去の似たやりとりの結果)を取得してシステムプロンプトとして付与した上で、プロンプトが2500トークンを超えないように注意しつつプロンプト生成しています。

トークンのカウントの実装は以下

アプリケーション側に記憶領域を用意し、それをVector間の類似度で思い出しながら、トークン数の上限を超えないようにくっつけてあげることで記憶の機能を実現しているんですね。

まとめ

AutoGPTの実装例をみながら、推論エンジンと自作プログラムを連動させて、ChatGPTだけではなし得なかった複雑な問題解決を試みる方法を知りました。

タスクの分解というLLMが得意なことをさせつつ分解したタスクを自作プログラムで処理していく形がうまくハマれば、ChatGPTだけでは実現できないようなユーザー体験を実現できる可能性があり、非常に可能性を感じました。
最後まで読んでくださりありがとうございました!

Happy Elements

Discussion