내 에이전트 오케스트레이터가 작업당 100만~200만 Opus 토큰을 소모했습니다. 사후 분석(Postmortem) 결과입니다.
요약
Claude Code를 위한 에이전트 오케스트레이션 설계 과정에서 발생한 막대한 토큰 비용 문제를 분석합니다. 순수 위임 방식의 설계 결함과 모델 선택 전략 부재로 인해 작업당 100만~200만 Opus 토큰이 소모된 원인을 다룹니다.
핵심 포인트
- 모든 작업을 하위 에이전트에게 위임하는 구조가 비용 폭증의 원인이 됨
- 단계별로 새로운 컨텍스트를 할당하는 방식은 컨텍스트 오염을 막지만 비용을 높임
- 모델 파라미터 미설정 시 상위 모델(Opus)이 상속되어 불필요한 비용 발생
- 작업 난이도에 따른 모델 티어링(Opus, Sonnet, Haiku) 전략의 중요성
저는 Claude Code를 위해 모든 것을 하위 에이전트(subagents)에게 위임하는 오케스트레이션 기술을 구축했습니다. 작동은 잘 되었습니다. 하지만 최종 diff가 단 몇 줄에 불과한 작업들을 포함하여, 작업당 약 100만~200만 Opus 토큰 정도의 비용이 발생했습니다.
무언가 고장 난 것은 아니었습니다. 개별적인 결정 하나하나가 정당했습니다. 세 가지의 적당한 승수(multipliers)가 중첩되었고, 그 전체 스택이 모든 요청마다 실행되었습니다.
이 글은 사후 분석(postmortem), 재설계, 그리고 제가 처음에 작성했어야 했던 강제 계층(enforcement layer)에 관한 기록입니다.
v1: 순수 위임 (pure delegation)
설계 목표는 컨텍스트 위생(context hygiene)이었습니다. 메인 세션은 파일 내용, 도구 출력, 막다른 길 등이 쌓이면서 빠르게 오염되며, 컨텍스트 윈도우(window)가 채워짐에 따라 판단력이 저하됩니다. 그래서 다음과 같이 결정했습니다: 메인 세션이 어떤 작업도 직접 하지 않게 할 것. 대신 코디네이터(coordinator) 역할을 맡기고, 실제 작업의 각 단위에는 새로운 컨텍스트를 부여하는 것입니다.
그 결과 네 가지 규칙이 만들어졌습니다:
- 엄격한 게이트(A hard gate). 메인 세션은 스스로 읽기, 편집 또는 실행하는 것이 금지되었습니다. 모든 동작은 하위 에이전트(subagent)를 통해 이루어졌습니다.
- 모든 작업에 적용되는 고정된 5단계 파이프라인(5-phase pipeline). 계획(Plan) → 승인(Approve) → 실행(Execute) → 검토(Review) → 보고(Report).
- 단계별 새로운 하위 에이전트(Fresh subagents per phase). 재사용하지 않습니다. 각 단계는 구조적으로 깨끗한 컨텍스트를 할당받았습니다.
- "깨끗해질 때까지 반복"을 명령하는 필수 검토자(Mandated reviewers). 아무것도 발견되지 않을 때까지 재실행되는 검토 단계입니다.
그리고 트리거는 광범위했습니다. 본질적으로 실행 가능한 모든 요청, 즉 "이것을 해줘", "구현해줘", "수정해줘", "만들어줘", "변경해줘" 등이 해당되었습니다.
정확성(correctness)의 관점이 아닌 비용(cost)의 관점에서 이 네 가지 규칙을 다시 읽어보십시오. 그것이 사후 분석의 핵심입니다.
세 가지 승수 (The three multipliers)
1. 디스패치 스키마가 model을 선택 사항으로 만듦
하위 에이전트 디스패치 도구는 model 파라미터를 받습니다. 제가 만든 기술은 이를 설정하지 않았습니다. 생략될 경우 부모 세션으로부터 상속받는데, 당시 부모 세션은 Opus 4.8이었습니다.
따라서 "이 파일을 읽고 요약하라"가 전체 작업인 하위 에이전트들을 포함하여, 모든 하위 에이전트가 사용 가능한 가장 비싼 티어에서 실행되었습니다.
이것이 실제 정가 기준으로 얼마나 비용이 발생하는지는 다음과 같습니다:
| 모델 | 입력 $/MTok | 출력 $/MTok | vs. Opus |
|---|---|---|---|
Claude Opus 4.8 (claude-opus-4-8) | $5.00 | $25.00 | 1× |
| ... |
여기서 한 가지 짚고 넘어가고 싶은 점이 있습니다. 이 사건에 대한 저의 첫 보고서에서 제가 잘못 파악했던 부분이기 때문입니다: Opus는 Sonnet보다 5배 비싼 것이 아닙니다. 약 1.7배 정도입니다. Opus는 Haiku보다는 정확히 5배 비쌉니다. 만약 티어링(tiering) 전략을 세우고 있다면, Opus에서 Sonnet으로 전환하는 것은 40%의 비용 절감이며, 정말 사소한 작업에 Opus에서 Haiku로 전환하는 것은 80%의 비용 절감입니다.
즉, 모델 세금(model tax)은 제가 계산했던 세 가지 배수 중 가장 작은 부분이었습니다. 저는 전체 비용의 원인이 모델 비용이라고 비난해 왔으나, 실제로는 전혀 그렇지 않았습니다.
2. "모든 컨텍스트 포함"과 메모리 부재의 결합 = 매번 발생하는 콜드 캐시 (cold cache)
이것이 가장 비용이 많이 드는 부분이며, 제가 파악하는 데 가장 오래 걸렸습니다. 왜냐하면 증상("에이전트가 저장소(repo)를 다시 읽음")이 토큰 수의 문제처럼 들리지만, 실제로는 캐시 접두사 (cache-prefix) 문제이기 때문입니다.
프롬프트 캐싱 (Prompt caching)은 접두사 일치 (prefix match) 방식입니다. 캐시 키는 렌더링된 프롬프트의 정확한 바이트에서 생성되며, tools → system → messages 순서로 각 cache_control 중단점까지 이어집니다. N번째 위치에서 단 1바이트만 달라도 N 이후의 모든 내용은 캐시 미스 (miss)가 됩니다.
이로 인한 경제적 차이는 다음과 같습니다:
- 캐시 읽기 (Cache read): 기본 입력 가격의 약 0.1×
- 캐시 쓰기 (Cache write): 기본 입력 가격의 1.25× (5분 TTL 기준; 1시간 TTL의 경우 2×)
따라서 캐시 읽기는 90% 할인이지만, 콜드 쓰기 (cold write)는 25%의 프리미엄이 붙습니다. 동일한 토큰에 대해 최상의 경우와 최악의 경우 사이의 격차는 대략 12배에 달합니다.
이제 여기에 새로운 하위 에이전트 (subagent)를 대입해 보십시오. 새로운 하위 에이전트는 새로운 접두사입니다. 하위 에이전트의 system, tools, model이 부모 에이전트와 바이트 단위로 완전히 일치하지 않는 한 부모의 캐시된 프롬프트를 상속받지 못합니다. 저의 경우 각 단계마다 맞춤형 지침이 부여되었기 때문에 일치하지 않았습니다. 제가 생성한 모든 하위 에이전트는 "모든 것을 포함(include ALL)"하라고 지시받은 전체 저장소 컨텍스트에 대해 콜드 쓰기 비용을 지불해야 했습니다.
병렬 처리를 수행하면 상황은 더 악화됩니다. 캐시 엔트리(cache entry)는 첫 번째 응답이 스트리밍(streaming)되기 시작해야만 읽을 수 있는 상태가 됩니다. 동일한 접두사(prefix)를 가진 5개의 하위 에이전트(subagent)를 동시에 실행하면, 5개 모두가 전체 비용을 지불해야 합니다. 그들 중 누구도 다른 에이전트가 아직 쓰고 있는 내용을 읽을 수 없기 때문입니다.
제 설계에는 에이전트당 최대 컨텍스트(context)를 보장하는 규칙, 에이전트당 새로운 접두사(prefix)를 보장하는 규칙, 그리고 동시적인 콜드 쓰기(cold writes)를 보장하는 팬아웃(fan-out) 패턴이 있었습니다. 세 가지 규칙이 하나의 청구서를 만들어낸 것입니다.
3. 무제한 루프에 의한 팬아웃(Fan-out)의 배가
작업당 최소 5개 이상의 에이전트가 할당되었습니다. 단계별로 하나씩, 단계가 병렬화될 때는 더 많이 할당되었습니다. 게다가 "정리될 때까지 루프(loop until clean)"라는 규칙은 리뷰 단계에 리뷰어 자신의 출력물에 대한 스스로의 판단 외에는 어떠한 종료 경계(termination bound)도 제공하지 않았습니다.
한 번의 패스(pass)마다 사소한 결함을 하나씩 찾아내는 리뷰어는 영원히 실행됩니다. 첫 번째 패스에서 아무것도 찾지 못한 리뷰어라 할지라도 에이전트 한 대 분량의 비용은 그대로 발생합니다.
곱셈 효과
이 중 어느 하나도 단독으로는 터무니없는 수준이 아닙니다. 하지만 이들을 곱하면 다음과 같습니다.
| 요소 | 배수 |
|---|---|
| 필요하지 않은 작업에 Sonnet 대신 Opus 사용 | ~1.7배 |
| ... |
이는 "잘 캐싱된 Sonnet 에이전트 하나가 작업을 수행한다"는 기준점과 비교했을 때 100배에서 300배 사이의 범위에 달합니다. 트리거가 광범위했기 때문에, 모든 실행 가능한 요청에 이 수치가 적용되었습니다.
100만~200만이라는 숫자는 더 이상 놀라운 일이 아닙니다. 그것이 바로 이 아키텍처가 설계된 대로 작동한 결과였습니다.
왜 알아차리지 못했는가
그 루프 어디에도 백프레셔(backpressure)가 없었기 때문입니다.
모델은 누적된 세션 비용을 볼 수 없습니다. 모델은 실행 합계(running total)도, 예산도, 에이전트 #14가 에이전트 #2와 다르다는 신호도 가지고 있지 않습니다. API는 작업 예산(output_config.task_budget)을 제공하지만, 이는 단일 에이전트 요청(하나의 루프 내에서의 사고, 도구 호출 및 출력)만을 제어합니다. 이는 제 비용이 발생한 지점인 세션 전체에 걸친 독립적인 하위 에이전트 디스패치(subagent dispatches)를 포괄하지 못합니다.
따라서 저와 150만 토큰의 청구서 사이를 가로막고 있던 유일한 것은 프롬프트(prompt)를 통해 중재된 모델 자체의 자제력이었습니다. 프롬프트는 선호도(preferences)입니다. 선호도는 컨텍스트 압박(context pressure) 하에서 저하되며, 긴 세션에서는 완전히 압축되어 사라집니다.
그것이 실제 버그였습니다. 모델 티어(model tier)의 문제도, 캐시(cache)의 문제도 아니었습니다. 저는 예산 정책(budget policy)을 강제될 수 없는 곳에 두었습니다.
v2: 디스패치 계약 (the dispatch contract)
동일한 위임(delegation) 모델을 사용하되, 각 근본 원인에 대해 다음과 같이 수정했습니다:
- 모든 디스패치(dispatch) 시 명시적 모델 지정. Haiku / Sonnet / Opus 중 작업별로 선택하며, 절대 상속되지 않습니다. 이를 통해 'Opus 기본 적용'으로 인한 추가 비용(tax)을 없앴고, 더 중요한 것은 모델 티어를 암묵적인 기본값이 아닌 가시적인 결정 사항으로 만들었다는 점입니다.
- "모두 읽기" 대신 큐레이션된 브리프(briefs). 저장소(repo)를 다시 로드하라는 지시 대신, 정확한 파일 경로와 요약본(digest)을 제공합니다. 이것이 핵심입니다. 이는 토큰 수(token count)를 직접적으로 공격하며, 토큰 수는 단가(unit price)보다 더 중요합니다. 입력 토큰을 5배 줄이는 것이 Opus에서 Sonnet으로 전환하는 것보다 더 많은 비용을 절감합니다.
- 범위가 지정된 트리거(Scoped trigger). 대규모 작업이나 진정으로 병렬적인 작업만 오케스트레이션(orchestrate)합니다. 사소한 편집은 메인 세션에서 직접 수행합니다. 서브에이전트(subagent)의 오버헤드(overhead) — 콜드 프리픽스(cold prefix), 브리프, 읽어야 할 보고서 — 는 작업량이 오버헤드보다 클 때만 지불할 가치가 있습니다.
- 2회 제한의 적정 규모 리뷰, 평면적 오케스트레이션(중첩된 에이전트 없음), 그리고 실행 중인 작업(in flight) 5개 이하 유지.
더 나아졌습니다. 하지만 여전히 프롬프트에 의존하고 있습니다.
비용 방어 훅(cost-guard hook): 모델 외부에서의 강제 실행
진정한 해결책은 위의 사항 중 그 어느 것도 판단(judgment)에 맡기지 않는 것입니다. 이는 PreToolUse 훅(hook)으로 실행됩니다. 이는 하네스(harness)가 도구 호출(tool call) 전에 실행하는 프로세스로, 모델은 이 판정에 대해 투표권을 갖지 않습니다.
서브에이전트 디스패치 도구에 대해 settings.json에 이를 등록합니다:
{
"hooks": {
"PreToolUse": [
...
훅은 stdin을 통해 JSON을 전달받습니다:
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
...
그리고 stdout으로 응답합니다:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
...
updatedInput은 이것을 단순한 방지벽(bouncer) 이상의 것으로 만듭니다. 이 후크는 단순히 호출을 거부하는 대신, 호출 자체를 재작성할 수 있습니다.
다섯 가지 가드:
| Guard | Decision | What it stops |
|---|---|---|
| Model downgrade | allow + updatedInput | 기본적으로 Opus 사용. Opus로 호출되는 것과 프롬프트가 없는 모델 비지정 디스패치를 Sonnet으로 재작성합니다. |
| ... | ||
| 여기 핵심은 이 부분입니다: |
#!/usr/bin/env node
// .claude/hooks/cost-guard.js - PreToolUse, matcher: "Task"
const fs = require("fs");
...
시간을 소모하게 만든 세 가지 구현 참고 사항:
updatedInput은 병합하는 것이 아니라 대체합니다. `{ model:
프롬프트 버전은 가장 필요할 때, 즉 긴 세션의 깊은 곳에서, 컨텍스트 압박(context pressure) 하에서, 그리고 압축(compaction) 과정에서 당신이 기록해 둔 단락을 조용히 삭제해 버린 직후에 정확히 성능이 저하됩니다. 훅(hook) 버전은 첫 번째 디스패치(dispatch #1)와 24번째 디스패치(dispatch #24)에서 동일하게 작동하며, 설득될 필요가 없습니다.
그 차이는 사실 비용에 관한 것이 아닙니다. 그것은 제약 사항(constraint)이 어느 계층에 속해야 하는지에 관한 것입니다. 모델이 무시했다는 사실을 알게 되었을 때 화가 날 만한 모든 것들 — 지출 한도(spend caps), 파괴적인 명령(destructive commands), 푸시 대상(push targets), 자격 증명 접근(credential access) — 은 시스템 프롬프트(system prompt)에 속해서는 안 됩니다. 그것은 모델이 논쟁할 수 없는 프로세스에 속해야 합니다.
저는 여전히 위임(delegate)합니다. 진정으로 크고 병렬적인 작업에 대한 순수한 위임은 올바른 형태이며, v1을 추진했던 컨텍스트 위생(context-hygiene) 논거는 결코 틀리지 않았습니다. 틀렸던 것은 아키텍처를 믿으면서 그 비용을 산정하는 것을 잊은 것 — 그리고 안전장치(safeguards)를 강제할 수 없는 단 한 곳에 작성한 것입니다.
만약 유사한 설정을 운영 중이라면: 디스패치(dispatches)가 model을 명시적으로 설정하는지 확인하고, 서브 에이전트(subagents)가 부모 에이전트와 접두사(prefix)를 공유하는지 확인하십시오. 이 두 가지 질문이 제 청구서의 대부분을 찾아냈습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기