병렬 Claude 에이전트 팀으로 C 컴파일러 만들기

TMT

https://www.anthropic.com/engineering/building-c-compiler

우리는 Opus 4.6에게 에이전트 팀을 사용해 C 컴파일러를 만들도록 시킨 뒤, 대부분의 시간 동안은 그냥 지켜보기만 했습니다. 여기서는 그 과정에서 우리가 자율적인 소프트웨어 개발의 미래에 대해 무엇을 배웠는지 공유합니다.

저는 우리가 “에이전트 팀(agent teams)”이라고 부르는, 언어 모델을 감독하는 새로운 접근 방식을 실험해 왔습니다.

에이전트 팀에서는 여러 개의 Claude 인스턴스가 사람의 직접적인 개입 없이, 병렬로 하나의 코드베이스에서 함께 작업합니다. 이 접근법은 LLM 에이전트로 달성할 수 있는 범위를 극적으로 넓혀 줍니다.

이를 스트레스 테스트하기 위해, 저는 16개의 에이전트에게 “리눅스 커널을 컴파일할 수 있는, Rust 기반 C 컴파일러를 완전히 처음부터 작성하라”는 과제를 부여했습니다. 거의 2,000회의 Claude Code 세션과 2만 달러에 달하는 API 비용을 들인 끝에, 이 에이전트 팀은 x86, ARM, RISC-V에서 Linux 6.9를 빌드할 수 있는 100,000라인짜리 컴파일러를 만들어냈습니다.

Demo Video

이 컴파일러 자체도 꽤 흥미로운 결과물이지만, 여기서는 저는 장시간 동안 자율적으로 동작하는 에이전트 팀을 위한 하네스를 어떻게 설계했는지에 초점을 맞추려 합니다. 구체적으로, 인간의 감독 없이도 에이전트를 올바른 방향으로 유지하게 만드는 테스트 작성법, 여러 에이전트가 병렬로 동시에 진전을 낼 수 있도록 작업을 구조화하는 방법, 그리고 이 접근이 어떤 지점에서 한계에 부딪히는지에 대해 이야기합니다.

장시간 실행되는 Claude 활성화하기

기존의 Claude Code 같은 에이전트 스캐폴드는, 함께 작업해 줄 운영자가 온라인 상태로 상주해 있어야 합니다. 길고 복잡한 문제의 해결을 요청하면, 모델은 그 일부를 해결할 수는 있지만, 결국에는 멈추고 계속 입력해 주길 기다리게 됩니다. 예를 들어 질문, 진행 상황 업데이트, 추가 설명 요청 같은 것을 기다리는 식입니다.

지속적이고 자율적인 진전을 이끌어 내기 위해, 저는 Claude를 아주 단순한 루프 안에 넣는 하네스를 만들었습니다(만약 Ralph-loop를 본 적이 있다면 익숙하게 느껴질 것입니다). 한 작업을 끝내면, Claude는 즉시 다음 작업을 집어 듭니다. (이 코드는 실제 머신이 아닌 컨테이너 안에서 실행하세요.)

#!/bin/bash

while true; do
    COMMIT=$(git rev-parse --short=6 HEAD)
    LOGFILE="agent_logs/agent_${COMMIT}.log"

    claude --dangerously-skip-permissions \
           -p "$(cat AGENT_PROMPT.md)" \
           --model claude-opus-X-Y &> "$LOGFILE"
done

에이전트 프롬프트에서는 Claude에게 어떤 문제를 풀어야 하는지 알려주고, 그 문제를 작은 조각으로 나누어 접근하고, 현재 무엇을 하고 있는지 추적하고, 다음에 무엇을 해야 할지 스스로 판단하며, 완벽해질 때까지 사실상 계속 작업을 이어가도록 지시합니다. (마지막 부분에 대해 말하자면, Claude에게 선택권은 없습니다. 이 루프는 영원히 돌아가도록 되어 있습니다. 실제로 한 번은 Claude가 실수로 pkill -9 bash⁠를 실행해 자기 자신을 죽이고 루프를 끝내 버린 적도 있습니다. 이런!)

