ABEJA Tech Blog

中の人の興味のある情報を発信していきます

GitHub Copilot はどのようにして空気を読むか?

"Everything is true," he said. "Everything anybody has ever thought."

—Philip K. Dick, Do Androids Dream of Electric Sheep?

この記事は ABEJAアドベントカレンダー2023 の 17 日目の記事です。

こんにちは。システム開発部の石川 (@ishikawa) です。昨年のいまごろは Objective-C ばかり書いていましたが、今年は念願の Swift で QR コードリーダーを書き直したり、React Native の C++ コードをデバッガで追いかけて不思議なバグの修正をした年でした。

普段の業務では TypeScript と Go、Python を書くことが大半です(こうして書き出してみると触っているプログラミング言語が若干多いのですが...)。

どのプログラミング言語で開発しているときも、GItHub Copilot が大活躍しています。いくつかの例をあげてみます: 1

  1. 定型処理の生成 いわゆる「必要だけど退屈な」コードを書くのに便利。記憶に印象深く残っているのだと、再帰下降パーサーや SVG によるグラフ描画のコードをスラスラ補完してくれたときは感動しました
  2. コメントからのコード生成 これから実装したい関数や処理の仕様をコメントで書いておくと、GitHub Copilot がそれっぽいコードを書いてくれます。また、コメントは「即席の指示」として使うこともできます。たとえば、// English: と書けば直前の日本語コメントを英訳してくれますし、処理の具体的な内容をステップごとに書けばその通りコードに落としてくれます(もちろん、不要なコメントはあとで削除します)2 特に、慣れないライブラリやコードベースに触れるときに助かります。3
  3. 単体テストの自動生成 単体テストを BDD スタイルで書いているので、テスト対象の関数と期待する挙動を書けば、土台として使えるくらいのコードは書いてくれますし、他に必要そうなテストも提案してくれます

今年は GitHub Copilot の性能が上がり、生産性の向上を身をもって感じた年だったように思います(Slack で自分の発言を振り返ってみると、昨年は「GitHub Copilot のサジェストいまいち 😥」みたいな文句も言っていたみたいなのですが 💦)。今年に入ってからの GitHub Copilot は、構文エラーになるようなサジェストをしなくなりましたし、ローカル環境でしか定義していない型定義なども考慮したコードを提案してくれることが多くなりました。

決して誇張ではなく、TAB キーを押すだけでコードが出来上がることもしばしばです。4 まさに、GitHub Copilot が「空気を読んで」コードを提案してくれたように感じることもあります。

どのようにして適切なコードを提案しているのか?

では、GitHub Copilot はどのようにして、ユーザーの求める適切なコードを提案しているのでしょうか?

これが本記事のテーマです。この記事では、GitHub Copilot がどのようにして開発者が満足するコード提案を(空気を読むように)行っているのかに焦点を当て、その背後にあるメカニズムを掘り下げていきます。

免責事項(クリックで展開) 本記事に記載されている情報は、公開されているブログ記事や論文、プレゼンテーションなどを基にしています。GitHub Copilot のソースコードは非公開であり、本記事ではリバースエンジニアリングの類は一切行っていません。したがって、提供する情報は過去の実装や考察に基づいたものであり、現在の実装や最新の動向と異なる可能性があります。この記事では、「かつてはこのような実装だった」「このような検討がされていた」という観点からお読みいただければと思います。

Codex モデル

GitHub Copilot のコード生成に中心的な役割を果たすのは、OpenAI の提供する Codex モデルです。5 GitHub Copilot は Codex の Web API(GitHub Copilot 専用にエンドポイントが用意されています)を呼び出すことで、コードの補完内容を生成しています。

では、GitHub Copilot は Codex の API を呼び出しているだけなのかというと、もちろんそんなことはなく、より良いコードを生成するために API を呼び出す前後でさまざまな技術的工夫を施しています。

Codex モデルの変遷

