Agent-First Development

フィードバックループの構築

はじめに

Agent-First Development で最も重要な洞察の一つ:

何かが失敗したとき、解決策は「もっと頑張る」ではなく「何の能力が足りないか?」を問うこと。

エージェントが間違ったコードを生成したとき、プロンプトを微調整して再試行するのは対症療法に過ぎない。根本的な解決策は、エージェントが自分で間違いに気づき、修正できるフィードバックループを構築することだ。

フィードバックループの全体像

エージェントがコードを生成
  ↓
型チェック → エラーがあれば修正を試みる
  ↓
Linter → 違反があれば修正を試みる
  ↓
テスト実行 → 失敗があれば修正を試みる
  ↓
CI/CD → 統合レベルの問題を検出
  ↓
人間のレビュー → アーキテクチャ・ビジネスロジックの判断

各段階で問題が早期に検出されるほど、修正コストは低くなる。

型システムを制約として活用する

なぜ型が重要か

型システムはエージェントにとって最も即座で明確なフィードバックだ。コンパイルエラーは曖昧さがなく、修正方法が明確。

実践パターン

strict な型定義

// 悪い例: string では何でも入る
type Patient struct {
    ID     string
    Status string
}

// 良い例: 型で制約を表現
type PatientID uuid.UUID

type PatientStatus int

const (
    PatientStatusActive PatientStatus = iota
    PatientStatusInactive
    PatientStatusDeceased
)

type Patient struct {
    ID     PatientID
    Status PatientStatus
}

エージェントが Status: "active" と書こうとすると、コンパイラが即座にエラーを返す。エージェントはエラーメッセージから正しい型を特定し、修正できる。

Interface による依存方向の制約

// domain 層で interface を定義
type PatientRepository interface {
    FindByID(ctx context.Context, id PatientID) (*Patient, error)
    Search(ctx context.Context, query SearchQuery) ([]Patient, error)
}

// infra 層で実装
// → domain が infra に依存しない構造が型レベルで保証される

Linter をフィードバックデバイスとして設計する

重要な原則: 修正方法を含むエラーメッセージ

Linter のエラーメッセージは「何が間違っているか」だけでなく「どう修正すべきか」を含めるべきだ。

悪いエラーメッセージ:
  error: domain package imports infrastructure package

良いエラーメッセージ:
  error: domain/patient/service.go imports infra/database
  Domain packages must not import infrastructure packages.
  Instead, define an interface in domain/patient/repository.go
  and implement it in infra/database/patient_repo.go.
  See: docs/architecture/dependency-rules.md

カスタム Linter の例

アーキテクチャ違反の検出

// tools/lint/dependency_check.go
// domain/ パッケージが外部に依存していないかチェック

func checkDomainDependencies(pkg *packages.Package) []Diagnostic {
    if !strings.HasPrefix(pkg.PkgPath, "raica/idp/src/domain") {
        return nil
    }

    var diagnostics []Diagnostic
    for _, imp := range pkg.Imports {
        if isExternalPackage(imp.PkgPath) {
            diagnostics = append(diagnostics, Diagnostic{
                Pos:     imp.Pos,
                Message: fmt.Sprintf(
                    "%s imports external package %s. "+
                    "Domain packages must have zero external dependencies. "+
                    "Move this dependency to the infra/ layer and use an interface. "+
                    "See: docs/architecture/dependency-rules.md",
                    pkg.PkgPath, imp.PkgPath,
                ),
            })
        }
    }
    return diagnostics
}

命名規約の強制

# .golangci.yml
linters-settings:
  revive:
    rules:
      - name: exported
        arguments:
          - "checkPrivateReceivers"
        severity: warning

構造テスト

テストの一種だが、ビジネスロジックではなくコードの構造を検証する。

func TestArchitecture_DomainHasNoExternalDeps(t *testing.T) {
    domainPkgs := loadPackages("raica/idp/src/domain/...")

    for _, pkg := range domainPkgs {
        for _, imp := range pkg.Imports {
            if isExternalPackage(imp) {
                t.Errorf(
                    "%s imports %s. Domain must not depend on external packages. "+
                    "Define an interface in domain/ and implement in infra/.",
                    pkg.PkgPath, imp,
                )
            }
        }
    }
}