Claude 병렬 실행하기

여러 개의 인스턴스를 병렬로 실행하면, 단일 에이전트 하네스가 가진 두 가지 약점을 보완할 수 있습니다.

  • 하나의 Claude Code 세션은 한 번에 한 가지 일만 할 수 있습니다. 특히 프로젝트의 범위가 커질수록, 여러 문제를 동시에 디버깅하는 것이 훨씬 효율적입니다.
  • 여러 Claude 에이전트를 실행하면 역할 분담과 전문화를 할 수 있습니다. 일부 에이전트는 실제 핵심 문제를 해결하는 데 집중시키고, 다른 특화된 에이전트는 예를 들어 문서를 유지 관리하거나 코드 품질을 모니터링하거나 특수한 하위 작업을 해결하는 등의 역할을 맡게 할 수 있습니다.

제가 구현한 병렬 Claude 시스템은 아주 기초적인 수준입니다. 우선 비어 있는 git 저장소를 새로 만들고, 각 에이전트마다 이 저장소를 /upstream⁠에 마운트한 Docker 컨테이너를 띄웁니다. 각 에이전트는 /workspace⁠로 로컬 복제본을 클론하고, 작업이 끝나면 자기 컨테이너의 로컬 저장소에서 upstream으로 변경 사항을 푸시합니다.

두 에이전트가 동시에 같은 문제를 해결하려 들지 않게 하기 위해, 하네스에는 단순한 동기화 알고리즘이 들어 있습니다.

  1. Claude는 current_tasks/ 디렉터리에 텍스트 파일을 하나 쓰는 방식으로 작업에 대한 “락(lock)”을 겁니다. 예를 들어 한 에이전트는 current_tasks/parse_if_statement.txt를, 또 다른 에이전트는 current_tasks/codegen_function_definition.txt를 락으로 잡는 식입니다. 두 에이전트가 같은 작업을 차지하려고 하면, git의 동기화 메커니즘 때문에 두 번째 에이전트는 다른 작업을 선택해야 합니다.
  2. Claude는 해당 작업을 진행한 뒤, upstream에서 변경 사항을 pull하고, 다른 에이전트의 변경을 merge한 뒤, 자신의 변경을 push하고, 마지막으로 락을 해제합니다. 머지 충돌은 자주 발생하지만, Claude는 이를 해결할 만큼 충분히 똑똑합니다.
  3. 이 무한 에이전트 생성 루프는 새로운 컨테이너에서 새로운 Claude Code 세션을 다시 띄우고, 이 사이클이 반복됩니다.

이것은 매우 초기 단계의 연구용 프로토타입입니다. 아직 에이전트 간 통신을 위한 다른 방식을 구현하지도 않았고, 상위 수준의 목표를 관리하기 위한 프로세스를 강제하지도 않았습니다. 오케스트레이션 전담 에이전트도 사용하지 않았습니다.

대신, 각 Claude 에이전트가 어떻게 행동할지 스스로 결정하도록 맡겨 두었습니다. 대부분의 경우 Claude는 “가장 다음에 처리해야 할 것이 분명한” 문제를 집어 듭니다. 버그에 걸려 막히면, Claude는 실패한 시도와 남아 있는 작업을 정리한 러닝 문서를 유지하는 경우가 많았습니다. 프로젝트의 git 저장소를 보면, 여러 작업의 락을 어떻게 잡고 풀었는지 히스토리를 통해 살펴볼 수 있습니다.

Claude 에이전트 팀으로 프로그래밍하며 얻은 교훈

이 스캐폴드는 Claude를 루프 안에서 계속 돌게 만들지만, 그 루프가 유용하려면 Claude가 스스로 어떻게 진전을 이뤄야 할지 알 수 있어야 합니다. 제 노력의 대부분은 Claude 주변 환경—테스트, 실행 환경, 피드백—을 설계해서, 제가 관여하지 않아도 Claude가 스스로 방향을 잡을 수 있도록 만드는 데 들어갔습니다. 여기에는 여러 Claude 인스턴스를 오케스트레이션할 때 특히 도움이 되었던 몇 가지 접근법이 포함됩니다.