Codex モデルは GPT-3 をベースに、全世界の公開されたソースコードで Fine-tuning されたモデルです。その初期実装について書かれた論文 Evaluating Large Language Models Trained on Code によると、GPT-3 が(コード生成に特化していないにも関わらず)Python の docstring から単純なプログラムを生成できる能力があることに着目し、また豊富なデータセットがすでにあることから Codex の開発に乗り出したそうです。実際に、パラメータ数 (300M, 12B) とデータセットを変えたいくつかの Codex が開発され良好な結果を示しました。

その後、Codex は発展した数種類のモデルが一般公開され、GitHub Copilot そのほかのプロダクトに採用されました。これまでに一般公開された Codex モデルの変遷は以下のとおりです。

  • code-cushman-001 は、論文で紹介されている 12B モデルより強力で多言語対応したモデル
  • code-davinci-001 はコンテキスト長が code-cushman-001 の 2 倍だが、code-cushman-001 の方が高速
  • code-davinci-002 Codex の中で最も高性能。GPT-3.5 ファミリー

プロンプト・エンジニアリング

Codex のような大規模言語モデル (LLM) では「プロンプト」と呼ばれる入力テキストでモデルに情報提供やタスク実行の指示を与える必要があります(このブログを読んでいる方であれば、ChatGPT などで LLM の一般的な特徴は知っているかと思うので、この辺の解説は省きます)。

LLM の出力結果や品質はプロンプトの内容に大きく左右されるので、GitHub Copilot でも、いかに効率的なプロンプトを組むか、ということに技術的工夫が集中しています。いわゆるプロンプト・エンジニアリングです。

コンテキストの理解

効率的なプロンプトを作るためには、まず、コンテキスト(文脈)を理解する必要があります。

もっとも大きな関心事はコーディングに関することです。ユーザーはどんなコードを書いているのか? これからどんなコードを書こうとしているのか? ユーザーが求める適切なコード提案をするためには、こうしたコンテキストをできるだけ収集・理解し、プロンプトに含めることが重要です。

GitHub Copilot はプロンプトを作成するときに、以下の情報を収集します。

  • 編集中のファイルのプログラミング言語とファイル名
  • ユーザーのセッション情報
  • キャレットの前後のコード 6
  • 現在開いているタブの類似コード
  • 埋め込みとベクターデータベース(実験的)

順番に見ていきましょう。

編集中のファイルパスとプログラミング言語

最初期の GitHub Copilot では提案するコードの言語を間違える傾向があったそうです。7

たとえば、以下のコードをそのままプロンプトに入れることを考えてみてください(キャレットの位置はコードの末尾だとします)。

# Write a simple function
# to compute Fibonacci numbers
def fibonacci(n)

しかし、このコード片だけではこの言語が Python なのか Ruby なのか区別できません。このような曖昧さがあると、Codex モデルが生成したコードが間違っている可能性が高まります。開発チームはこの問題に対処するため、プロンプトの先頭に言語名とファイル名を入れるようにしました。ファイル名には拡張子から言語を推定できるだけでなく、名前は書かれるであろう内容のヒントになります(しかも短い!)。

#!/usr/bin/env python3
# filename: fibonacci.py
#
# Write a simple function
# to compute Fibonacci numbers
def fibonacci(n)

なお、この例では分かりやすさのために shebang ラインも追加していますが、Codex モデルが学習したソースコードのうち shebang ラインを持つものはシェルスクリプトなどに偏っていると考えられるため、必要な場合のみ追加するようにしているそうです(新規ファイルでファイル名が不明の場合など)。

ユーザーのセッション情報

ユーザーの操作履歴を含むセッション情報も活用しているそうです。具体的な例としては、直前のコード提案がユーザーによって受け入れられたかどうかなどです(この情報はプロンプトではなく、後述するコンテキストフィルターモデルで使われているみたいです)。

キャレットの前後のコード

初期の Codex モデルではプロンプトで指定されたコードの続きを補完することしかできませんでした。そのため、GitHub Copilot もエディタのキャレットの直前までのコードをプロンプトに仕込んでいました。

