AI가 내 컴파일러를 작성했습니다. 수학적 증명이 매 빌드마다 작업 내용을 검증합니다.
요약
AI 에이전트가 작성하기 최적화된 언어인 Almide 설계 사례를 소개합니다. 모델의 성능을 높이기 위해 '수정 생존율(MSR)'이라는 지표를 도입하여, 에러 메시지를 통한 재시도 루프가 성공적으로 작동하도록 언어의 구문과 설계를 최적화하는 과정을 다룹니다.
핵심 포인트
- AI 에이전트용 언어 설계는 모델의 학습 데이터보다 언어 자체의 설계에 좌우됨
- 수정 생존율(MSR)은 모델이 에러를 통해 코드를 올바르게 수정할 수 있는지를 측정하는 핵심 지표
- 에이전트의 성공을 위해 에러 메시지와 구문은 복구 가능성에 초점을 맞춰야 함
- 모델의 선호도를 묻는 대신 실험적 검증(grammar-lab)을 통해 최적의 구문을 결정
4개월 전, 저는 근거는 부족하지만 한 가지 주장을 발표했습니다. AI가 특정 언어를 얼마나 쉽고 정확하게 작성할 수 있는지는 어떤 모델을 사용하느냐가 아니라, 그 언어의 설계에 달려 있다는 것이었습니다. 당시의 근거는 제가 제시한 8개의 테스트 문제를 모두 통과한 하나의 토이 언어(toy language)였지만, Ruby보다 결과에 도달하는 데 3배 더 오래 걸렸습니다. 저의 가설은 그 격차가 단순히 학습 데이터(training-data)의 문제라는 것이었습니다. 인터넷에 이 언어의 코드를 더 많이 올리면, 모델이 결국 스스로 따라잡을 것이라고 생각했습니다.
하지만 그런 일은 일어나지 않았습니다. 오늘날 공개된 인터넷상에 Almide 코드는 4개월 전보다 거의 늘어나지 않았습니다. 그런데도 Claude의 현재 모델들은 이제 10년 치의 학습 데이터를 보유한 성숙한 언어들보다 더 빠르고 더 적은 줄의 코드로, 제가 제시한 20개의 테스트 문제를 첫 번째 또는 두 번째 시도 만에 모두 해결합니다.
대신 실제로 변한 것은 다음과 같습니다.
모든 것에 답을 주는 단 하나의 지표
Almide는 AI 에이전트(AI agents)가 작성하도록 설계된 언어이며, 지난 4.5개월 동안 저는 컴파일러의 소스 코드를 거의 직접 만지지 않았습니다. 커밋 로그(commit log)의 Co-Authored-By: Claude 트레일러(trailers)가 제가 말하는 것보다 더 명확하게 그 사실을 말해줍니다.
이 정도 규모의 코드베이스를 통해 상태가 없는(stateless) 에이전트 군단을 조종하려면, 에이전트 중 그 누구도 지난 세션을 기억하지 못한다는 사실을 극복할 수 있는 정책이 필요했습니다. 해결책은 평범했습니다. 모든 에이전트가 작업을 수행하기 전에 읽는 파일을 만드는 것이었습니다. CLAUDE.md의 첫 번째 줄은 다음과 같습니다:
모든 설계 결정은 단 하나의 지표를 따릅니다: 수정 생존율 (modification survival rate).
수정 생존율 (Modification Survival Rate, MSR)은 "모델이 첫 번째 시도에 맞게 작성했는가"를 의미하는 것이 아닙니다. 그것은 다음과 같습니다: 모델이 코드를 작성하게 하고, 컴파일(compile)하게 합니다. 만약 컴파일에 실패하면, 모델에게 가공되지 않은 에러 메시지(raw error message)를 전달하고 최대 3번까지 재시도하게 합니다. 측정되는 것은 첫 번째 초안이 완벽했는가가 아니라, 이 루프가 작동하는 코드로 수렴(converge)하는지 여부입니다.
모델이 코드 작성 → 컴파일
성공 → 통과
실패 → 가공되지 않은 컴파일러 에러 표시 → 모델 재시도 → 다시 컴파일
...
그러한 프레임워크 설정 — 즉, 첫 번째 초안이 아니라 재시도 루프 (retry loop)를 평가하는 것 — 은 이 프로젝트에서 가장 영향력이 컸던 단 하나의 결정으로 판명되었습니다. 이는 모든 구문 (syntax) 선택, 모든 에러 메시지, 모든 표준 라이브러리 (stdlib) 함수 이름이 단 하나의 질문에 의해 판단됨을 의미합니다: "이것이 모델이 단순히 처음부터 틀리지 않게 하는 것을 넘어, 틀렸을 때 복구하는 데 도움이 되는가?"
증거가 있는 것들로 구문 축소하기
MSR (Model Success Rate)이 지표가 되자, 구문에 대한 논쟁은 취향의 문제가 아니라 측정 가능한 영역이 되었습니다. 첫 번째 논쟁은 이것이었습니다: 람다 (lambda)를 fn(x) => x + 1로 작성해야 할까요, 아니면 (x) => x + 1로 작성해야 할까요? 단 두 글자 차이였지만, 저는 더 짧은 쪽이 모델이 정확하게 작성하기 더 쉬울 것이라고 확신했습니다.
모델에게 어떤 것을 선호하는지 묻는 것은 무용지물입니다. 모델은 기본적으로 아첨하는 성향 (sycophantic)이 있어, 당신이 선호하는 쪽을 암시하면 그 옵션을 검증해 버리기 때문입니다. 그래서 저는 grammar-lab을 구축했습니다: 동일한 코드 수정 작업을 두 가지 구문에 대해 모두 실행하고, 출력이 컴파일되고 통과하는지 여부에 따라 점수를 매기는 방식입니다.
https://github.com/almide/almide/tree/develop/research/grammar-lab/
| 모델 (Model) | fn-lambda | paren-lambda | p-value |
|---|---|---|---|
| claude-sonnet-4-6 | 100% (25/25) | 100% (25/25) | 1.0 |
| claude-haiku-4-5 | 86% (26/30) | 86% (26/30) | 1.0 |
Sonnet은 두 경우 모두 한계치에 도달하여 신호가 없었습니다. 차이가 존재한다면 실제로 나타날 곳은 더 약한 모델인 Haiku였습니다. 하지만 차이는 없었습니다: 동일한 통과율, p=1.0, 가능한 가장 평탄한 귀무 가설 (null result) 결과였습니다. 차이가 있다는 증거가 없다는 것은 추가된 fn 글자들이 순전한 노이즈(noise)였다는 것을 의미하므로, 저는 이를 삭제했습니다. 최첨단 모델 (Frontier models)은 이전에 본 적 없는 언어에 대해 구문을 틀리는 경우가 거의 없습니다. 실패는 그런 곳에서 발생하지 않습니다.
그렇다면 실패는 실제로 어디에서 발생하는가
다른 AI 대상 언어에 대한 누군가의 평가 방식에서 빌려온, 이를 알아낼 수 있는 깔끔한 방법이 있습니다: 모델에게 치트 시트 (cheatsheet) 하나와 다른 맥락은 전혀 주지 않은 채, 사람들이 평소에 가볍게 작성하지 않는 것 — 레드-블랙 트리 (red-black tree) — 를 처음부터 구현하게 하는 것입니다.
저는 Almide에서 동일한 설정을 실행했습니다: Claude Opus, CHEATSHEET.md 파일 1개, 두 가지 문제(기본 계산기와 레드-블랙 트리 (red-black tree) 삽입/삭제), 각각 5회의 독립적인 실행, 그리고 정답을 모르는 별도의 심사 모델(judge model)을 통한 채점. 10번의 실행은 모두 결국 완료되었습니다. 10번 중 단 2번만이 첫 시도에 깨끗하게 성공했습니다. 나머지 8번은 모두 최소 한 번 이상 실패했으며, 실패 유형은 거의 완벽하게 한쪽으로 치우쳐 있었습니다:
- 구문 오류 (Syntax errors): 0
- 의미론적 오류 (Semantic errors): 0
- 표준 라이브러리 환각 (Stdlib hallucinations): 첫 실패 9건 중 9건 모두
최첨단 모델 (Frontier models)은 본질적으로 새로운 언어의 문법을 틀리는 법이 거의 없습니다. 이들이 틀리는 부분은 본 적 없는 라이브러리를 추측하는 것입니다. io.read_line은 Result를 반환하지 않지만, 모델은 반환한다고 가정하고 그 뒤에 ! unwrap을 붙여버렸습니다. 실제 함수는 int.parse이지만, 모델은 다른 수많은 언어에서 사용하는 방식인 int.from_string을 만들어냈습니다. 실제 간극을 메우는 것은 모욕적일 정도로 간단했습니다. 치트시트(cheatsheet)의 "stdin & parsing" 섹션에 13줄을 추가하자 해당 유형의 실패는 사라졌습니다.
두 번째 실패 모드는 달랐습니다. 다른 언어에서 통째로 가져온 습관들이었습니다. OCaml의 let ... in과 while ... do, 불리언 부정(boolean negation)을 위한 !x, 그리고 존재하지 않는 .to_upper() 메서드 발명 등이 그것입니다. 이는 Almide의 문법을 모르는 문제가 아닙니다. 모델이 지금까지 본 모든 언어를 평균 내어 그 혼합물(blend)을 기본값으로 사용하고 있는 것입니다. 전용 탐지기(detector)가 이를 포착하여 "Almide는 이것을 extunderscore extunderscore extunderscore 로 작성합니다"라고 명확하게 진단 결과를 반환합니다.
수정되는 순서에 따른 세 가지 실패 범주는 다음과 같습니다:
- 표준 라이브러리 추측 → 실제 시그니처 (signature)를 한 번 적어줌으로써 수정
- 교차 언어 습관 (Cross-language habits) → 탐지기와 진단 결과로 수정
- 그리고 찾아내는 데 가장 오래 걸렸던 것: 컴파일러 자체의 조언이 모델을 속이는 경우
에러 메시지는 인간을 위한 문단이 아니라 API입니다
대부분의 언어에서 컴파일 에러는 사람을 대상으로 하는 산문(prose)입니다. Almide에서 해당 에러를 읽는 주체는 코드를 작성하고 곧 수정안을 작성할 바로 그 모델이며, 이 과정에는 인간이 개입하지 않습니다(no human in the loop). 이는 에러 메시지의 성격을 완전히 재정의합니다. 즉, 에러 메시지는 문서가 아니라 API 응답이며, 다른 모든 API 응답과 마찬가지로 호출자를 적극적으로 오도할 수 있는 방식으로 틀릴 수 있습니다.
실제 사례 하나를 들면: 한 모델이 let (value, symbol) = pair라고 작성했습니다. 여기서 value는 버려지는 로컬 변수명으로 의도되었습니다. 그런데 Almide에는 문자 그대로 value라는 이름을 가진 stdlib(표준 라이브러리) 모듈이 포함되어 있습니다. 당시 타입 체커(type checker)는 이 구조 분해(destructuring) 패턴을 잘못 처리하여 value를 정의되지 않은 것으로 오진했고, 다음과 같이 제안했습니다:
error: undefined variable 'value'
hint: Add `import value` ← 틀림
모델은 충실하게 해당 import를 추가했고, 스스로 더 깊은 수렁에 빠졌습니다. 에러는 단순히 도움이 되지 않았을 뿐만 아니라 적극적으로 거짓말을 했고, 모델은 이를 신뢰했습니다. 해결책은 value나 error와 같은 흔한 로컬 변수 이름을 import 제안 휴리스틱(heuristic)에서 제외하는 것이었습니다. 모델을 더 나쁜 상태로 적극적으로 유도하는 진단(diagnostics) 사례들은 계속 목록에 기록되며 하나씩 해결해 나가고 있습니다.
여기서 참고할 만한 지점은 컴파일 에러를 판결이 아닌 대화로 취급하는 것으로 잘 알려진 Elm입니다:
https://elm-lang.org/news/compiler-errors-for-humans
Almide는 대화의 상대방을 인간에서 모델로 바꿈으로써 이를 한 단계 더 밀어붙입니다. 현재의 진단 결과는 다음과 같은 모습입니다:
error[E001]: type mismatch in fn 'sum_digits': expected Int but got Unit
--> unit_leak.almd:2:25
in fn 'sum_digits'
...
error[E001]은 31개의 에러 코드 중 하나이며, 각 코드에는 자체 문서와 재현 테스트 (repro test)가 포함되어 있습니다. CI(지속적 통합)는 이 중 하나라도 누락된 새로운 진단 코드를 거부합니다. try:는 단순히 채워 넣기 위한 템플릿이 아닙니다. 그것은 해당 실패 사례에 특화되어 바로 붙여넣을 수 있는 (paste-able) 수정 사항입니다. "타입 불일치 (Type mismatch)"가 판결이라면, "다음 줄에 abs_n을 추가하세요"는 모델이 추가적인 추론 없이도 실행할 수 있는 지침이며, 바로 이 부분이 재시도 루프 (retry loop)를 실제로 진전시키는 요소입니다.
각 코드에 대한 문서는 외부 어디에도 호스팅되지 않습니다:
almide explain E010
이 명령은 컴파일러 바이너리 (compiler binary)에서 직접 전체 설명(일반적인 원인, 실제 진단 샘플, 수정 옵션)을 반환합니다. 여기에는 두 가지 이유가 있습니다. 이 정도로 새로운 언어는 의지할 수 있는 Stack Overflow 답변이 없으며, 문서는 실행 중인 컴파일러와 동일한 바이너리에 포함되어 배포되므로 컴파일러와 문서 간의 버전 불일치 (drift out of sync)가 절대 발생할 수 없기 때문입니다.
다음 질문은 자연스럽게 이어집니다. 만약 try:가 말 그대로 붙여넣을 수 있는 코드라면, 모델은 왜 그것을 붙여넣는 것일까요?
almide fix app.almd
이 명령은 문맥에 관계없이 정확한 재작성 (rewrite) 방식이 단 하나뿐인 네 가지 카테고리에 대해 결정론적 수정 (deterministic fixes)을 직접 적용합니다:
- 누락된 임포트 (imports) (
json,fs등): 자동으로 추가됨 - 환각된 (Hallucinated) 비교 함수 (
int.gt(n, 0)): 실제 연산자 (n > 0)로 재작성됨 - 잘못 섞인 OCaml의
let ... in: 마지막in을 제거함 - 불필요한
return: Almide는 마지막 표현식 반환 (trailing-expression returns) 방식을 사용하므로 제거함
almide fix --json은 사람이 읽기보다는 에이전트(agent)의 자체 재시도 루프에서 소비되도록 설계된 기계 판독 가능 (machine-readable) 보고서를 출력합니다:
컴파일 실패 → almide fix가 결정론적 재작성 적용 → 재확인
성공 → 모델 불필요
에러 잔류 → 판단이 필요한 수정 사항만 모델에 전달됨
almide fix가 적용하는 모든 결정론적 수정은 모델을 거치는 왕복 횟수(round trip)를 한 번 줄여주며, 기존 오류를 수정하는 동안 새로운 실수를 도입할 가능성을 한 번 줄여줍니다.
의도적으로 선정된 약한 모델들을 대상으로 매일 효과 측정하기
진단(diagnostic)을 수정하는 것과 수정이 도움이 되었다고 느끼는 것은 서로 다른 문제입니다. Almide Dojo는 그 간극을 메우는 일일 측정 도구입니다:
https://github.com/almide/almide-dojo
- Fizzbuzz부터 레드-블랙 트리(red-black tree)까지, 난이도별로 분류된 31개의 문제
- GitHub Actions 작업이 매일 두 모델을 대상으로 31개 문제를 모두 실행
- 컴파일 실패 시 원본 진단(diagnostic) 메시지를 보여주며 최대 3회까지 재시도
- 결과는 매일 날짜가 지정된 커밋(commit)으로 기록
이 모델들은 의도적으로 Claude를 제외했습니다. Dojo는 Cloudflare Workers AI를 통해 두 개의 Llama 모델을 실행합니다: 3.3 70B 모델과 훨씬 더 작은 3.1 8B 모델입니다. 비용이 이유의 절반을 차지합니다. 나머지 절반은 측정 민감도(instrument sensitivity) 때문입니다. Dojo가 출시된 날 Claude는 29/30점을 기록했는데, 성능이 한계치(ceiling)에 고정된 모델은 수정 사항이 실제로 진전을 보였는지 여부를 보여줄 수 없습니다. Haiku를 문법 실험실(grammar-lab)에 배치했던 것과 동일한 논리로 Llama를 Dojo에 배치했습니다.
두 가지 크기의 모델을 나란히 실행하면 단일 모델로는 알 수 없는 사실이 드러납니다:
- 70B: 첫 시도에서 16/31 정답, 재시도가 허용되면 22/31까지 상승.
- 8B: 첫 시도에서 10/31에 머물며, 그 상태로 유지됨. 21개의 실패한 문제 × 3회의 재시도 = 63번의 추가 시도가 있었으나, 단 하나도 수렴(converge)하지 않음.
이 두 수치 사이의 간극은 다른 모든 요소를 배제한 채, "이 모델이 에러를 읽고 수정 방향으로 나아갈 수 있는가"를 정확히 나타냅니다. 이것이 대시보드의 핵심 지표가 첫 시도 정확도(first-try accuracy)가 아닌, 3회 시도 내 해결 비율인 이유입니다. 첫 시도 정확도는 모델을 측정하지만, 이 지표는 대화의 컴파일러 측면을 측정합니다.
대시보드의 또 다른 중요한 수치는 3회의 재시도를 모두 소진한 후에도 여전히 모델을 가로막고 있는 진단 코드(diagnostic codes)의 순위입니다. 그 순위가 바로 백로그(backlog)입니다. 이는 재시도 루프를 얼마나 자주 무력화하는지에 따라 컴파일러의 어느 부분이 여전히 작업이 필요한지를 직접적으로 알려줍니다.
매일 12:00 UTC: 모델이 31개 문제 해결
→ 실패 로그 기록
→ 진단(diagnostic) / 표준 라이브러리(stdlib) 수정 후보로 전환
...
두 Llama 모델 모두 몇 달 동안 변하지 않았습니다. 하지만 통과율(pass rate)은 변했습니다. 이것이 바로 정적 모델(static models)을 대상으로 매일 실행하는 핵심 이유입니다. 수치의 모든 변화는 AI가 아닌 언어(language)에 기인한 것입니다.
구문을 삭제하는 것은 가장 어려운 결정이자, 올바른 결정입니다
Almide v0.1.0은 서로 관련 없는 두 가지 작업, 즉 루프 본문(loop bodies)을 감싸는 것과 이펙트 함수(effect-fn) 본문을 감싸는 것에 do라는 단일 키워드를 사용했습니다. 하나의 키워드에 두 가지 의미 — 이것이 바로 모델이 생성 중간에 주저하게 만들고 일관되지 않은 출력(inconsistent output)을 생성하게 만드는 모호함의 전형입니다. 저는 어느 날 오후에 삭제 계획을 작성했고, 그날 밤 저장소(repo)에 있는 66개의 do 블록을 모두 while/guard로 다시 작성했습니다.
처음에는 경고를 포함한 폐기 기간(deprecation period)을 고려했습니다. 하지만 그렇게 하지 않았습니다. 1.0 버전에 도달하지 않은 언어는 누구에게도 마이그레이션 기간(migration window)을 제공할 의무가 없으며, do가 절반만 유효한 상태로 남아 있는 매일은 모델이 이를 생성할 수 있는 또 다른 날이 되기 때문입니다. 오늘 Almide에 do를 작성하면 컴파일러는 정확히 한 줄을 반환합니다:
`do` 블록이 제거되었습니다 — 루프에는 `while`을 사용하거나 이펙트 함수(effect fn) 본문에서 `do`를 제거하십시오
대체 수단(fallback)도, 부드러운 경고(soft warning)도 없습니다. 그저 대신 무엇을 작성해야 하는지를 가리키는 신호뿐입니다.
실제로 무엇을 최적화하고 있었는가
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기