극도로 높은 품질의 테스트를 작성하세요

Claude는 제가 주는 문제라면 무엇이든 자율적으로 해결하려고 합니다. 그렇기 때문에, 작업을 검증하는 장치(테스트)가 거의 완벽에 가까워야 합니다. 그렇지 않으면 Claude는 잘못된 문제를 풀어버릴 겁니다. 테스트 하네스를 개선하기 위해, 저는 고품질의 컴파일러 테스트 스위트를 찾고, 오픈소스 소프트웨어 패키지를 위한 검증 도구와 빌드 스크립트를 작성하고, Claude가 어떤 실수를 반복하는지 관찰한 다음, 그 실패 양상에 맞춰 새로운 테스트를 설계해야 했습니다.

예를 들어 프로젝트 막바지에는, Claude가 새로운 기능을 구현할 때마다 기존 기능을 자주 깨뜨리는 문제가 나타났습니다. 이를 해결하기 위해 저는 지속적 통합(CI) 파이프라인을 구축하고, Claude가 자신의 작업을 더 잘 테스트하도록 더 엄격한 규칙을 추가했습니다. 그 결과, 새로운 커밋이 기존 코드를 망가뜨릴 수 없도록 했습니다.

Claude의 입장에서 생각하세요

저는 계속해서 “이 테스트 하네스는 나 자신이 아니라 Claude를 위해 작성하는 것”이라는 점을 상기해야 했습니다. 즉, 테스트가 결과를 전달하는 방식을, 사람이 아니라 언어 모델의 관점에서 다시 생각해야 했습니다.

예를 들어, 각 에이전트는 아무런 맥락 없이 새 컨테이너에 떨어지며, 특히 큰 프로젝트의 경우 현재 상황을 파악하는 데 상당한 시간을 쏟게 됩니다. 테스트를 돌리기 전에, 먼저 Claude가 스스로를 도울 수 있도록, 현재 상태를 자주 업데이트하는 상세한 README와 진행 상황 문서를 유지하라는 지시를 포함시켰습니다.

또한 언어 모델에는 고유한 한계가 있다는 점을 고려해야 했습니다. 이 프로젝트에서는 다음과 같은 한계를 우회할 수 있도록 설계해야 했습니다.

  • 컨텍스트 윈도우 오염: 테스트 하네스가 수천 바이트의 쓸모없는 출력으로 로그를 채워서는 안 됩니다. 출력은 많아야 몇 줄로 제한하고, 중요한 정보는 모두 파일에 기록해 두어, Claude가 필요할 때 찾아볼 수 있도록 해야 합니다. 로그 파일은 자동으로 처리하기 쉽게 만들어야 합니다. 예를 들어 에러가 발생하면 Claude가 ERROR라는 단어를 적고, 같은 줄에 이유를 적어 두면 grep으로 바로 찾을 수 있습니다. 또한 Claude가 직접 다시 계산하지 않아도 되도록, 요약 통계는 미리 계산해 두는 것이 좋습니다.
  • 시간에 대한 맹목성: Claude는 시간을 인지하지 못하고, 그대로 두면 몇 시간 동안 테스트만 돌리면서 실질적인 진전을 못 내기도 합니다. 그래서 하네스는 컨텍스트를 오염시키지 않을 정도로만 간헐적으로 진행 상황을 출력하며, 기본적으로 --fast⁠ 옵션을 두어 전체의 1% 또는 10%만 무작위로 샘플링해서 테스트를 돌리도록 했습니다. 이 하위 샘플은 에이전트별로는 결정론적이지만, VM 사이에서는 랜덤이 되도록 구성해, 전체 파일은 여전히 모두 커버하면서도, 각 에이전트가 회귀(regression)를 정확히 식별할 수 있도록 했습니다.

병렬화를 쉽게 만들기

