TL;DR
- LLM が書いたコードは「動く」が「理解しやすい」とは限らない
- 保守可能性を維持するには 意図の外在化・境界の明確化・テスト可読性の確保 の 3 軸が必要
- チーム開発においては「コードを読む人間がいる前提」の設計習慣を LLM 使用後も崩さない
背景: LLM コーディング時代に浮上した「理解ギャップ」
GitHub Copilot、Cursor、Claude Code など、LLM を使ったコーディング支援ツールが普及し、個人開発者からエンタープライズチームまで日常的に AI 生成コードをリポジトリにマージするようになった。
生産性は上がった。しかし新たな問題が浮上している。
「自分で書いていないコードを、自分がどれだけ理解しているか?」
Hacker News や Aha! Engineering Blog などで繰り返し話題になるこの問いは、2026 年時点でまだ明確な答えが出ていない実践的課題だ。LLM が生成するコードは以下の特性を持ちやすい。
- 局所的に正しいが全体文脈から浮いている: 関数単体は動くが、既存の命名規則・エラー処理パターンと乖離している
- なぜそう実装したかのコメントがない: LLM はコメントを省略しがちで、意図が見えない
- テストが「通るだけ」のものを生成しがち: assertion が甘く、境界値・エラー系が薄い
本記事では、この「理解ギャップ」を埋めるための 3 つのアーキテクチャ的・運用的戦略 を解説する。
戦略 1: 意図を外在化する — ADR (Architecture Decision Record) × LLM プロンプトログ
なぜ意図の外在化が重要か
LLM に「この関数を書いて」と頼んだとき、あなたの頭の中には「なぜその実装が必要か」という文脈がある。しかし生成されたコードにはその文脈が存在しない。
3 ヶ月後に別のメンバーがそのコードを見たとき、意図がわからなければリファクタリングも拡張も怖くなる。
実践: プロンプトを docs/adr/ に残す
docs/
adr/
0001-use-exponential-backoff-for-stripe-retry.md
0002-paginate-user-list-with-cursor.md
ADR の形式は Michael Nygard の提案 が広く使われている。
# ADR-0002: ユーザー一覧のページネーションにカーソル方式を採用
## ステータス: 承認済み (2026-07-17)
## コンテキスト
ユーザー数が 10 万件を超えた場合、オフセットページネーションは
OFFSET N のスキャンコストが線形増加する。
## 決定
cursor-based pagination (keyset pagination) を採用する。
実装は LLM (Claude Code) で生成し、以下のプロンプトを使用した:
> 「PostgreSQL + Prisma で cursor-based pagination を実装して。
> cursor は createdAt + id の複合キー。next_cursor を
> opaque な base64 文字列で返すこと」
## 影響
- フロントエンド側は page_number 廃止・next_cursor に対応が必要
- ソート条件変更時は cursor の再設計が必要
ポイント: プロンプトを ADR に残すと「なぜこのコードになったか」が追跡できる。LLM が特定の実装パターンを選んだ理由も言語化される。
戦略 2: 境界を明確化する — モジュール分割と型による「意図の表明」
LLM 生成コードが「境界を侵食する」問題
LLM に大きなタスクを投げると、既存モジュールの責務を無視して「動くコード」を生成することがある。
例えば「注文確認メールを送る処理を追加して」と頼むと:
// LLM が生成しがちなコード (悪例)
async function createOrder(userId: string, items: Item[]) {
// DB 保存
const order = await db.order.create({ data: { userId, items } });
// ここで突然メール送信ロジックが混入
const user = await db.user.findUnique({ where: { id: userId } });
await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SENDGRID_KEY}` },
body: JSON.stringify({ to: user.email, subject: "注文確認" }),
});
return order;
}
createOrder がメール送信の詳細を知ってしまっている。テストも難しくなる。
実践: 事前に境界を型と interface で宣言してから LLM に渡す
// 先にインターフェースを人間が定義する
interface OrderService {
createOrder(userId: string, items: Item[]): Promise<Order>;
}
interface NotificationService {
sendOrderConfirmation(order: Order): Promise<void>;
}
この型定義を LLM への指示に含める:
「以下の interface に従って
createOrderを実装して。
メール送信はNotificationServiceに移譲すること。
直接 sendgrid を呼ばないこと。」
// LLM が生成した実装 (境界が守られた版)
class OrderServiceImpl implements OrderService {
constructor(private notificationService: NotificationService) {}
async createOrder(userId: string, items: Item[]): Promise<Order> {
const order = await db.order.create({ data: { userId, items } });
await this.notificationService.sendOrderConfirmation(order);
return order;
}
}
インターフェースファーストで LLM に渡すと、生成コードが既存設計に収まりやすくなる。
Rust の場合: trait で表明する
// trait を先に定義 → LLM に「この trait を実装して」と渡す
pub trait NotificationPort {
async fn send_order_confirmation(&self, order: &Order) -> Result<(), NotificationError>;
}
pub struct OrderService<N: NotificationPort> {
db: Arc<Database>,
notifier: N,
}
Rust の型システムは LLM 生成コードの「境界侵食」を コンパイル時に検出できる という強みがある。型が合わなければコンパイルエラーになるため、レビューコストを下げられる。
戦略 3: テストの可読性を「仕様書」として維持する
LLM が生成するテストの典型的な問題
// LLM が生成しがちなテスト (悪例)
it("works", async () => {
const result = await createOrder("user1", [{ id: "item1", qty: 1 }]);
expect(result).toBeTruthy();
});
このテストは:
-
何を検証しているか不明 (
toBeTruthyは何でも通る) -
テスト名が仕様を表していない (
worksは意味がない) - 失敗時にどの仕様が壊れたかわからない
実践: Given-When-Then 形式を LLM へのプロンプトに組み込む
LLM に次のように依頼する:
「以下の仕様をカバーするテストを Given-When-Then 形式で書いて。
テスト名は '〇〇のとき、△△すると、××になる' の形式にすること。」
仕様:
- 在庫が 0 の商品を注文しようとした場合、
InsufficientStockErrorが発生する - 注文が成功した場合、
NotificationService.sendOrderConfirmationが 1 回呼ばれる
// LLM が生成したテスト (可読性を指定した場合)
describe("OrderService.createOrder", () => {
describe("在庫が 0 の商品を注文しようとしたとき", () => {
it("InsufficientStockError を throw する", async () => {
// Given
const item = { id: "item-out-of-stock", qty: 1 };
await db.inventory.update({ where: { itemId: item.id }, data: { stock: 0 } });
// When / Then
await expect(
orderService.createOrder("user1", [item])
).rejects.toThrow(InsufficientStockError);
});
});
describe("注文が成功したとき", () => {
it("NotificationService.sendOrderConfirmation が 1 回呼ばれる", async () => {
// Given
const mockNotifier = { sendOrderConfirmation: jest.fn() };
const service = new OrderServiceImpl(mockNotifier);
// When
await service.createOrder("user1", [{ id: "item1", qty: 1 }]);
// Then
expect(mockNotifier.sendOrderConfirmation).toHaveBeenCalledTimes(1);
});
});
});
テストが「仕様書」として機能する状態を保つことで、LLM 生成コードを人間がレビューする際のコストを大幅に下げられる。
ボーナス: チームでの運用ルール例
LLM コーディングをチームで運用する際に効果的だったプラクティスをまとめる。
コードレビュー時の追加確認事項
| 確認項目 | 理由 |
|---|---|
| LLM 生成コードのプロンプトが ADR / PR 説明に記載されているか | 意図の追跡可能性 |
| 既存モジュールの責務境界を侵食していないか | 設計一貫性 |
| テスト名が仕様を自然言語で表現しているか | 仕様書としての機能 |
| エラーハンドリングが既存パターンと統一されているか | LLM は独自のエラー処理を混入させやすい |
| 命名規則が既存コードベースと一致しているか | LLM は命名スタイルにブレが出やすい |
.cursor/rules や GitHub Copilot Instructions でプロジェクトルールを明示する
Cursor では .cursor/rules、GitHub Copilot では .github/copilot-instructions.md にプロジェクト固有のルールを書くことで、LLM が生成するコードの品質を事前に制御できる。
# .cursor/rules (例)
## エラーハンドリング
- ドメインエラーは `src/errors/` に定義されたクラスを使用すること
- `console.error` を直接呼ばず、`logger.error` を使うこと
## 依存性注入
- クラスは constructor injection を使うこと
- 直接 import したサービスのインスタンスを生成しないこと
## テスト
- テスト名は 「〇〇のとき、△△すると、××になる」形式にすること
- `toBeTruthy` / `toBeFalsy` は使わず、具体的な値を検証すること
まとめ
LLM 生成コードとの長期的な付き合いには、以下の 3 つが核心となる。
- 意図を外在化する: ADR とプロンプトログで「なぜこの実装か」を残す
- 境界を型で宣言する: インターフェース・trait を先に定義してから LLM に実装させる
- テストを仕様書として維持する: Given-When-Then と意味のあるテスト名を LLM への指示に組み込む
「LLM が書いたから仕方ない」では技術的負債が蓄積するだけだ。LLM をチームの新人メンバーとして扱い、既存の設計思想・命名規則・テスト文化に従わせる習慣を作ること が、コードベースの長期健全性を守るカギになる。
参考リンク
- Documenting Architecture Decisions - Michael Nygard (2011)
- Staying familiar with the code when it's written by an LLM - Aha! Engineering
- GitHub Copilot custom instructions
- cursor.directory - Cursor Rules Examples
- TypeScript Handbook - Interfaces
- The Rust Programming Language - Traits
✍️ 本記事の著者: 合同会社ジモラボ (@locallab_jp)
ジモラボは、八王子を拠点に AI を活用した SaaS を多数開発しています。本記事の技術検証もそうした開発過程の副産物です。
- 🌐 公式サイト: https://locallab.jp
- 🔍 AI SEO 最適化 SaaS: lookupai.jp
- 📺 YouTube: @locallab_llc
- ✉️ お問い合わせ: info@locallab.jp
興味を持っていただけたら、ぜひ各 SNS のフォローもお願いします!