
그 경고, 정상 운전 중에 몇 번이나 울립니까? ── LLM의 자기 보고를 제로로 만들기까지의 2개 릴리스 (C3 v2.47.0 /
요약
멀티 에이전트 프레임워크 C3(Claude Code Conductor)의 v2.47.0 업데이트 내용을 다룹니다. LLM의 선의에 의존하던 모델 배분(tier-routing) 방식을 도구 호출 가로채기(hook)를 통한 강제 주입 방식으로 전환하여 시스템의 신뢰성을 높였습니다.
핵심 포인트
- LLM의 자기 보고에 의존하던 honor system을 기계적 주입 방식으로 개선
- PreToolUse hook을 사용하여 모델 지정이 누락된 경우 권장 Tier를 자동 주입
- 실제 적용된 값을 시스템이 직접 기록하여 LLM의 보고 오류 리스크 제거
- Thompson sampling 기반의 bandit 학습을 통한 모델 배분 최적화
이전 기사: https://zenn.dev/satoh_y_0323/articles/a97c90e41348a1
C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor / PyPI: https://pypi.org/project/claude-code-conductor/ / 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
본 기사의 범위: 자체 제작한 멀티 에이전트 프레임워크 C3 (Claude Code Conductor)의 v2.46.0 이후 ~ 현재 (v2.48.0). 테마는 「tier-routing (에이전트 모델 배분)을 LLM의 선의에 의존하는 honor system에서, 자기 보고가 없는 완전 자동 루프로 전환하기」입니다. 같은 날 작성하는 두 번째 기사입니다. 하루 만에 v2.46.0 → v2.47.0 → v2.48.0으로 3번의 릴리스가 진행되었기에 기록으로 남깁니다.
서론 ── 「학습 데이터 수집 중」 표시가 사라진 날
C3의 tier-routing은 태스크의 복잡도에 따라 worker 에이전트의 모델 (haiku / sonnet / opus)을 bandit 학습으로 선택하는 메커니즘입니다. v2.42.0부터는 「소프트 적용」── UserPromptSubmit hook이 상위 Claude에게 "developer를 실행할 때는 model:에 이 Tier를 명시해 주세요"라고 부탁하는 방식으로 동작하고 있었습니다.
이것은 honor system입니다. 상위 Claude가 지시를 읽지 않고 지나치면, 조용히 기존 동작으로 돌아갑니다. 실제로는 잘 지켜지고 있었지만, 「지켜지지 않았음을 감지할 수단이 없다」는 잔여 리스크가 계속 설계 메모에 남아 있었습니다.
이날, c3 tier stats에서 「학습 데이터 수집 중 (27/30)」 표시가 사라졌습니다. Thompson sampling의 시도 횟수가 임계치에 도달하여, bandit이 자신의 판단으로 Tier를 선택하기 시작했다는 뜻입니다. 권장 사항이 진짜가 되었다면, 적용 또한 진짜로 만든다 ── 이전에 결정해 두었던 착수 트리거가 성립된 것입니다.
제1화 ── 「부탁」을 「주입」으로 바꾸기 전, 딱 3번만 haiku를 호출하다
기계적 적용 설계는 심플합니다. PreToolUse hook이 Agent 도구 호출을 가로채고, model:이 지정되지 않은 developer 실행에 권장 Tier를 updatedInput으로 주입합니다. 명시적인 지정이 있다면 이를 존중하여 그대로 통과시킵니다. 두 경우 모두, hook이 「실제로 적용된 값」을 JSONL에 기계적으로 기록합니다 ── 적용한 주체가 기록하므로, LLM의 자기 보고가 루프에서 사라집니다.
문제는 하나입니다. updatedInput으로 도구 인수를 실행 전에 변경할 수 있다는 점은 공식 문서에 적혀 있지만, 우리 코드베이스에서 사용한 전례가 제로였습니다. 문서와 실제 동작이 어긋나 있다면 설계 전체가 무너집니다.
그래서 구현 전에 관측 전용 스파이크(spike) hook을 settings.local.json (배포물을 더럽히지 않는 장소)에 심어두고, haiku를 3번 실행하여 확인했습니다.
- 명시한
model:이 tool_input에 나타나는가 → 나타난다 updatedInput으로 추가한model:이 정말로 작동하는가 → 작동한다 (실행된 에이전트에게 "자신의 모델명을 보고하라"고 시켜서, 자기 보고·기록·transcript의 3가지 경로로 haiku를 확인)- payload에 session_id가 포함되어 있는가 → 포함되어 있다
두 가지 소소한 이야기가 있습니다. 전환을 환경 변수로 처리하려 했으나 hook 프로세스에 환경 변수가 전달되지 않아 플래그 파일 방식으로 변경했습니다. 또한, 스파이크 hook의 첫 실행 시에만 로그가 0바이트로 침묵했고 다시는 재현되지 않았습니다. fail-safe 설계 (모든 예외 상황에서 그대로 통과) 덕분에 실질적인 피해는 없었으며, 「원인 불명인 채로 깊이 파고들지 않고 기록만 남긴다」는 전략으로 진행했습니다.
검증한 3가지 사항이 모두 긍정적이었기에 본 구현으로 넘어갔습니다. 구현을 마치고 hook을 등록한 순간부터, 이 워크플로우 자체의 developer 실행에 방금 만든 hook이 주입과 기록을 시작했습니다. fix-cycle을 돌리는 것 자체가 신기능의 실운용 검증이 됩니다. dogfooding의 가장 빠른 형태입니다.
제2화 ── 설계 감사 5라운드를 통과한 설계가, E2E 실데이터 1회에 찔리다
v2.47.0에는 또 하나, 「학습 기록 결락 검지」가 포함되어 있습니다. 에이전트의 기동은 hook이 기계적으로 기록하는 반면, 결과(승인/거부)의 기록은 LLM이 커맨드를 호출하는 honor system 방식 그대로였습니다. 그래서 Stop 시점에 「기동 N건 vs 기록 M건」을 대조하여, 결락이 있으면 경고하는 검지기를 추가했습니다.
이 설계는 구현 전 design-critic 감사를 5라운드 돌리며 19건의 지적 사항을 전부 해결한 뒤 구현에 들어갔습니다. 그럼에도 불구하고, E2E 실데이터 1회에 멋지게 찔리고 말았습니다.
대조 대상에 tester를 포함하고 있었던 것입니다. C3의 워크플로우 계약상, tester는 기동 6회에 대해 기록은 1~2회가 정상입니다 (최종 확인 게이트에서만 기록함). 즉, 이 검지기는 아무런 문제가 없는 세션에서도 매번 「결락 4건」이라고 경고를 보냅니다. 문서 간의 대조만으로는 보이지 않는, 운영의 카디널리티 (Cardinality) 지식이 필요한 맹점이었습니다.
저는 이것을 묵인하지 않고 「리뷰에 대한 인수인계 사항」으로 명문화하였고, code-reviewer가 High 등급으로 「시정 필요」를 확정하여, tester를 대조 대상에서 제외함으로써 수렴했습니다. 이때 얻은 교훈이 본 기사의 제목입니다.
검지 계통의 기능은, 「정상 운전 중에 몇 번이나 발화하는가」를 실데이터로 가늠해 본 뒤 설계를 확정한다.
리뷰는 fix-cycle 5회를 통해 27건을 모두 대응했습니다. 게다가 security-reviewer가 수정 패턴의 수평 전개 점검을 하던 도중, 변경 대상이 아닌 파일에 잠복해 있던 기존 버그(비밀 정보 마스킹의 적용 순서가 반대여서, 200자 경계를 넘어가는 PEM 블록이 통과되는 문제)를 발견하고 입증했습니다. 「변경 대상이 아니니까 보지 않는다」라는 태도를 버리는 문화의 실익을, 이것으로 3개 릴리스 연속으로 확인했습니다.
도중에 주간 세션 상한에 걸려 작업이 한 차례 중단되었으나, 「작업 트리의 실제 상태 확인 → 동일 지시로 클린 재시작」이라는 패턴으로 손실 없이 복구했습니다. v2.47.0로서 릴리스했습니다.
제3화 ── 「task_id로 판별할 수 있다면, 그것으로 충분하지 않나요?」
v2.47.0에는 미결 사항이 하나 남아 있었습니다. 병렬 실행 (parallel-agents) 경로만은, 여러 worker가 동일 세션에서 거의 동시에 기동하기 때문에, 「session 일치 최신 행」만으로는 기록의 대조가 모호해집니다. 그래서 병렬에 대해서만 기존 방식대로 --task를 사용하는 수동 신고 방식을 남겨두고 있었습니다. 마지막으로 남은 자기 보고(self-reporting)였습니다.
착수의 계기는 제가 세션 기록을 읽다가 던진 한마디였습니다.
「세션 ID는 같더라도, task_id로 판별할 수 있다면 그것으로 충분하다고 생각합니다만."
조사해 보니, task_id는 이미 양측 모두에 존재하고 있었습니다. 병렬 기동 프롬프트에는 task_id가 포함되어 있고, 기록 커맨드는 --task를 받고 있습니다. 나머지는 hook이 기동 프롬프트에서 task_id를 추출하여 JSONL에 싣고, 기록 측에서 (session, role, task_id)의 전체 일치 여부로 대조하면 됩니다. 참고로 「에이전트 ID로 대조한다」는 안도 검토했으나, hook이 발화하는 시점은 기동 전 (PreToolUse)이라 자식 에이전트의 ID가 아직 존재하지 않기 때문에, task_id가 승리했습니다.
설계 감사는 4라운드. 여기서 이 날 가장 기분 좋은 일이 일어납니다.
라운드 1의 지적 대응 과정에서 「대조에 실패하면 경고를 내보내는 가드」를 설계에 추가했습니다. 그러자 라운드 2의 감사에서, 그 경고 가드 자체를 High 등급으로 찔러버린 것입니다. 이유: 순차 경로도 --task를 전달하는 운용 방식인데, 마커는 병렬에만 주입됩니다. 따라서 순차적인 정상 운전 시 매번 오발화하게 됩니다 ── 제2화와 완전히 동일한 형태의 결함입니다.
차이점은 단 하나입니다. 지난번에는 E2E 실데이터까지 도달하고 나서야 비로소 드러났던 것이, 이번에는 설계서 단계에서 잡혔습니다. 「발화 빈도를 가늠하라」는 교훈이 감사의 관점으로 승격되어, 우리가 한 릴리스 전에 밟았던 구덩이를 이번에는 도면 위에서 찾아낸 것입니다. 프로세스가 학습했다는 것에 대한 이보다 더 확실한 증거는 없다고 생각합니다 (수정 사항은 「task_id가 포함된 행이 존재함에도 불일치할 때만 울리도록」 한정. 순차 0회 · 병렬 정상 0회).
또 하나, 라운드 2에서는 「E2E 검증 데이터를 JSONL에서 삭제하여 정리한다」라는 계획도 중단했습니다. 이 JSONL은 락(lock)이 걸린 **추가 전용 스토어 (append-only store)**로, 행 삭제 수단을 가지고 있지 않습니다. 수동으로 read→filter→write를 수행하면 병렬 쓰기와 레이스 컨디션 (race condition)이 발생합니다. 정리의 대칭성은 '삭제'가 아니라 '양쪽에 남겨서 균형을 맞추는 것'(기동+2 / 기록+2로 차분 제로)으로 해결했습니다. 검증의 편의를 위해 스토어의 설계 불변 조건 (invariant)을 깨뜨리지 않습니다.
제4화 ── 5점의 실측과, 마지막에 남은 「검문소의 순서」
구현은 TDD 3태스크 + 리뷰 fix-cycle 2회(code-review 4건 · security-review 1건을 모두 대응)를 거쳤습니다. 그리고 마무리 단계인 E2E 미니 런(mini-run)입니다. 병렬 워커 (worker)를 2개, 실제 worktree 분리로 동시에 기동하여 5개 항목을 실측했습니다.
- JSONL에 2행, 동일 session · 서로 다른 task_id · 주입된 Tier가 기록됨 → PASS
- worktree 측에 JSONL이 생기지 않음 (쓰기는 main 측에서만 수행) → PASS (이전 버전부터 가지고 있던 「worktree 분리로 인해 읽지 못할 수도 있다」는 보수적인 우려를 여기서 정식으로 반증)
- 기록 명령이
--tier없이 올바른 Tier를 기계적으로 해결함 → PASS (DB의 실제 값으로 확인) - 대조 실패 경고가 울리지 않음 → PASS (stderr 0바이트)
- 결락 탐지가 오발화되지 않음 → PASS (제외 창을 벗어난 최악의 케이스에서도 차분 제로)
은근히 마음에 드는 부분은 E2E 직전에 발견한 절차 간의 충돌입니다. 리뷰 완료 시 실행하는 「최종 기록」 명령에는 권장 Tier 파일을 삭제하는 부작용 (side effect)이 있습니다. 그런데 E2E는 기동 시 해당 파일이 유효하다는 것을 전제로 합니다. SKILL의 절차를 글자 그대로의 순서로 실행했다면, 스스로 전제를 지워버린 뒤 검증에 들어갈 뻔했습니다. 기록을 E2E 뒤로 연기하여 회피──기계의 검문소는, 순서 또한 기계적으로 생각해야 합니다.
v2.48.0을 릴리스하며, tier-routing은 다음과 같이 바뀌었습니다.
UserPromptSubmit ──► 권장 Tier 확정 (bandit이 선택)
│
PreToolUse(Agent) ─► model: 에 자동 주입 (명시적 지정은 존중)
...
권장 → 주입 → 기록 → 대조 → 학습. 이 루프의 어디에도 LLM의 「제대로 수행했습니다」라는 신고가 끼어들 틈이 없습니다. v2.42.0에서 「부탁」으로 시작된 메커니즘이, 3번의 릴리스를 거쳐 완전 자동화된 폐쇄 루프 (closed loop)가 되었습니다.
업무 · 개인 개발에 활용할 수 있는 점
1. 탐지계 · 경고계는 「정상 운전 시의 발화 횟수」로부터 설계한다
이상 징후를 올바르게 탐지할 수 있는지보다 앞서, 정상적일 때 몇 번 울리는지를 운영 데이터로 추산해야 합니다. 매번 울리는 경고는 양치기 소년 (crying-wolf)이 되어, 정말로 탐지하고 싶었던 것들까지 무시하게 만듭니다. 이번에 이 관점은 첫 번째 릴리스에서 실제 데이터에 의해 타격을 입었고, 두 번째 릴리스에서 감사 관점으로 승격되어 설계 단계에서 동일한 형태의 결함을 잡아낼 수 있었습니다. 실패를 교훈 문구로 끝내지 말고, 다음 리뷰의 관점 표에 올리는 것까지 해야 제대로 회수할 수 있습니다.
2. 지적 대응으로 추가한 수정 사항은, 다음 라운드에서 「신규 도입 사항」으로서 재감사한다
라운드 1의 지적을 해소하기 위해 추가한 경고 가드가 라운드 2의 High 이슈였습니다. 수정은 지적을 종결함과 동시에 새로운 설계입니다. 「이전 라운드의 대응이 새로운 문제를 가져오지는 않았는가」를 감사 프롬프트에 명시하면, 이러한 종류의 결함을 구조적으로 잡아낼 수 있습니다.
3. undocumented / 전례 없는 API 의존은, 구현 전에 「저렴한 실험」으로 흑백을 가린다
공식 문서에 적혀 있더라도 자신의 환경에서 동작한다는 보장은 없습니다. 이번에는 관측 전용 hook + haiku 3회(판단 기준을 미리 결정)를 통해 최대의 불확실성을 구현 전에 제거했습니다. 스파이크 (spike) 비용은 설계를 다시 하는 비용보다 항상 저렴합니다.
4. 추가 전용 스토어에 「삭제」를 도입하지 않는다
락이 걸린 append-only JSONL에서 검증 데이터를 지우고 싶다면, 그것은 설계 불변 조건을 깨뜨리라는 신호입니다. 이번에는 「삭제하여 정리」하는 대신 「양쪽에 남겨서 차분을 균형 있게 맞추는 것」으로 해결했습니다. 정리 요건은 스토어의 설계에 맞춰 재정의되는 경우가 많습니다.
5. 자동화의 「검문소」는, 순서의 의존 관계까지 포함하여 점검한다
기록 명령의 부작용(상태 파일 삭제)이 후속 검증의 전제 조건을 망가뜨린다──개별 절차는 올바르더라도, 직렬로 나열하는 순간 망가지는 조합이 있습니다. 절차서대로 실행하기 전에 "이 조작의 부작용에 의존하는 후속 작업은 없는가"를 단 한 번만 확인하십시오. 자동화가 진행될수록 이 한 번의 수고가 갖는 가치는 높아집니다.
6. 신기능은 「만들고 있는 워크플로 자신」에게 가장 빠르게 먹인다
hook을 등록한 순간부터, 그 기능을 만들고 있는 워크플로 자체가 실제 운영 데이터를 생성하기 시작했습니다. 릴리스 전에 실제 데이터가 쌓여 있는 상태는 E2E (End-to-End) 테스트보다 더 많은 것을 말해줍니다. dogfooding(자사 제품 사용)은 정신론이 아니라, 릴리스 판단을 위한 재료를 수집하기 위해 처음부터 공정에 포함시켜야 하는 것이었습니다.
요약
v2.47.0: tier-routing 기계 적용 (PreToolUse hook이 updatedInput에서 model:을 자동 주입 · 「적용자 = 기록 SSOT」로 자기 보고를 배제) + 학습 기록 결락 검지 (기동 N vs 기록 M · 경고만 발생). 구현 전 스파이크(Spike) 3회로 사양의 가부를 확정. 설계 감사 5라운드 19건 → 0건, 리뷰 5사이클 27건 → 0건. E2E 실제 데이터가 "tester를 대조하면 매번 오경고가 발생한다"는 감사의 사각지대를 적출.
v2.48.0: 병렬 경로의 --tier 자기 보고를 폐지 (C3_TASK_ID 마커 → hook 추출 → (session, role, task_id) 대조). 설계 감사 4라운드 14건 → 0건 ── 이전 릴리스의 교훈인 "발화 빈도 추정"이 감사 관점으로 기능하여, 동일 유형의 결함을 이번에는 구현 전에 포착. E2E 5개 지점 실측 PASS · 파괴적 변경 없음.
시리즈 완결: 권장 → 주입 → 기록 → 대조 → 학습의 루프를 통해 LLM의 자기 보고가 사라졌다. 발단은 "부탁 기반으로 해도 괜찮을까?"라는 설계 메모였고, 마지막 퍼즐 조각은 "task_id로 판별할 수 있다면 충분하지 않을까"라는 대화 한 마디였다.
honor system(신뢰 기반 시스템)을 기계로 대체하는 작업은 기능을 추가하는 작업이라기보다 「신뢰했던 부분을 하나씩 검증 가능하게 만드는」 작업이었습니다. 그리고 아이러니하게도, 그 과정에서 가장 의지가 되었던 것은 자신의 과거 실패를 관점표로 바꾸어 가져오는 감사 루프──즉, 기계화된 프로세스였습니다.
C3를 써보고 싶다면 ── 시작하기
pip install claude-code-conductor # C++ 컴파일러 불필요
cd your-project
c3 init # .claude/ 디렉토리에 에이전트 정의 · skill · hook이 전개됨
그다음은 Claude Code에서 /start.
tier-routing은 처음에 "학습 데이터 수집 중" 단계부터 시작하며, 워크플로를 돌릴수록 권장 사항이 똑똑해집니다. v2.47.0 이후부터는 권장 Tier가 worker에 자동으로 적용되므로, 모델 분배를 의식할 필요가 없습니다. 어댑터는 Claude / Codex / Cursor / OpenCode 4종을 지원합니다 (c3 init --platform ...).
"여기서 막혔다", "이 부분이 이상하다" ── 막히는 부분에 대한 보고가 가장 큰 도움이 됩니다. Issue나 PR로 언제든 편하게 말씀해 주세요.
링크
C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor
C3 PyPI: https://pypi.org/project/claude-code-conductor/
C3 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
v2.47.0 릴리스 노트: https://github.com/satoh-y-0323/claude-code-conductor/releases/tag/v2.47.0
v2.48.0 릴리스 노트: https://github.com/satoh-y-0323/claude-code-conductor/releases/tag/v2.48.0
이전 기사 (『그 반려 횟수, 리뷰어의 공로 아닌가요?』 / C3 v2.46.0): https://zenn.dev/satoh_y_0323/articles/a97c90e41348a1
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기