실패하는 테스트가 여러 개 있는 상황에서는, 병렬화는 매우 단순합니다. 각 에이전트가 서로 다른 실패 테스트를 하나씩 잡아 해결하면 됩니다. 테스트 스위트의 성공률이 99%에 도달한 뒤에는, 각 에이전트는 서로 다른 소규모 오픈소스 프로젝트(SQlite, Redis, libjpeg, MQuickJS, Lua 등)를 컴파일하는 작업을 맡아 진행했습니다.

하지만 에이전트들이 리눅스 커널을 컴파일하기 시작했을 때 상황이 꼬이기 시작했습니다. 수백 개의 독립 테스트로 이루어진 테스트 스위트와 달리, 리눅스 커널을 컴파일하는 작업은 하나의 거대한 단일 작업에 가깝습니다. 모든 에이전트가 똑같은 버그에서 막히고, 그 버그를 고치면, 서로의 변경 사항을 덮어써 버리는 상황이 반복됐습니다. 16개의 에이전트를 돌리고 있어도, 결국 모두 같은 문제를 붙잡고 있는 셈이라 병렬화의 이점이 사라졌습니다.

이를 고치기 위해, 저는 GCC를 “온라인 상에서 정답을 알려주는 컴파일러 오라클”로 사용하는 방식을 도입했습니다. 새 테스트 하네스를 만들어, 커널의 대부분은 랜덤하게 GCC로 컴파일하고, 나머지 파일만 Claude의 C 컴파일러로 컴파일하도록 했습니다. 커널이 정상 동작하면, 문제는 Claude가 담당한 파일들에는 없다는 뜻이고, 깨진다면 GCC로 다시 컴파일할 파일들을 바꾸어 가며 Claude의 담당 영역을 점점 좁혀 갈 수 있습니다. 이 방식 덕분에, 각 에이전트는 서로 다른 파일의 다른 버그를 병렬로 수정할 수 있었고, 결국 Claude의 컴파일러가 모든 파일을 컴파일할 수 있게 되었습니다. (이 과정이 끝난 뒤에도, 두 파일을 함께 컴파일할 때만 실패하고 각각 따로 컴파일하면 성공하는 경우를 찾기 위해 델타 디버깅 기법을 사용하는 작업이 여전히 필요했습니다.)

여러 에이전트 역할

병렬화는 역할 분담과 전문화를 가능하게 해 줍니다. LLM이 작성한 코드는 기존 기능을 다시 구현하는 경우가 흔하기 때문에, 저는 한 에이전트를 “중복 코드를 찾아 통합하는 역할”에 전담 배치했습니다. 또 다른 에이전트에게는 컴파일러 자체의 성능을 개선하도록 맡겼고, 세 번째 에이전트는 생성되는 기계어 코드가 더 효율적이도록 하는 데 집중하게 했습니다. 또 다른 에이전트에게는 Rust 개발자의 관점에서 프로젝트 설계를 비판적으로 검토하고, 전체적인 코드 품질을 개선하기 위한 구조 변경을 수행하게 했으며, 또 다른 에이전트는 문서 작업을 담당하게 했습니다.

에이전트 팀의 한계를 스트레스 테스트하기

이 프로젝트는 역량(capability) 벤치마크를 염두에 두고 설계되었습니다. 저는 LLM이 “지금 당장 겨우겨우 할 수 있는 일”의 한계를 밀어붙여 보고, 이를 통해 앞으로 모델들이 “안정적으로 할 수 있는 일”에 대비하는 데 관심이 있습니다.

저는 C 컴파일러 프로젝트를 Claude 4 전체 모델 계열에 대한 벤치마크로 사용해 왔습니다. 이전 프로젝트들에서 그랬듯이, 먼저 제가 원하는 결과물을 설계하는 것부터 시작했습니다. 즉, 의존성이 없고, GCC와 호환되며, 리눅스 커널을 컴파일할 수 있고, 여러 백엔드를 지원하도록 설계된, 최적화 기능을 갖춘 컴파일러를 완전히 처음부터 만드는 것이 목표였습니다. 저는 SSA IR을 사용해 여러 최적화 패스를 가능하게 하는 등 설계의 일부 측면을 명시하긴 했지만, 그 구현 방법에 대해서는 구체적인 지시를 하지 않았습니다.

