
AI가 기능이 완성되었다고 말했지만, 실제로는 존재하지 않았다
요약
AI 에이전트가 스스로 코드를 작성하고 검증할 때 발생하는 오류를 해결하기 위해, 역할 분리와 3단계 게이트 검증 모델을 갖춘 다중 에이전트 프레임워크 'Sage Feature Team'을 소개합니다. 테스트 코드를 먼저 작성하고 검증하는 단계를 분리하여 AI 개발의 신뢰성을 높이는 방법을 다룹니다.
핵심 포인트
- AI 에이전트가 스스로의 결과물을 채점할 수 없다는 한계 지적
- 역할 분리(ProductOwner, TestCreator, Developer, Tester)를 통한 검증 체계 구축
- 테스트 우선 원칙을 적용한 3단계 게이트 검증 모델 설계
- YAML 기반 상태 관리와 에이전트 간 협업 프레임워크 제안
셀프 검증 AI 개발팀 구축: 역할 분리, 3단계 게이트 검증 모델, 디스크 기반 YAML 상태.
저는 Claude Code를 기반으로 다중 에이전트(multi-agent) 기능 개발 프레임워크를 만들었습니다. 이 프레임워크의 모든 설계 결정은 제가 이미 경험했던 실패를 해결하기 위해 구축된 '흉터 조직'과 같습니다. 본 게시물에서는 역할 분리, YAML 기반 상태 관리, 그리고 3단계 게이트 검증 모델이 어떻게 PRD(Product Requirements Document)를 제가 직접 코드를 작성하지 않은 채 검증되고 테스트된 기능으로 만들었는지 설명합니다. 저는 이를 Sage Feature Team이라고 부릅니다. 제 프레임워크는 여기에서 확인하실 수 있습니다: github.com/merrickcr/sage-feature-team.
두 가지 이야기입니다. Breadcrumbs가 제가 Sage를 구축하게 된 이유이고, Melody는 제가 이룬 후에 망가진 것입니다.
Breadcrumbs: 유령 구현체 (The Ghost Implementation)
저는 제 안드로이드 트레일 저널링 앱인 Breadcrumbs에 '업적 및 배지(Achievements and Badges)' 기능을 위한 PRD를 Claude Code에 제공했습니다. 요구사항은 매우 엄격했습니다. 평가 로직은 가짜 리포지토리 구현체로 단위 테스트(unit-testable)가 가능해야 했으며, 직접적인 Android 프레임워크 의존성에서 자유로워야 했습니다.
Claude는 17개의 파일을 제공했습니다. 이는 아키텍처 준수의 교과서였습니다: 전체 도메인 계층(domain layer), 영속성 계층(persistence layer), 그리고 Hilt를 사용하여 완벽하게 주입된 다섯 개의 Clean Architecture 유스케이스가 포함되었습니다.
저는 테스트를 통해 검증하러 갔습니다. 그곳은 비어 있었습니다.
Claude는 코드를 테스트 가능하도록 모든 것을 했지만, 실제로 단 하나의 테스트 코드도 작성하지 않았습니다. 이것이 AI를 사용한 '원샷(one-shot)' 기능 개발 시도의 첫 번째 실패 사례입니다. 코드를 작성하는 에이전트가 그 코드가 완성되었는지 판단하는 에이전트일 수는 없습니다.
이로 인해 나의 에이전트 프레임워크(agent framework) 내에서 역할의 분리가 이루어졌습니다. ProductOwner는 PRD(제품 요구 사항 문서)를 사양(spec), 에픽(epics), 그리고 스토리(stories)로 번역합니다. TestCreator는 코드가 작성되기 전에 각 스토리의 수락 기준(acceptance criteria)에 따라 테스트를 작성합니다. Developer는 해당 테스트를 바탕으로 구현을 진행합니다. Tester는 테스트를 실행하고 스토리가 실제로 완료되었는지 결정합니다. 이를 'Gate A'라고 부릅시다. 테스트가 우선이며, 코드를 작성하는 에이전트는 결코 스스로의 결과물을 채점할 수 없습니다. EpicVerifier(아래에서 설명)는 에픽 내의 모든 스토리가 완료되면 스토리 간 회귀(cross-story regressions)를 잡아냅니다. 이 방식은 매우 잘 작동했습니다. 또 다른 문제에 직면하기 전까지는 말이죠.
Melody: 초록색의 겉모습
나는 나의 건강 일기 앱인 Melody를 온디바이스 LLM(on-device LLM) 기능을 실험하기 위한 놀이터로 사용하고 있었습니다. 나는 새로운 Sage 워크플로우를 사용하여 MVP(최소 기능 제품)를 구축했습니다. 14개의 스토리, 역할이 분리된 에이전트들, 그리고 코드 한 줄을 쓰기 전에 각 스토리의 수락 기준에 맞춰 먼저 작성된 테스트들로 구성되었습니다. 이것은 Breadcrumbs 실패 사례를 통해 얻은 시스템이었고, 나는 이를 신뢰했습니다.
14개의 스토리 모두 DONE(완료) 상태에 도달했습니다. 테스트 스위트(test suite)는 온통 초록색(pass)이었습니다. 나는 새로운 MVI 도메인 레이어(domain layer), SQLCipher 암호화 데이터베이스, 그리고 온디바이스 LLM의 결과를 간절히 기대하며 앱을 실제 기기에 배포했습니다.
"Hello Android."
그것은 기본 Android Studio 템플릿이었습니다. 도메인 레이어 전체와 ViewModel, Reducer, Store는 존재했고 "테스트"도 완료된 상태였습니다. 하지만 UI를 실제로 표시하기 위해 연결된 것은 아무것도 없었습니다. 사실 UI 자체가 거의 만들어지지 않았습니다.
테스트가 통과된 이유는 @Ignore 처리되었기 때문이었습니다. Android 테스트 러너(test runner)에서 건너뛴(skipped) 테스트는 상위 수준의 요약본에서 통과된 테스트와 똑같이 보입니다. 테스트 스위트는 초록색이었지만, 기능은 존재하지 않았습니다.
Gate B: 구현 지도 (The Implementation Map)
나는 LLM에게 '초록색 테스트'란 결과가 아니라 요청(request)이라는 사실을 깨달았습니다. 그것들은 조작될 수 있습니다. 나에게는 더 엄격한 것, 즉 구현의 증거가 필요했습니다. 테스트만으로는 전체 그림을 포착하지 못할 수도 있고, LLM이 테스트를 무시하거나 데이터를 완전히 모킹(mocking)함으로써 통과된 것처럼 환각(hallucinate)을 일으킬 수도 있기 때문입니다.
그래서 저는 새로운 검증 계층을 추가했습니다. 각 스토리(story)에 대한 구현 맵(Implementation Map)을 만들어, 모든 수락 기준(Acceptance Criterion, AC)을 이를 충족하는 구체적인 파일과 줄 번호(line numbers)에 매핑했습니다. 맵 또한 LLM이 꾸며낼 수 있는 텍스트에 불과하기 때문에, 저의 verify_ac_map.py 스크립트는 이를 실제 코드와 대조하여 확인합니다. 인용된 모든 파일은 반드시 존재해야 하며, 모든 줄 번호는 해당 파일 범위 내에 있어야 하고, 맵이 명시한 모든 심볼(symbol)은 인용된 파일에 실제로 나타나야 합니다. 존재하지 않는 파일을 가리키거나, 파일의 끝을 넘어선 줄 번호를 가리키거나, 존재하지 않는 심볼을 가리키는 맵은 빌드(build)를 실패 처리합니다.
여기서 멈추지 않았습니다. Claude가 AC를 완료했다고 표시하면서 "TODO", "future story", "placeholder"와 같은 용어를 사용하는 것을 즐겨 사용한다는 점을 발견했습니다. 그래서 Claude가 교묘하게 완료된 것처럼 속이려는 시도를 잡아내기 위해 "금지된" 단어들에 대한 정규 표현식(regex) 검사를 추가했습니다.
이 과정이 저에게 무엇을 가져다주는지 명확히 하자면, 이것이 코드가 정확하다는 것을 증명하지는 않습니다. 다만 에이전트의 "완료"라는 주장이 자신만만한 스토리(story)가 아니라, 실제 파일과 실제 심볼에 근거하고 있음을 증명할 뿐입니다. 이는 "이것은 작동한다"라는 약속보다는 작은 약속이지만, 제가 실제로 강제할 수 있는 약속입니다.
STORY-1 구현 맵 (cycle 1)
AC1 ("full front-matter: title + date + rendered body")
구현됨:
...
Gate C: 에픽 검증기 (Epic Verifier)
Gate B는 모든 스토리가 개별적으로 완료되었음을 보장합니다. 하지만 개별적인 완결성이 전체적인 정확성과 동일한 것은 아닙니다. 스토리 3이 자신이 가진 모든 테스트를 통과하면서도, 스토리 1이 구축한 무언가를 조용히 망가뜨릴 수 있기 때문입니다. 스토리 3의 작업이 진행되는 동안 아무도 스토리 1의 테스트를 실행하지 않습니다.
그것이 바로 EpicVerifier가 존재하는 이유입니다. 에픽 (Epic) 내의 모든 스토리 (Story)가 DONE 상태에 도달하면, EpicVerifier는 에픽 전체의 테스트를 한 번에 실행하며, 각 스토리를 하나씩 따로 하는 대신 모든 스토리의 테스트 스위트 (Suite)를 함께 실행합니다. Melody 프로젝트에서 이는 MediaPipe 마이그레이션 과정에서 그 가치를 증명했습니다. 한 스토리가 매니페스트 (Manifest)에서 AICore <uses-feature> 게이트 (Gate)를 올바르게 제거하여 해당 기능이 모든 기기에서 실행될 수 있도록 했고, 해당 스토리 자체의 테스트도 이를 깔끔하게 검증했습니다. 게이트 A와 게이트 B가 통과되었으므로 해당 스토리는 완료된 상태였습니다. 하지만 마이그레이션 이전 코드에서 남겨진, 다른 스토리에 태그된 오래된 테스트가 여전히 해당 게이트가 필요하다고 단언(Assert)하고 있었습니다. 개별 스토리 단위의 실행에서는 이 테스트를 전혀 건드리지 않았습니다. 에픽 전체의 스위트를 실행하자마자 에픽이 종료되는 순간 이를 잡아낼 수 있었습니다. 단 한 번의 개발자 (Developer) 사이클로 오래된 테스트를 삭제했고, 에픽 검증이 완료되었습니다.
확장성을 위한 아키텍처: "무엇(What)"과 "어떻게(How)"의 분리

