Agent-First Development

エージェントに優しいアーキテクチャ

はじめに

エージェントが良いコードを書けるかどうかは、プロンプトの質だけでなく、コードベースの構造に大きく依存する。明確な境界、予測可能なパターン、機械的に強制されるルールがあるリポジトリでは、エージェントの出力品質が劇的に向上する。

本モジュールでは、エージェントが迷わず正しいコードを生成できるアーキテクチャ設計を学ぶ。

厳格な境界が助けになる理由

エージェントの特性

エージェントは以下の状況で良い結果を出す:

  • パターンが明確: 「この層ではこう書く」というルールがある
  • 参照例がある: 同じパターンの既存実装がある
  • 制約が機械的に検証可能: 間違えたらすぐエラーになる

逆に、以下の状況では品質が低下する:

  • 暗黙の規約が多い
  • 例外だらけのルール
  • 同じことの書き方が複数ある

境界の明確化

明確な境界:
  domain/  → ビジネスロジック。外部依存なし。
  api/     → HTTP 層。domain の interface を利用。
  infra/   → 外部接続。domain の interface を実装。

曖昧な境界:
  src/     → すべてが混在。何がどこに属するか不明確。

レイヤー分離と依存方向の強制

Clean Architecture の実践

外側 ← 依存方向 → 内側

[HTTP Handler] → [Use Case / Service] → [Domain Model]
      ↑                    ↑                    ↑
   api/              domain/service/        domain/model/
      |                    |
[DB Repository]     [External API Client]
      ↑                    ↑
   infra/db/          infra/external/

ディレクトリ構造

src/
  domain/
    patient/
      model.go       # Patient 型、値オブジェクト
      repository.go  # Repository interface
      service.go     # ビジネスロジック
      errors.go      # ドメイン固有エラー

  api/
    handlers/
      patient.go     # HTTP ハンドラ
      middleware.go   # 認証、ログ等
    router.go        # ルーティング定義

  infra/
    database/
      patient_repo.go  # Repository 実装
      migrations/      # DB マイグレーション
    external/
      insurance_client.go  # 外部 API クライアント

依存ルールの機械的強制

ドキュメントに「domain は外部に依存しない」と書くだけでは不十分。エージェントはドキュメントを見落とすことがある。テストで強制する。

// tests/architecture/dependency_test.go

func TestDomainHasNoExternalDependencies(t *testing.T) {
    pkgs, _ := packages.Load(&packages.Config{
        Mode: packages.NeedImports,
    }, "raica/idp/src/domain/...")

    allowedPrefixes := []string{
        "raica/idp/src/domain/",
        "context",
        "fmt",
        "errors",
        "time",
        "strings",
        // 標準ライブラリのみ許可
    }

    for _, pkg := range pkgs {
        for _, imp := range pkg.Imports {
            if !isAllowed(imp.PkgPath, allowedPrefixes) {
                t.Errorf(
                    "ARCHITECTURE VIOLATION: %s imports %s\n"+
                    "Domain packages must only depend on standard library and other domain packages.\n"+
                    "If you need external functionality, define an interface in domain/ "+
                    "and implement it in infra/.\n"+
                    "Example: see domain/patient/repository.go (interface) and "+
                    "infra/database/patient_repo.go (implementation).",
                    pkg.PkgPath, imp.PkgPath,
                )
            }
        }
    }
}

"Boring" Technology の選択

なぜ「退屈な」技術がエージェントに優しいか

OpenAI の Harness チームが強調するポイント: composable で、安定した API を持ち、トレーニングデータに十分含まれている技術 をエージェントは最も上手く扱える。

特性Agent-FriendlyAgent-Unfriendly
API の安定性PostgreSQL, Redis最新の experimental DB
ドキュメントの充実度Go standard library社内独自フレームワーク
コミュニティの大きさReact, Expressニッチなフレームワーク
予測可能性REST API独自プロトコル
構成可能性Unix 哲学のツールモノリシックなツール

実践的な判断基準

