AI가 실패하면 다른 AI가 이어받는다: 《AI Development Orchestra 3/3》

이경규·2026년 7월 10일

orchestra

목록 보기
3/3

AI가 실패하면 다른 AI가 이어받는다: Consensus·Self-Healing·Eval Gate 운영법

1편에서는 AI 코딩 에이전트의 역할을 나눴다.

Planner
Implementer
Reviewer
Tester
Final Judge

2편에서는 Claude, Codex, Cursor를 중앙 Orchestrator로 연결했다.

Claude Planner
→ Codex Implementer
→ Claude Reviewer
→ Codex Fixer
→ Tester
→ Cursor Final Review
→ Human

이제 마지막 문제가 남는다.

정상적으로 흘러갈 때는 어렵지 않다.

Planner가 계획을 만들고, Implementer가 코드를 수정하고, Reviewer가 통과시키고, 테스트가 성공하면 된다.

실무는 그렇게 끝나지 않는다.

Reviewer가 Planner의 계획이 잘못됐다고 판단할 수 있다.

Codex가 구현을 끝내지 못할 수 있다.

테스트가 계속 실패할 수 있다.

Claude와 Codex가 서로 다른 해결책을 제시할 수 있다.

필요한 수정이 보호된 파일에 걸릴 수 있다.

에이전트가 위험한 명령을 실행하려 할 수 있다.

이때 많은 멀티 에이전트 시스템이 단순한 방법을 쓴다.

실패
→ 다시 시도
→ 또 실패
→ 다른 모델로 다시 시도
→ 또 실패

겉으로 보면 Self-Healing처럼 보인다.

하지만 이건 복구가 아니다.

비용을 태우면서 같은 실패를 반복하는 것이다.

진짜 Self-Healing은 실패 원인을 분류하고, 그 실패를 해결할 수 있는 역할에게 필요한 정보만 넘기는 구조다.

코드 문제
→ Fixer

테스트 해석 문제
→ Test Analyzer

실행 환경 문제
→ Human

정책 위반
→ 즉시 중단

모호한 요구사항
→ Planner 또는 Human

모델 장애
→ Fallback Provider

이번 글에서는 AI 개발 오케스트라가 실패했을 때 무한 재시도하지 않고, 적절한 담당자에게 작업을 넘기는 방법을 다룬다.

여기에 Consensus Workflow, Eval Gate, Tool Allowlist, Human Approval Gate를 붙여 실제 운영 가능한 구조를 완성한다.


1. Self-Healing은 무한 재시도가 아니다

Self-Healing이라는 표현을 들으면 AI가 알아서 문제를 발견하고 끝까지 고치는 모습을 떠올리기 쉽다.

테스트 실패
→ AI가 로그 분석
→ 코드 수정
→ 테스트 재실행
→ 성공

이 흐름 자체는 가능하다.

문제는 실패 원인을 구분하지 않은 채 같은 작업을 반복할 때다.

예를 들어 iOS 프로젝트에서 테스트가 실패했다고 해보자.

CheckoutViewModelTests.testRetryAfterFailure

Expected:
isSubmitting == false

Actual:
isSubmitting == true

이 실패는 구현 코드 문제일 가능성이 높다.

Codex Fixer에게 관련 파일과 최신 실패를 넘기면 된다.

하지만 다음 실패는 다르다.

xcodebuild: error:
Unable to find a destination matching the provided destination specifier

이건 구현 코드 문제가 아니다.

시뮬레이터 환경이나 테스트 명령의 문제다.

Codex에게 소스 코드를 다시 고치라고 해도 해결되지 않는다.

또 다른 실패를 보자.

PaymentAPI.swift를 수정해야 해결할 수 있지만
protected-files.yaml에서 approval_required로 지정됨

이건 기술 실패가 아니다.

정책에 의한 중단이다.

AI가 계속 우회해서 수정하려 하면 안 된다.

따라서 Self-Healing의 첫 단계는 재시도가 아니다.

실패 분류다.


2. 실패 유형을 먼저 분류한다

오케스트라에서 발생하는 실패를 최소한 다음 정도로 나누는 것이 좋다.

CODE_FAILURE
TEST_FAILURE
ENVIRONMENT_FAILURE
POLICY_BLOCK
AMBIGUOUS_REQUIREMENT
PROVIDER_FAILURE

각 실패는 담당자가 다르다.


CODE_FAILURE

구현 코드 자체의 문제다.

컴파일 오류
잘못된 상태 전환
nil 처리 누락
동시성 오류
리뷰에서 발견된 기능 버그

이 경우 Codex Fixer나 Implementer 역할로 돌려보낸다.

단, 전체 작업을 처음부터 다시 시키지 않는다.

현재 diff와 발견된 문제만 전달한다.


TEST_FAILURE

테스트가 실패했지만 코드 문제인지 테스트 문제인지 바로 알 수 없는 경우다.

테스트 기대값이 잘못됨
Mock이 실제 동작과 다름
비동기 대기 방식이 불안정함
테스트가 상태 변경 전에 assertion을 실행함

이 경우 바로 Fixer에게 넘기지 않는다.

먼저 Test Analyzer가 실패를 분류한다.

제품 코드 버그인가
테스트 코드 버그인가
실행 순서 문제인가
flaky test인가
환경 문제인가

분석 결과가 코드 문제라면 Fixer에게 넘긴다.

테스트 자체의 문제라면 테스트 수정 계획을 새로 만든다.


ENVIRONMENT_FAILURE

코드와 직접 관계없는 실행 환경 문제다.

시뮬레이터 없음
Xcode 버전 불일치
의존성 다운로드 실패
인증서 문제
네트워크 차단
테스트 계정 없음
샌드박스 결제 환경 없음

이 경우 자동 코드 수정을 중단한다.

환경 문제를 코드 수정으로 해결하려 하면 쓸데없는 diff만 늘어난다.


POLICY_BLOCK

에이전트가 정책상 허용되지 않은 행동을 요구하는 경우다.

보호 파일 수정 필요
신규 라이브러리 추가 필요
운영 설정 변경 필요
배포 명령 필요
secret 접근 필요
허용되지 않은 shell 명령 필요

정책 위반은 실패가 아니다.

의도적으로 멈춘 것이다.

따라서 다른 모델로 우회하면 안 된다.

Claude에서 차단
→ Codex로 다시 시도

금지

Provider를 바꿔 정책을 우회하면 Allowlist의 의미가 사라진다.


