16개의 Claude 에이전트를 병렬로 돌려 Rust 기반 C 컴파일러를 처음부터 만들고, 리눅스 커널을 빌드할 수 있을 때까지 장기간 자율 실행 에이전트 팀을 운영하며 얻은 교훈을 정리한다.
URL: https://www.anthropic.com/engineering/building-c-compiler
Title: 병렬 Claude 팀과 함께 C 컴파일러 만들기
작성: Safeguards 팀 연구원 Nicholas Carlini
저는 우리가 “에이전트 팀(agent teams)”이라고 부르는, 언어 모델을 감독하는 새로운 접근을 실험해 왔습니다.
에이전트 팀에서는 여러 개의 Claude 인스턴스가, 사람의 적극적인 개입 없이, 하나의 공유 코드베이스에서 병렬로 작업합니다. 이 접근은 LLM 에이전트로 달성할 수 있는 범위를 극적으로 넓혀 줍니다.
이를 스트레스 테스트하기 위해, 저는 16개의 에이전트에게 Rust 기반 C 컴파일러를 처음부터 작성하도록 맡겼습니다. 목표는 리눅스 커널을 컴파일할 수 있을 정도의 수준이었습니다. 약 2,000번의 Claude Code 세션과 2만 달러의 API 비용을 들여, 에이전트 팀은 리눅스 6.9를 x86, ARM, RISC-V에서 빌드할 수 있는 10만 라인 규모의 컴파일러를 만들어 냈습니다.
이 컴파일러는 그 자체로도 흥미로운 산출물이지만, 이 글에서는 장시간 실행되는 자율 에이전트 팀을 위한 하네스(harness)를 설계하면서 배운 점—사람의 감독 없이도 에이전트가 궤도에서 벗어나지 않도록 하는 테스트 작성법, 여러 에이전트가 병렬로 진척을 낼 수 있게 작업을 구조화하는 방법, 그리고 이 접근이 맞닥뜨리는 한계—에 초점을 맞춥니다.
Claude Code 같은 기존의 에이전트 스캐폴드(scaffold)는 운영자가 온라인 상태로 함께 작업할 수 있어야 합니다. 길고 복잡한 문제의 해결을 요청하면 모델이 일부를 해결하긴 하지만, 결국에는 입력이 이어지길 기다리며 멈춥니다—질문, 상태 업데이트, 또는 추가 설명 요청 등 말이죠.
지속적이고 자율적인 진척을 끌어내기 위해, 저는 Claude를 단순한 루프에 넣어두는 하네스를 만들었습니다(ralph-loop를 본 적이 있다면 익숙할 겁니다). 한 작업을 끝내면 즉시 다음 작업을 집어 듭니다. (이건 실제 머신이 아니라 컨테이너에서 실행하세요.)
bash#!/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는 아주 기본적인 형태입니다. 새로운 bare git 저장소를 만들고, 각 에이전트마다 저장소를 /upstream에 마운트한 Docker 컨테이너를 띄웁니다. 각 에이전트는 /workspace에 로컬 복사본을 클론하고, 작업이 끝나면 컨테이너 내부의 로컬에서 upstream으로 푸시합니다.
두 에이전트가 동시에 같은 문제를 풀려고 하는 것을 막기 위해, 하네스는 단순한 동기화 알고리즘을 사용합니다.
이건 아주 초기 단계의 연구용 프로토타입입니다. 에이전트들 사이의 다른 커뮤니케이션 방법은 아직 구현하지 않았고, 상위 목표를 관리하는 프로세스를 강제하지도 않습니다. 오케스트레이션 에이전트도 사용하지 않았습니다.
대신 각 Claude 에이전트가 어떻게 행동할지 스스로 결정하게 두었습니다. 대부분의 경우 Claude는 “다음으로 가장 очевид한(가장 자연스러운) 문제”를 집어 듭니다. 버그에 막히면, Claude는 실패한 접근들과 남은 작업을 정리한 실행 중 문서를 유지하는 경우가 많습니다. 프로젝트의 git 저장소에서 히스토리를 읽어보면, 다양한 작업에 락을 걸며 진행하는 모습을 볼 수 있습니다.
스캐폴딩은 Claude를 루프 안에서 돌리지만, Claude가 어떻게 진척을 낼지 알 수 있을 때만 그 루프는 유용합니다. 제 노력의 대부분은 Claude 주변 환경—테스트, 환경, 피드백—을 설계해, 제가 옆에 없어도 Claude가 스스로 방향을 잡을 수 있게 만드는 데 들어갔습니다. 여러 Claude 인스턴스를 오케스트레이션할 때 특히 도움이 됐던 접근은 다음과 같습니다.
Claude는 제가 준 문제를 해결하기 위해 자율적으로 작업합니다. 그래서 태스크 검증기가 거의 완벽해야 합니다. 그렇지 않으면 Claude는 엉뚱한 문제를 해결해 버릴 수 있습니다. 테스트 하네스를 개선하려면 고품질 컴파일러 테스트 스위트를 찾고, 오픈소스 소프트웨어 패키지용 검증기와 빌드 스크립트를 작성하고, Claude가 저지르는 실수를 관찰한 뒤 그 실패 모드를 식별하면서 새로운 테스트를 설계해야 했습니다.
예를 들어 프로젝트 후반부에, Claude는 새로운 기능을 구현할 때마다 기존 기능을 자주 깨뜨리기 시작했습니다. 이를 해결하기 위해 저는 CI 파이프라인을 구축하고, Claude가 자신의 작업을 더 엄격하게 테스트할 수 있도록 강제하여, 새 커밋이 기존 코드를 깨지 못하게 했습니다.
저는 이 테스트 하네스를 ‘나’를 위해 쓰는 게 아니라 ‘Claude’를 위해 쓰고 있다는 점을 계속 상기해야 했고, 그 과정에서 테스트가 결과를 전달하는 방식에 대한 여러 가정을 다시 생각하게 됐습니다.
예를 들어 각 에이전트는 아무 맥락도 없는 새 컨테이너에 떨어지며, 특히 대형 프로젝트에서는 스스로 상황을 파악하는 데 상당한 시간을 씁니다. 테스트 이전 단계에서조차, Claude가 스스로를 돕도록, 현재 상태를 자주 업데이트해야 하는 광범위한 README와 진행 상황 파일을 유지하라는 지시를 포함했습니다.
또한 언어 모델에는 본질적인 한계가 있고, 이번 경우에는 이를 고려해 설계해야 했습니다. 예를 들면:
--fast 옵션으로 1% 또는 10%의 랜덤 샘플만 실행합니다. 이 서브샘플은 에이전트별로는 결정적(deterministic)이지만 VM 간에는 랜덤이어서, Claude는 여전히 전체 파일을 커버하면서도 각 에이전트가 회귀(regression)를 완벽히 식별할 수 있습니다.서로 다른 실패 테스트가 많이 있을 때는 병렬화가 쉽습니다. 각 에이전트가 다른 실패 테스트를 골라 해결하면 됩니다. 테스트 스위트가 99% 통과율에 도달한 뒤에는, 각 에이전트가 서로 다른 작은 오픈소스 프로젝트(예: SQlite, Redis, libjpeg, MQuickJS, Lua)를 컴파일하도록 작업했습니다.
하지만 에이전트들이 리눅스 커널을 컴파일하기 시작하면서 막혔습니다. 수백 개의 독립 테스트가 있는 테스트 스위트와 달리, 리눅스 커널 컴파일은 하나의 거대한 작업입니다. 모든 에이전트가 같은 버그를 맞닥뜨리고, 그 버그를 고친 다음, 서로의 변경을 덮어쓰게 됐습니다. 16개의 에이전트를 돌려도 각자 같은 일을 풀고 있어 도움이 되지 않았습니다.
해결책은 GCC를 온라인의 “정답 컴파일러(known-good compiler) 오라클”로 사용해 비교하는 것이었습니다. 저는 커널의 대부분 파일은 랜덤하게 GCC로 컴파일하고, 나머지 파일만 Claude의 C 컴파일러로 컴파일하는 새로운 테스트 하네스를 작성했습니다. 커널이 동작하면 문제는 Claude의 서브셋 파일에 있지 않았던 것입니다. 반대로 깨지면, 이들 파일 중 일부를 GCC로 다시 컴파일하면서 범위를 더 좁힐 수 있습니다. 이를 통해 각 에이전트가 서로 다른 파일에서 서로 다른 버그를 병렬로 고칠 수 있었고, 결국 Claude의 컴파일러가 모든 파일을 컴파일할 수 있게 됐습니다. (이게 동작한 뒤에도, 함께 실패하지만 각각은 단독으로는 동작하는 파일 쌍을 찾기 위해 델타 디버깅 기법을 적용할 필요가 있었습니다.)
병렬성은 전문화도 가능하게 합니다. LLM이 작성한 코드는 종종 기존 기능을 재구현하곤 하므로, 저는 한 에이전트에게 중복 코드를 찾아 통합(coalesce)하도록 맡겼습니다. 또 다른 에이전트는 컴파일러 자체 성능을 개선하도록 했고, 세 번째는 효율적인 컴파일 결과 코드를 출력하는 것을 책임지게 했습니다. 다른 에이전트에게는 Rust 개발자 관점에서 프로젝트 설계를 비판하고 전체 코드 품질을 높이기 위한 구조 변경을 수행하게 했고, 또 다른 에이전트는 문서를 맡겼습니다.
이 프로젝트는 역량 벤치마크(capability benchmark)로 설계했습니다. 저는 LLM이 오늘날 “간신히” 달성 가능한 한계를 스트레스 테스트함으로써, 미래에 모델들이 무엇을 안정적으로 달성하게 될지 대비하는 데 관심이 있습니다.
저는 C 컴파일러 프로젝트를 Claude 4 모델 시리즈 전체에 걸친 벤치마크로 사용해 왔습니다. 이전 프로젝트들에서 했던 것처럼, 먼저 원하는 목표를 초안으로 작성했습니다. 의존성 없이(from scratch) 최적화하는 컴파일러, 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 플랜과 비교해도 매우 비싼 프로젝트였습니다. 하지만 그 총액은 제가 이를 직접 만들 비용—팀 전체는 말할 것도 없고—의 일부에 불과합니다.
이 구현은 클린룸(clean-room) 방식입니다(개발 과정에서 Claude는 어떤 시점에도 인터넷 접근이 없었습니다). Rust 표준 라이브러리에만 의존합니다. 10만 라인의 이 컴파일러는 x86, ARM, RISC-V에서 부팅 가능한 Linux 6.9를 빌드할 수 있습니다. 또한 QEMU, FFmpeg, SQlite, postgres, redis를 컴파일할 수 있고, GCC torture test suite를 포함한 대부분의 컴파일러 테스트 스위트에서 99%의 통과율을 보입니다. 그리고 개발자의 궁극적 리트머스 테스트도 통과합니다. Doom을 컴파일하고 실행할 수 있습니다.
하지만 이 컴파일러에도 한계가 있습니다. 예를 들면:
결과물은 Opus 능력의 한계에 거의 도달했습니다. 위 제한들 중 몇 가지는 제가 (정말 열심히!) 고치려 했지만 완전히 성공하진 못했습니다. 새로운 기능과 버그 수정은 기존 기능을 자주 깨뜨렸습니다.
특히 어려웠던 예로, Opus는 16비트 리얼 모드로 부팅하기 위해 필요한 16비트 x86 코드 생성기를 구현하지 못했습니다. 컴파일러는 66/67 opcode prefix를 통해 올바른 16비트 x86을 출력할 수는 있지만, 결과 바이너리가 60kb를 넘어서 리눅스가 강제하는 32k 코드 제한을 크게 초과합니다. 그래서 Claude는 이 단계에서는 그냥 “치트”를 써 GCC를 호출합니다(이는 x86에서만 해당합니다. ARM이나 RISC-V에서는 Claude의 컴파일러만으로 완전히 컴파일 가능합니다.)
컴파일러 소스 코드는 여기서 확인할 수 있습니다. 다운로드해 코드를 읽어보고, 좋아하는 C 프로젝트에 적용해 보세요. 저는 언어 모델이 무엇을 할 수 있는지 이해하는 최고의 방법은, 한계까지 밀어붙인 다음, 어디서부터 무너지기 시작하는지 연구하는 것이라고 꾸준히 느껴 왔습니다. 앞으로 며칠간도 Claude가 이 제한들을 해결하려는 시도를 계속하도록 변경을 밀어 넣을 예정이니, 진행 상황을 따라오고 싶다면 지켜봐 주세요.
각 세대의 언어 모델은 그것을 활용하는 새로운 방식들을 열어줍니다. 초기 모델은 IDE에서 탭 완성에 유용했습니다. 곧 모델은 도크스트링만으로 함수 본문을 완성할 수 있게 됐습니다. Claude Code의 출시로 에이전트가 주류로 들어왔고, 개발자는 Claude와 페어 프로그래밍을 할 수 있게 됐습니다. 하지만 이러한 제품들은 모두 사용자가 작업을 정의하고, LLM이 수 초 또는 수 분 동안 실행해 답을 반환한 뒤, 사용자가 후속 질문을 하는 전제를 깔고 있습니다.
에이전트 팀은 전체적이고 복잡한 프로젝트를 자율적으로 구현할 가능성을 보여줍니다. 이는 이러한 도구의 사용자로서 우리가 목표를 더 야심차게 세울 수 있게 해 줍니다.
우리는 아직 초기 단계이며, 완전 자율 개발에는 실제 위험이 따릅니다. 사람이 개발 과정에서 Claude와 함께 앉아 있으면, 일관된 품질을 보장하고 오류를 실시간으로 잡을 수 있습니다. 자율 시스템에서는 테스트가 통과하는 것을 보고 일이 끝났다고 가정하기 쉽지만, 실제로는 그렇지 않은 경우가 드뭅니다. 저는 예전에 침투 테스트(penetration testing) 일을 하며 대기업 제품의 취약점을 악용하는 일을 했는데, 프로그래머가 스스로 검증하지 않은 소프트웨어를 배포할 수 있다는 생각은 정말 우려스럽습니다.
그래서 이 실험이 저를 흥분시키는 동시에 불안하게도 합니다. 이 컴파일러를 만드는 과정은 최근에 제가 경험한 가장 재미있는 일들 중 하나였지만, 2026년 초 이렇게 이른 시점에 이게 가능에 가까울 거라고는 기대하지 않았습니다. 언어 모델과, 그들과 상호작용하기 위해 사용하는 스캐폴드의 빠른 발전은 엄청난 양의 신규 코드를 작성할 수 있는 문을 열어줍니다. 긍정적 활용이 부정적 활용을 능가하리라 기대하지만, 우리는 안전하게 항해하기 위한 새로운 전략이 필요한 새로운 세계에 들어서고 있습니다.
도움과 기여를 해 준 Anthropic의 Josef Bacik, Edwin Chen, Bernardo Meurer Costa, Jake Eaton, Dan Kelley, Felix Klock, Jannet Park, Steve Weis, 그리고 다른 많은 분들께 특별한 감사를 전합니다.