Visual Studio 2022にはバージョン17.10よりGitHub CopilotとGitHub Copilot Chatが統合されたビルトインのコンポーネントが規定で含まれるようになります。
また、VS Codeの拡張機能には同様にGitHub CopilotとGitHub Copilot Chatが存在し、これらをインストールする事で同様のCopilotサポートを受けることができます。
この記事では、これらの機能を用いてシステム開発の生産性をどのように上げていくことができるのか、また、どういうことが(現状では)できないのかを、実際の開発体験から解説します。
GitHub Copilotによる開発支援は、大きく次の二つのカテゴリに分類されると考えています。
それぞれ具体的に見ていきましょう。
何らかの定義データのリストや、既存のソースコードを入力として、別のソースコードを生成させることができます。この生成ルールは複雑なものではなく、1, 2, 3 を A, B, C に変換するような1:1の写像変換に近いケースが失敗しにくく安定しています。
あまり複雑な変換ルールを指定するとミスを起こす場合もありますが、1:1で対応させるようなシンプルなケースだとあまりミスを起こしません。
例として、エラーを示す列挙型の定義とそれに対応するエラーメッセージを取得する関数を仕様書から生成するケースを考えてみます。
現在、仕様書としてExcelに「HTTPステータスコード」と「コードの意味」が定義されているとします(この内容はMicrosoftのサイトからサンプルとして拝借しています)。
Excelは選択した範囲をコピーしてテキストにTSV(タブ区切りデータ)としてペーストすることができ、GitHub Copilotはそれを正しく表として認識します。
このTSVデータを元に「C#の列挙型定義」と「その列挙型を与えられた時にメッセージを返す関数の定義」をCopilotに指示するには、次のようなプロンプトを用います。プロンプト後半の「HTTPステータスコード表」は、先ほどのExcelからコピー&ペーストしたTSVテキストです。
以下の表は、HTTPステータスコードのフィールド名称とその値、および、そのコードの表す意味の対応を示す。
この表から次のC#コードを生成せよ。
- MyHttpStatusCode列挙型の定義
- MyHttpStatusCode列挙型を受取り、その値に応じたメッセージとしてコードの意味を文字列として返す関数GetMessageの定義
HTTPステータスコード表
ステータス名 値 ステータスの意味
Accepted 202 要求は、さらに処理するために受け入れ済みです。
AlreadyReported 208 要求された操作の結果の一部が既に報告されています。
BadGateway 502 中間プロキシ サーバーが、別のプロキシまたは配信元サーバーから不適切な応答を受信しました。
BadRequest 400 サーバーが要求を理解できませんでした。 この状態コードは、他のエラーが適用されない場合、または正確なエラーが不明であるか、独自のエラー コードがない場合に送信されます。
Conflict 409 サーバーで競合が発生したため、要求を実行できませんでした。
:(以下、表が続く)
上記の内容をVisual Studio 2022のGitHub Copilot Chatへと送信すると、以下のような結果が得られます。
using System;
public enum MyHttpStatusCode
{
Accepted = 202,
AlreadyReported = 208,
BadGateway = 502,
BadRequest = 400,
Conflict = 409,
Continue = 100,
Created = 201,
ExpectationFailed = 417,
FailedDependency = 424,
Forbidden = 403,
Found = 302,
GatewayTimeout = 504,
:(中略)
VariantAlsoNegotiates = 506
}
public static class HttpStatusCodeHelper
{
public static string GetMessage(MyHttpStatusCode statusCode)
{
return statusCode switch
{
MyHttpStatusCode.Accepted => "要求は、さらに処理するために受け入れ済みです。",
MyHttpStatusCode.AlreadyReported => "要求された操作の結果の一部が既に報告されています。",
MyHttpStatusCode.BadGateway => "中間プロキシ サーバーが、別のプロキシまたは配信元サーバーから不適切な応答を受信しました。",
MyHttpStatusCode.BadRequest => "サーバーが要求を理解できませんでした。 この状態コードは、他のエラーが適用されない場合、または正確なエラーが不明であるか、独自のエラー コードがない場合に送信されます。",
MyHttpStatusCode.Conflict => "サーバーで競合が発生したため、要求を実行できませんでした。",
MyHttpStatusCode.Continue => "クライアントは要求を続行する必要があります。",
MyHttpStatusCode.Created => "要求の結果、応答が送信される前に新しいリソースが作成されました。",
MyHttpStatusCode.ExpectationFailed => "Expect ヘッダーで指定された 期待 値をサーバーが満たすことができませんでした。",
MyHttpStatusCode.FailedDependency => "要求されたアクションが別のアクションに依存し、そのアクションが失敗したため、リソースに対して メソッドを実行できませんでした。",
MyHttpStatusCode.Forbidden => "サーバーは要求の実行を拒否します。",
MyHttpStatusCode.Found => "要求された情報は、 Location ヘッダーで指定された URI にあります。 この状態を受信したときの既定のアクションは、応答に関連付けられている Location ヘッダーに従います。 元の要求メソッドが POST の場合、リダイレクトされた要求では GET メソッドが使用されます。",
MyHttpStatusCode.GatewayTimeout => "中間プロキシ サーバーが、別のプロキシまたは配信元サーバーからの応答を待機している間にタイムアウトしました。",
:(中略)
MyHttpStatusCode.VariantAlsoNegotiates => "サーバーに内部構成エラーがあります。 選択したバリアント リソースは、透過的なコンテンツ ネゴシエーション自体に関与するように構成されているため、ネゴシエーション プロセスの適切なエンドポイントではありません。",
_ => "不明なステータスコードです。"
};
}
}
このようなコードを手入力で作るのは非常に面倒で時間がかかります。また、ケアレスミスも起こりやすいでしょう。頻繁に更新されるような定義の場合、Excelマクロを用いてコード生成する事にもメリットがありますが、最初の一回だけ生成するようなケースで都度そのようなマクロを記述するのは非効率です。
このような使い方でGitHub Copilotを用いると、筆者の経験ではほぼ正確なコードを生成してくれました(もちろん、人間による確認は必要です)。
但し、前述の通り複雑な変換をしようとすると、プロンプトの試行錯誤が必要となります。正しく変換ルールを伝えるためのプロンプトを試行錯誤するぐらいであれば、Excelマクロを書くか、TSVを読み込んでコード生成するためのプログラムを別途書いた方が速いでしょう。
仕様書ではなく、自分たちの書いた既存コードを参照し、それを入力として何らかのコード生成を行うこともできます。
GitHub Copilotは現在開いているソースコード(特に、選択範囲)をコンテキストとして利用することができます。
例えば、C#のメソッド呼び出し形式で書かれた以下のようなLINQコードをクエリ式に変換したいとします。
var products = db.Products
.Join(db.ProductGroups,
p => p.ProductGroupId,
pg => pg.Id,
(p, pg) => new { p, pg })
.Where(x => x.p.PublishStart <= now)
.Where(x => x.p.PublishEnd == null || x.p.PublishEnd > now)
.OrderByDescending(x => x.p.PublishStart)
.ThenBy(x => x.p.ProductName)
.Select(x => new
{
ProductId = x.p.ProductId,
ProductName = x.p.ProductName,
ProductCode = x.p.ProductCode,
ProductGroupId = x.p.ProductGroupId,
ProductGroupName = x.pg.ProductGroupName,
Price = x.p.Price,
});
エディタ上でこの部分を選択し、Copilotに「クエリ式に変換」と入力すると、以下のような出力が得られます。
var products =
from p in db.Products
join pg in db.ProductGroups on p.ProductGroupId equals pg.Id
where p.PublishStart <= now
&& (p.PublishEnd == null || p.PublishEnd > now)
orderby p.PublishStart descending, p.ProductName ascending
select new
{
ProductId = p.ProductId,
ProductName = p.ProductName,
ProductCode = p.ProductCode,
ProductGroupId = p.ProductGroupId,
ProductGroupName = pg.ProductGroupName,
Price = p.Price,
};
このような、期待する変換後の結果が明確であるケースでは特にCopilotは威力を発揮します。とはいえ100%間違いがないとは言い切れないので、人間による確認は常に必要です。
単純化して考えると、生成AIのやっていることは入力(プロンプト)に対する「最もそれらしい結果」を出力する変換処理です。
今後のAI自身の推論能力向上によってある程度その辺りがカバーされるようになることが期待されますが、それを扱う側の人間自身の表現力の限界もあるため、複雑な変換処理をさせようとするとどうしても「必要な結果を得る為のプロンプトの試行錯誤」が必要になります。
Copilotの利用経験者であれば、どのようにプロンプトを変えても無意味なコードが生成されるループから抜け出せなくなったことは一度や二度ではないでしょう。
そのため、ある程度の品質を得るためにはプロンプトの単純化をした方が効率的です。より具体的には、入力(プロンプト)に対する結果が1:1になるぐらいの明確な指示ができるように落とし込むようにします。これはつまり、1:1の写像変換を頭の中にイメージするということです。
このように、Copilotに依頼する作業がそのような1:1の写像変換としてプロンプトで定義できるかどうかを意識すると、より効率的にGitHub Copilotを利用することができるでしょう。
GitHub Copilotは膨大なコードベースを学習したAIモデルです。その知識を問い合わせることで、単に検索サービスで検索するよりも効率的に期待する結果を得ることができます。これは使い方次第では便利ですが、制限や限界もあります。
例えばGitHub Copilotに「.NETの最新バージョンは?」と聞くと「.NET7」と回答します。調べると、記事執筆時点(2024年10月)では、GitHub Copilotの知識は2023年10月までのものだということが分かります。
そのため、ある程度枯れた技術の知識、定型的なコード・スニペットなどを問い合わせる場合にはとても有用です。反面、古いAPIを用いたコードを生成したりなど、最新の知識が必要な場面では役に立たないこともあります。
特に問題となるのが、Copilotが知らない知識について問い合わせた場合です。Copilotが「それについては知りません」と答えることは稀で、何かしら意味がありそうな回答が無理やり生成されます。これを「ハルシネーション(幻覚、幻影)」と呼び、その分野の知識が質問者に不足していると、回答の正当性について判断が難しく、間違った回答を鵜呑みにしてトラブルになることがあります。
例として、正規表現を扱った場合について説明します。
正規表現は大変便利ですが、人間にとっての可読性が高いとは言えず、よほど慣れていない限りは必要な正規表現を一から記述したり、既存の正規表現の意味を理解したりするのが困難です。
例えば次のような正規表現をソースコード中で見つけたものの、何をチェックしているのかいまいちわからないとします。
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,30}$
これについてCopilotに次のように質問します。
この正規表現は何をチェックしている?
回答は次の通りです。
この正規表現は、パスワードの強度をチェックするためのものです。具体的には、以下の条件を満たす文字列を検証します:
^(?=.[a-z]):少なくとも1つの小文字を含む。
(?=.[A-Z]):少なくとも1つの大文字を含む。
(?=.*\d):少なくとも1つの数字を含む。
[A-Za-z\d]{8,30}$:8文字以上30文字以下の長さで、英字(大文字・小文字)と数字のみを含む。
この正規表現を満たす文字列は、英字と数字を含む8文字以上30文字以下のパスワードです。
この正規表現が上記の条件を満たす強固なパスワードチェックをしていることが分かります。
逆に、逆に、必要な正規表現を生成してもらいたい場合もあるでしょう。この場合、エディタ上で直接コード補完の機能を利用できます。
電話番号の妥当性チェックをしたい場合、以下のようにエディタに bool IsValidPhoneNumber(
まで入力した段階で、以下のコードが自動的に補完されます。
bool IsValidPhoneNumber(string phoneNumber)
{
string pattern = @"^\d{10}$";
return Regex.IsMatch(phoneNumber, pattern);
}
しかし残念ながら、この正規表現はハイフン付きの電話番号を通してくれません。
その場合、次のようにCopilotに修正を依頼します。
/fix ハイフン付きの電話番号も妥当
/fix
はGitHub Copilotコマンドの一つであり、このプロンプトがコードの修正指示であることを明確にします。
Copilotは次のような修正を提案します。
bool isValidPhoneNumber(string phoneNumber)
{
string pattern = @"^\d{10}$|^\d{3}-\d{4}-\d{4}$";
return Regex.IsMatch(phoneNumber, pattern);
}
しかしこれでは9999-99-9999
のような固定電話のパターンを通してくれません(ハイフン前後の数字の桁数が違います)。
これは、現時点でのCopilotが「日本の電話番号」についての正確な定義を知識として持っていない可能性が高いことから起こるハルシネーションです。正しい回答にたどり着くためには、妥当としたい番号の定義をさらに明確に伝える必要があるでしょう。
電話番号とは別の例として、例えば「12桁固定の英大文字と数字から構成される文字列で、先頭は必ず英大文字」という定義は十分に明確です。その場合、先ほどの定義をコメントとして記述すれば、次の行にその正規表現が補完されます。
// 「12桁固定の英大文字と数字から構成される文字列で、先頭は必ず英大文字」の正規表現パターン
string pattern = @"^[A-Z][A-Z0-9]{11}$";
どちらにせよ、生成された正規表現が妥当かどうか判断できる程度の正規表現の知識は必要です。
判断が難しい難解な正規表現が生成された場合には、妥当な値と妥当でない値のそれぞれのパターンのリストをプロンプトに与え、「これらの入力パターンでのテストコードも併せて生成」と指示すれば、より確実な結果を得られる可能性が高くなります。
GitHub Copilotのモデルは汎用的な知識を持っていますが、我々のソースコード・リポジトリに関する知識は持っていません。@workspace
や #file
のような機能は徐々に実装されつつありますがまだ限定的です。
モデルに知識がない代わりに、GitHub Copilotはプロンプトの暗黙的なコンテキストとして以下の情報を参照することができます。
これらすべてが毎回プロンプトとして送信されるわけではなく、ある程度必要そうな箇所が推定され、その部分のみが参照されるため、「前回の質問では参照してくれたのに、今回の質問では参照してくれなくなった」ということが頻繁に起こります。
#file:<filename>
を指定したファイル参照も4~5ファイル程度で限界のようで、たくさんのファイルを指定しても内容の多くが端折られて参照されているようです(※記事執筆時点)。
そのため、現状では基本的に現在開いているコードのみが参照されていることを前提にした方が良いでしょう。その場合であっても、行数の多いコードの場合には自動的に要約されたものが送られるため、可能な限り対象コードを範囲選択で選択して明示した方が安定した結果を得られます。
以下のようなHTMLのモックアップ(一部抜粋)があり、これからそこへVue3を用いた動的なコードを組み込みたいとします。
<section id="introduction" class="py-5 bg-light">
<div class="container">
<h2 class="display-5 text-center mb-3">News</h2>
<ul>
<li><a href="https://example.com/news1">サンプルニュース1</a> (2024/3/25)</li>
<li><a href="https://example.com/news2">サンプルニュース2</a> (2024/3/21)</li>
<li><a href="https://example.com/news3">サンプルニュース3</a> (2024/3/7)</li>
<li><a href="https://example.com/news4">サンプルニュース4</a> (2024/3/7)</li>
<li><a href="https://example.com/news5">サンプルニュース5</a> (2024/3/6)</li>
<li><a href="https://example.com/news6">サンプルニュース6</a> (2024/3/2)</li>
<li><a href="https://example.com/news7">サンプルニュース7</a> (2024/3/2)</li>
<li><a href="https://example.com/news8">サンプルニュース8</a> (2024/3/1)</li>
</ul>
</div>
</section>
Vue3での開発は久しぶりであるため、どのようにすべきか思い出す作業が必要ですが、これをCopilotに依頼してみましょう。
まず、エディタ上で上記のコードを範囲選択し、次のようなプロンプトで指示をします。
選択範囲のHTMLはニュースリリース一覧である。このHTMLを以下のように修正
- ニュースリリースは Url, Text, ReleaseDate で構成される
- ニュースリリースは最新10件を表示する
- 上記をVue3で実装する
すると、GitHub Copilotは以下のようなコードを提案します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ニュースリリース</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<section id="introduction" class="py-5 bg-light">
<div class="container">
<h2 class="display-5 text-center mb-3">News</h2>
<ul>
<li v-for="news in newsReleases" :key="news.id">
<a :href="news.url">{{ news.text }}</a> ({{ new Date(news.releaseDate).toLocaleDateString() }})
</li>
</ul>
</div>
</section>
</div>
<script>
const app = Vue.createApp({
data() {
return {
newsReleases: []
};
},
mounted() {
fetch('/api/news') // APIエンドポイントを適宜変更
.then(response => response.json())
.then(data => {
this.newsReleases = data.sort((a, b) => new Date(b.releaseDate) - new Date(a.releaseDate)).slice(0, 10);
});
}
});
app.mount('#app');
</script>
</body>
</html>
元となるモックアップのHTMLの構造を維持したまま、適切なマークアップとスクリプトが生成されています。慣れていれば手で書いた方が速いこともありますが、ある程度項目が多い場合などにはCopilotに任せた方が楽なことが多いでしょう。
このスケルトン・コードを元に、実際に必要な処理へと自分で修正していくことができます。
但し、このコードはプロンプトで指示していない以下の仕様が前提になっていることに注意が必要です。
上記の仕様が実際の仕様とマッチしていない場合、再度プロンプトを修正したり、生成結果に手を加える必要があります。作業者自身にもVueについての十分な知識が必要であり、「Vueの知識がないからCopilotに生成させる」という用途に使うのは困難です。
それ以外にも、まったく想定外の仕様を前提にしたコードを生成することもあり、その場合には生成されたコードが丸ごと意味のないものになります。初学者ではそこに気づくのが難しい為、袋小路に陥る可能性があります。
Vueの知識が十分にあり、スケルトン・コードなしですらすらとコーディングできるのであれば、生成されるコードの正しさを確認する時間よりも、最初から自分で書いた方が速いケースが多いでしょう。
古いコードをメンテナンスしていると、コメントが書かれていないために意図を把握しづらい数百行のコードに出会うことがあります。そういうコードをコード・リーディングで解読しようとする代わりに、Copilotに説明させることで時間を短縮できます。
例えば、以下のサンプルコードの意図をパッと見で把握できるでしょうか。
using System;
using System.Collections.Generic;
using System.Linq;
namespace DifficultCode
{
class Program
{
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("引数が必要です。");
return;
}
ProcessData(args);
}
static void ProcessData(string[] args)
{
var a = args.ToList();
var b = new List<Tuple<string, int>>();
foreach (var c in a)
{
var d = c.Split(':');
var e = d[0];
var f = int.Parse(d[1]);
b.Add(new Tuple<string, int>(e, f));
}
var g = b.Select(h => h.Item1).ToList();
var i = b.Select(j => j.Item2).ToList();
var k = new Dictionary<string, int>();
for (int l = 0; l < g.Count; l++)
{
k[g[l]] = i[l];
}
var m = k.Keys.ToList();
var n = k.Values.ToList();
var o = new List<string>();
for (int p = 0; p < m.Count; p++)
{
o.Add(m[p] + ":" + n[p]);
}
var q = string.Join(",", o);
Console.WriteLine(q);
}
}
}
ある程度時間をかければ理解できそうな内容ですが、関数名や変数名が ProcessData
のように抽象的な名前になっていることもあり、詳しくコードを読んで理解するのは少し面倒です。ただ、何らかの文字列配列を受け取り、ParseしてConsoleに出力していることはわかります。いったいどの様なデータを与えればよいのでしょうか。
これをCopilotに説明してもらうために、以下のようなプロンプトを用います。
ProcessDataに渡す引数の値の例と、出力サンプルを提示。詳細な説明は不要。
Copilotの回答は以下の通りです。
引数の例
apple:3 banana:2 cherry:5
出力サンプル
apple:3,banana:2,cherry:5
コードの説明
このコードは、コマンドライン引数として渡された文字列を処理し、特定のフォーマットで出力します。引数は「キー:値」の形式で渡され、最終的に同じ形式でコンマ区切りの文字列として出力されます。
引数の形式と結果を見るとどういうシーンで使う関数かが見えてくる場合が多く、それ以上コードを深追いする労力を省くことができます。 これまでと同様、回答が間違っていることもありますので注意は必要です。
現在のところGitHub Copilotは、エディタで現在開いているコードのみを安定して参照できます。
この記事の執筆時点では@workspace
という「ワークスペースの知識を持つ」コマンドの送り先が使えるようになったり、#file:<filename>
という、指定したファイルを参照するコマンドがあったりします。
しかしプロンプトとして送信できる量には限界があるため、例えば「ワークスペースに含まれるすべてのファイル一覧」や「ワークスペースのすべてのファイルに含まれるクラス名の一覧」のようなシンプルな質問すら、プロンプトの意図通りのすべてのファイルが得られないといった不完全な結果が出力されることが多いようです。
例えば以下の例では、「このワークスペースの最上位フォルダにあるすべてのファイルの一覧」を求めているのに対して、実際に存在するファイルを除外して表示しています。恐らく何らかの理由で(lockファイルは一般的にリストアップしないことが多いから?)除外しているものと思われますが、このプロンプトの意図は「すべて」ですから、結果は明確に間違っています。
これらの制限は、今後GitHub Copilotの性能向上によって改善されていく可能性があります。記事執筆時点でも、GitHub Copilot Enterprise限定でGitHub Copilotのカスタムモデルをファインチューニングできる機能がベータ公開されました(参考: GitHub Copilot Enterpriseの限定公開ベータでモデルのファインチューニングが可能に - GitHubブログ)。
生成AIは驚くべき速度で進化していますので、近い将来、ユーザーリポジトリを全て学習したモデルに不具合の箇所を特定させたり、修正による影響範囲を調べさせたりといったことが現実的な実用性を伴って可能になるかもしれません。
この記事でも何度か例に出てきたように、生成されるコードは用途を限定すればある程度の品質を持つ結果が得られますがそれ以外だと問題のあるコードが生成されることも多々あります。
そのため、常に結果に対する正誤判定をする知識と経験が、利用者に求められます。
初学者がサンプルコードの検索の為に使うのであれば有用だと思いますが、生成されたコードを何の検証や裏付けの確認も行わずに採用してしまうのには大きな問題があります。
また、これは筆者の経験ですが、GitHub Copilotを使って補完・生成したコードはあまり記憶に定着しないように思います。そのため、別のシーンで同様の事をCopilot無しで行おうとすると、つい最近やったはずなのにそれを思い出せないことが多いのです。
これは恐らく、記憶の定着には「試行錯誤の繰り返し」が必要であることが関係しているように思います。
それも含めると、初学者がGitHub Copilotを利用する際は、十分な注意と使用ガイダンスの整備が必要でしょう。
恐らく最もGitHub Copilotの効果が高いのがこの層です。ある程度の知識を背景として何か新しいことを始めるようなケースにおいて、Copilotのコード補完は作業効率を多いに高めます。
必要なのは、Copilotの生成結果を批判的に見て、正誤判定を十分に行うことです。それができる経験や知識が備わっているのであれば、大いに利用すべきでしょう。
作業を行っている分野のエキスパートである場合、Copilotのコード補完はむしろ邪魔になることが多いようです。
補完されるコードは「大多数の最適解」であることが多いものの、最新の知識や深い経験を持つエキスパートにとっての最適解には届かないこともあり、補完されるコードの正誤判定をいちいち目視で行うよりも最初からコードを自分で書いた方が速い場面が多いでしょう。
これは今後のAIの能力向上によって大きく改善される可能性があります。
GitHub Copilotはプログラミングの生産性を向上させる強力なツールですが、その効果はタスクの種類やユーザーのスキルに依存します。シンプルで定型的な作業においては特に有用で、普遍的な知識を迅速に活用できますが、複雑な生成や最新情報には限界があります。
初学者は正誤を判断する力を養うことで、Copilotをより効果的に活用できるでしょう。中級者は批判的に生成結果を評価することで、最大限の恩恵を享受できます。シニアエンジニアは、自分の知識と経験を活かしてプロンプトを工夫することで、Copilotの補完を効率的に利用できるでしょう。
Copilotを含むAIによるコーディング支援能力は今後さらなる発展を遂げることが予想されていますので、この記事で挙げたような制限はもうまもなく撤廃されるかもしれません。しかし、使う側の心構えは大きく変わらないように思います。ツールのより効果的な使い方をこれからも探っていきます。
お問い合わせはこちらから
問い合わせる