AMBIGUOUS_REQUIREMENT

요구사항이 불명확해 더 진행할 수 없는 경우다.

실패 시 재시도 버튼을 보여줘야 하는가
자동 재시도해야 하는가
결제 완료 화면으로 언제 이동해야 하는가
기존 UX를 유지해야 하는가
API 계약 변경이 허용되는가

AI가 빈칸을 알아서 채우게 하지 않는다.

Planner에게 다시 보내거나 사람에게 질문을 올린다.


PROVIDER_FAILURE

모델 또는 도구 자체가 정상 실행되지 않은 경우다.

CLI 비정상 종료
API timeout
rate limit
응답 파일 생성 실패
잘못된 JSON 반환
세션 연결 실패

이 경우에만 Fallback Provider를 고려한다.

Claude CLI 장애
→ Codex fallback

Codex 실행 장애
→ Claude fallback

잘못된 구현을 다른 모델로 덮는 것이 아니라, 실행 자체가 불가능할 때만 공급자를 바꾼다.


3. 실패 분류 결과를 구조화한다

실패 분류도 자연어 보고서로만 남기면 다음 단계에서 다시 해석해야 한다.

JSON으로 고정한다.

{
  "failure_type": "test_failure",
  "severity": "medium",
  "retryable": true,
  "next_role": "test_analyzer",
  "summary": "실패 후 isSubmitting 상태가 복구되지 않습니다.",
  "evidence": {
    "command": "xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16'",
    "failed_test": "CheckoutViewModelTests.testSubmitOrder_whenFailed_allowsRetry",
    "error": "XCTAssertFalse failed: isSubmitting remained true"
  },
  "relevant_files": [
    "CheckoutViewModel.swift",
    "CheckoutViewModelTests.swift"
  ],
  "policy_blocks": [],
  "human_approval_required": false
}

Orchestrator는 failure_type과 next_role을 기준으로 다음 단계를 결정한다.


4. Failure Classifier 만들기

실패 분류는 규칙 기반으로 먼저 처리하고, 애매한 경우에만 AI를 사용한다.

from dataclasses import dataclass
from enum import StrEnum


class FailureType(StrEnum):
    CODE_FAILURE = "code_failure"
    TEST_FAILURE = "test_failure"
    ENVIRONMENT_FAILURE = "environment_failure"
    POLICY_BLOCK = "policy_block"
    AMBIGUOUS_REQUIREMENT = "ambiguous_requirement"
    PROVIDER_FAILURE = "provider_failure"


@dataclass(frozen=True)
class FailureDecision:
    failure_type: FailureType
    retryable: bool
    next_role: str
    human_approval_required: bool
    reason: str


class FailureClassifier:
    def classify(
        self,
        *,
        exit_code: int,
        log: str,
        policy_blocked: bool,
        provider_error: bool,
        requirement_ambiguous: bool,
    ) -> FailureDecision:
        normalized = log.lower()

        if policy_blocked:
            return FailureDecision(
                failure_type=FailureType.POLICY_BLOCK,
                retryable=False,
                next_role="human",
                human_approval_required=True,
                reason="작업이 정책에 의해 차단됐습니다.",
            )

        if requirement_ambiguous:
            return FailureDecision(
                failure_type=FailureType.AMBIGUOUS_REQUIREMENT,
                retryable=False,
                next_role="planner",
                human_approval_required=True,
                reason="요구사항이 충분히 명확하지 않습니다.",
            )

        if provider_error:
            return FailureDecision(
                failure_type=FailureType.PROVIDER_FAILURE,
                retryable=True,
                next_role="fallback_provider",
                human_approval_required=False,
                reason="에이전트 공급자가 정상적으로 응답하지 않았습니다.",
            )

        environment_patterns = (
            "unable to find a destination",
            "no such module",
            "certificate",
            "provisioning profile",
            "network is unreachable",
            "connection timed out",
            "simulator device failed",
        )

        if any(pattern in normalized for pattern in environment_patterns):
            return FailureDecision(
                failure_type=FailureType.ENVIRONMENT_FAILURE,
                retryable=False,
                next_role="human",
                human_approval_required=True,
                reason="실행 환경 문제로 판단됩니다.",
            )

        if "test failed" in normalized or "xctassert" in normalized:
            return FailureDecision(
                failure_type=FailureType.TEST_FAILURE,
                retryable=True,
                next_role="test_analyzer",
                human_approval_required=False,
                reason="테스트 실패 원인을 추가 분석해야 합니다.",
            )

        if exit_code != 0:
            return FailureDecision(
                failure_type=FailureType.CODE_FAILURE,
                retryable=True,
                next_role="fixer",
                human_approval_required=False,
                reason="컴파일 또는 구현 코드 문제로 판단됩니다.",
            )

        return FailureDecision(
            failure_type=FailureType.AMBIGUOUS_REQUIREMENT,
            retryable=False,
            next_role="human",
            human_approval_required=True,
            reason="실패 유형을 자동 분류하지 못했습니다.",
        )

규칙으로 명확히 분류할 수 있는 실패는 AI에게 다시 묻지 않는 편이 좋다.


5. Retry Policy: 몇 번까지 다시 시킬 것인가

에이전트가 실패할 때마다 계속 다른 모델을 호출하면 비용과 diff가 함께 커진다.

따라서 위험도별로 최대 수정 횟수를 정해야 한다.

.ai/policies/retry.yaml

version: 1

max_total_attempts: 4

risk:
  low:
    max_fix_rounds: 2
    human_approval_required: false

  medium:
    max_fix_rounds: 2
    human_approval_required: true

  high:
    max_fix_rounds: 1
    human_approval_required: true

failure_routes:
  code_failure:
    next_role: fixer
    retry: true

  test_failure:
    next_role: test_analyzer
    retry: true

  environment_failure:
    next_role: human
    retry: false

  policy_block:
    next_role: human
    retry: false

  ambiguous_requirement:
    next_role: planner
    retry: false

  provider_failure:
    next_role: fallback_provider
    retry: true
    max_provider_fallbacks: 1

핵심은 위험도가 높을수록 자동 수정 횟수를 줄이는 것이다.

Low Risk
→ 최대 2회 자동 수정

Medium Risk
→ 최대 2회
→ 최종 사람 승인

High Risk
→ 최대 1회
→ 바로 사람 검토

결제, 인증, 개인정보, 배포와 가까운 작업을 AI가 세 번, 네 번 연속으로 고치게 하면 변경 이력을 사람이 따라가기 어려워진다.