開発者は普通、頭から 1 行ずつ順番にコードを書くわけではありません。いくつかの関数のシグネチャを書いてからそれぞれの実装を埋める人もいますし、既存のコードを編集するときはファイルの途中からはじめます。しかし、Codex モデルが学習したソースコードは「完成した」ソースコードです。キャレットの直前までのコードを補完するだけでは(たとえ正しいコードでも)キャレットより後の部分とそぐわないナンセンスなコードが生成されるでしょう。

そこで OpenAI は Codex モデルをアップデートし、保管するコードの直前だけでなく、直後のコードも指定できるようにし、Codex モデルがそのあいだを埋めるように保管することができるようにしました(Fill-in-the-Middle と呼ばれていたようです)。8

現在開いているタブの類似コード

開発者は現在作業中のファイルで参照やインポートしている関数などの定義を確認するために、参照先のファイルをタブで開いている可能性が高いと考えられます(これは本当にその通りで、自分の普段のコーディングを振り返っても、関連するファイルは作業中タブのそばに配置してすぐ見れるようにしていることが多いように思います)。

開発者にとって有用なのであれば、AI にとってもそうでしょう。GitHub Copilot では直近開いた 20 個までのタブを対象にして、プロンプトに含めるようにしているそうです。9

しかし、ここでふたつの問題があります。

  • プロンプトに含めるにはコードが長すぎる Codex を含め多くの LLM が扱えるプロンプトの長さには上限があります
  • 対象タブのコードには編集中のコードに無関係なコードも含まれている 無関係な情報は逆に応答の正確性を落とす

これらを解決するため、隣接するタブに表示されているコードは 60 行程度のコード断片に分割され、編集中のコードとの類似度によってスコアリングされます。

類似度の計算にはジャカード類似度が使われています。ジャカード類似度は要するに、比較する対象同士の要素数の合計のうち、共通要素の数の割合です(何を要素としているかは明記されていませんが、おそらくプログラミング言語のトークンかと思います)。

 \displaystyle
\text{Jaccard Similarity} = \frac{ |A \cap B| }{ |A \cup B| }

この計算は比較的高速であり、GitHub Copilot のレイテンシを損なわずに実行できます。また、A/B テストの結果、類似度の閾値はかなり低めに設定されているそうです。10

GitHub Copilot の評価手法

ここまで説明していませんでしたが、GitHub Copilot のチームがどのようにコード提案のパフォーマンスを評価しているかを説明します。評価のひとつに、ユーザーとの対話結果からパフォーマンスを評価するオンライン評価があります。

GitHub Copilotのオンライン評価は受け入れ率(開発者が示された補完をどれだけ頻繁に受け入れるか)と保持率(開発者が受け入れた補完をどれだけ頻繁に、そしてどの程度編集するか)を通じて測定されます。

埋め込みとベクターデータベース(実験的)

リポジトリ全体のコード断片の埋め込み (Embedding) をローカルのベクターデータベースに保存して使う、いわゆる Retrieval Augmented Generation (RAG) も実験中とのことです。11 開いているタブだけでなくワークスペース全体をコンテキストとして捉えられれば、提案の精度がさらに上がりそうです。

この手法を応用して、コード以外もコンテキストの対象にすることを検討しているそうなので、期待が高まりますね。12 個人的に、仕様書などのドキュメント類は Markdown で Git 管理したい派なのですが、GitHub Copilot がドキュメント類もコンテキストに入れるようになれば、Git でドキュメント管理する文化が広まるかもしれません。

また、これまでに紹介した以外にも、より広くより深くコンテキストを理解できるように、GitHub Copilot は改良されています。13

プロンプト構築からコード生成まで

こうして収集されたコンテキスト情報は、優先順位付けされたあと、Codex が扱えるコンテキスト長におさまるものだけが残されます。それから、文書内の意図した順番で並び替えられ、コメントとして元のコードに挿入されます。

# filename: fancy_module.py
#
# compare this snippet from utils/concatenate.py:
#
# def crazy_concat(a, b):
#   return str(a) + str(b)[::-1]
#
def fancy_concat(a, b):

興味深いのは、コンテキスト情報を通常のコメントとして埋め込んでいることです。実際に、文書化の目的で残されたコメントのように見えることが肝要です。

