はじめに
最近の開発現場ではAI活用が当たり前になってきました。
エラーが出てもAIが一瞬で直してくれるし、書き方がわからなくてもAIが手取り足取りサポートしてくれます。
先日、私はOJTトレーナーとして、配属された新人にプログラミングの基礎を教えるミッションを任されました。当初は
- AIが手取り足取り教えてくれるから、先輩の負担が減って教育が楽になるはず!
と考えていましたが、現実は…
- 新人は「考える」より先にまず「生成」してしまう
- 複雑なコードも一瞬で書ける(生成できる)けど、なぜ動くのかよくわかってない
- 新人の、AIへの依存度だけが高まっていく…
新人がAIでコーディングしている様子は、いわば 「他人の筋トレを見て、自分の体を鍛えようとしている」 のに等しい状態。
この記事では、その試行錯誤の結果にたどり着いた、Cursorを使いながらプログラミング基礎を身につける方法を紹介します。
ふたりで試行錯誤
AIを使いこなしつつ自分も成長するために、具体的にどうすればいいのか。
私たちがたどった試行錯誤のプロセスです。
1. AI禁止令
「AIを禁止して、基礎から手書きさせる」
結論: 却下
一瞬頭をよぎりましたが、即却下しました。もはやAIを使わない開発現場はありえません。「AIを使う」という前提は崩さずに、どう成長させるかを考える必要があります。
2. Cursorの「Rules」で、AIの挙動をガチガチに縛る
次に試したのが、CursorのRulesを使うことでした。
Rulesを使用すると、AIに常に守らせたい「方針」や「前提知識」を定義することができ、自動的にこのルールを守った回答をさせることができます。
ここでは、次のようなルールでAIの挙動を教育的に制限しました。
- あなたは教育熱心なメンター
- 基礎を解説しろ
- コードの構成や設計意図を解説しろ
結論:失敗
ルール通りには動くものの、Cursor本来の「いい感じのサジェスト機能」が死んでしまいました。
元の良い指摘の代わりに、無駄な基礎解説で埋め尽くされ…。
「コレジャナイ感」が強く、開発効率も落ちてしまったため不採用としました。
3. 教師役の「Commands」(ヒントモード)
今度は、CursorのCommandsを試しました。
Commandsは、繰り返し行う定型作業を効率よく行うテンプレートとして使用します。ユーザーが手動で実行します。Commandsであれば、セッションの途中からでも適用できるという柔軟性があります。
ここでは、次のようなコマンドでAIに教師役として振る舞うよう指示しました。
- プログラムを書くのはあなたじゃない、プログラマー自身だ
- 答えは教えず、考え方を教えろ
AIは、このコマンドを受け取って以降、指示に沿った回答に徹するようになります。もし「コードを修正してください」というプロンプトが与えられたとしても、コードは書かずに「何がわからないのか」を聞いてきます。
結論:微妙
じらされている感がすごい。
AIは「正解(コード)」を示さなくなるので、ラリーが増え、むしろ「次はどうすればいいですか?」とAIの指示を待つ「指示待ち」姿勢を助長しかねない結果に。
4. ご意見番の「Commands」(ガイドモード)
最終的にたどり着いたのが、 「回答は普通にさせておいて、あとから『今の解説して』と聞けるコマンドを用意する」 という方法でした。
普段の開発ではAIの爆速コーディングを享受し、新人が「これどうなってるんだ?」と思ったときや、トレーナーが「ここ理解してる?」と聞いたときに、サッと解説を引き出せるようにしました。
このモードについて、これから詳しく説明していきます。
実践例
実際のAI回答例を紹介するためにサンプルコードを用意しました。
実業務のコードを引用するわけにはいかないので、このコードは説明のために用意した架空のコードです。
JavaScriptの、forEachと非同期処理を例に話を進めていきます。
新人が書いたコード (Bad)
新人は、ユーザーIDのリストから順番にデータを取得したいのですが、結果が空のまま処理が進んでしまいます。
const userIds = [1, 2, 3];
const results = [];
// 非同期でデータを取ってきたい
userIds.forEach(async (id) => {
const data = await fetchUserData(id);
results.push(data);
});
console.log(results); // 結果:[] (!? データが入っていない)
JavaScriptに慣れている方なら「forEach は await を待ってくれないから当然だ」と気づきます。しかし、基礎知識がまだ浅い新人は「エラーも出ないのに、なぜデータが空になるのか」が分かりません。
AIによる修正 (Good code / Bad learning)
ここでAIに「修正して」と頼めば、きっと一瞬で直してくれるはずです。
// AIが提案した修正コード
const promises = userIds.map(id => fetchUserData(id));
const results = await Promise.all(promises);
console.log(results); // 成功!
コードは動くようになりますが、ここで問題なのは 新人が「なぜforEachではダメだったのか」を理解しないまま進んでしまう ことです。これでは、また別の場所で同じミスを繰り返します。
プロンプトとレスポンスの例
RulesやCommandsをなにも使用しない場合
普通に「修正してください」と依頼した場合。
Cursorらしい、ちょうど良い回答がもらえます。
原因は、forEach内のasyncが待機されないため、console.log(results)の時点でresultsが空になることです。Promise.allとmapに置き換えて修正します。
...
修正完了。変更点は以下の通りです。
問題点:
forEachは非同期処理を待たないため、console.log(results)の時点でresultsが空でした。
修正内容:
- Promise.allとmapを使用して、すべての非同期処理の完了を待つように変更
- 即時実行関数(IIFE)でasync/awaitをトップレベルで使用可能に
これで、すべてのfetchUserDataの完了後にresultsが出力されます。
修正後のコード:
// 省略
いくらか理由も添えてくれていて、普通ならばこれで十分です。
しかし、新人OJTとしては、技術の解説をさせたいので、次のようなCommandsを用意しました。
ガイドモードの適用例
Commands: /guide
# 新人OJTトレーナー・アドバイス
## 概要
提供されたテキスト(ソースコードや技術的な回答)に含まれる技術要素を抽出し、新人エンジニア(初学者)向けに教育的な解説を行ってください。
## チェックリスト
- 言語仕様の解説(Java, HTML, CSS, JavaScript, TypeScript)
- フレームワーク機能の解説(Angular, Spring Framework, QueryDSL)
- (可能な場合) 学習すべき単元のリストを提示
- (可能な場合) **公式ドキュメントのリンク**を提示
指示内容は、自分のプロジェクトや新人の元々の力量などを勘案してちょうど良い感じになるように調整してください。
これを先ほどのレスポンスの後に与えると、次のようなガイドがもらえます。
技術解説:非同期処理とPromise.all
今回のコードで扱った技術要素
1. 非同期処理の問題点:
forEachとasync/await問題のあったコード:
userIds.forEach(async (id) => { const data = await fetchUserData(id); results.push(data); }); console.log(results); // 空配列が出力されるなぜ空になるのか:
forEachはコールバックの完了を待たないasyncコールバックはPromiseを返すが、forEachはそれを無視する- そのため
console.logが先に実行され、resultsは空のまま学習ポイント:
forEachは非同期処理を待たないasync/awaitを使っても、呼び出し側が待機しないと意味がない2. 解決策:
Promise.all()とArray.map()修正後のコード:
const promises = userIds.map(id => fetchUserData(id)); const results = await Promise.all(promises);
Promise.all()の動作:
- 複数の
Promiseを並列実行- すべてが解決(または1つでも拒否)するまで待機
- 解決したら結果の配列を返す
Array.map()の役割:
- 各要素を
Promiseに変換forEachと違い、新しい配列を返す並列実行のメリット:
- 順次実行より高速
- すべての処理が完了するまで待機できる
3. 即時実行関数(IIFE: Immediately Invoked Function Expression)
(async () => { // async/awaitを使える })();なぜ必要か:
- トップレベルで
awaitは使えない(モジュールを除く)async関数内でawaitを使うため、IIFEで囲む代替案:
- ES Modules(
type: "module")ならトップレベルawaitが使える
...
長くなるので引用はここまでにしておきます。このあと、
- 学習すべき単元
- 公式ドキュメント
- 実践的な理解を深めるために(ちいさなサンプルコード)
などが続きます。学習コンテンツがてんこ盛りです。
この運用により、AIのスピード感を殺さずに、知りたい時だけ効率よく学習するサイクルが回るようになりはじめました。
おまけ:ヒントモードの適用例
「ふたりで試行錯誤」の章で紹介した失敗例:ヒントモードのCommandsも紹介しておきます。
# ヒントモード
## 目的
ユーザー(新人エンジニア)の**自律的な問題解決能力**を育成すること。
これ以降、直接的な正解(修正された完全なコード)を与えず、**ヒント、問いかけ、概念の解説**を通じて、ユーザー自身が答えに辿り着けるよう誘導してください。
## ヒントモードのルール
- これ以降の指示では、エージェントは新人OJTとしての回答を行う
- ヒントモードに移行したときは `ヒントモードに移行しました` とだけ回答する
- 自ら考えて試行することを促す
- 修正コードの提示を要求された場合は、何が分からないのかを問い直してから、ヒントを提示する
- **コードファイルの直接修正は行わない**
- ヒント内に埋め込む**コードブロックは、10行以下**とする。それ以上のコードになる場合は、何が分からないのかを確認する
- 不明点があれば、まず何が分からないかを確認してからヒントを提示する
## サポート内容
- エラーメッセージの解読と原因の推測
- コードの理解を深めるためのヒント
- デバッグ手法(ログ出力箇所の提案、ブレークポイントの位置など)
- 実装方針の考え方
- 技術的な概念の説明
このモードでは、「コードを修正して」という依頼に対して以下のようなレスポンスを返します。
resultsが空になる原因は、forEach内の非同期処理を待機していないためです。ヒント
非同期処理の待機
forEachは非同期処理を待ちません。forEach内のasyncコールバックは並行実行され、forEach自体は即座に終了します。確認ポイント
console.log(results)の実行タイミングは、forEachの完了前か後か?- 非同期処理を順次待つには、どの方法があるか?
解決の方向性
Promise.all()を使う方法for...ofループを使う方法map()とPromise.all()を組み合わせる方法どの方法で進めますか?また、
Promise.all()やfor...ofの使い方は分かりますか?
プロンプトの調整が不十分だったので、コードは絶対返さないうえにアドバイスも半端です。それとも、スパルタなトレーナーはこれくらいのほうが好みでしょうか。
また、ここではCommandsとして紹介しましたが、Rulesでもこのプロンプトが使えると思います。選択的にCommands実行した場合だけでなく、常にこういう回答だけをさせることもできます。
これから
OJTあるあるの「それ前も言ったよね?」問題を解決するため、今後はまず 「教えたことリスト」 の自動蓄積を構想しています。
AI自身に「今日教えたこと」をリストに追記させ、自分のプロジェクト専用のナレッジとして育てていきます。
そして、AIにそのリストを参照させ、「すでに教えたこと」は省略して 「まだ教えていない未知の領域」だけ を詳しく説明させます。
そんな、新人と一緒に成長するAIメンター環境を作っていきたいと考えています。
おわりに
AIは、手ぶらの新人に持たせるには強力すぎる武器です。
AIに使われるのではなく、AIをパートナーとして使いこなすためには、これからも様々な工夫をしていかなくてはならないと思っています。
うまくいった施策や良い案があったらぜひ聞かせてください。