6. Retry Policy 구현하기

from pathlib import Path

import yaml


class RetryPolicy:
    def __init__(self, policy_file: Path) -> None:
        self.policy = yaml.safe_load(
            policy_file.read_text(encoding="utf-8")
        )

    def can_retry(
        self,
        *,
        failure_type: str,
        risk: str,
        current_fix_rounds: int,
        total_attempts: int,
    ) -> bool:
        if total_attempts >= int(self.policy["max_total_attempts"]):
            return False

        route = self.policy["failure_routes"].get(failure_type)

        if route is None or not route.get("retry", False):
            return False

        risk_policy = self.policy["risk"][risk]
        max_fix_rounds = int(risk_policy["max_fix_rounds"])

        return current_fix_rounds < max_fix_rounds

    def next_role(self, failure_type: str) -> str:
        route = self.policy["failure_routes"].get(failure_type)

        if route is None:
            return "human"

        return route.get("next_role", "human")

Orchestrator는 재시도 전에 정책을 확인한다.

if not retry_policy.can_retry(
    failure_type=failure.failure_type.value,
    risk=task_risk,
    current_fix_rounds=state.fix_attempts,
    total_attempts=state.total_attempts,
):
    state.move_to(RunState.WAITING_HUMAN)
    return

7. 실패한 작업 전체를 다음 AI에게 넘기면 안 된다

테스트가 실패했다고 이전 대화와 전체 빌드 로그를 Fixer에게 모두 전달하는 경우가 많다.

초기 요구사항
전체 소스 파일
첫 번째 계획
첫 번째 구현
첫 번째 리뷰
전체 빌드 로그
두 번째 구현
두 번째 빌드 로그
이전 모델의 설명

이렇게 넘기면 Fixer는 현재 실패보다 과거 흐름에 더 많은 영향을 받을 수 있다.

필요한 것은 Failure Context 압축이다.

무엇이 실패했는가
어떤 명령에서 실패했는가
기대 동작은 무엇인가
현재 실제 동작은 무엇인가
관련 파일은 무엇인가
수정할 수 있는 범위는 어디까지인가

8. failure-context.json 만들기

{
  "failure_type": "test_failure",
  "task_id": "checkout-duplicate-submit",
  "attempt": 2,
  "command": "xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16' -only-testing:MyAppTests/CheckoutViewModelTests",
  "failed_test": "CheckoutViewModelTests.testSubmitOrder_whenFailed_allowsRetry",
  "error": "XCTAssertFalse failed: isSubmitting remained true",
  "expected": "주문 생성이 실패하면 isSubmitting이 false가 되고 사용자가 다시 시도할 수 있어야 합니다.",
  "actual": "실패 후에도 isSubmitting이 true로 유지됩니다.",
  "relevant_files": [
    "CheckoutViewModel.swift",
    "CheckoutViewModelTests.swift"
  ],
  "allowed_to_edit": [
    "CheckoutViewModel.swift",
    "CheckoutViewModelTests.swift"
  ],
  "do_not_edit": [
    "PaymentAPI.swift",
    "PaymentRepository.swift",
    "NetworkClient.swift"
  ],
  "previous_attempts_summary": [
    "중복 진입 방지 guard를 추가했습니다.",
    "실패 후 재시도 테스트를 추가했습니다."
  ]
}

Fixer에게는 이 파일과 현재 diff만 전달한다.

failure-context.json
current diff.patch
approved plan.json
review.json

과거 전체 대화는 전달하지 않는다.


9. Failure Context Builder 구현하기

import json
from pathlib import Path
from typing import Any