新しい技術を導入するとき、以下を問う:

  1. エージェントはこの技術のコードを正しく生成できるか?

    • トレーニングデータに十分な例があるか
    • ドキュメントが充実しているか
  2. エラーメッセージは明確か?

    • エージェントがエラーから自己修正できるか
  3. 既存のツールチェーンと統合できるか?

    • linter, テストフレームワーク, CI のサポートがあるか
  4. チーム全体が理解できるか?

    • 特定の専門家しか扱えない技術は避ける

例: 技術選択の比較

HTTP Router の選択:

Option A: 標準の net/http + gorilla/mux
  ✅ 豊富なトレーニングデータ
  ✅ 安定した API
  ✅ 広く知られたパターン
  → エージェントが正確にコードを生成できる

Option B: 最新の高機能フレームワーク
  ❌ トレーニングデータが少ない
  ❌ API が頻繁に変わる
  ❌ 独自の概念が多い
  → エージェントが古い API を使ったり、間違ったパターンを生成する

Boundary Validation: 境界でデータを解析する

原則

外部からのデータは、システムの境界で検証し、内部の型に変換する。内部では常に検証済みの型を使う。

// API 層(境界)
func (h *PatientHandler) CreatePatient(w http.ResponseWriter, r *http.Request) {
    // 1. リクエストを Parse
    var req CreatePatientRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest,
            "Invalid JSON body. Ensure Content-Type is application/json "+
            "and the body is valid JSON.")
        return
    }

    // 2. 境界でバリデーション
    patient, err := req.ToDomain()
    if err != nil {
        respondError(w, http.StatusBadRequest, err.Error())
        return
    }

    // 3. 以降は検証済みの domain 型のみを使う
    result, err := h.service.CreatePatient(r.Context(), patient)
    // ...
}

// リクエスト → ドメイン型への変換
func (r *CreatePatientRequest) ToDomain() (*domain.Patient, error) {
    id, err := uuid.Parse(r.ID)
    if err != nil {
        return nil, fmt.Errorf(
            "patient ID must be a valid UUID v4 (got: %q). "+
            "Generate a UUID using uuid.New().", r.ID)
    }

    if r.Name == "" {
        return nil, fmt.Errorf("patient name is required and must not be empty")
    }

    return &domain.Patient{
        ID:   domain.PatientID(id),
        Name: r.Name,
    }, nil
}

なぜこれがエージェントに優しいか

  • パターンが明確: 「boundary で validate、内部では validated type」
  • 参照例がある: 既存の handler が同じパターンに従っている
  • テストしやすい: validation ロジックが独立しているため単体テストが書きやすい

共有ユーティリティの活用

問題: Hand-rolled helpers の増殖

エージェントは問題を解決するために、その場で helper 関数を作りがち。結果、似て非なる utility が散乱する。

// handler_a.go に書かれた helper
func parseUUID(s string) (uuid.UUID, error) { ... }

// handler_b.go に書かれた微妙に違う helper
func toUUID(s string) (uuid.UUID, error) { ... }

// handler_c.go にまた別の helper
func validateUUID(s string) (uuid.UUID, error) { ... }

解決策: pkg/ に共有ユーティリティを集約

// pkg/parse/uuid.go
package parse

// UUID は文字列を UUID に変換する。
// 無効な形式の場合、修正方法を含むエラーを返す。
func UUID(s string) (uuid.UUID, error) {
    id, err := uuid.Parse(s)
    if err != nil {
        return uuid.Nil, fmt.Errorf(
            "invalid UUID format: %q. "+
            "Expected UUID v4 format like '550e8400-e29b-41d4-a716-446655440000'. "+
            "Use uuid.New() to generate a new UUID.",
            s,
        )
    }
    return id, nil
}

CLAUDE.md に記載:

## 共有ユーティリティ
- UUID パース: pkg/parse.UUID() を使う。独自の UUID パース関数を作らない。
- エラーラップ: pkg/errors.Wrap() を使う。
- ページネーション: pkg/pagination.Parse() を使う。

ファイルサイズと命名規則

ファイルサイズの制限

大きなファイルはエージェントのコンテキストを圧迫し、的確な変更を難しくする。

