こんにちは。
ファインディ株式会社 で Tech Lead をやらせてもらってる戸田です。
現在のソフトウェア開発の世界は、生成AIの登場により大きな転換点を迎えています。
GitHub Copilotやチャットベースの開発支援ツールなど、生成AIを活用した開発支援ツールが次々と登場し、開発者の日常的なワークフローに組み込まれつつあります。
そのような状況の中で、「プロンプトの書き方がわからない」「プロンプトが思った動作をしてくれない」といった声をよく耳にします。
そこで今回は、生成AIに対するプロンプトの書き方のコツを紹介します。プロンプトの書き方のコツを知るだけで、生成AIの精度は驚くほど向上します。
それでは見ていきましょう!
長い文章よりも、簡潔な階層構造
長文のプロンプトよりも、マークダウン記法などを利用して作成した階層構造のプロンプトの方が、生成AIの理解度が高い傾向にあるようです。
例えば、次のプロンプトを実行するとします。
buttonを作成して返すReact Componentを作ってください。propsにはテキストと無効フラグとクリック時のイベントを受け取ることが出来ます。テキストは必須です。無効フラグは任意で `is` のprefixを付けてデフォルト値は無しです。クリック時のイベントは任意です。
このプロンプトの実行結果は次のようになりました。
import React from 'react'; type ButtonProps = { text: string; isDisabled?: boolean; onClick?: () => void; }; export const ButtonComponent: React.FC<ButtonProps> = ({ text, isDisabled = false, onClick }) => { return ( <button type="button" disabled={isDisabled} onClick={onClick}> {text} </button> ); };
プロンプトの意図が伝わっていない箇所が見られました。 isDisabled にデフォルト値が設定されてしまっています。
今度は、次のようにマークダウン記法の階層構造に書き直して実行してみます。
buttonを作成して返すReact Componentを作ってください。 - props - テキスト - 必須 - 無効フラグ - 任意 - `is` のprefixを付ける - デフォルト値は無し - クリック時のイベント - 任意
import React from 'react'; type ButtonProps = { text: string; isDisabled?: boolean; onClick?: () => void; }; export const ButtonComponent: React.FC<ButtonProps> = ({ text, isDisabled, onClick }) => { return ( <button type="button" disabled={isDisabled} onClick={onClick}> {text} </button> ); };
isDisabled にデフォルト値が設定されなくなりました。この結果から、階層構造で書かれたプロンプトの生成結果の精度が上がることを理解できたかと思います。
このようにプロンプトに長文が書かれていた場合、文章を分解してマークダウン記法などの段落構造などに書き変えることをオススメします。
記述の順序
プロンプトやサンプルコードを記述する順番によって、生成AIが出力する内容が変わるケースがあります。
例えば、次のプロンプトを実行するとします。
次のコードを参考にして、UserSetting用のRepositoryクラスを作ってください ```typescript export interface UserSetting extends Entity { userId: string; notificationTarget: 'email' | 'slack'; } ``` ```typescript export interface IRepository<T> { findById(id: string): Promise<T | null>; } export interface Entity { id: string; createdAt: Date; updatedAt: Date; } ``` ```typescript export interface User extends Entity { name: string; email: string; role: 'admin' | 'user'; } export class UserRepository implements IRepository<User> { private users: User[] = []; async findById(id: string): Promise<User | null> { return this.users.find(user => user.id === id) || null; } async findAll(): Promise<User[]> { return [...this.users]; } async findByEmail(email: string): Promise<User | null> { return this.users.find(user => user.email === email) || null; } async findByRole(role: 'admin' | 'user'): Promise<User[]> { return this.users.filter(user => user.role === role); } } ``` ```typescript export interface Organization extends Entity { name: string; contractStatus: 'active' | 'inactive' | 'pending'; } export class OrganizationRepository implements IRepository<Organization> { private organizations: Organization[] = []; async findById(id: string): Promise<Organization | null> { return this.organizations.find(org => org.id === id) || null; } async isContract?(org: Organization): Promise<boolean> { return org.contractStatus === 'active'; } } ```
実装内容が多い UserRepository を先に記述して、実装内容が少ない OrganizationRepository を後に記述しています。
このようなプロンプトの場合、先に書いた大きい方の内容が優先され、後に書いた小さい内容が無視されることがあります。
今回のケースだと、後述した OrganizationRepository の isContract? メソッドの例が無視される可能性があります。
実行結果は次のようになりました。
export class UserSettingRepository implements IRepository<UserSetting> { private userSettings: UserSetting[] = []; async findById(id: string): Promise<UserSetting | null> { return this.userSettings.find(setting => setting.id === id) || null; } async findAll(): Promise<UserSetting[]> { return [...this.userSettings]; } async findByUserId(userId: string): Promise<UserSetting | null> { return this.userSettings.find(setting => setting.userId === userId) || null; } async findByNotificationTarget(target: 'email' | 'slack'): Promise<UserSetting[]> { return this.userSettings.filter(setting => setting.notificationTarget === target); } }
生成されたコードは全て this.userSettings に対するもののみで、 UserSetting のデータに対しての関数が生成されていないことがわかります。
次に、実行したプロンプトの OrganizationRepository に対する記述と UserSettingRepository に対する記述を入れ替えて実行してみます。
実行結果は次のようになりました。
export class UserSettingRepository implements IRepository<UserSetting> { private userSettings: UserSetting[] = []; async findById(id: string): Promise<UserSetting | null> { return this.userSettings.find(setting => setting.id === id) || null; } async findAll(): Promise<UserSetting[]> { return [...this.userSettings]; } async findByUserId(userId: string): Promise<UserSetting | null> { return this.userSettings.find(setting => setting.userId === userId) || null; } async findByNotificationTarget(target: 'email' | 'slack'): Promise<UserSetting[]> { return this.userSettings.filter(setting => setting.notificationTarget === target); } async hasActiveNotification(setting: UserSetting): Promise<boolean> { return setting.notificationTarget !== null && setting.notificationTarget !== undefined; } }
UserSetting のデータを受け取ってチェックを行う hasActiveNotification が追加されていることがわかります。
この結果から、プロンプトの内容の記述順が生成結果に影響が出ることが理解できるかと思います。
対象と実行内容を絞る
生成AIからの結果の精度が低い場合、一度に多くの対象を選択しているケースがあります。
例えば、次のプロンプトを実行するとします。
次の変更を実行してください ## 変更対象 - 次の条件の全てに合致するファイル全て - `app/lib/**/*.tsx` に合致する - `app/lib/**/*_spec.tsx` に合致しない ## 変更内容 - `window.alert` を `toast.error` に変更する - `toast` は `@findy/ui` からimportする - propsで定義されているboolean型の項目の型定義を `boolean` から `boolean | undefined` に変更する
対象のファイル全てに対して複数の変更内容を実行するプロンプトになっています。
階層構造になっていますし、対象と内容を具体的に指定しており、特に問題ないように見えます。
しかし、このプロンプトを実行した場合、場合によっては実行結果の精度が落ちる可能性があります。
変更対象となっている app/lib 配下には多くのファイルが含まれている可能性があります。生成AIに対して一度の多くの対象を指定すると、対象を絞り込むことに対してリソースを使ってしまうため結果の精度が落ちてしまう傾向にあります。
このようなケースの場合、合致するフォルダの指定を app/lib/hoge/ 以下を見るようにプロンプトを変更して複数回の実行に分けると良いでしょう。
次の変更を実行してください ## 変更対象 - 次の条件の全てに合致するファイル全て - `app/lib/hoge/**/*.tsx` に合致する - `app/lib/hoge/**/*_spec.tsx` に合致しない ## 変更内容 - `window.alert` を `toast.error` に変更する - `toast` は `@findy/ui` からimportする - propsで定義されているboolean型の項目の型定義を `boolean` から `boolean | undefined` に変更する
変更対象のフォルダを指定して、一度に対象とするファイルを限定的にしました。このプロンプトを実行後、次は別のフォルダに対して同様のプロンプトを実行することで、生成AIの結果の精度が向上する可能性があります。
これで解決かと思いきや、更に改善の余地が見つかりました。変更内容が複数ある場合、プロンプトを分割して実行すると更に精度が上がります。今回のケースだと処理自体の変更と型定義の変更で異なる変更の指示が含まれています。
こういったケースの場合、プロンプトと実行を分割すると良いです。
次の変更を実行してください ## 変更対象 - 次の条件の全てに合致するファイル全て - `app/lib/hoge/**/*.tsx` に合致する - `app/lib/hoge/**/*_spec.tsx` に合致しない ## 変更内容 - `window.alert` を `toast.error` に変更する - `toast` は `@findy/ui` からimportする
次の変更を実行してください ## 変更対象 - 次の条件の全てに合致するファイル全て - `app/lib/hoge/**/*.tsx` に合致する - `app/lib/hoge/**/*_spec.tsx` に合致しない ## 変更内容 - propsで定義されているboolean型の項目の型定義を `boolean` から `boolean | undefined` に変更する
今回のケースのように複数の対象、内容の全てを一度に指定するのではなく、対象を絞って単一の変更内容を指示してプロンプトの実行そのものを分けることで、生成AIの結果の精度が向上する傾向にあります。
まとめ
いかがでしたでしょうか?
ちょっとした工夫や気遣いでプロンプトの精度が上がることがわかったかと思います。今後もトライアンドエラーを繰り返しながら、生成AIを活用するためのプロンプトの書き方を磨いていきましょう。
カスタムインストラクションでのプロンプトのコツも紹介しているので、是非こちらも参考にしてみてください。
現在、ファインディでは一緒に働くメンバーを募集中です。
興味がある方はこちらから ↓ herp.careers