class FailureContextBuilder:
    def build(
        self,
        *,
        task_dir: Path,
        failure: dict[str, Any],
        allowed_to_edit: list[str],
        do_not_edit: list[str],
        previous_attempts: list[str],
    ) -> Path:
        context = {
            "failure_type": failure["failure_type"],
            "task_id": task_dir.name,
            "attempt": failure.get("attempt", 1),
            "command": failure.get("command"),
            "failed_test": failure.get("failed_test"),
            "error": failure.get("error"),
            "expected": failure.get("expected"),
            "actual": failure.get("actual"),
            "relevant_files": failure.get("relevant_files", []),
            "allowed_to_edit": allowed_to_edit,
            "do_not_edit": do_not_edit,
            "previous_attempts_summary": previous_attempts[-3:],
        }

        output = task_dir / "failure-context.json"
        output.write_text(
            json.dumps(context, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

        return output

이전 시도는 최대 몇 줄만 요약해서 유지한다.


10. Fixer는 실패 한 건만 수정한다

.ai/agents/fixer.md

# Fixer Agent

## Role

Fix only the issues explicitly listed in failure-context.json or review.json.

## Inputs

- approved plan.json
- current diff.patch
- failure-context.json
- review.json
- project coding rules

## Restrictions

- Do not restart the implementation.
- Do not expand the approved scope.
- Do not modify files outside allowed_to_edit.
- Do not add dependencies.
- Do not change public APIs.
- Do not refactor unrelated code.
- Do not modify tests only to make a real product bug disappear.

## Stop If

- The failure cannot be fixed inside allowed_to_edit.
- A protected file must change.
- The expected behavior is ambiguous.
- The test expectation conflicts with task.md.

## Required Output

{
  "status": "fixed | blocked | failed",
  "issues_addressed": [],
  "files_changed": [],
  "commands_run": [],
  "remaining_risks": [],
  "blocked_reasons": [],
  "not_changed": []
}

Fixer가 전체 기능을 다시 구현하게 하면 기존 변경과 새 변경이 섞인다.

수정 대상은 최대한 좁아야 한다.


11. Test Analyzer를 따로 두는 이유

테스트가 실패했다고 제품 코드를 바로 고치면 안 된다.

테스트 자체가 잘못됐을 수도 있다.

예를 들어 이런 테스트가 있다고 하자.

func test_submitOrder_whenFailed_allowsRetry() async {
    await viewModel.submitOrder()
    XCTAssertFalse(viewModel.isSubmitting)
}

Mock이 실제로 비동기 실패를 반환하기 전에 assertion이 실행된다면 테스트 구조 문제일 수 있다.

Test Analyzer는 다음을 판단한다.

제품 코드 버그
테스트 코드 버그
Mock 문제
비동기 대기 문제
환경 문제
flaky test

출력은 구조화한다.

{
  "verdict": "product_code_failure",
  "confidence": 0.88,
  "reason": "submitOrder 실패 경로에서 isSubmitting을 false로 복구하지 않습니다.",
  "evidence": [
    "Mock은 동기적으로 failure를 반환합니다.",
    "catch 블록에서 isSubmitting이 변경되지 않습니다."
  ],
  "next_role": "fixer",
  "allowed_files": [
    "CheckoutViewModel.swift",
    "CheckoutViewModelTests.swift"
  ]
}

테스트 기대값이 잘못됐다면 이렇게 나온다.

{
  "verdict": "test_code_failure",
  "confidence": 0.81,
  "reason": "현재 요구사항상 성공 직후 route가 설정되므로 canSubmit이 false인 것이 정상입니다.",
  "next_role": "planner",
  "human_approval_required": true
}

12. Consensus Workflow: AI끼리 의견이 다르면 어떻게 할까

Planner와 Reviewer가 다른 판단을 내릴 수 있다.

예를 들어 현재 작업은 결제 버튼 중복 탭 방지다.

Planner는 이렇게 판단했다.

CheckoutViewModel에서 중복 진입을 막는다.
PaymentAPI는 수정하지 않는다.

Reviewer는 다른 문제를 발견했다.

ViewModel guard만으로는 네트워크 재시도나
다른 화면에서 발생하는 중복 주문을 막을 수 없다.

서버 idempotency가 필요하다.

둘 다 맞을 수 있다.

문제는 현재 작업의 범위다.

Reviewer가 발견한 위험이 유효하다고 해서 현재 PR에 Payment API 변경까지 바로 넣으면 작업이 크게 확대된다.

이럴 때 Consensus Workflow를 실행한다.


13. Consensus는 자유 토론이 아니다

Consensus Workflow를 AI끼리 자유롭게 이야기하는 구조로 만들면 다시 컨텍스트가 길어진다.

다음 세 역할만 둔다.

Proposal Agent
→ 기존 계획의 근거 제시

Challenge Agent
→ 반대 의견과 위험 제시

Judge Agent
→ 근거 비교 후 결정

필요하면 마지막에 Human이 결정한다.

중요한 규칙은 다음과 같다.

자유 토론 금지
최대 한 라운드
코드 수정 금지
파일과 테스트 근거 필수
다수결 금지
현재 작업과 후속 이슈 분리 가능

AI 세 개가 같은 의견을 냈다고 정답이 되는 것은 아니다.

근거가 중요하다.


14. Consensus 정책 파일

.ai/policies/consensus.yaml

version: 1

trigger:
  - reviewer_requests_scope_change
  - architecture_disagreement
  - disputed_high_severity_issue
  - test_expectation_disagreement

participants:
  proposal_agent: planner
  challenge_agent: reviewer
  judge_agent: final_judge

rules:
  max_rounds: 1
  code_editing_allowed: false
  evidence_required: true
  file_references_required: true
  test_references_required_when_available: true
  majority_vote_allowed: false

outcomes:
  - accept_original_plan
  - approve_scope_change
  - create_follow_up_issue
  - request_human_decision

15. Proposal Agent 산출물

{
  "position": "keep_original_scope",
  "claim": "현재 사용자 중복 탭 문제는 CheckoutViewModel의 상태 제어로 해결할 수 있습니다.",
  "evidence": [
    {
      "file": "CheckoutViewModel.swift",
      "description": "결제 버튼의 isSubmitting 상태를 ViewModel이 소유합니다."
    },
    {
      "file": "CheckoutView.swift",
      "description": "결제 요청은 이 화면에서 submitOrder를 통해 시작됩니다."
    }
  ],
  "tests": [
    "여러 번 호출해도 createOrder가 한 번만 실행되는 테스트"
  ],
  "risks": [
    "다른 진입점이나 네트워크 재시도는 현재 범위에서 보호하지 못합니다."
  ]
}

16. Challenge Agent 산출물

{
  "position": "expand_scope",
  "claim": "클라이언트 상태 제어만으로는 주문 중복 생성을 완전히 막을 수 없습니다.",
  "evidence": [
    {
      "file": "PaymentAPI.swift",
      "description": "createOrder 요청에 idempotency key가 없습니다."
    },
    {
      "file": "CreateOrderUseCase.swift",
      "description": "다른 화면에서도 같은 UseCase를 호출할 수 있습니다."
    }
  ],
  "tests": [
    "동일한 주문 요청이 두 번 전달될 때 서버가 한 건만 생성하는 통합 테스트"
  ],
  "risks": [
    "API 계약 변경이 필요합니다.",
    "Payment 영역은 사람 승인 대상입니다."
  ]
}

17. Judge Agent 산출물

{
  "decision": "accept_original_plan",
  "reason": "현재 작업의 명시된 목표는 Checkout 화면의 중복 탭 방지입니다. 서버 idempotency는 중요하지만 API 계약과 Payment 영역 변경이 필요한 별도 작업입니다.",
  "current_task_action": "ViewModel guard와 관련 테스트만 완료합니다.",
  "follow_up_issue": {
    "required": true,
    "title": "주문 생성 API에 idempotency key 추가",
    "risk": "high",
    "human_review_required": true
  },
  "human_approval_required": false
}

현재 작업은 작게 끝내고, 발견된 더 큰 위험은 별도 이슈로 남긴다.

이게 좋은 Consensus다.


18. Consensus Orchestrator 예시

import json
from pathlib import Path


class ConsensusRunner:
    def __init__(
        self,
        *,
        task_dir: Path,
        role_runner,
    ) -> None:
        self.task_dir = task_dir
        self.role_runner = role_runner

    async def run(self) -> dict:
        await self.role_runner(
            role="proposal_agent",
            output_file=self.task_dir / "proposal.json",
        )

        await self.role_runner(
            role="challenge_agent",
            output_file=self.task_dir / "challenge.json",
        )

        await self.role_runner(
            role="judge_agent",
            output_file=self.task_dir / "consensus-result.json",
        )

        return json.loads(
            (self.task_dir / "consensus-result.json").read_text(
                encoding="utf-8"
            )
        )

Judge Agent에게는 proposal.json, challenge.json, task.md, plan.json만 제공한다.

구현 코드를 수정할 권한은 주지 않는다.


19. Eval Gate: 테스트가 통과해도 완료가 아닐 수 있다

테스트가 성공하면 작업이 끝났다고 생각하기 쉽다.

하지만 다음 변경도 테스트는 통과할 수 있다.

관련 없는 파일까지 수정
민감 정보 로그 추가
새 의존성 무단 추가
보호 파일 변경
public API 변경
구현 보고서와 실제 diff 불일치

그래서 테스트 뒤에 Eval Gate가 필요하다.

Eval Gate는 작업이 완료 조건을 만족하는지 여러 관점에서 검사한다.

Scope
Behavior
Tests
Security
Architecture
Maintainability
Evidence

20. Eval Gate 체크리스트

.ai/evals/final-gate.yaml

version: 1

gates:
  scope:
    required: true
    checks:
      - changed_files_within_allowed_scope
      - protected_files_unchanged
      - not_goal_not_implemented

  behavior:
    required: true
    checks:
      - expected_behavior_satisfied
      - failure_states_handled
      - retry_behavior_safe

  tests:
    required: true
    checks:
      - required_tests_added
      - tests_actually_executed
      - failures_resolved
      - skipped_tests_documented

  security:
    required: true
    checks:
      - no_sensitive_data_logged
      - no_secret_file_access
      - protected_domains_require_approval

  architecture:
    required: true
    checks:
      - layer_boundaries_preserved
      - no_unapproved_public_api_change

  maintainability:
    required: true
    checks:
      - no_unrelated_refactoring
      - no_unapproved_dependency
      - diff_is_reviewable

  evidence:
    required: true
    checks:
      - implementation_report_matches_diff
      - commands_have_exit_codes
      - test_results_have_logs

21. Eval Gate 출력

{
  "verdict": "waiting_human",
  "gate_results": {
    "scope": "pass",
    "behavior": "pass",
    "tests": "pass",
    "security": "pass",
    "architecture": "pass",
    "maintainability": "pass",
    "evidence": "pass"
  },
  "review_findings": {
    "critical": 0,
    "high": 0,
    "medium": 0,
    "low": 1
  },
  "remaining_risks": [
    "서버 측 idempotency는 별도 작업으로 남아 있습니다."
  ],
  "human_inspection_targets": [
    "결제 성공 후 route 전환 시점",
    "후속 idempotency 이슈 생성 여부"
  ]
}

Eval Gate의 최종 상태는 다음 중 하나다.

PASS
NEEDS_CHANGES
BLOCKED
WAITING_HUMAN

PASS라고 해도 자동 머지를 의미하지는 않는다.

팀 정책에 따라 Human Approval이 필요할 수 있다.


22. Eval Gate 구현 예시

from dataclasses import dataclass


@dataclass(frozen=True)
class GateResult:
    name: str
    passed: bool
    reason: str


class EvalGate:
    def evaluate(
        self,
        *,
        scope_violations: list[str],
        unresolved_review_issues: list[dict],
        tests_passed: bool,
        tests_executed: bool,
        protected_files_changed: list[str],
        unapproved_dependencies: list[str],
        report_matches_diff: bool,
    ) -> dict:
        gates = [
            GateResult(
                name="scope",
                passed=not scope_violations,
                reason=(
                    "Approved scope respected."
                    if not scope_violations
                    else f"Out-of-scope files: {scope_violations}"
                ),
            ),
            GateResult(
                name="review",
                passed=not unresolved_review_issues,
                reason=(
                    "No unresolved review issues."
                    if not unresolved_review_issues
                    else "Unresolved review findings remain."
                ),
            ),
            GateResult(
                name="tests",
                passed=tests_executed and tests_passed,
                reason=(
                    "Required tests executed and passed."
                    if tests_executed and tests_passed
                    else "Tests were not executed or did not pass."
                ),
            ),
            GateResult(
                name="protected_files",
                passed=not protected_files_changed,
                reason=(
                    "Protected files unchanged."
                    if not protected_files_changed
                    else f"Protected files changed: {protected_files_changed}"
                ),
            ),
            GateResult(
                name="dependencies",
                passed=not unapproved_dependencies,
                reason=(
                    "No unapproved dependencies."
                    if not unapproved_dependencies
                    else f"Unapproved dependencies: {unapproved_dependencies}"
                ),
            ),
            GateResult(
                name="evidence",
                passed=report_matches_diff,
                reason=(
                    "Implementation report matches diff."
                    if report_matches_diff
                    else "Implementation report does not match diff."
                ),
            ),
        ]

        failed = [gate for gate in gates if not gate.passed]

        return {
            "verdict": "pass" if not failed else "blocked",
            "gates": [
                {
                    "name": gate.name,
                    "passed": gate.passed,
                    "reason": gate.reason,
                }
                for gate in gates
            ],
        }

AI가 판단할 필요가 없는 항목은 코드로 검사한다.


23. Tool Allowlist: AI에게 모든 명령을 주지 않는다

에이전트가 실패를 고치려고 하다 위험한 명령을 실행할 수 있다.

rm -rf DerivedData
git reset --hard
pod update
swift package update
cat .env
printenv
curl https://example.com/install.sh | sh
git push
fastlane deploy

AI가 악의적으로 행동하지 않아도 문제는 생긴다.

“빌드가 안 되니 의존성을 업데이트해보자”는 판단이 전체 프로젝트 상태를 바꿀 수 있다.

따라서 허용된 명령만 실행하게 한다.

.ai/policies/tool-allowlist.yaml

version: 1

default: deny

allowed_patterns:
  - "^git status( --short)?$"
  - "^git diff( --stat| --binary| --name-only)?$"
  - "^swift test$"
  - "^swiftlint( --strict)?$"
  - "^swiftformat --lint .+$"
  - "^xcodebuild test -scheme [A-Za-z0-9_-]+ .+$"

blocked_patterns:
  - "rm -rf"
  - "git reset --hard"
  - "git clean"
  - "git push"
  - "curl .+\\| sh"
  - "curl .+\\| bash"
  - "wget .+\\| sh"
  - "cat .env"
  - "printenv"
  - "pod update"
  - "swift package update"
  - "fastlane"
  - "deploy"
  - "kubectl"
  - "terraform apply"

기본값은 deny다.

허용되지 않은 명령은 실행하지 않는다.


24. 프롬프트 정책과 시스템 정책은 다르다

프롬프트에 다음처럼 적을 수 있다.

위험한 명령을 실행하지 마.
.env 파일을 열지 마.
배포 명령을 실행하지 마.

이 규칙은 필요하다.

하지만 충분하지 않다.

에이전트가 지침을 놓치거나, 다른 문서의 악성 지시를 따르거나, 필요한 작업이라고 잘못 판단할 수 있다.

따라서 실행 직전에 코드가 검사해야 한다.

프롬프트:
행동 기준을 알려준다.

Policy Engine:
행동을 실제로 차단한다.

25. Command Gate 구현하기

import re
from pathlib import Path

import yaml


class CommandGate:
    def __init__(self, policy_file: Path) -> None:
        self.policy = yaml.safe_load(
            policy_file.read_text(encoding="utf-8")
        )

    def validate(self, command: str) -> tuple[bool, str]:
        for pattern in self.policy.get("blocked_patterns", []):
            if re.search(pattern, command):
                return False, f"Blocked command pattern: {pattern}"

        for pattern in self.policy.get("allowed_patterns", []):
            if re.fullmatch(pattern, command):
                return True, "Command explicitly allowed."

        return False, "Command is not explicitly allowed."

사용 예시는 다음과 같다.

allowed, reason = gate.validate(
    "xcodebuild test -scheme MyApp "
    "-destination 'platform=iOS Simulator,name=iPhone 16'"
)

if not allowed:
    raise PermissionError(reason)

26. 파일 쓰기 전에도 정책을 검사한다

.ai/policies/protected-files.yaml

version: 1

never_read:
  - ".env"
  - ".env.*"
  - "**/*.p8"
  - "**/Secrets.plist"
  - "~/.ssh/**"
  - "~/.aws/**"

read_only:
  - "Sources/Auth/**"
  - "Sources/Payment/**"
  - "Sources/Security/**"
  - ".github/workflows/**"
  - "fastlane/**"

approval_required_to_edit:
  - "Package.swift"
  - "Podfile"
  - "Tuist/**"
  - "Sources/Network/**"
  - "Sources/Analytics/**"
  - "Database/**"

파일 접근은 네 단계로 구분하는 것이 좋다.

일반 읽기·수정 가능
읽기만 가능
사람 승인 후 수정 가능
절대 열람 금지

27. File Gate 구현하기

import fnmatch
from pathlib import Path

import yaml


class FileGate:
    def __init__(self, policy_file: Path) -> None:
        self.policy = yaml.safe_load(
            policy_file.read_text(encoding="utf-8")
        )

    def can_read(self, file_path: str) -> tuple[bool, str]:
        if self._matches_any(
            file_path,
            self.policy.get("never_read", []),
        ):
            return False, "File is marked never_read."

        return True, "Read allowed."

    def can_write(
        self,
        file_path: str,
        *,
        approved: bool,
    ) -> tuple[bool, str]:
        if self._matches_any(
            file_path,
            self.policy.get("never_read", []),
        ):
            return False, "File is marked never_read."

        if self._matches_any(
            file_path,
            self.policy.get("read_only", []),
        ):
            return False, "File is read_only."

        if self._matches_any(
            file_path,
            self.policy.get(
                "approval_required_to_edit",
                [],
            ),
        ):
            if not approved:
                return False, "Human approval is required."

        return True, "Write allowed."

    @staticmethod
    def _matches_any(
        file_path: str,
        patterns: list[str],
    ) -> bool:
        return any(
            fnmatch.fnmatch(file_path, pattern)
            for pattern in patterns
        )

28. Hook으로 도구 실행 전에 막는다

Claude Code Hook이나 CLI wrapper를 사용하면 도구 호출 직전에 Policy Engine을 실행할 수 있다.

개념적인 설정은 다음과 같다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .ai/runtime/check_command.py"
          }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .ai/runtime/check_file_write.py"
          }
        ]
      }
    ]
  }
}