func TestArchitecture_HandlersUseInterfaces(t *testing.T) {
    handlerPkgs := loadPackages("raica/idp/src/api/handlers/...")

    for _, pkg := range handlerPkgs {
        for _, imp := range pkg.Imports {
            if strings.Contains(imp, "infra/") {
                t.Errorf(
                    "Handler %s directly imports infra package %s. "+
                    "Handlers must use domain interfaces for data access. "+
                    "See: src/api/handlers/medication.go for the correct pattern.",
                    pkg.PkgPath, imp,
                )
            }
        }
    }
}

テスト戦略

エージェント生成コードのテスト

エージェントが生成したコードの品質を保証するために、テストは 3 つのレベルで設計する。

Level 1: ユニットテスト

func TestPatientSearch_PartialNameMatch(t *testing.T) {
    tests := []struct {
        name     string
        query    string
        patients []Patient
        want     []Patient
    }{
        {
            name:     "名前の部分一致で検索できる",
            query:    "田中",
            patients: []Patient{
                {Name: "田中太郎"},
                {Name: "佐藤次郎"},
                {Name: "田中花子"},
            },
            want: []Patient{
                {Name: "田中太郎"},
                {Name: "田中花子"},
            },
        },
        {
            name:     "空のクエリは全件返す",
            query:    "",
            patients: []Patient{{Name: "田中太郎"}},
            want:     []Patient{{Name: "田中太郎"}},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // テーブル駆動テストでエッジケースを網羅
        })
    }
}

Level 2: 統合テスト

func TestPatientSearchAPI_Integration(t *testing.T) {
    // テスト用 DB をセットアップ
    db := setupTestDB(t)
    seedPatients(t, db)

    srv := setupTestServer(t, db)

    resp, err := http.Get(srv.URL + "/api/v1/patients?q=田中&limit=10")
    require.NoError(t, err)
    require.Equal(t, http.StatusOK, resp.StatusCode)

    var result PaginatedResponse[Patient]
    json.NewDecoder(resp.Body).Decode(&result)
    assert.Len(t, result.Items, 2)
}

Level 3: Contract テスト

API の契約(リクエスト/レスポンス形式)が変更されていないことを検証する。

func TestPatientAPI_ResponseContract(t *testing.T) {
    // レスポンスが定義された JSON スキーマに準拠しているか
    resp := callPatientSearchAPI(t, "田中")

    schema := loadJSONSchema(t, "docs/api/schemas/patient-list-response.json")
    err := schema.Validate(resp.Body)
    assert.NoError(t, err, "Response does not match the contract schema")
}

CI/CD を品質ゲートとして活用する

パイプラインの設計

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  quality-gates:
    runs-on: ubuntu-latest
    steps:
      # Gate 1: コンパイル
      - name: Build
        run: make build

      # Gate 2: 型チェックと lint
      - name: Lint
        run: make lint

      # Gate 3: ユニットテスト
      - name: Unit Tests
        run: make test-unit

      # Gate 4: 構造テスト
      - name: Architecture Tests
        run: make test-architecture

      # Gate 5: 統合テスト
      - name: Integration Tests
        run: make test-integration

      # Gate 6: セキュリティチェック
      - name: Security Scan
        run: make security-scan

エージェントが CI 結果を活用する

Claude Code は CI の失敗を見て自動的に修正を試みることができる。そのために:

  1. CI のログを明確にする: どのテストが、なぜ失敗したかが分かるように
  2. 再現可能にする: ローカルで同じチェックを実行できるコマンドを用意する
  3. 段階的にする: すべてを一度に実行するのではなく、段階的に検証する
# Makefile - エージェントが直接実行できるコマンド

.PHONY: check
check: build lint test  ## 全品質チェックを実行

.PHONY: build
build:  ## ビルド
	go build ./...

.PHONY: lint
lint:  ## Lint 実行
	golangci-lint run ./...