이전 Opus 4 모델들은 “어찌어찌 돌아가는 수준”의 컴파일러를 겨우 만들어낼 수 있을 정도였습니다. Opus 4.5는 큰 테스트 스위트를 통과할 수 있는 기능적 컴파일러를 처음으로 만들어 낼 수 있었지만, 여전히 실제 대형 프로젝트를 컴파일할 수 있는 수준은 아니었습니다. Opus 4.6에서는 이 한계를 다시 한 번 시험해 보는 것이 목표였습니다.

평가

2주 동안 거의 2,000회의 Claude Code 세션을 거치며, Opus 4.6은 입력 토큰 20억 개, 출력 토큰 1억 4천만 개를 사용했고, 총 비용은 약 2만 달러에 조금 못 미쳤습니다. 가장 비싼 Claude Max 요금제를 기준으로 보더라도, 이 프로젝트는 상당히 고가였습니다. 하지만 제가 혼자서, 혹은 실제 팀과 함께 이 작업을 수행하는 데 들어갈 비용과 비교하면, 이 총액은 그 일부에 불과합니다.

이 프로젝트는 완전한 “클린룸 구현”이었습니다(개발 기간 동안 Claude는 인터넷에 전혀 접속할 수 없었습니다). 의존성은 Rust 표준 라이브러리뿐입니다. 이 10만 라인짜리 컴파일러는 x86, ARM, RISC-V에서 부팅 가능한 Linux 6.9를 빌드할 수 있습니다. 또한 QEMU, FFmpeg, SQlite, Postgres, Redis를 컴파일할 수 있으며, GCC torture test suite를 포함한 대부분의 컴파일러 테스트 스위트에서 99% 이상의 통과율을 기록했습니다. 개발자가 생각하는 궁극의 리트머스 시험도 통과했습니다. 이 컴파일러로 Doom을 컴파일하고 실행할 수 있습니다.

물론 이 컴파일러에도 한계가 있습니다. 대표적으로 다음과 같은 점들이 있습니다.

  • 리눅스를 실모드(real mode)에서 부팅하는 데 필요한 16비트 x86 컴파일러가 없습니다. 이 부분은 GCC를 호출해 처리합니다(x86_32와 x86_64용 컴파일러는 Claude의 구현입니다).
  • 자체 어셈블러와 링커가 없습니다. 이 부분은 Claude가 자동화를 시작했던 마지막 단계에 해당하며, 아직 다소 버그가 있습니다. 데모 영상은 GCC의 어셈블러와 링커를 사용해 제작했습니다.
  • 이 컴파일러는 많은 프로젝트를 성공적으로 빌드할 수 있지만, 모든 프로젝트에 대해 성공하는 것은 아닙니다. 아직 “실제 컴파일러를 완전히 대체할 수 있는 수준”은 아닙니다.
  • 생성되는 코드가 그다지 효율적이지 않습니다. 모든 최적화를 활성화하더라도, GCC에서 최적화를 전부 꺼 둔 상태보다 덜 효율적인 코드를 내놓습니다.
  • Rust 코드의 품질은 합리적인 수준이지만, 숙련된 Rust 전문가가 작성한 코드와 비교하면 여전히 상당한 차이가 있습니다.

결과적으로 이 컴파일러는 Opus의 역량 한계에 거의 도달한 상태입니다. 저는 위에 언급한 여러 한계를 고치기 위해 정말 많은 시도를 했지만, 모두 완전히 해결하지는 못했습니다. 새로운 기능과 버그 수정이 기존 기능을 다시 깨뜨리는 경우도 자주 발생했습니다.