AI가 실행하려는 명령이나 파일 경로를 Hook에 넘기고, 정책 위반이면 종료 코드로 차단한다.

보안과 권한은 부탁하는 것이 아니다.

강제하는 것이다.


29. Human Approval Gate: 자동화가 멈춰야 하는 지점

AI 오케스트라의 목표는 사람을 완전히 제거하는 것이 아니다.

사람이 모든 코드를 처음부터 읽지 않아도 되도록 검증 결과를 압축하는 것이다.

다음 조건은 자동 완료하지 않는 것이 좋다.

결제 코드 변경
인증 흐름 변경
개인정보 처리 변경
보안·암호화 로직 변경
DB 마이그레이션
배포 설정 변경
신규 의존성 추가
public API 변경
테스트 미실행
snapshot 대량 변경
보호 파일 변경

30. Human Approval 정책 파일

.ai/policies/human-approval.yaml

version: 1

require_human_approval:
  risk_levels:
    - high

  paths:
    - "Sources/Payment/**"
    - "Sources/Auth/**"
    - "Sources/Privacy/**"
    - "Sources/Security/**"
    - "Database/**"
    - ".github/workflows/**"
    - "fastlane/**"

  changes:
    - public_api
    - new_dependency
    - database_schema
    - production_config
    - analytics_payload
    - secret_handling

  test_conditions:
    - tests_not_run
    - flaky_test_ignored
    - snapshot_updated
    - ui_tests_skipped_for_high_risk_task