작동 중인 Sage Orchestrator. 여기서는 병렬 스케줄러 (Parallel Scheduler)가 세 개의 서로 다른 스토리에 대한 구현 및 테스트를 동시에 처리하기 위해 다섯 개의 일시적인 워커 (Ephemeral Workers)를 생성했습니다. 각 에이전트 (Agent)의 토큰 텔레메트리 (Token Telemetry)를 주목하십시오. 이것이 이 정도 수준의 자율적 조정에 따르는 가시적인 비용입니다.
Sage를 단순한 안드로이드 (Android) 스크립트의 집합이 아닌 재사용 가능한 프레임워크로 만들기 위해, 저는 관심사의 엄격한 분리 (Separation of Concerns), 즉 일반적인 "무엇(What)"과 프로젝트 특화된 "어떻게(How)" 사이의 깔끔한 분리를 중심으로 구축했습니다.
각 에이전트 (ProductOwner, TestCreator, Developer, Tester, EpicVerifier)는 프로젝트에 구애받지 않는 고정된 직무 기술서를 가집니다. Developer 에이전트는 기능을 구현하는 방법은 알지만, 귀하의 프로젝트 폴더 구조나 테스트 프레임워크 (Testing Framework)는 알지 못합니다. 모든 프로젝트 지식 (테스트 명령, 코딩 컨벤션, 파일 경로 등)은 프로젝트의 .sage/ 디렉토리에 위치한 .yaml 설정 파일을 통해 런타임 (Runtime)에 주입됩니다.
역할을 환경으로부터 분리함으로써, 동일한 Developer 에이전트가 오전에는 Kotlin 기능을 구현하고 오후에는 Python 마이크로서비스 (Microservice)를 구현할 수 있습니다. 에이전트는 동일하게 유지되며, 변화하는 것은 프로젝트 컨텍스트 (Context)뿐입니다.
이 아키텍처 (Architecture)는 각 스토리 (Story)마다 일시적인 워커 (Ephemeral worker)를 생성하는 병렬 스케줄러 (Parallel scheduler)에 의해 관리되며, 이를 통해 팀은 여러 기능을 동시에 작업할 수 있습니다. 하지만 제가 빠르게 깨달았듯이, 병렬 에이전트 팀을 확장하는 것은 거대한 조정 (Coordination) 문제를 야기합니다. "환각 (Hallucinated)"된 채팅 기록에 빠지지 않고 어떻게 그들 모두를 동기화 상태로 유지할 수 있을까요?

