TL;DR — AI生成コードには「命名の一貫性なし」「過剰抽象化」「コメント意図不明」「コンテキスト依存変数名」「重複実装」の5類型の可読性問題がある。各類型に対してレビューチェックリスト・命名規約・ESLintルールを設計し、チームで合意することで属人化したレビューを仕組みに変換できる。
はじめに:AI生成コードはなぜ「読みづらい」のか
GitHub CopilotやClaude、ChatGPTを使いこなすチームが増えた一方で、「AIが書いたコードはレビューしにくい」という声が現場のテックリード・シニアエンジニアから増えています。
その違和感の正体は、一般的なコード品質問題ではなく、AI固有の問題です。人間のエンジニアが書くコードの可読性問題とは根本的な原因が異なります。
LLMがコードを生成するとき、モデルは与えられたプロンプトの文脈だけを参照します。プロジェクト全体の命名規約、過去の設計意図、チームの暗黙知——これらはモデルには見えません。結果として、技術的には動くが「このプロジェクトの文脈では読みづらい」コードが生成されます。
この記事では:
- AI生成コード固有の可読性問題を5つの類型に整理する
- 各類型に対応するレビューチェックリスト・命名規約を設計する
- ESLint/Biomeのカスタムルールで自動検出する方法を示す
を一気通貫で解説します。チームで合意できるレビュー基準の骨格を持ち帰ってください。
AI生成コードの可読性問題:5つの類型
類型1:命名の一貫性なし
AIは文脈ごとに「最もそれらしい」命名を選びます。同一概念でも、プロンプトや会話コンテキストが変わると別の名前が生成されます。
悪い例
// ファイルAでAIが生成
async function fetchUserData(userId: string) {
const userData = await api.get(`/users/${userId}`);
return userData;
}
// ファイルBで同じ概念をAIが生成(別のプロンプト)
async function getUserInfo(id: string) {
const userInfo = await api.get(`/users/${id}`);
return userInfo;
}
// ファイルCでさらに別の表現
async function loadMemberProfile(memberId: string) {
const profile = await api.get(`/users/${memberId}`);
return profile;
}
良い例
// naming-convention.md で "ユーザー取得は fetchUser + データ型" と合意
async function fetchUser(userId: UserId): Promise<User> {
return api.get(`/users/${userId}`);
}
// すべてのファイルで同一パターンを使う
async function fetchUserList(filter: UserFilter): Promise<User[]> {
return api.get('/users', { params: filter });
}
命名の一貫性は、コードを読んだときに「この関数は何をするか」が名前だけで推測できる状態を指します。AIを使うチームでは、命名規約をドキュメント化し、プロンプトに含めることが必須です。
類型2:過剰な抽象化
AIは「汎用的で再利用可能なコード」を好む傾向があります。一箇所でしか使わない処理でも、Factory・Strategy・Observerパターンを持ち出すケースが多発します。
悪い例
// ユーザーの表示名を取得するだけのロジックにAIがパターンを適用
interface UserDisplayStrategy {
getDisplayName(user: User): string;
}
class FullNameStrategy implements UserDisplayStrategy {
getDisplayName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
}
class UsernameStrategy implements UserDisplayStrategy {
getDisplayName(user: User): string {
return user.username;
}
}
class UserDisplayNameFactory {
static create(type: 'fullName' | 'username'): UserDisplayStrategy {
return type === 'fullName' ? new FullNameStrategy() : new UsernameStrategy();
}
}
良い例
// YAGNI: 今必要なのはフルネーム表示だけ
function getUserDisplayName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
レビューシグナル: 抽象クラス・インターフェースの実装が1つしかない場合は過剰抽象化の疑いが高い。
類型3:コメントの意図不明
AIはコメントを「何をしているか(what)」ベースで書きます。コードを読めばわかる情報しかないコメントが大量に生成される一方、「なぜこうするのか(why)」は書かれません。
悪い例
// ユーザーを取得する
const user = await fetchUser(userId);
// ユーザーが存在するかチェックする
if (!user) {
// エラーをスローする
throw new Error('User not found');
}
// ユーザーのメールアドレスを返す
return user.email;
良い例
const user = await fetchUser(userId);
if (!user) {
// 削除済みユーザーへのアクセスはこのレイヤーで止める。
// 上位でキャッチしてHTTP 404に変換する(error-handler.ts参照)
throw new UserNotFoundError(userId);
}
return user.email;
コメントが価値を持つのは「コードを読んでもわからない意図・制約・背景」を書いた場合だけです。AIが生成したコメントは原則として削除し、必要なwhyコメントを人間が追記するレビュープロセスを設けるのが現実的です。
類型4:コンテキスト依存の変数名
AIは会話の直近コンテキストで「意味が通じる」短い変数名を使います。会話が終わったあとでコードを読むと意味が失われます。
悪い例
// プロンプト: "ユーザーリストから条件に合うものを抽出して"
const result = users.filter(u => u.active && u.role === 'admin');
const data = result.map(u => ({ id: u.id, name: u.name }));
const final = data.sort((a, b) => a.name.localeCompare(b.name));
return final;
良い例
const activeAdmins = users.filter(
(user) => user.active && user.role === 'admin'
);
const adminSummaries = activeAdmins.map((user) => ({
id: user.id,
name: user.name,
}));
return adminSummaries.sort((a, b) => a.name.localeCompare(b.name));
result・data・finalは処理ステップを示しているだけで意味がありません。変数名には「何が入っているか」を常に明示します。
類型5:重複実装
AIはコンテキストウィンドウの範囲外の既存実装を参照できません。同一ロジックが複数ファイルに独立して生成され、時間差でバグが乖離します。
悪い例
// auth/utils.ts でAIが生成
function formatUserName(firstName: string, lastName: string): string {
return `${firstName} ${lastName}`.trim();
}
// profile/helpers.ts で別のタイミングにAIが生成
function buildFullName(first: string, last: string): string {
return [first, last].filter(Boolean).join(' ');
}
// notification/formatter.ts でさらに生成
const getDisplayName = (user: { firstName: string; lastName: string }) =>
`${user.firstName} ${user.lastName}`;
良い例
// shared/user-utils.ts に単一実装
export function formatUserDisplayName(
firstName: string,
lastName: string
): string {
return [firstName, lastName].filter(Boolean).join(' ');
}
チームで合意するレビュー基準テンプレート
以下のチェックリストをコードレビューのプルリクエストテンプレートに組み込んでください。
## AI生成コードレビューチェックリスト
### 命名の一貫性
- [ ] 関数名が命名規約(fetch/get/create/update/delete)に従っている
- [ ] 変数名がコンテキストから意味が読み取れる(result/data/tempは要確認)
- [ ] 同一概念の名前が既存コードと統一されている
### 抽象化の適切さ
- [ ] 抽象クラス・インターフェースの実装が2つ以上ある(1つなら要根拠)
- [ ] パターン(Factory/Strategy等)の適用に2つ以上のユースケースが想定される
- [ ] シンプルな処理に対して過剰なラッパーがない
### コメントの質
- [ ] コメントが「なぜ(why)」を説明している
- [ ] コードを読めばわかることだけのコメントが削除されている
- [ ] TODO/FIXMEがある場合、issueトラッカーへの参照がある
### 変数・定数の明示性
- [ ] マジックナンバーが名前付き定数に置き換えられている
- [ ] ループ変数・一時変数の名前が処理内容を表している
### 重複の確認
- [ ] 同一ロジックが既存コードに存在しないことをGrepで確認した
- [ ] 新規ユーティリティ関数は共有モジュールに配置されている
命名規約の設計:AIプロンプトに含めるドキュメント
命名規約はただ定めるだけでなく、AIへのプロンプトに含められる形式にすることが重要です。以下のフォーマットをチームのリポジトリに docs/naming-convention.md として置き、AIツールのコンテキストとして渡します。
# 命名規約
## 動詞プレフィックスの使い分け
- `fetch*`: APIリクエストを伴う非同期取得 → fetchUser, fetchOrderList
- `get*`: メモリ内データの同期取得 → getUserById, getActiveItems
- `create*`: リソースの新規作成 → createUser, createOrder
- `update*`: 既存リソースの更新 → updateUserProfile, updateOrderStatus
- `delete*`: 論理削除・物理削除 → deleteUser, deleteOrder
- `is/has/can`: boolean返却 → isActive, hasPermission, canEdit
## 型・インターフェース命名
- エンティティ型: 名詞のみ(User, Order, Product)
- DTOは `Dto` サフィックス: UserDto, CreateOrderDto
- レスポンス型は `Response` サフィックス: FetchUserResponse
## 定数
- SCREAMING_SNAKE_CASE: MAX_RETRY_COUNT, DEFAULT_PAGE_SIZE
## ファイル命名
- コンポーネント: PascalCase(UserProfile.tsx)
- ユーティリティ: kebab-case(user-utils.ts)
- 定数ファイル: kebab-case(api-constants.ts)
ESLintカスタムルールによる自動検出
手動レビューで類型1・4・5を検出するのは限界があります。ESLintのカスタムルールで機械的に検出します。
インストール
pnpm add -D @typescript-eslint/eslint-plugin eslint-plugin-unicorn
設定例(eslint.config.js)
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import unicorn from 'eslint-plugin-unicorn';
export default [
{
languageOptions: {
parser: tsParser,
},
plugins: {
'@typescript-eslint': tsPlugin,
unicorn,
},
rules: {
// 類型4対策: 意味のない変数名を禁止
'unicorn/prevent-abbreviations': [
'error',
{
replacements: {
res: { response: true },
req: { request: true },
err: { error: true },
val: { value: true },
msg: { message: true },
},
allowList: {
i: true, // forループインデックスは許容
},
},
],
// 類型1対策: 命名パターンの強制(カスタム例)
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'function',
format: ['camelCase'],
},
{
selector: 'variable',
format: ['camelCase', 'UPPER_CASE'],
leadingUnderscore: 'forbid',
},
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '^I[A-Z]',
match: false, // "I" プレフィックスは禁止(Airbnbスタイル)
},
},
],
},
},
];
ESLintの詳細な設定オプションについてはESLint公式ドキュメントを、TypeScript向けのルール詳細は@typescript-eslint公式ドキュメントを参照してください。
Biomeを使う場合
Biome公式ドキュメントでは、noImplicitAnyLet・useNamingConvention・noForEachなど命名・可読性に関わるルールが提供されています(経験則: ESLintより設定が簡潔)。
{
"linter": {
"rules": {
"style": {
"useNamingConvention": "error",
"useFilenamingConvention": "error"
},
"complexity": {
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": { "maxAllowedComplexity": 15 }
}
}
}
}
}
LLMがこうしたコードを書く理由:推論特性のメモ
なぜAIはこれらの問題を繰り返すのか理解すると、対策の優先順位がつけやすくなります。
トークン予測の最適化: LLMは次のトークンとして「学習データ中で最も頻出する、文脈的に適切な続き」を選択します。result・dataは汎用的なため、文脈を問わず高い確率で選ばれます(transformer推論の一般的な特性)。
コンテキストウィンドウの制約: モデルは参照できる範囲のコードしか見ません。プロジェクト全体の既存実装は、コンテキストに含めない限り不可視です。重複実装が発生する構造的な原因です。
学習データのバイアス: Stack Overflowやオープンソースに含まれる「汎用的で再利用可能なコード例」を大量に学習しているため、パターン適用を好む傾向があります。Design Patternの教科書コードが最も確率の高い「次のトークン」になります。
これらはモデルの欠陥ではなく、確率的テキスト生成の特性です。プロジェクト固有の文脈を明示的にプロンプトに含める・IDE拡張でコードベースを参照させる・自動検出ルールで補完するアプローチが有効です。
参考: Google Engineering Practices - コードレビューガイド では、レビュー時に「設計・機能性・複雑性・テスト・命名・コメント・スタイル」の順で確認することを推奨しています。AI生成コードのレビューでも同じ観点が適用できます。また Airbnb JavaScript Style Guide はインターフェース名に "I" プレフィックスを付けないことを推奨しており、TypeScript向け命名規約の参考として広く使われています(経験則)。
関連記事
AI生成コードの品質管理を仕組みとして設計するには、可読性の担保に加えてレビュープロセスの自動化も必要です。
- AIコードレビューを自動化するRiver Reviewerの設計 — AIを使ってAI生成コードをレビューするパイプラインの実装例
- LLMアウトプット品質ゲートの設計 — LLM出力を本番に入れる前の品質チェック設計パターン
- LLMガードレール設計の実践 — プロダクションでのLLM利用に必要なガードレールの設計手順
FAQ
AIが書いたコードはなぜ可読性が低くなりやすいのですか?
LLMはプロンプトの文脈だけを参照してコードを生成するため、プロジェクト固有の命名規約・設計意図・チームの暗黙知が反映されません。コンテキストウィンドウの制約と確率的テキスト生成の特性が根本原因です。
AI生成コードのレビュー基準はどう設計すればいいですか?
AI固有の5類型(命名の一貫性なし・過剰抽象化・コメント意図不明・コンテキスト依存変数名・重複実装)に対応したチェックリストを作成し、プルリクエストテンプレートに組み込むのが最初のステップです。その後、ESLintカスタムルールで機械的に検出できる項目を自動化します。
AI生成コードに対して有効な命名規則はありますか?
動詞プレフィックスの統一(fetch/get/create/update/delete)・boolean変数の疑問形(is/has/can)・型名のサフィックス規約(Dto/Response)を命名規約ドキュメントとして文書化し、AIへのプロンプトコンテキストに含めることが最も効果的です。
AI生成コードのlintルールはどう設定すればいいですか?
eslint-plugin-unicorn の prevent-abbreviations で意味のない変数名を禁止し、@typescript-eslint/naming-convention で命名パターンを強制するのが実践的な出発点です。Biomeを使う場合は useNamingConvention と noExcessiveCognitiveComplexity が対応します。
チームでAI生成コードの品質基準を合意するにはどうすればいいですか?
最初は「命名規約ドキュメントをリポジトリに置く」「レビューチェックリストをPRテンプレートに組み込む」の2点だけを合意します。全員が違和感を覚えた実例を週次で持ち寄り、チェックリストを更新するサイクルを作ることで、属人化したレビューが仕組みに変わります。
本記事はシリーズ「AIコードレビューを導入する実践ガイド」の一部です。
まとめ
AI生成コードの可読性問題は、プロジェクト固有の文脈がモデルに伝わらないことが根本原因です。対策は3層で設計します。
| 層 | 対策 | 効果 |
|---|---|---|
| 予防 | 命名規約をプロンプトコンテキストに含める | AI生成コードの品質向上 |
| 検出 | ESLint/Biomeカスタムルール | 自動で類型1・4検出 |
| 是正 | PRテンプレートのチェックリスト | レビューの属人化防止 |
完璧な自動検出は難しいですが、5類型を意識するだけでレビューの視点が変わります。まず命名規約ドキュメントを1ページ書き、PRテンプレートにチェックリストを追加するところから始めてください。