31. Human Approval 상태 저장

{
  "state": "waiting_human",
  "reason": "high_risk_payment_change",
  "required_reviewers": [
    "ios-owner",
    "payment-owner"
  ],
  "human_inspection_targets": [
    "CheckoutViewModel.swift",
    "CheckoutViewModelTests.swift",
    "final-report.md"
  ],
  "unresolved_risks": [
    "서버 idempotency가 아직 구현되지 않았습니다."
  ]
}

사람은 모든 실행 로그를 읽을 필요가 없다.

확인해야 할 파일과 위험만 보면 된다.


32. Follow-up Issue: 범위 밖 문제를 놓치지 않는 방법

현재 작업을 작게 유지하면 더 큰 문제가 남을 수 있다.

결제 중복 탭은 ViewModel에서 막았지만 서버 idempotency가 없다는 문제가 대표적이다.

이 문제를 현재 PR에 억지로 넣지 않는다.

별도 후속 이슈로 남긴다.

{
  "title": "주문 생성 API에 idempotency key 추가",
  "reason": "클라이언트 중복 탭 방지는 네트워크 재시도와 다른 진입점의 중복 요청을 막지 못합니다.",
  "risk": "high",
  "suggested_owner": "payment-platform",
  "evidence": [
    {
      "file": "PaymentAPI.swift",
      "description": "createOrder 요청에 idempotency key가 없습니다."
    }
  ],
  "auto_create": false
}