ガイドライン:

  • 1 ファイル 300 行以下を目安にする
  • 500 行を超えたら分割を検討する
  • 関数は 50 行以下を目安にする
悪い例:
  handlers/patient.go (1200 行 - CRUD + 検索 + バリデーション + ヘルパー)

良い例:
  handlers/
    patient_create.go   (80 行)
    patient_read.go     (60 行)
    patient_update.go   (90 行)
    patient_search.go   (100 行)
    patient_validate.go (70 行)

命名規則

一貫した命名規則により、エージェントが新しいファイルの名前を予測できるようになる。

パターン: {entity}_{operation}.go

既存:
  handlers/medication_create.go
  handlers/medication_search.go

エージェントが新しいハンドラを作るとき、自然に:
  handlers/patient_create.go
  handlers/patient_search.go
と命名する。

テストファイルの命名

パターン: {対象ファイル名}_test.go

  patient_create.go     → patient_create_test.go
  patient_search.go     → patient_search_test.go

再実装 vs 外部ライブラリ

判断基準

観点自前実装外部ライブラリ
機能が単純推奨過剰
ドメイン固有推奨不適合な可能性
広く使われる汎用機能車輪の再発明推奨
メンテナンスコスト自チーム負担コミュニティが負担
エージェントの理解コードが見えるドキュメント依存

実践的なルール

## ライブラリ選択の原則(CLAUDE.md に記載)

- UUID 生成/パース: github.com/google/uuid を使う
- HTTP ルーティング: 標準 net/http を使う
- JSON 処理: 標準 encoding/json を使う
- DB アクセス: sqlx を使う(独自 ORM を作らない)
- バリデーション: 独自の validation 関数を domain 層に書く
  (汎用 validation ライブラリは使わない - ドメインルールが明示的になるため)
- ログ: 標準 log/slog を使う

アーキテクチャの機械的強制: まとめ

ドキュメントだけでは不十分。以下の手段で機械的に強制する:

ルール強制手段
依存方向構造テスト (architecture test)
命名規則linter (revive, custom rules)
ファイルサイズCI でのチェックスクリプト
import ルールgo vet, custom analyzer
API パターンテンプレート + テスト
エラーハンドリングcustom linter rule
# CI で実行されるアーキテクチャチェック
make check-architecture

# 内部的には:
# 1. 依存方向テスト
# 2. ファイルサイズチェック
# 3. 命名規則チェック
# 4. import ルールチェック

Try This: アーキテクチャの改善

Exercise 1: 依存方向の確認

自分のプロジェクトの import グラフを確認する:

  1. domain 層が外部パッケージに依存していないか?
  2. handler 層が infra 層を直接参照していないか?
  3. 違反があれば、interface を導入して修正する

Exercise 2: ファイルサイズの監査

300 行を超えるファイルをリストアップする:

find src/ -name "*.go" -exec awk 'END{if(NR>300) print NR, FILENAME}' {} \;

分割候補を特定し、計画を立てる。

Exercise 3: 共有ユーティリティの整理

プロジェクト内で重複している helper 関数を特定する:

  1. 似た名前の関数を検索する
  2. pkg/ に統一版を作成する
  3. CLAUDE.md に「この utility を使うこと」と記載する

Exercise 4: 構造テストの追加

アーキテクチャルールを 1 つ選び、テストとして実装する。エラーメッセージには必ず:

  • 何が間違っているか
  • どう修正すべきか
  • 参照すべきドキュメントやコード

を含める。

まとめ

エージェントに優しいアーキテクチャの原則:

  1. 厳格な境界: レイヤー間の依存方向を明確にし、機械的に強制する
  2. "Boring" Technology: 安定した、よく知られた技術を選ぶ
  3. Boundary Validation: 外部データは境界で検証し、内部では型安全に
  4. 共有ユーティリティ: 重複を避け、pkg/ に集約する
  5. 予測可能な構造: 一貫した命名規則、適切なファイルサイズ
  6. 機械的強制: ルールはテストと linter で強制する。ドキュメントだけに頼らない

次のステップ