こんにちは。
Findy で Tech Lead をやらせてもらってる戸田です。
現在のソフトウェア開発の世界は、生成AIの登場により大きな転換点を迎えています。
GitHub Copilotやチャットベースの開発支援ツールなど、生成AIを活用した開発支援ツールが次々と登場し、開発者の日常的なワークフローに組み込まれつつあります。
しかし、これらのツールを導入すれば即座に開発生産性が向上する、というわけではありません。
生成AIを効果的に活用し、真の意味で開発生産性を向上させるためには、適切な準備と理解が不可欠です。
今回は生成AIと既存コードの関係性を掘り下げ、開発生産性を最大化するための具体的な準備について詳しく解説します。
単なるツールとしての導入にとどまらず、組織全体で生成AIと協調して働くための基盤づくりに焦点を当てています。
それでは見ていきましょう!
生成AI活用のための準備
生成AIの効果は既存のコードベースの品質と密接に関連していると言われています。(1、2)
生成AIは与えられたコンテキストを基に学習して提案を行います。既存のコードベースが高品質である場合、生成AIはより精度が高い提案をすることができるはずです。
そのため、生成AIを効果的に活用し、真の意味で開発生産性を向上させるためには、適切な準備が不可欠です。
既存コードの最適化
不要なコードの削除
不要なコードが残っていると、生成AIはそれらのコードも学習してしまい、不適切な提案をする可能性があります。
例として次のような実装コードがあるとします。
export const getNotEmptyStrList = (data: string[]): string[] => {
const result = [];
for (let i = 0; i < data.length; i++) {
if (data[i].length > 0) {
result.push(data[i].trim());
}
}
return result;
};
export const filterStrList = (data: string[]): string[] => {
return data.filter((item) => item.length > 0).map((item) => item.trim());
};
気付いた読者の方もいると思いますが、この例は関数名と実装コードが違うだけで、やっていることは同じです。 getNotEmptyStrList
の方は冗長なコードとなっています。
このコードの状態で次のようなプロンプトを実行したとします。
このコードを参考にして、数値配列から0より大きい値だけを抽出して、その値を2倍にする関数を作成してください。
このプロンプトを複数回実行すると、生成されるコードにバラツキが出てしまいました。
export const filterAndDoubleNumbers = (data: number[]): number[] => {
const result = [];
for (let i = 0; i < data.length; i++) {
if (data[i] > 0) {
result.push(data[i] * 2);
}
}
return result;
};
export const filterAndDoubleNumList = (data: number[]): number[] => {
return data.filter((item) => item > 0).map((item) => item * 2);
};
このように、不要なコードが残っていると生成AIからの提案の精度が低下してしまいます。
このようなケースの場合、不要なコードを削除して既存コードを最適化することで、生成AIの理解度を向上させることができます。
実装コードから不要なコードを削除して次のように変更します。
export const filterStrList = (data: string[]): string[] => {
return data.filter((item) => item.length > 0).map((item) => item.trim());
};
filterとmapを使ったモダンなコードのみに変更しました。この状態で同じプロンプトを複数回実行すると、生成されるコードが一貫性を持つようになります。
export const filterAndDoubleNumList = (data: number[]): number[] => {
return data.filter((item) => item > 0).map((item) => item * 2);
};
統一されたコーディング規約
Google の Style Guides のようなスタイルガイドを採用し、統一されたコーディング規約に従ったコードベースは、生成AIの理解度を向上させます。
例えば、命名規則が一貫していれば、生成AIはその規則を学習し、同様の命名パターンを提案できます。
例として次のような実装コードがあるとします。
import { useState } from 'react';
type SelectOption = {
label: string;
value: number;
};
export const useTestHooks = () => {
const [select_option, setSelectOption] = useState<SelectOption>();
const [submitValue, setSubmitValue] = useState<number>();
const handleSelectOption = (option: SelectOption) => {
setSelectOption(option);
};
const HandleClickSubmitButton = () => {
if (!select_option) return;
setSubmitValue(select_option.value);
};
return {
handleSelectOption,
HandleClickSubmitButton,
} as const;
};
各種名称にキャメルケースとスネークケースなどが混在してしまっています。
このコードの状態で次のようなプロンプトを実行するとします。
select_option.valueを3倍にしてsubmitValueにsetするハンドラー関数を追加してください
プロンプトを複数回実行すると、実行されるたびに生成されるコードにバラツキが出てしまいました。
const HandleClickTripleSubmitButton = () => {
if (!select_option) return;
setSubmitValue(select_option.value * 3);
};
const handleClickTripleSubmitButton = () => {
if (!select_option) return;
setSubmitValue(select_option.value * 3);
};
そこで、命名規約をキャメルケースに統一するように変更します。
import { useState } from 'react';
type SelectOption = {
label: string;
value: number;
};
export const useTestHooks = () => {
const [selectOption, setSelectOption] = useState<SelectOption>();
const [submitValue, setSubmitValue] = useState<number>();
const handleSelectOption = (option: SelectOption) => {
setSelectOption(option);
};
const handleClickSubmitButton = () => {
if (!selectOption) return;
setSubmitValue(selectOption.value);
};
return {
handleSelectOption,
handleClickSubmitButton,
} as const;
};
この状態で同じプロンプトを複数回実行すると、生成されるコードが一貫性を持つようになります。
const handleClickTripleSubmitButton = () => {
if (!selectOption) return;
setSubmitValue(selectOption.value * 3);
};
単純な例ですが、既存コードに統一性がない場合、生成AIがどの情報を正とするかが曖昧になり、提案の精度が低下してしまう一例でした。
このように、コーディングスタイルの一貫性は生成AIの提案の精度や一貫性に大きく影響します。
コードの設計の一意性
アーキテクチャやデザインパターンが一貫している場合、生成AIはそのパターンを学習し、より適切な提案をすることができます。
例えば、次のようなコードがあるとします。
export interface IRepository<T> {
findAll(): Promise<T[]>;
findById(id: string): Promise<T | null>;
}
export interface Entity {
id: string;
createdAt: Date;
updatedAt: Date;
}
export interface User extends Entity {
name: string;
email: string;
role: 'admin' | 'user';
}
export class UserRepository implements IRepository<User> {
private users: User[] = [];
async findAll(): Promise<User[]> {
return [...this.users];
}
async findById(id: string): Promise<User | null> {
return this.users.find(user => user.id === id) || null;
}
}
interfaceとclassを使ってリポジトリパターンを実装しています。このコードが存在する状態で、次のようなコードを追加します。
export interface Order extends Entity {
userId: string;
products: Array<{
productId: string;
quantity: number;
}>;
totalAmount: number;
status: 'pending' | 'completed' | 'cancelled';
}
そして、追加したinterface用のRepositoryクラスを生成してもらうためにプロンプトを実行します。
Order用のRepositoryクラスを生成してください
すると、生成されたコードは次のようになります。
export class OrderRepository implements IRepository<Order> {
private orders: Order[] = [];
async findAll(): Promise<Order[]> {
return [...this.orders];
}
async findById(id: string): Promise<Order | null> {
return this.orders.find(order => order.id === id) || null;
}
}
既存コードの UserRepository
を参考にして、 OrderRepository
が生成されたことがわかると思います。
このようにデザインパターンや型定義などが一貫していると、生成AIはそのパターンを学習し、より精度の高いコード生成できます。
ドキュメンテーションの充実
生成AIが読み込む内容は実装コードだけでなく、READMEなどのドキュメントも含まれます。そのため、ドキュメントの充実は生成AIの学習に少なからず影響します。
生成AI時代のドキュメンテーション能力は、更に求められるようになってくるでしょう。
docコメントやAPIドキュメント
関数やクラスに記載されるdocコメントは、生成AIがコードの意図を理解する上で重要な情報源となります。
docコメントは実装コードを読み込んで生成AIが自動生成することも可能ですが、実装コードの内容によっては必ずしも十分な情報を提供できない場合があります。このようなケースの場合、まずdocコメントを生成AIに自動生成してもらい、意図や仕様を補足することで、生成AIの理解度を向上させることができます。
また、REST APIの仕様を記述したSwagger/OpenAPIドキュメントやGraphQLのスキーマ定義などは、生成AIがAPIの仕様やクエリを理解する上で重要な情報源となります。
生成AIがAPIの仕様を理解することでモックデータの自動生成などが容易になり、開発効率を向上させることができます。
カスタムインストラクション
生成AI向けのカスタムインストラクションを調整するのも良いでしょう。
カスタムインストラクションは生成AIに対して、特定のコンテキストやルールを教えるための手段です。これを調整して育てることで、生成されるコードや提案内容の精度を向上させることができます。
プロジェクトやリポジトリのコーディング規約やドメイン知識などをカスタムインストラクションに記載しておくことで、それらの内容を生成AIに読み込ませ、提案内容に反映できます。
カスタムインストラクションについては別の記事でも紹介していますので、是非参考にしてみてください。
tech.findy.co.jp
tech.findy.co.jp
プロンプトを記録
実際に利用したプロンプトを記録して残しておくことも重要です。コード内のコメントや各種ドキュメント、Pull requestなどに記載しておくとよいでしょう。
実際に求めている変更内容とプロンプトの内容が一致しているのかをレビューする必要があるからです。
また、そのコードがどんなプロンプトを利用して生成されたのかが残っていなかった場合、機能追加や修正時にまた一からプロンプトを作らないといけなくなってしまいます。
実装コードを変更したいならプロンプトを変更する。といったパラダイムシフトが来るのはそう遠くない未来なのかもしれません。
最近だと自分は、プロンプトのテンプレートもドキュメントに残すようにしています。状況と変更内容が近いのであれば、プロンプトをコピーして調整して実行するだけで、同じような作業を横展開することが簡単になるからです。
継続的な改善を可能にする開発文化
ここまで説明した内容を実践して維持するためには、継続的な改善や技術的負債の解消を可能にする開発文化が不可欠です。
なぜならば、ドキュメントの整備や既存コードの最適化などは一度だけで終わるものではなく、継続的な取り組みが必要だからです。
テストコードとCI/CDの整備
このような開発文化を作る上でテストコードの整備は特に重要な要素と言えます。一定以上のテストコードが存在することで、継続的な変更に対する心理的安全性を確保できます。
またCI/CDを整備し、自動化されたビルド、テスト、デプロイを行うことで、開発プロセスを効率化し、品質を維持できます。
テストコードとCI/CDの整備については別の記事でも紹介していますので、是非参考にしてみてください。
tech.findy.co.jp
Pull requestの粒度
適切な粒度のPull requestを維持することも重要です。Pull requestの粒度が適切であれば、生成AIに対して「これを実現させるためにはこの修正が必要」とピンポイントで学習させることができます。
tech.findy.co.jp
Pull requestの粒度はプロンプトの精度にも繋がってきます。
生成AIで実行するプロンプトは可能な限り単一責務の方が実行結果の精度が高い傾向にあります。1行のプロンプトで全ての内容を実行するのではなく、プロンプトを分解して階層構造にしたり、対話形式で実行することで、より精度の高い実行結果を実現できます。(*3)
日頃からタスク分解やPull requestの粒度を意識した開発文化にすることで、プロンプトを作成する際にも応用できるのです。
tech.findy.co.jp
これらの準備ステップを着実に実行することで、生成AIをより効果的に活用するための基盤を整えることができます。
まとめ
いかがでしたでしょうか?
生成AI活用のための準備ステップは、単なる事前作業ではなく、生成AIの効果を最大化するための基盤づくりです。
生成AIの活用は目的ではなく、より良いソフトウェアをより効率的に開発するための手段です。
堅固な開発基盤と文化こそが、生成AIの真の力を引き出す鍵となります。
小さな一歩から、大きな変革が始まります。技術の進化に伴い、私たち開発者の役割も進化していきます。
この変化を恐れるのではなく、積極的に受け入れ、活用していくことで、私たちはソフトウェア開発の新たな時代を切り開いていこうと考えています。その未来に向けて、今日から準備を始めましょう。
現在、ファインディでは一緒に働くメンバーを募集中です。
興味がある方はこちらから ↓
herp.careers
参考文献
- Best practices for using GitHub Copilot in VS Code #Provide context to Copilot
Copilot works best when it has sufficient context to know what you're doing and what you want help with.
- Best practices for using GitHub Copilot in VS Code #Be consistent and keep the quality bar high
Copilot is going to latch on to your code to generate suggestions that follow the existing pattern, so the adage "garbage in, garbage out" applies.
- Best practices for using GitHub Copilot in VS Code #Be specific and keep it simple
When you ask Copilot to do something, be specific in your ask and break down a large task into separate, smaller tasks.