auto_create의 기본값은 false가 좋다.

AI가 발견한 모든 가능성을 자동으로 이슈 트래커에 올리면 노이즈가 커진다.

사람이 검토한 뒤 GitHub Issue나 Linear에 등록한다.


33. Follow-up Issue Builder

import json
from pathlib import Path


class FollowUpIssueBuilder:
    def create(
        self,
        *,
        task_dir: Path,
        title: str,
        reason: str,
        risk: str,
        evidence: list[dict],
        suggested_owner: str | None = None,
    ) -> Path:
        issue = {
            "title": title,
            "reason": reason,
            "risk": risk,
            "suggested_owner": suggested_owner,
            "evidence": evidence,
            "auto_create": False,
        }

        output = task_dir / "follow-up-issue.json"
        output.write_text(
            json.dumps(issue, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

        return output

34. Self-Healing 전체 흐름

전체 흐름은 다음처럼 된다.

테스트 실패
    │
    ▼
Failure Classifier
    │
    ├── CODE_FAILURE
    │      ▼
    │   Codex Fixer
    │
    ├── TEST_FAILURE
    │      ▼
    │   Claude Test Analyzer
    │      ▼
    │   Codex Fixer 또는 Planner
    │
    ├── ENVIRONMENT_FAILURE
    │      ▼
    │   Human
    │
    ├── POLICY_BLOCK
    │      ▼
    │   Human
    │
    ├── AMBIGUOUS_REQUIREMENT
    │      ▼
    │   Planner / Human
    │
    └── PROVIDER_FAILURE
           ▼
       Fallback Provider

수정 후에는 바로 완료하지 않는다.

Fixer
→ diff 재생성
→ Scope 검사
→ Reviewer
→ Tester
→ Eval Gate

복구 작업도 동일한 검증 과정을 다시 통과해야 한다.


35. Orchestrator에 복구 흐름 추가하기

class Orchestra:
    async def handle_failure(
        self,
        *,
        failure_input: dict,
        task_risk: str,
    ) -> bool:
        decision = self.failure_classifier.classify(
            exit_code=failure_input["exit_code"],
            log=failure_input["log"],
            policy_blocked=failure_input.get(
                "policy_blocked",
                False,
            ),
            provider_error=failure_input.get(
                "provider_error",
                False,
            ),
            requirement_ambiguous=failure_input.get(
                "requirement_ambiguous",
                False,
            ),
        )

        if not self.retry_policy.can_retry(
            failure_type=decision.failure_type.value,
            risk=task_risk,
            current_fix_rounds=self.state.fix_attempts,
            total_attempts=self.state.total_attempts,
        ):
            self.state.move_to(RunState.WAITING_HUMAN)
            return False

        if decision.next_role == "test_analyzer":
            analysis = await self.run_test_analyzer(
                failure_input
            )

            next_role = analysis["next_role"]

            if next_role not in {"fixer", "planner"}:
                self.state.move_to(RunState.WAITING_HUMAN)
                return False

            await self.run_role(next_role)

        elif decision.next_role == "fixer":
            await self.run_role("fixer")

        elif decision.next_role == "fallback_provider":
            await self.run_fallback_provider()

        else:
            self.state.move_to(RunState.WAITING_HUMAN)
            return False

        await self.diff_manager.capture(self.task_dir)
        self.validate_scope()

        review = await self.review()

        if review["verdict"] != "pass":
            self.state.move_to(RunState.WAITING_HUMAN)
            return False

        test_result = await self.test()

        return test_result["status"] == "passed"

36. 최종 운영 보고서

최종 보고서는 개발자가 짧은 시간 안에 판단할 수 있어야 한다.

# AI Orchestra Final Report

## Verdict

WAITING_FOR_HUMAN_APPROVAL

## Summary

CheckoutViewModel에서 결제 버튼 중복 진입을 차단하고, 실패 후 다시 시도할 수 있도록 상태를 복구했습니다.

## Files Changed

- CheckoutViewModel.swift
- CheckoutViewModelTests.swift

## Agent Chain

- Planner: Claude
- Implementer: Codex
- Reviewer: Claude
- Test Analyzer: Claude
- Fixer: Codex
- Final Review: Cursor

## Attempts

- Initial implementation: 1
- Fix rounds: 1
- Provider fallbacks: 0

## Commands Run

- xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16' -only-testing:MyAppTests/CheckoutViewModelTests

## Tests

- Unit tests: passed
- UI tests: not run
- Build: passed

## Eval Gate

- Scope: PASS
- Behavior: PASS
- Tests: PASS
- Security: PASS
- Architecture: PASS
- Maintainability: PASS
- Evidence: PASS

## Review Findings

- Critical: 0
- High: 0
- Medium: 0
- Low: 1

## Policy Blocks

- PaymentAPI 수정은 사람 승인 대상이므로 현재 작업에서 제외했습니다.

## Remaining Risks

클라이언트 중복 탭은 차단했지만 서버 측 idempotency는 구현되지 않았습니다.

## Follow-up Issue

주문 생성 API에 idempotency key 추가

## Not Changed

- PaymentAPI
- PaymentRepository
- NetworkClient
- DesignSystem
- Analytics
- Production configuration

## Human Inspection Targets

- 결제 성공 후 route 전환 시점
- 실패 후 재시도 UX
- 서버 idempotency 후속 이슈 등록 여부

이 보고서가 있어야 사람이 전체 대화와 로그를 읽지 않고도 결정을 내릴 수 있다.


37. 단계적으로 도입한다

이 구조를 한 번에 모두 자동화할 필요는 없다.

1단계: 수동 파일 전달

Claude 계획
→ 사람이 확인
→ Codex 구현
→ Claude 리뷰
→ 테스트 실행
→ Cursor 최종 확인

실패하면 사람이 직접 원인을 분류한다.


2단계: 로컬 Orchestrator

Python script
상태 저장
역할별 산출물 전달
diff 자동 저장

3단계: Policy Engine

Protected Files
Tool Allowlist
Stop Conditions
Schema Validation
Scope Validation

명확한 정책 위반은 코드로 차단한다.


4단계: 제한된 Self-Healing

저위험 작업만
최대 두 번
최신 실패만 전달
환경 실패는 사람에게 전달

5단계: Consensus와 Human Approval

범위 변경 의견 충돌
고위험 리뷰 이슈
요구사항 불일치
후속 이슈 제안
사람 승인 대기

6단계: Cloud Agent와 Webhook

GitHub Issue 이벤트
Linear 작업 이벤트
Cloud Agent 실행
완료 알림
사람 승인 대기

Cloud 자동화는 가장 마지막에 붙이는 것이 좋다.

로컬에서 통제되지 않는 워크플로를 클라우드로 옮기면 문제도 같이 확대된다.


38. 오케스트라가 오히려 비효율적인 작업

모든 작업에 멀티 에이전트와 Self-Healing을 적용하면 안 된다.

다음 작업은 하나의 에이전트로 충분하다.

문구 수정
단순 포맷 변경
명확한 한 줄 버그
테스트 이름 변경
README 일부 수정
주석 오타 수정

오케스트라가 효과적인 작업은 다음과 같다.

여러 계층이 함께 바뀌는 작업
중간 이상 위험도를 가진 작업
AI가 자주 범위를 확대하는 작업
인증·결제·개인정보와 가까운 작업
독립 리뷰가 필요한 작업
장기 실행과 반복 수정이 필요한 작업
실패 원인 분류가 중요한 작업

AI를 많이 쓰는 것이 목표가 아니다.

검증 효과가 실행 비용보다 큰 작업에만 사용해야 한다.


39. 실패 복구 시스템을 망치는 운영 방식

같은 요청을 그대로 반복한다

실패 원인을 분류하지 않으면 같은 오류가 반복된다.


전체 로그를 매번 다시 전달한다

최신 실패가 과거 로그에 묻힌다.


다른 모델로 정책을 우회한다

정책 차단은 Provider fallback 대상이 아니다.


무한 수정 라운드를 허용한다

diff가 점점 커지고 처음 계획에서 멀어진다.


AI 다수결로 결정한다

세 AI가 같은 의견을 냈다고 정답은 아니다.

근거와 테스트를 확인해야 한다.


Reviewer가 직접 코드를 고친다

독립 검토와 구현의 경계가 무너진다.


테스트 통과만 보고 완료한다

범위 위반, 보안 위험, 의존성 변경은 테스트가 잡지 못할 수 있다.


Follow-up Issue를 자동 등록한다

AI가 발견한 모든 가능성을 이슈로 만들면 노이즈가 커진다.


사람 승인 단계를 형식적으로 둔다

개발자가 봐야 할 파일과 위험이 정리되지 않으면 Approval Gate가 병목이 된다.


40. 3부작 전체 정리

1편에서는 AI 개발팀의 역할을 나눴다.

Planner
Implementer
Reviewer
Tester
Final Judge

핵심 질문은 이것이었다.

누가 무엇을 담당할 것인가?

2편에서는 역할을 실제 파이프라인으로 연결했다.

Orchestrator
Provider Adapter
Model Router
State Store
Context Builder
Diff-first Review
Git worktree
Cursor Control Room

핵심 질문은 이것이었다.

서로 다른 AI와 작업 결과를 어떻게 이어줄 것인가?

3편에서는 실패와 위험을 통제했다.

Failure Classification
Retry Policy
Failure Context
Consensus Workflow
Eval Gate
Tool Allowlist
Human Approval

핵심 질문은 이것이다.

실패, 충돌, 보안 위험이 생겼을 때
어디까지 자동화하고 어디서 멈출 것인가?

41. 개발자가 왜 이 구조를 알아야 할까

AI 코딩 에이전트가 강해질수록 개발자의 역할은 코드를 직접 입력하는 일에서 조금씩 바뀐다.

앞으로 개발자는 다음을 설계하게 된다.

어떤 AI가 계획할 것인가
어떤 AI가 구현할 것인가
누가 독립적으로 리뷰할 것인가
어떤 파일을 수정할 수 있는가
어떤 명령을 실행할 수 있는가
실패를 몇 번까지 복구할 것인가
어떤 실패는 즉시 사람에게 넘길 것인가
AI 의견이 충돌하면 무엇을 근거로 판단할 것인가
최종적으로 어떤 증거를 남길 것인가

이건 프롬프트를 잘 쓰는 기술과 다르다.

작은 개발 조직과 실행 시스템을 설계하는 일에 가깝다.

Agent
→ 작업자

Orchestrator
→ 업무 흐름 관리자

Policy Engine
→ 권한 시스템

Artifacts
→ 작업 기록과 증거

Eval Gate
→ 품질 기준

Human Approval
→ 최종 책임

모델은 계속 바뀔 것이다.

오늘 Planner 역할에 적합한 모델과 내일 적합한 모델이 다를 수 있다.

하지만 이 구조는 쉽게 바뀌지 않는다.


42. 마무리

Self-Healing은 AI가 실패해도 끝까지 혼자 고치게 만드는 기술이 아니다.

실패를 분류하고, 필요한 역할에게 필요한 정보만 넘기며, 일정 횟수를 넘으면 멈추는 기술이다.

Consensus는 AI끼리 긴 토론을 시키는 기술이 아니다.

서로 다른 의견을 구조화하고, 파일과 테스트 근거를 비교해 현재 작업과 후속 이슈를 분리하는 기술이다.

Eval Gate는 테스트가 통과했는지만 확인하는 단계가 아니다.

범위를 지켰는가
기대 동작을 만족했는가
테스트를 실제로 실행했는가
보안 위험이 없는가
아키텍처를 지켰는가
새 의존성이 들어오지 않았는가
보고서와 실제 diff가 일치하는가

이 모든 것을 확인하는 단계다.

결국 AI 개발 오케스트라의 핵심은 AI를 많이 붙이는 것이 아니다.

계획하는 AI
구현하는 AI
검토하는 AI
실패를 분석하는 AI가

각자의 권한 안에서만 움직이고,

검증 가능한 산출물을
다음 역할에 전달하게 만드는 것

좋은 오케스트라는 AI가 멈추지 않고 계속 일하는 시스템이 아니다.

위험하면 멈추고, 모호하면 사람에게 묻고, 실패하면 원인을 분류하고, 근거가 부족하면 승인하지 않는 시스템이다.

AI에게 일을 많이 시키는 것보다 중요한 것은 AI가 일을 못 해야 하는 순간을 정의하는 것이다.

한 줄로 정리하면 이렇다.

AI 자동화의 완성은 사람을 없애는 것이 아니다.

사람이 개입해야 할 순간을 정확하게 만드는 것이다.

profile
iOS 앱 개발자

0개의 댓글