フィードバックループの構築
はじめに
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 の失敗を見て自動的に修正を試みることができる。そのために:
- CI のログを明確にする: どのテストが、なぜ失敗したかが分かるように
- 再現可能にする: ローカルで同じチェックを実行できるコマンドを用意する
- 段階的にする: すべてを一度に実行するのではなく、段階的に検証する
# 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 つ確認する:
- エージェントがそのメッセージだけで問題を修正できるか?
- 修正方法のヒントが含まれているか?
- 関連するドキュメントへの参照があるか?
不足しているものを改善する。
Exercise 2: 構造テストの追加
プロジェクトのアーキテクチャルール 1 つを構造テストとして実装する:
- 例: 「handler/ は infra/ を直接 import してはならない」
- 例: 「domain/ の型は外部パッケージの型を embed してはならない」
- エラーメッセージに修正方法を含める
Exercise 3: ローカル CI の整備
以下のコマンドが動作するか確認する:
make build # ビルドが通るか
make lint # lint が通るか
make test # テストが通るか
動作しない場合、エージェントがこれらを実行できるよう整備する。
まとめ
フィードバックループの構築原則:
- 機械的検証を優先する: 人間の目視確認に頼らない
- エラーメッセージに修正方法を含める: エージェントが自己修正できるように
- 段階的に検証する: 型 → lint → テスト → CI の順で早期検出
- 失敗から学ぶ: 「もっと頑張る」ではなく「何が足りないか」を問う
- 再現可能にする: ローカルで CI と同じチェックを実行できるようにする
次のステップ
- エージェントに優しいアーキテクチャ - 構造的な制約の設計
- レビューと品質管理 - エージェント生成コードのレビュー方法