특히 어려웠던 예로, 실모드로 부팅하기 위해 필요한 16비트 x86 코드 생성기를 구현하는 데 Opus가 끝내 성공하지 못했습니다. 이 컴파일러는 66/67 opcode 프리픽스를 사용해 올바른 16비트 x86 코드를 출력할 수는 있지만, 결과 바이너리가 60KB를 넘어가 리눅스가 요구하는 32KB 코드 제한을 크게 초과해 버립니다. 대신 Claude는 이 부분에서 그냥 “꼼수”를 쓰고 GCC를 호출합니다(이건 오로지 x86에만 해당됩니다. ARM이나 RISC-V의 경우, Claude의 컴파일러만으로 전체 컴파일 과정을 끝낼 수 있습니다).

컴파일러 소스 코드는 공개되어 있습니다. 직접 내려받아 코드를 읽어 보고, 여러분이 좋아하는 C 프로젝트에 적용해 보세요. 제가 경험한 바에 따르면, 언어 모델이 무엇을 할 수 있는지 이해하는 가장 좋은 방법은, 그 한계까지 밀어붙여 보고, 어디서부터 무너지는지 면밀히 살펴보는 것입니다. 앞으로 며칠 동안, 저는 Claude에게 이 한계들을 해결해 보도록 새로운 변경 사항을 계속 밀어 넣을 예정입니다. 관심이 있다면 Claude가 이 한계들을 계속해서 어떻게 다뤄 가는지 지켜보는 것도 좋을 것입니다.

앞으로를 내다보며

언어 모델의 세대가 바뀔 때마다, 우리는 이들을 다루는 새로운 방식을 얻게 됩니다. 초기의 모델들은 IDE에서 탭 자동완성을 돕는 데 유용한 수준이었습니다. 곧이어, 모델은 함수의 본문을 그 docstring만 보고 완성할 수 있게 되었습니다. Claude Code의 출시는 에이전트를 대중화했고, 개발자가 Claude와 함께 페어 프로그래밍을 할 수 있게 해 주었습니다. 하지만 이러한 제품들은 모두, 사용자가 작업을 정의하면 LLM이 몇 초에서 몇 분 동안 실행되었다가 답을 반환하고, 그다음은 사용자가 후속 요청을 보내는 방식이라는 전제를 깔고 있습니다.

에이전트 팀은 “복잡한 전체 프로젝트를 전부 자율적으로 구현하는 것”이 가능하다는 희망을 보여 줍니다. 이는 이러한 도구를 사용하는 우리에게, 목표를 훨씬 더 야심 차게 설정할 수 있는 가능성을 열어 줍니다.

아직은 초기 단계이며, 완전한 자율 개발에는 분명한 위험이 따릅니다. 사람이 Claude와 함께 앉아서 개발을 진행하면, 일관된 품질을 유지하고, 오류를 실시간으로 잡아낼 수 있습니다. 반면 자율 시스템에서는 테스트가 통과하는 것만 보고 “이제 끝났다”고 착각하기 쉽습니다. 실제로는 거의 그렇지 않은데도 말입니다. 저는 이전에 대기업 제품의 취약점을 악용하는 침투 테스트(penetration testing) 일을 했고, 개발자가 직접 검증해 보지도 않은 소프트웨어를 배포하는 상황을 떠올리면 걱정이 앞섭니다.

그래서 이 실험은 저를 매우 흥분시키는 동시에, 한편으로는 불안하게도 만듭니다. 이 컴파일러를 만드는 일은 최근 들어 제가 가장 즐겁게 했던 작업이지만, 2026년 초에 이 정도 수준이 가능할 거라고는 전혀 예상하지 못했습니다. 언어 모델과, 이를 둘러싼 스캐폴드의 빠른 발전은 엄청난 양의 새로운 코드를 작성할 수 있는 문을 열어 주고 있습니다. 긍정적인 활용 사례가 부정적인 결과를 압도하리라 기대하지만, 우리는 이제 완전히 새로운 세계로 들어서고 있고, 이 세계를 안전하게 헤쳐 나가기 위한 새로운 전략이 필요할 것입니다.

Edit this page