と思ってやってみたら結構実現できてウケたので解説します。
はじめに
最近のGPT(LLMs)アプリケーション開発界隈は「プロンプトの内容を試行錯誤して結果を期待する」フェーズから「LLMsの特性を生かした今までできなかった自動化を実現」という段階が訪れつつあって楽しい時期です。
LlamaIndexというOSSではDBのスキーマと自然言語からSQLを自動生成してその場で実行するというクレイジーな機能があるのですが(A Guide to LlamaIndex + Structured Dataを参照)
これと同じ発想でソースコード全体からpatch(patch - Wikipedia)を生成してその場で適用するというアイデアを思いついたのでしばらく検証していました。
「コミットメッセージを先に書いてそれを満すコミットをGPTに生成してもらう」ようなイメージ。
書いたコードはpmonというコマンドラインツールにしてGitHubで公開しています
※名前の由来は知る人ぞ知るパッチモンスターという用語からです*1
使い方
pmonはコマンドラインから実行できるエージェントとして機能します。
先ず以下のように第一引数に変更対象のファイルかディレクトリを指定して起動します。
$ echo 'pass' > examples/python/simple.py $ pmon examples/python/simple.py >>>How do you want to change the code? (e.g. "Add test case for generate()"):
そうするとどんな変更をしたいか質問してくるので入力します。
「main関数をいい感じに実装」と入力してエンター。
main関数をいい感じに実装 ```diff --- examples/python/simple.py 2023-04-09 09:28:36 +++ examples/python/simple.py 2023-04-09 09:31:45 @@ -1 +1,8 @@ -pass \ No newline at end of file +def main(): + print("Hello, World!") + +if __name__ == "__main__": + main() + + + ```>>>Do you want to apply this patch? /myproject/out/patches/20230409-091227.patch Yes or No or Retry or Quit (Y/n/r/q):**
ストリーミングでpatchの提案が流れてくるので適用してよさそうならエンター(Yes)。
別のシェルからファイルの中身を確認してみる。
patching file 'examples/python/simple.py' $ cat examples/python/simple.py def main(): print("Hello, World!") if __name__ == "__main__": main()
さらに変更してみます。「コマンドラインからURLを1つ指定すると、そのURLのページをダウンロードします。」と入力
>>>Enter a new requirement: コマンドラインからURLを1つ指定すると、そのURLのページをダウンロードします。 ```diff --- examples/python/simple.py 2023-04-09 10:00:10 +++ examples/python/simple.py.orig 2023-04-09 10:01:37 @@ -1,5 +1,11 @@ +import sys +import urllib.request + def main(): - print("Hello, World!") + url = sys.argv[1] + response = urllib.request.urlopen(url) + content = response.read() + print(content) if __name__ == "__main__": main() ```>>>Do you want to apply this patch? /myproject/out/patches/20230409-100924.patch Yes or No or Retry or Quit (Y/n/r/q): patching file 'examples/python/simple.py' The patch has been applied. >>>Enter a new requirement:
>>>Enter a new requirement: asyncioベースにして ```diff --- examples/python/simple.py 2023-04-09 10:00:10 +++ examples/python/simple.py.orig 2023-04-09 10:01:37 @@ -1,9 +1,14 @@ +import asyncio import sys import urllib.request +from aiohttp import ClientSession +async def fetch(url, session): + async with session.get(url) as response: + return await response.text() -def main(): +async def main(): url = sys.argv[1] - response = urllib.request.urlopen(url) - content = response.read() + async with ClientSession() as session: + content = await fetch(url, session) print(content) if __name__ == "__main__": - main() \ No newline at end of file + asyncio.run(main()) ```>>>Do you want to apply this patch?
仕組み
- Chat completions APIにソースコードと入力テキスト(requirement)を投げる
- diff形式で結果を受け取りpatch保存
- patchコマンドを呼び出して適用する
1を視覚化すると要するにこういうプロンプトを送信しています。
これはCodexというGPT-3ベースのコード生成モデルのEdit APIでできていたことに似ているけど、今後の主流はGPT-4のChat系のモデルなので
- 入力を会話形式の構造に置き換えて与える
- 出力をpatchで補完することでトークン数を抑える
ということをしています。
2の処理はChatGPT自身をAPIサーバーにするで説明したLangChain内部実装を真似しました。
3は素朴なロジックです。
cmd = f"patch --no-backup-if-mismatch --ignore-whitespace < {patch_path}" subprocess.run(cmd, shell=True, check=True) print("The patch has been applied.")
使用感
自分自身を書き換えられるようになった段階でpmon自身の編集にもpmonを使うようにしてみたのですけど「コンパイラがコンパイラ自身のソースコードをコンパイルできる」みたいな状態になって楽しい。
ただpatch適用成功率は変更する箇所によっては低くなりがちでした。
patch自体を通るように編集して試行錯誤したり、目grepして手mergeしたり。
複雑になると駄目かというとそうでもなくて、ソースコードの量が増えた方がpatch通りやすくなったりして不思議でした。
あと当方GPT-4 API対応人材なので(inviteされただけ)、3.5と比較していますがやっぱり4のが意図にそったpatchを作ってくれます。頭いい。
API利用料はこの記事書くまでに10ドルぐらい溶けてて、ただEmbedding API叩き過ぎて100ドル飛ばしてしまったことと比較したら安いもんよ。
実現できそうなアイデア
- patchファイルを履歴で保存しているのでそれも情報源にする
- revertできるようにする
- 入力をコミットメッセージにしたり操作をGitと連携する
- ブランチ作成してプルリクエストを立てたりGitHub APIとも連携する
課題
PoCとしては面白くはあるんだけど以下の課題を解決できるともう一段階上の精度が出そうなので解決策を考えています
- 不正なdiffを作ってくる
- 変更したいファイルと参照したいファイルはことなるので区別したい
- トークン数に応じてコスト(主に処理時間)が増加する
「1. 不正なdiffを作ってくる」というのは
--- examples/python/simple.py 2023-04-09 10:00:10 +++ examples/python/simple.py 2023-04-09 10:01:37 @@ -1,9 +1,21 @@
の行指定「@@ -1,9 +1,21 @@」の番号が間違っているのでpatchとして適用できない、などがよく起きてしまっています。
これはpatchコマンドの呼び出しじゃなくてunidiffで書き換え処理を実装したらなんとかならないものかと考えています。
「2. 3. 」の問題は予想どうりソースコードの規模が大きくなるほどネックになってくるのでどう解決しようかな〜と頭をひねっています。
後述するVSCodeは交換するトークンを絞ることで機能を実現しているが、人間は読めないがLLMは理解できる中間表現やメタデータ(ASTやSourceMapみたいなものを想像)を駆使して差分生成をできるようになると進化しそう。
類似する試み
VSCodeの野良ChatGPT拡張たちは内部で行っているアプローチが近い。
これらはファイルの一部分や選択範囲をGPTに送信して置き換えスニペットを取得しているが、トークン上限の制約がなければ全体の置き換えなども対応できる。
GitHub Copilot LabsというMS公式の拡張でも既にexplainとかrefactoringとかは提供されているので、そのうちCopilotでも同じことができるようになるかもしれない。
Auto-GPT
Auto-GPTは自然言語の要求を受けてソースコードを生成→実行するツールらしい。
おわりに
誰か似たようなことやってる人や論文知ってる人いたら教えてください。
あといっしょに開発しましょう。
追記
この記事をポストした後に見付けた情報を記録します。