こうして作られたプロンプトは、最後に「コンテキストフィルターモデル」を経て、Codex モデルに送信されます。

コンテキストフィルターモデル

GitHub Copilot のローカル拡張機能内で最後に行われるステップはコンテキストフィルターモデルです。この軽量なクライアントサイドモデルは、プロンプトやセッションの情報(前述した、最後の提案が受け入れられたかどうか、など)に基づいて、リクエストがモデルに送信されるべきかどうかを判断します。これによって、開発者の邪魔になる可能性のある不要な提案を減らしています。

最後に

思ったよりも長い記事になってしまいました。これでもかなり端折った内容となっているので、興味のある方は参考記事を当たっていただければと思います。

他の興味深い記事

GitHub Copilot について書かれた記事には、他にも興味深い話題があります。ここではポイントだけ紹介したいと思います。

We Are Hiring!

ABEJA はテクノロジーの社会実装に取り組んでいます。技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方のエントリーもお待ちしております)


  1. ここでは紹介しきれなかった機能として、最近リリースされたチャット機能が便利です。/fix/simplify といったスラッシュコマンドはとりあえず試してみて参考にするだけでもいいですし、エディタで選択しているコードについて相談することもできます。コミットメッセージの自動生成も非常に便利です。
  2. 指示を出すのは In-Editor Chat を使うこともできます (Mac の場合は Cmd+I Windows/Linux の場合は Ctrl+I)。他にも GitHub Copilot の興味深い活用方法が 8 things you didn’t know you could do with GitHub Copilot - The GitHub Blog で紹介されています。
  3. 最近も、ISUCON13 で GitHub Copilot に助けられました。Go 言語で参戦したのですが、普段使っている Gin (Web フレームワーク)、GORM (ORM ライブラリ) ではなく、Echosqlx でコードベースが書かれていました。HTTP ヘッダの読み取りや SQL で JOIN するコードなどは、まず GitHub Copilot に書いてもらい、あとは関数リファレンスだけを見て実装することができました。同様に、GitHub Copilot の開発者も、はじめて触る TypeScript での開発において GitHub Copilot が大活躍したそうです。
  4. Visual Studio Code のブログでは、このような開発体験を a smooth "Tab-Tab-Tab" development flow と表現していました。言い得て妙ですね。
  5. OpenAI の Codex モデルは API のベータ版が一般公開されていましたが、ベータ版のまま 2023 年 3 月に非推奨となりました(GPT-3.5 以降の利用を推奨)。GitHub Copilot も GPT-4 への移行を発表しています。
  6. キャレット エディタでテキストの挿入ポイントを示す点滅する縦線のこと。「カーソル」と呼ばれることもありますが、マウスのカーソルと区別するため、この記事ではより具体的な「キャレット」を用います。
  7. "In the beginning, GitHub Copilot also had the tendency to suggest lines of code in a completely different programming language" Inside GitHub: Working with the LLMs behind GitHub Copilot - The GitHub Blog
  8. "The completions endpoint also supports inserting code within code by providing a suffix prompt in addition to the prefix prompt. This can be used to insert a completion in the middle of a function or file." Code completion - OpenAI API
  9. このあたりの記述は A developer's guide to prompt engineering and LLMs - The GitHub Blog を参考にしています。
  10. "They found that setting a very low bar for when to include a match actually made for the best coding suggestions." How GitHub Copilot is getting better at understanding your code - The GitHub Blog
  11. RAG について詳細は弊社テックブログの記事「外部データを Retrieval して LLM 活用する上での課題と対策案 - ABEJA Tech Blog」などを参考にしてください。
  12. "This got us thinking, what if we could use this for retrievals of things other than just code," How we’re experimenting with LLMs to evolve GitHub Copilot - The GitHub Blog
  13. 最近リリースされた GitHub Copilot Chat ではエージェント機能が導入されました。そのうち、@workspace がローカルのリポジトリ全体を対象にさまざまなタスクを実行できます。検索方法としては、上位 10,000 の公開リポジトリは GitHub の検索エンジンで、それ以外はローカルで普通にテキスト検索されるそうです。 Pursuit of wicked smartness in VS Code