.PHONY: test
test: test-unit test-architecture  ## 全テスト実行

.PHONY: test-unit
test-unit:  ## ユニットテスト
	go test -short ./...

.PHONY: test-architecture
test-architecture:  ## アーキテクチャテスト
	go test ./tests/architecture/...

Worktree によるエージェント並列実行

問題: 1 つのリポジトリで 1 つのエージェント

通常、1 つの作業ディレクトリでは 1 つのエージェントしか安全に動かせない。

解決策: Git Worktree

# メインの作業ディレクトリ
cd /path/to/raica-backend

# worktree を作成して別タスクを並列実行
git worktree add ../raica-backend-patient-search feature/patient-search
git worktree add ../raica-backend-notification feature/notification

# 各 worktree で別々のエージェントを実行
# Terminal 1:
cd ../raica-backend-patient-search
claude "患者検索 API を実装して"

# Terminal 2:
cd ../raica-backend-notification
claude "通知システムを実装して"

Worktree のポイント

  • 各 worktree は独立した作業ディレクトリを持つ
  • アプリケーションが worktree ごとに起動できる設計にする(ポートの動的割り当て等)
  • エージェントがテストを実行しても他の worktree に影響しない
  • 完了後は git worktree remove でクリーンアップ

Observability: エージェントに見えるようにする

ログの構造化

// エージェントが解析しやすい structured logging
slog.Error("patient search failed",
    "error", err,
    "query", query,
    "user_id", userID,
    "duration_ms", elapsed.Milliseconds(),
)

ヘルスチェックエンドポイント

// エージェントがアプリケーションの状態を確認できる
// GET /health
{
    "status": "healthy",
    "database": "connected",
    "cache": "connected",
    "version": "1.2.3"
}

失敗からの学び方

パターン: 失敗 → 能力の追加

失敗: エージェントが domain/ で外部パッケージを import した
原因分析: エージェントにアーキテクチャルールを伝えていなかった
対策: 構造テストを追加 + CLAUDE.md にルールを記載

失敗: エージェントが既存と異なるエラーハンドリングパターンを使った
原因分析: エラーハンドリングの規約がドキュメント化されていなかった
対策: docs/conventions/error-handling.md を作成 + linter ルール追加

失敗: エージェントが生成したテストが CI で落ちた
原因分析: テスト用 DB の接続情報がローカルと CI で異なった
対策: 環境変数での設定を統一 + テスト設定ドキュメントを追加

重要: 「もっと頑張る」は解決策ではない

悪いアプローチ:
  プロンプトを少し変えて再試行 → また失敗 → さらに変えて再試行

良いアプローチ:
  なぜ失敗したか分析する → 足りない能力を特定する → 仕組みで解決する

Try This: フィードバックループの強化

Exercise 1: エラーメッセージの改善

プロジェクトの linter ルールまたはテストのエラーメッセージを 5 つ確認する:

  1. エージェントがそのメッセージだけで問題を修正できるか?
  2. 修正方法のヒントが含まれているか?
  3. 関連するドキュメントへの参照があるか?

不足しているものを改善する。

Exercise 2: 構造テストの追加

プロジェクトのアーキテクチャルール 1 つを構造テストとして実装する:

  • 例: 「handler/ は infra/ を直接 import してはならない」
  • 例: 「domain/ の型は外部パッケージの型を embed してはならない」
  • エラーメッセージに修正方法を含める

Exercise 3: ローカル CI の整備

以下のコマンドが動作するか確認する:

make build   # ビルドが通るか
make lint    # lint が通るか
make test    # テストが通るか

動作しない場合、エージェントがこれらを実行できるよう整備する。

まとめ

フィードバックループの構築原則:

  1. 機械的検証を優先する: 人間の目視確認に頼らない
  2. エラーメッセージに修正方法を含める: エージェントが自己修正できるように
  3. 段階的に検証する: 型 → lint → テスト → CI の順で早期検出
  4. 失敗から学ぶ: 「もっと頑張る」ではなく「何が足りないか」を問う
  5. 再現可能にする: ローカルで CI と同じチェックを実行できるようにする

次のステップ