Opus, Sonnet, Haiku를 한 달간 병행 사용하며 배운 점
요약
코딩 에이전트 운영 시 작업 유형에 따라 Claude Opus, Sonnet, Haiku 모델을 분리하여 사용하는 전략을 소개합니다. 모델 라우팅을 통해 API 비용을 35% 절감하고 지연 시간을 줄였으며, 어려운 작업의 품질을 높이는 성과를 거두었습니다.
핵심 포인트
- 작업의 모호성에 따라 모델을 분리하여 비용 효율성 극대화
- 단순 반복 작업에 최상위 모델의 자원을 낭비하지 않음
- 모델 라우팅을 통해 어려운 작업의 처리 품질 향상
- Haiku(대량/저모호성), Sonnet(기본), Opus(고난도) 계층화
요약 (TL;DR)
저는 간단한 린트(lint) 수정부터 다중 파일 리팩토링(refactor)까지 모든 것을 처리하는 완전 자율 코딩 에이전트(coding agent)를 운영하고 있습니다. 몇 달 동안 저는 모든 작업을 동일한 모델에 할당했습니다. 그러다 한 달 동안 습관 대신 작업 유형에 따라 Claude Opus, Sonnet, Haiku로 업무를 나누어 배정해 보았습니다. 그 결과 월간 API 지출은 약 35% 감소했고, 평균 작업 지연 시간(latency)도 줄어들었습니다. 그리고 — 저를 놀라게 했던 부분은 — 어려운 작업의 품질이 실제로 상승했다는 점입니다. 최고의 모델이 단순 반복 작업에 주의력(attention)을 낭비하는 것을 멈췄기 때문입니다. 제가 도달한 라우팅(routing) 로직, 그 과정에서 발생한 문제들, 그리고 첫날부터 알았더라면 좋았을 단 하나의 규칙을 소개합니다.
문제점
제 에이전트 설정은 하루에도 수십 개의 작은 코딩 작업들을 수행합니다. 깨진 테스트 수정, 모듈 전체에서 변수 이름 변경, 한 줄짜리 버그 수정 같은 작업부터, 때로는 "이 큐 컨슈머(queue consumer)의 재시도 로직을 재설계하라" 또는 "왜 이 레이스 컨디션(race condition)이 부하 상황에서만 발생하는지 파악하라"와 같이 까다로운 작업까지 포함됩니다.
오랫동안 저는 이 모든 것을 하나의 모델 — 당시의 최상위 Claude 모델 — 을 통해 실행했습니다. 제 논리는 게을렀지만 안전하다고 느껴졌습니다. "그냥 가장 좋은 것을 사용하자, 그러면 더 이상 고민할 필요가 없으니까."라고 생각했죠.
결국 두 가지 요인이 저로 하여금 재고하게 만들었습니다:
- 청구서. 제 일일 작업량의 상당 부분은 사소한 것들이었습니다 — 이것의 이름을 바꾸거나, 이 임포트(import)를 추가하거나, 이 린트 경고를 수정하는 등의 작업 말입니다. 저는 훨씬 더 저렴한 모델이 한 번에 처리할 수 있는 작업들에 대해 최상위 모델의 가격을 지불하고 있었습니다.
- 대기열(Queue). 열 개의 작은 작업과 하나의 진정으로 어려운 작업이 모두 동일한 모델에 할당되었을 때, 어려운 작업은 쉬운 작업들과 똑같은 줄에서 기다려야 했습니다. 제 라우팅 방식에는 "이 작업은 더 많은 주의가 필요하다"라고 말하는 부분이 없었기에, 어떤 작업도 더 많은 주의를 받지 못했습니다.
진정한 문제는 비용이나 속도 그 자체가 아니었습니다. 문제는 제가 "모든 것에 가장 좋은 모델"을 하나의 전략으로 취급했다는 점인데, 사실 그것은 전략이 없는 상태를 의미했습니다. 20분 동안 거대한 아키텍처 결정을 머릿속에 담아두는 데 탁월한 모델이 변수 이름을 바꾸는 데에도 당연히 적합한 도구는 아닙니다. 그리고 저는 그 가설을 실제로 검증해 본 적이 없었습니다.
해결 방법
저는 작업 큐(task queue)를 세 가지 버킷(bucket)으로 나누고, 각 버킷을 모델 계층에 맞게 매칭했습니다. 대량 처리를 위한 Haiku, 기본 작업용인 Sonnet, 그리고 틀렸을 때의 비용이 큰 작업을 위한 Opus로 나누었습니다.
버킷 1: Haiku — 기계적이고, 모호성이 낮으며, 대량인 작업
여기에 해당하는 작업들: 린트 수정(lint fixes), 임포트 정렬(import sorting), 파일 전반에 걸친 심볼 이름 변경(renaming a symbol), diff를 통한 커밋 메시지 작성, PR이 테스트를 수정하는지 소스 코드를 수정하는지 분류, 로그 파일 요약 등입니다. 핵심적인 특징은 "작은 작업"이 아니라 **낮은 모호성(low ambiguity)**입니다. 기본적으로 정답이 하나뿐이며, 모델이 정답에 도달하기 위해 트레이드오프(trade-offs)를 고민할 필요가 없는 작업들입니다.
def pick_model(task):
if task.category in ("lint_fix", "rename", "commit_message", "log_summary"):
return "claude-haiku-4-5"
...
이 방식은 의도적으로 단순하게 설계되었습니다. 실시간으로 결정하는 스마트한 분류기(classifier)가 아니라 카테고리 조회(category lookup) 방식입니다. 저는 모델 호출을 통해 어떤 모델로 라우팅할지 "결정"하는 메타 에이전트(meta-agent)를 구축하려고 시도해 보았으나, 작업 유형만으로 카테고리가 명확한 경우에는 모델 호출 자체가 낭비였습니다. 정답이 변하지 않는 80%의 작업에 대해서는 동적 라우터(dynamic router)보다 정적 규칙(static rules)이 더 효과적이었습니다.
버킷 2: Sonnet — 기본 작업용(default workhorse)
명백하게 기계적이거나 명백하게 위험 부담이 큰 작업이 아닌 모든 것이 여기에 해당합니다. 일반적인 기능 구현(feature implementation), 대부분의 버그 수정, 기존 코드에 대한 테스트 작성, 일상적인 리팩터링(refactor) 등이 포함됩니다. 이것이 저의 기본 설정입니다. 어떤 작업이 어느 버킷에 속하는지 확실하지 않다면, "혹시 모르니까" Opus로 올리는 것이 아니라 Sonnet으로 보냅니다. 이 단 하나의 습관 변화(상향 기본값이 아닌 하향 기본값 설정)가 Haiku 버킷을 사용했을 때보다 더 많은 비용 절감을 가져왔습니다.
버킷 3: Opus — 틀렸을 때 비용이 큰 작업
이 버킷은 의도적으로 작게 구성했습니다. 아키텍처 결정(architectural decisions), 증상을 임시방편으로 가리는 대신 근본 원인을 해결해야 하는 간헐적 실패(intermittent failures) 디버깅, 인증(auth)이나 데이터 무결성(data integrity)과 관련된 모든 작업, 그리고 에이전트가 장시간 최소한의 감독 하에 작동해야 하는 작업들이 여기에 해당합니다. 이들의 공통점은 여기서 잘못된 답변이 나올 경우 단순히 재시도(retry) 비용만 발생하는 것이 아니라, 후속 작업(downstream)의 수 시간 분량의 정리 작업이 필요하거나, 추적 비용이 많이 드는 버그를 배포하게 된다는 점입니다.
flowchart LR
A[Incoming task] --> B{Category known?}
B -- mechanical/high-volume --> C[Haiku]
...
에스컬레이션 경로 (The escalation path)
이 방식을 실제로 안전하게 배포할 수 있게 만든 부분은 라우팅 테이블(routing table) 자체가 아니라, 폴백 규칙(fallback rule)이었습니다. 만약 Haiku 또는 Sonnet 작업이 검증(validation)에 두 번 실패하면, 자동으로 한 단계 상위 티어로 에스컬레이션(escalate)됩니다. "검증 실패"란 테스트 스위트(test suite)가 여전히 실패하거나, 디프(diff)가 깔끔하게 적용되지 않거나, 후속 확인 과정에서 변경 사항이 부분적으로만 완료된 것으로 표시되는 경우를 의미합니다. 이 규칙이 없다면, 잘못 분류된 작업은 잘못된 티어에서 재시도 비용만 낭비할 뿐, 실제로 필요한 추가적인 추론(reasoning)을 얻지 못하게 됩니다. 이 규칙 덕분에 저는 저렴한 티어를 매우 공격적으로 저렴하게 사용할 수 있었습니다. 분류가 처음부터 정확할 것이라는 것에 모든 작업을 걸지 않아도 되기 때문입니다.
def run_task(task, attempt=0):
model = escalate(task.model, attempt) if attempt > 0 else pick_model(task)
result = call_model(model, task)
...
실제 수치는 어떠했는가
저는 계층형 라우팅(tiered routing)으로 전환하기 전후 4주 동안 이를 추적했습니다. 두 기간 모두 최대한 통제 가능한 범위 내에서 동일한 작업 부하(workload mix)를 유지했습니다.
| 지표 (Metric) | 단일 모델 기준선 (Single-model baseline) | 계층형 라우팅 (Tiered routing) |
|---|---|---|
| 월간 API 지출 (Monthly API spend) | 100% (기준선) | ~65% |
| ... |
비용 절감보다 더 놀라웠던 점은 중앙값 처리 시간(median turnaround)의 감소였습니다. 저는 지연 시간(latency)이 주로 작업 복잡도(task complexity)에 달려 있다고 가정했지만, 실제로는 상당 부분이 대기열(queueing) 문제였습니다. Haiku와 Sonnet 모두 Opus보다 호출당 응답 속도가 빠르기 때문에, Opus가 필요하지 않은 89%의 작업을 Opus로부터 분리하여 라우팅한 결과, 개별 작업뿐만 아니라 전체 파이프라인의 속도가 빨라졌습니다.
9%의 에스컬레이션 비율(escalation rate)은 현재 제가 가장 면밀히 관찰하는 수치입니다. 이 비율이 서서히 올라간다면, 이는 대개 제 카테고리 목록이 실제 작업 혼합(task mix)과 괴리되었음을 의미합니다. 즉, 이는 계층형 라우팅 자체가 실패하고 있다는 증거가 아니라, 라우팅 테이블(routing table)을 업데이트해야 한다는 신호입니다.
배운 점 (Lessons Learned)
-
상향이 아닌 하향을 기본값으로 설정하라 (Default down, not up). 가장 큰 비용 절감 효과는 Haiku 버킷에서 온 것이 아니었습니다. 확신이 서지 않을 때마다 반사적으로 최상위 모델을 찾는 대신, "불분명한" 작업에 대해 Sonnet을 기본값으로 설정한 것이 핵심이었습니다. 제가 최고 모델이 필요하다고 생각했던 대부분의 작업은 사실 그렇지 않았습니다.
-
축(axis)은 크기가 아니라 모호함이어야 한다. 처음에는 "이 작업이 몇 줄의 코드를 건드리는가"를 기준으로 라우팅을 시도했으나 결과가 좋지 않았습니다. 미묘한 레이스 컨디션(race condition)을 해결하기 위한 한 줄짜리 수정은 규모는 작지만 모호함이 낮은 작업은 아닙니다. 기준을 "명확하게 정답이 하나 존재하는가"로 바꾼 후, 라우팅의 정확도가 훨씬 높아졌습니다.
-
대부분의 작업에는 스마트한 동적 라우터보다 멍청한 정적 라우터가 낫다. 초기 버전에서는 모델 호출이 어떤 모델로 라우팅할지를 결정하게 하여 실제 비용을 낭비했습니다. 카테고리가 명확한 약 80%의 작업에 대해, 이는 이미 조회 테이블(lookup table)이 알고 있는 것을 결정하기 위해 모델 호출을 한 번 더 소모하는 꼴이었습니다.
-
실패 시 에스컬레이션(Escalation-on-failure)이 저가형 계층을 안전하게 만든다. 자동 에스컬레이션이 없다면, 작업을 Haiku로 라우팅하는 것은 회복 기회 없이 단 한 번의 도박을 거는 것과 같습니다. 하지만 에스컬레이션이 있다면, 이는 안전망이 있는 저렴한 첫 번째 시도가 됩니다. 제가 가장 저렴한 계층에 어떤 작업이든 보낼 수 있었던 유일한 이유는 바로 이 점 때문이었습니다.
어려운 작업들이 단순히 저렴해진 것이 아니라, 더 좋아졌습니다. 이것이 진짜 놀라운 점이었습니다. Opus가 전체 볼륨의 100% 대신 약 10% 정도만 처리하게 되자, 저는 더 이상 Opus의 용량을 "낭비"하고 있다는 느낌을 받지 않게 되었습니다. 이는 곧 제가 어려운 작업들에 대해 간결한 설명 대신, 더 많은 컨텍스트 (Context), 더 많은 제약 조건 (Constraints), 그리고 더 많은 주변 코드를 제공하기 시작했음을 의미합니다. 모델이 더 똑똑해진 것이 아니라, 모든 작업에 모델을 억지로 끼워 맞출 필요가 없어졌기에 제가 입력값(Inputs)에 대해 덜 인색해진 것입니다.
- 비용뿐만 아니라 에스컬레이션 비율 (Escalation rate)을 추적하세요. 비용 절감은 분류가 잘못되었을 때조차 계층형 라우팅 (Tiered routing)이 성공적인 것처럼 보이게 만들 수 있습니다. 왜냐하면 Haiku는 실패하고 재시도할 때조차 저렴하기 때문입니다. 에스컬레이션 비율은 라우팅 테이블이 실제 작업 혼합 (Task mix)과 일치하는지를 실제로 알려주는 지표입니다. 저는 이제 이를 매주 확인하며, 이 수치가 상승하면 나중에 품질 문제로 나타나기 전에 카테고리 목록을 재검토하는 신호로 삼습니다.
다음 단계
저는 라우팅 카테고리가 스스로 업데이트되도록 만드는 작업을 진행 중입니다. 현재 "아키텍처 (Architecture)" 대 "일상적인 리팩터링 (Routine refactor)"는 수동으로 관리되는 작업 유형 문자열 목록이며, 제 코드베이스와 워크플로가 변함에 따라 점차 어긋나고 있습니다. 저는 초기 추측(이 글을 쓰게 만들 정도로 자주 틀렸던 그 추측들)에서 유도된 카테고리보다는, 결과(역사적으로 어떤 작업 유형이 실제로 에스컬레이션을 필요로 했는지)로부터 유도된 카테고리를 갖기를 원합니다.
또한, 동일한 3단계 분할 방식이 코딩 이외의 에이전트 작업 — 연구 요약, 데이터 추출 — 에도 유효할지, 아니면 도메인별로 모호성 축 (Ambiguity axis)을 재정의해야 할지도 궁금합니다. 만약 코딩 에이전트 이외의 분야에서 다계층 라우팅 (Multi-tier routing)을 시도해 보셨다면, 어떤 기준으로 경계를 나누었는지 진심으로 듣고 싶습니다.
마무리 / CTA
만약 Claude Code 에이전트를 실행하면서 여전히 모든 작업을 동일한 모델로 지정하고 있다면, 다음 20개의 작업을 실행하기 전에 "기계적인 (mechanical)", "일반적인 (normal)", 그리고 "틀렸을 때 비용이 많이 드는 (expensive-to-be-wrong)" 작업으로 나누어 보세요. 아마도 마지막 범주에 속하는 작업이 얼마나 적은지 보고 놀라게 될 것입니다. 이 내용이 유익했다면, 이곳 Dev.to에서 저를 팔로우해 주세요. 저는 실수까지 포함하여 이 모든 build-in-public 시리즈를 진행하며 작성하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기