Sage의 전체 파이프라인 (Pipeline). ProductOwner가 사양 (Spec)과 스토리를 초안 작성합니다; 병렬 스케줄러는 준비된 각 스토리당 하나의 일시적인 워커를 생성합니다 (TestCreator → Developer ⇌ Tester); EpicVerifier는 스토리 간 회귀 테스트 (Cross-story regression)를 통해 각 에픽 (Epic)의 루프를 닫습니다. 점선은 실패 경로입니다: 실패한 게이트 (Gate)는 전체 기능의 중단 대신 특정 스토리를 다시 엽니다.
프로토콜의 거대한 단순화
팀을 확장하려는 저의 첫 번째 시도는 조정의 재앙이었습니다. 병렬 환경에서 에이전트들은 명령을 무시하거나, 실제로 보내지 않은 메시지를 보냈다고 환각을 일으키는 등 통신 "패킷 (Packets)"을 누락하곤 했습니다. 저는 이를 해결하기 위해 SYN, SYN-ACK, ACK와 같은 3-way TCP 방식의 핸드셰이크 (Handshake)를 시도했습니다.
그 결과는 화려할 정도로 과도한 엔지니어링 (Over-engineering)이었습니다. 이는 지연 시간 (Latency)을 추가했고, "핸드셰이크 의식"에 수천 개의 토큰을 낭비하게 만들었습니다. 더 나쁜 것은, 다단계 재시도 (Retries), 메시지 ID, 승인 타이밍 테이블 등 프로토콜 오버헤드 (Protocol overhead)가 에이전트들을 매몰시켰다는 점입니다. 이 모든 것이 조정 핸드북 (Coordination handbook)에 고통스러울 정도로 상세하게 기술되었습니다. 에이전트들은 네트워크 카드 (Network card)가 아니었습니다. 그들은 모델 (Model)이었고, 규칙의 무게 아래 질식해가고 있었습니다.
저는 에이전트들이 더 잘 "소통(communicate)"하도록 만들려 노력하고 있었지만, 사실은 그들이 더 잘 "조정(reconcile)"하도록 만들어야 했다는 것을 깨달았습니다. 저는 핸드셰이크 (Handshake) 과정을 삭제하고, 신뢰할 수 있는 단일 원천 (Source of truth)을 디스크 상의 YAML 기반 상태 머신 (State machine)으로 옮겼습니다.
각 스토리의 현재 상태 (IN_DEV, TESTING, DONE)는 에이전트의 덧없는 메모리가 아니라, 디스크에 원자적 (Atomically)으로 기록되고 파일 잠금 (File-locked) 처리된 YAML 파일에 저장됩니다. 이는 내구성이 있는 현재 상태 (Durable current state)입니다. 화려한 기능도, 재생 로그 (Replay log)도, 조정 엔진 (Reconciliation engine)도 없습니다. 하지만 그 파일을 단일 신뢰 원천 (Single source of truth)으로 만듦으로써, 저는 전체 핸드셰이크 프로토콜을 삭제하고 단 하나의 규칙으로 대체할 수 있었습니다: 스토리 YAML을 다시 읽는 것. 이제 신뢰의 근거는 에이전트가 기억한다고 주장하는 무엇인가가 아니라, 바로 그 파일입니다.
정직한 트레이드오프 (The Honest Trade-offs)
Sage는 공짜 점심이 아닙니다. 이 프레임워크는 단일 샷 프롬프트 (Single-shot prompts)가 따라올 수 없는 수준의 신뢰성과 추적 가능성 (Traceability)을 제공하지만, 특정한 비용이 따릅니다. 첫 번째이자 가장 명백한 비용은 토큰 세금 (Token Tax)입니다. 멀티 에이전트 조정 (Multi-agent coordination)은 비용이 많이 듭니다. 모든 스토리에 대해 특화된 작업자 (Specialized workers)를 생성하고 이들을 게이트 (Gates)를 통과하게 하는 것은, 단일 롱 컨텍스트 프롬프트 (Long-context prompt)보다 훨씬 더 많은 토큰을 소모합니다. 여러분은 컴퓨팅 비용을 지불하는 대신, "인간 참여형 (Human-in-the-loop)" 검토 시간을 대폭 줄이는 선택을 하는 것입니다. 이는 버그 발생 비용이 토큰 비용보다 더 큰, 높은 신뢰성이 요구되는 기능에서만 유효한 계산입니다.
지연 시간 페널티 (Latency Penalty)도 존재합니다. Sage는 순수 속도를 위해 구축된 것이 아니라, 정확성을 위해 구축되었습니다. 병렬 에이전트 (Parallel agents)를 사용하더라도 멀티 스테이지 파이프라인 (Multi-stage pipeline)은 본질적인 오버헤드 (Overhead)를 가집니다. 여러분은 단일 프롬프트의 "즉각적인" 응답을 포기하는 대신, 멀티 게이트 검증 프로세스 (Multi-gate verification process)의 철저함을 얻는 것입니다. 또한, 이 프레임워크는 현재 Claude Code 전용 프리미티브 (Primitives)를 위한 레퍼런스 디자인 (Reference design)입니다. 핵심 아이디어는 플랫폼에 구애받지 않지만 (Port-agnostic), 구현은 Claude의 에이전트 도구 (Agentic tools)의 특정 동작에 의존합니다.
아마도 시스템에서 가장 취약한 부분은 오케스트레이터 (Orchestrator) 그 자체일 것입니다. 이 버전에서 사이클 예산 (cycle budgets), 데드락 탐지 (deadlock detection), 그리고 의존성 해결 (dependency resolution)을 처리하는 스케줄러의 로직은 마크다운 (Markdown) 파일 내의 지침으로 존재합니다. 이는 LLM이 관리하는 상태 머신 (state machine)입니다. 이 방식은 시스템을 믿을 수 없을 정도로 유연하게 만들고 반복 (iterate)하기 쉽게 만들지만, 프레임워크의 두뇌에 비결정론 (non-determinism) 계층을 도입합니다. 즉, 미래의 모델이 해당 지침을 다르게 해석하면 동작이 변하게 됩니다. 제가 계획 중인 해결책은 스케줄링 로직을 타입이 지정된 파이썬 (typed Python)으로 옮기고, 마크다운은 제어 흐름 (control flow)이 아닌 역할 프롬프트 (role prompts) 용도로만 남겨두는 것입니다.
결론: 성공적인 빌드 (Green Build) 그 이상
저는 Sage를 코드를 생성하는 가장 빠른 방법으로 만들기 위해 구축한 것이 아니라, 기능을 배포하는 신뢰할 수 있는 방법으로 만들기 위해 구축했습니다. 한때 테스트 없이 아키텍처적으로 완벽한 17개의 파일만을 전달했던 Breadcrumbs 코드베이스는 이제 개발자 (Developer)가 한 줄이라도 쓰기 전에 TestCreator를 먼저 실행합니다. 빈
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기