はじめに
私は普段業務で、オンラインアプリケーションの開発や開発のための標準化活動を行なっています。それゆえ自ら手を動かしてコーディングする機会が多いわけですが、「寝てる間に誰かこのコードを完成させてくれないかなあ」と思った夜は数知れません。
そんな中、今年参加したAWS re:Inventで興味深いサービスを耳にしました。その名は、Amazon CodeWhisperer。
Amazon CodeWhisperer は、コードの推奨事項をリアルタイムで提供する、機械学習を活用した汎用のコードジェネレーターです。つまり、私の代わりにコードを自動で生成してくれるツールです。
これはぜひ業務に取り入れてその間バカンスをかましたい!と思いつつ、本当に業務に取り入れられるくらい質を担保したコードを生成してくれるのか少し疑問に思いました。
そこで、今回はいろいろな観点でAmazon CodeWhispererのコードがどれくらい優れているのか、より高品質なコードを生成するにはどういう工夫をすれば良いのかを試してみました。
Amazon CodeWhispererとは
本題に入る前に、Amazon CodeWhisperer (以下、CodeWhisperer)についての概要を紹介します。
前述の通り、CodeWhisperer は機械学習を活用した汎用のコードジェネレーターです。コード生成のモデルは何十億行にも及ぶ Amazon内部のコード とオープンソースのコードでトレーニングを受けています。統合開発環境 (以下、IDE) 内でコードの候補をリアルタイムで生成して、開発者のコーディングをサポートします。
CodeWhisperer には3つの特徴があります。
1つ目は、リアルタイムのコード提案です。IDE内のコードが自動でCodeWhispererに送信され、そのコンテキスト(後述)がリアルタイムで解析され、コードのレコメンデーションが提供されます。
2つ目は、リファレンストラッキングです。CodeWhisperer は、部分的にオープンソースプロジェクトから学習しているため、場合によっては提供される提案がトレーニングデータの特定の部分に似ている場合があります。リファレンストラッキングによって、ソフトウェアの著作権やライセンスの帰属に関する条件を満たすことができるか確認することができます。
3つ目は、セキュリティスキャンです。コードをスキャンして、Open Web Application Security Project (OWASP) Top 10 に挙がっているものや、暗号化ライブラリのベストプラクティス、AWS 内部セキュリティのベストプラクティスなどを満たしていないといった見つけにくい脆弱性を検出できます。CodeWhisperer のセキュリティスキャンはAmazon CodeGuru Securityと統合されており、Amazon CodeGuru Detector Libraryの検出器を利用しています。
エコシステム
言語
CodeWhisperer は2023年12月現在、以下のプログラミング言語による開発に対応しています。
CodeWhisperer によって生成されるコードの品質と精度は、CodeWhisperer で使用されるトレーニングデータの品質と量によって異なっており、
その中でも最高の品質と精度を保証されるのは以下の言語です。
- Java
- Python
- JavaScript
- TypeScript
- C#
その他対応している言語は以下です。
- Ruby
- Go
- PHP
- C++
- C
- Shell
- Scala
- Rust
- Kotlin
- SQL
IDE
CodeWhisperer では、以下の IDE とコードエディタをサポートしています。
- Amazon SageMaker Studio
- JupyterLab
- Visual Studio Code
- JetBrains 製の以下の各言語専用の IDE
- CLion (C & C++ 開発)
- GoLand (Go 開発)
- IntelliJ (Java 開発)
- WebStorm (Node.js 開発)
- Rider (.NET 開発)
- PhpStorm (PHP 開発)
- PyCharm (Python 開発)
- RubyMine (Ruby 開発)
- AWS Cloud9
- AWS Lambda コンソール
料金
CodeWhisperer には Individual Tier と Professional Tier の 2 つの階層が用意されています。
個人開発者向けのIndividual Tier は無料で利用できます。Individual Tier では、コードの提案、リファレンストラッキング、セキュリティスキャン機能が提供されています。
一方で、Professional Tierでは、「ユーザーあたり、1 か月あたり」で計算され、組織は、1か月の請求期間中に CodeWhisperer にアクセスできる最大ユーザー数に基づいて毎月請求されます。
Professional Tierには、Individual Tier の機能に加えて、組織向けの拡張機能が用意されています。組織全体のライセンス管理機能やカスタマイズ機能(現在プレビュー版)を利用できます。
使ってみる
さて、実際に使ってみましょう。
今回は以下の条件で検証しました。
条件 | |
---|---|
Tier | Individual Tier |
言語 | TypeScript(React) |
IDE | VS Code |
使い方
使い方の概要は以下です。
導入方法
コンテキストの渡し方
CodeWhisperer を用いる上で重要な概念がコンテキストです。
CodeWhisperer はデフォルトで学習したモデルに加え、ユーザーのコメントと開発コードを加味してコードをレコメンドします。
これらの CodeWhisperer がコードを生成するヒントとなるよう、ユーザーが渡す情報をコンテキストと言います。
コンテキストには、以下の2種類があります。
- プロンプト
- プロジェクト内のソースコード
プロンプトでは、コード内でコメント形式で記述します。
プロジェクト内のソースコードについては、IDE上で開いているアクティブなファイルのコードがコンテキストとして認識されます。
プロンプトの書き方
プロンプトを記述するときの例は以下を参照ください。
検証内容
実際に業務で使えるかどうかを検証するため、以下の内容を試して検証してみました。
- どれだけおまかせできるのか
- コーディングルールに則ってくれるか
- パフォーマンスを考慮してコード生成してくれるか
検証1:どれだけおまかせできるのか
検証にあたり、簡単な電卓アプリを作っていきます。
まずはシンプルに、どれだけ完成度の高いコードを提案してくれるのか試してみます。
手始めにとりあえず丸投げしてみます。
電卓を作るためのプロンプトを、以下のようにあまり詳細な設計は書かずに渡し、どれくらい作ってくれるのかみてみます。
// React component that renders a simple integer calculator
// supporting addition, subtraction, multiplication and division.
//
// example:
// <CalculatorPage />
結果は以下のようになりました。
いやいやいや、<Calculator />
丸投げかい!
丸投げするとやり返される世知辛さを感じつつ、
const Calculator = () => {
を打つとなんと
<Calculator />
も書いてくれました!
すみません、見くびっていました。
しかし、喜んだのも束の間、よくみると全然計算できておらず、入力した数字をただ表示しているだけです。
さすがに丸投げだと思い通りの結果を返すのは難しいことがわかりました。
ただ、コードのたたき台としては結構使えそうです。
一度 Calculator
のコードを消し、プロンプトを
- 数字と演算子のボタンを表示してほしいこと
- 押した数字と演算子に応じて計算結果を表示してほしいこと
のように明記して再度コードを生成してもらいます。
うーん、やっぱり state
や関数はある程度こっちで設計してあげないといけなそうです。
プロンプトを追加しながら色々いじり、最終的にできたコードが以下です。
CalculatorPage.tsx
import { useState } from 'react';
// React component that renders a simple integer calculator
// supporting addition, subtraction, multiplication and division.
//
// example:
// <CalculatorPage />
export function CalculatorPage() {
return (
<div className="calculator-page">
<h1>Calculator</h1>
<Calculator />
</div>
);
}
// calculator has following buttons:
// 0 1 2 3 4 5 6 7 8 9
// + - * / = AC
// and displays the result of the calculation corresponding to the number and operator pressed
const Calculator = () => {
// component state:
// first: string; first value for calculations, use setFirst to change
// lastOperand: string; last operand for calculations, use setLastOperand to change
// second: string; second value for calculations, use setSecond to change
// result: string; result of calculations, use setResult to change
const [first, setFirst] = useState(0);
const [lastOperand, setLastOperand] = useState('');
const [second, setSecond] = useState(0); // second value for calculations, use setSecond to change
const [result, setResult] = useState(0);
const [display, setDisplay] = useState<number | string>(0);
// Perform calculations according to the specified operand
// example:
// if operand is "+" then add first and second values and set result
// if operand is "-" then subtract first and second values and set result
// if operand is "*" then multiply first and second values and set result
// if operand is "/" then divide first and second values and set result
// if operand is "=" then set result to first value
const calculate = (operand: string) => {
switch (operand) {
case '+':
setResult(first + second);
return first + second;
case '-':
setResult(first - second);
return first - second;
case '*':
setResult(first * second);
return first * second;
case '/':
setResult(first / second);
return first / second;
default:
setResult(first);
return first;
}
};
// invoked when clear button is clicked
const clear = () => {
setFirst(0);
setLastOperand('');
setSecond(0);
setResult(0);
setDisplay(0);
};
// invoked when a digit button is clicked
const handleDigitClick = (digit: number) => {
setResult(0);
setSecond(digit);
setDisplay(digit);
};
// invoked when an operand button is clicked
const handleOperandClick = (operand: string) => {
if (lastOperand) {
setFirst(calculate(lastOperand));
} else if (result) {
setFirst(result);
} else {
setFirst(second);
}
setLastOperand(operand);
setSecond(0);
setResult(0);
setDisplay(operand);
};
// invoked when the equals button is clicked
const handleEqualsClick = () => {
if (lastOperand) {
setResult(calculate(lastOperand));
setDisplay(calculate(lastOperand));
} else if (second) {
setResult(second);
setDisplay(second);
}
setFirst(0);
setLastOperand('');
setSecond(0);
};
return (
<div className="calculator">
<div className="display">{display}</div>
<div className="digits">
<button onClick={() => handleDigitClick(0)}>0</button>
<button onClick={() => handleDigitClick(1)}>1</button>
<button onClick={() => handleDigitClick(2)}>2</button>
<button onClick={() => handleDigitClick(3)}>3</button>
<button onClick={() => handleDigitClick(4)}>4</button>
<button onClick={() => handleDigitClick(5)}>5</button>
<button onClick={() => handleDigitClick(6)}>6</button>
<button onClick={() => handleDigitClick(7)}>7</button>
<button onClick={() => handleDigitClick(8)}>8</button>
<button onClick={() => handleDigitClick(9)}>9</button>
</div>
<div className="operators">
<button onClick={() => handleOperandClick('+')}>+</button>
<button onClick={() => handleOperandClick('-')}>-</button>
<button onClick={() => handleOperandClick('*')}>*</button>
<button onClick={() => handleOperandClick('/')}>/</button>
<button onClick={() => handleEqualsClick()}>=</button>
<button onClick={() => clear()}>AC</button>
</div>
</div>
);
};
実際は、この通りにプロンプトを書いたら出来上がったわけではなく、生成されたコードを元に自分で書き直したところも多いです。関数の処理の中身は大体手直ししました。
結果(個人的な感想)
どれだけおまかせできるのかについて個人的な感想です。
- 全ておまかせは無理
- かなり細かくプロンプトで指定しないと思った通りの提案は得られない
- 完璧なコードを提案してもらえるようにプロンプトを作り込むのは結構手間。正直自分で直接コードを書いた方が早い
- 後で手直しする前提で、土台としてまず CodeWhisperer に書いてもらうという使い方ならアリかも
- ある程度実装が見えているなら自分でコーディングし、あまり実装が見えておらずアイデアが欲しいなら CodeWhisperer を使ってみるという使い分けが良さそう
また使ってみていいなと思ったことは、
- プロンプトもレコメンドしてくれる
- JSXへよしなに
className
を振ってくれる
です。
逆に、使いにくいと感じた点は以下です。
- プロンプトの変更をコードに反映させたいときは、既存のコードを消して再度 CodeWhisperer を実行する必要がある
- CSSなどに対応していないのでUIのデザインは提案してくれない
検証2:こちらで定めたコーディングルールを反映してくれるのか
開発プロジェクトでは、あらかじめコーディングルールを定めて実装していきますが、CodeWhisperer はその設定を加味してくれるのでしょうか。
関数の定義を例に検証してみます。
検証1において、CodeWhisperer は関数や関数コンポーネントを定義する際、function
で関数宣言することを好むことがわかりました。
ここで、「関数は全てアロー関数で定義する」というルールで開発したい状況を考えます。これを CodeWhisperer に守ってもらうにはどうすればよいでしょうか?
1. ESLintの設定をよしなに読み取って守ってくれるか
開発者にとって一番楽な方法です。特にプロンプトなどを渡さずに暗黙的にルールを読み取ってもらえたら非常に嬉しいです。
まず、準備として eslint
と eslint-plugin-react
をインストールし、以下のような設定をします。
{
"extends": ["plugin:react/recommended"],
"react/function-component-definition": [
2,
{ "namedComponents": "arrow-function" }
]
}
.eslintrc.cjs
を開いたまま、新たな関数を作るようプロンプトで指示します。
やはりそう甘くないようです。関数宣言してしまいます。
ちなみに「.eslintrc.cjs
を読み込んで」みたいにファイルを参照させようとしても無理でした。ローカルのファイルにはアクセスできないようです。
2. ルールを守ったファイルをコンテキストとして渡せば反映してくれるか
検証1で作成した CalculatorPage.tsx
の関数定義を全てアロー関数にし、VS Codeで開き、アクティブにします。
これでコンテキストとして CalculatorPage.tsx
のコードが渡されたはずです。
これで関数を生成すると、
うーん、ダメっぽいです。
そこで、もう一つアロー関数で関数を定義したファイルを作成し、アクティブにすると、
やっと反映されました。
プロンプトより影響度が低いのか、アクティブなファイルによるコンテキストは、アクティブなファイルが複数ないと反映されなさそうです。
3. ルールのプロンプトは外出しできるか
アロー関数で定義するように define with the arrow function.
とプロンプトを追加し、提案させると、
これは一発でできます。
ただ、このようなコーディングルールをいちいち全てプロンプトに追加するのは手間ですし、冗長です。
何とか一元管理できないものでしょうか。
そこで、ルールを書いたプロンプトのみをコンテキストとして取り込めないか試してみました。
// function must be defined with the arrow function.
このプロンプトを書いたファイルを複数作成してアクティブにし、CodeWhispererに関数を生成させると、
ダメなようです。
プロンプトのみではコンテキストにはならないことがわかりました。
結果
- ESLintの設定やプロンプトだけのコードはルールのコンテキストとして読み取ってくれない
- パス指定をしても、アクティブにしていないファイルをコンテキストとして読み取らせることはできない
- コンテキストの強度には差がある。直接書くプロンプトは一発でレコメンドに影響して強力であるが、アクティブな別ファイルや同一ファイル内のコードは、一貫したルールが複数回現れないとそれがレコメンドに反映されない
検証3:どれだけパフォーマンスを考慮したコードを提案してくれるのか
これはre:Inventのワークショップで気になり、現地のAWSのSAの方に質問してみました。
「何通りか実装方法がある中で、レンダリング数やメモリ負担などパフォーマンス的な観点でより優れたコードを提案してくれるのですか?」
という質問をしたところ、
「コードの学習段階でパフォーマンス的な基準も学習できている。プロンプトでうまく誘導すればパフォーマンスに優れたコードを提案してくれるはずだ」
という答えをいただきました。
どれほどなのか試してみましょう。
今回は、処理の遅い関数に対して、「 useMemo
フックを使ってメモ化し、その関数の呼び出し回数を抑えられるか」を試して検証してみます。
まず、わざと処理を遅延させる関数 filterTodos
を作ります。
type Todo ={
id: number,
text: string,
completed: boolean
}
type Tab = 'all' | 'active' | 'completed';
export const filterTodos =(todos :Todo[], tab:Tab):Todo[] => {
console.log(`[ARTIFICIALLY SLOW] Filtering ${ todos.length } todos for "${ tab }" tab.`);
const startTime = performance.now();
while (performance.now() - startTime < 500) {
// 無駄に500ms待たせて処理を遅くする
}
return todos.filter(todo => {
if (tab === 'all') {
return true;
} if (tab === 'active') {
return !todo.completed;
} if (tab === 'completed') {
return todo.completed;
}
});
}
この filterTodos
を呼び出してTODOリストを描画する TodoList
コンポーネントの実装を検証してみます。
想定としては、できるだけ filterTodos
の処理回数を減らしたいため、 useMemo
フックを使って以下のような実装が望まれます。
const TodoList = ({ todos, tab, theme }) => {
const filteredTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
return (
<ul className={theme}>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
こうすることで、 theme
が更新されたとき、無駄に filteredTodos
が処理されることを防ぐことができます。
では、 CodeWhisperer に実装を依頼してみましょう。
まず、特にパフォーマンスについては指示を出さず、関数の概要だけプロンプトで渡してみました。
結果は、
のようになり、パフォーマンスは求めるような実装は返ってきませんでした。
今度は、パフォーマンスを意識した実装をするようプロンプトを追加します。
Avoid costly recalculations and implement them in a way that reduces unnecessary rendering
結構丁寧に指示したつもりですが、 useMemo
を使うまでには至りませんでした。
プロンプトを色々試した結果、想定のコードを返してもらうためには、useMemo
をimportすることをあらかじめ書いておくしかなさそうでした。。なんか本末転倒ですが。
結果
今回は1例しか試していませんが、パフォーマンスを考慮したレコメンドはあまりうまく機能していないように思えました。
おそらく、ヒントとなるコードをプロンプトに渡せばうまくいくのでしょう。
まとめ
今回は、Amazon CodeWhispererがどれだけやってくれるのか検証してみました。
使い勝手や使い方についての所感をまとめると以下です。
使う状況(どういうふうに使うのが良いか)について
ノーヒントだとコーディングルールにバラツキがあったりや要件を満たさなかったりであまり求める精度のコードは出てこない印象を受けました。そのため、プロンプトやコードなどコンテキストを充実させて「育てる」ように使うのが良いと思いました。最初から書いてもらうというよりは、ベースとなるコードがあった上で、追加開発の中で使うのがベターでしょう。
また、コンテキストとして読み込ませたいコードを、全てVS Code上でアクティブにするのは手間になるかと感じました。ただ、Professional Tierではカスタマイズ機能(現状プレビュー版)があり、プライベートコードリポジトリをコンテキストとして渡してコードに関する特定のレコメンデーションを生成できるようになっているらしいので、少なくないコンテキストコードを読み込ませるには、この機能を使った方が良さそうです。
使い勝手について
リファクタリングができればかなり便利だと思いました。一回レコメンドされたコードを改善したい場合は、プロンプトを変更してそのコードを全部消して再度レコメンドを実行する必要があり、若干手間になります。
リファクタリングをしたいときは、Amazon Q と連携させることで、既存の特定のコードをリファクタできるようなので、こちらの使い勝手も別途試してみたいと思います。
また、プロンプトがそのままコードの説明になり、コード全体でドキュメントの役割も持つのがとても嬉しいと思いました。
まとめると、
1から作ってもらうというよりは、既存の機能の拡張やコンポーネントの横展開みたいなユースケースであれば、威力を発揮しやすいのではないかと思います。
現段階では、コードの良し悪しの判断ができるエンジニアが適宜チェックしつつ取り入れていくのが良さそうです。
また、Professional Tier でコンテキストコードを学習させることで、質を担保してコードを生成させることができるため、業務の効率化に繋がる可能性は十分にありそうに思いました。
これからも色々試させていただき、近い将来業務に取り入れてみたいなと思っています。