무료 모델 5인조로 에이전트 함대 만들기 — 프로필 배치와 그 대가

TL;DR

  • 유료 API 한 푼 없이 무료 모델 5개로 역할 분담된 에이전트 함대(pm/dev/infra/qa/ops)를 굴려봤다.
  • 결론부터: 돌아간다. 단, 공짜의 대가는 “성능”이 아니라 “가용성”으로 청구된다.
  • 무료 티어에서 실제로 나를 죽인 건 모델의 지능이 아니라 429 rate-limited upstream 이었다.
  • 그래서 배치의 핵심은 모델 고르기가 아니라 역할별 실패 허용도(failure tolerance) 설계다. 죽으면 안 되는 자리와 죽어도 되는 자리를 나눠야 한다.
  • 그리고 폴백은 같은 벤더로 걸면 의미가 없다. 이건 내가 직접 틀리고 나서 배웠다.

들어가며

앞선 Kanban으로 멀티에이전트 업무 분해하기 에서 “일을 카드로 만들고 상태를 강제하라”는 얘기를 했다. 그 글이 업무 라우팅 편이었다면, 이 글은 그 다음 질문이다.

그래서 그 카드를 누가 집어가는가? 그 “누구”에게 어떤 모델을 붙일 것인가?

나는 이번에 조건을 하나 걸고 시작했다. 전부 무료 모델로만 구성한다. 카드값이 나가는 순간 “실험”이 “지출”이 되고, 실험은 위축된다. 그래서 :free 티어만으로 5인조를 짰다.

이 글은 그 배치의 기록이고, 동시에 무료 티어에서 무엇이 먼저 부러지는지에 대한 사후 보고서다. 잘 됐다는 자랑이 아니라, 어디서 깨졌고 왜 깨졌는지를 남기는 쪽에 무게를 뒀다.


1. 왜 5인조인가 — 역할을 쪼개는 이유

에이전트를 하나만 쓰면 그 하나가 기획도 하고 코딩도 하고 검수도 한다. 문제는 자기가 쓴 코드를 자기가 검수하면 통과시킨다는 것이다. 이건 모델이 나빠서가 아니라 구조가 나빠서 생긴다.

그래서 역할을 나눴다.

역할 코드네임 책임 하지 않는 일
pm 마늘쫑 업무 분해, 카드 생성, 담당 배정 직접 코딩
dev 양파 실제 구현 (FE/BE) 자기 결과 승인
infra 무시 빌드, 패키징, 배포, 셸 작업 기능 설계
qa 샐러리 검수, 과설계 감사, 반려 권한 직접 수정
ops 버섯 모니터링, 상태 보고, 실패 아카이빙 코딩·아키텍처

핵심은 마지막 열이다. “하지 않는 일”을 명시하는 게 역할 정의의 절반이다. 이게 없으면 모든 에이전트가 “조금 짜고 조금 계획”하는 상태로 수렴한다.

특히 qa에게 반려(Reject) 권한을 준 게 컸다. 통과시킬 권한만 주면 QA는 통과 도장 찍는 기계가 된다. 되돌려 보낼 수 있어야 검수다.


2. 실제 배치 — 어떤 무료 모델을 어디에 붙였나

이번에 굴린 최종 배치는 이렇다.

프로필 provider 주 모델 크기/성격
pm openrouter nvidia/nemotron-3-ultra-550b-a55b:free 최대 모델 — 분해 판단용
dev openrouter poolside/laguna-s-2.1:free 코딩 특화
infra openrouter nvidia/nemotron-3-super-120b-a12b:free 중간급 — 셸 작업
qa nous tencent/hy3:free 검수·판정
ops nous tencent/hy3:free 보고·요약

배치에 깔린 원칙은 세 개였다.

(1) 판단이 무거운 자리에 큰 모델을 준다. pm은 “이 요구사항을 몇 개 카드로 쪼갤 것인가”를 결정한다. 여기서 잘못 쪼개면 하위 전체가 헛돈다. 그래서 무료 중 가장 큰 550B를 붙였다.

(2) 실행하는 자리에는 특화 모델을 준다. dev는 범용 추론보다 코드 생성 품질이 중요하다. 그래서 코딩 특화 모델로.

(3) 검수자는 실행자와 다른 계보를 쓴다. dev가 openrouter/poolside 계열이면 qa는 nous 계열로 뒀다. 같은 모델 패밀리끼리 검수하면 같은 실수를 같은 방식으로 놓친다. 이건 사람 코드리뷰에서 같은 팀원끼리 리뷰할 때 생기는 편향과 같다.


3. provider 편중 — 눈에 안 보이는 단일 장애점

위 표를 다시 보자. openrouter에 3개(pm/dev/infra), nous에 2개(qa/ops)가 붙어 있다.

이 배치의 문제는 openrouter 계정 하나가 rate limit에 걸리면 pm·dev·infra가 동시에 마비된다는 것이다. 기획·구현·배포가 한꺼번에 멈춘다. 남는 건 검수자와 비서뿐인데, 검수할 산출물이 안 나오니 사실상 함대 전체 정지다.

이건 모델 성능 표에는 절대 안 나타나는 리스크다. 그리고 무료 티어에서는 이게 가장 먼저 터진다.


4. 실제로 터진 것 — 429 rate-limited upstream

여기가 이 글의 핵심이다.

dev에게 실제 작업 카드를 하나 넘겼다. 결과는 이랬다.

#402 crashed  @dev  61s
      ! pid 115092 not alive
#403 crashed  @dev  182s
      ! pid 133260 not alive
→ gave_up {failures: 2, effective_limit: 2}

카드는 두 번 시도 후 자동으로 blocked 처리됐다. 로그를 파보니 원인은 명확했다.

API call failed (attempt 1/3): RateLimitError [HTTP 429]
  message: 'Provider returned error'
  raw: 'poolside/laguna-s-2.1:free is temporarily rate-limited upstream.
        Please retry shortly, or add your own key to accumulate your rate limits'
  provider_name: 'Poolside'
❌ Rate limited after 3 retries
💀 Final error: HTTP 429

모델이 문제를 못 푼 게 아니다. 모델에 말을 걸 수조차 없었다.

그리고 이게 무료 티어의 본질이다. 무료 모델의 리스크를 “품질이 좀 떨어짐” 정도로 예상하고 있었는데, 실제로 청구된 대가는 간헐적 완전 불가용이었다. 품질 저하는 결과를 보고 고칠 수 있지만, 불가용은 결과 자체가 없다.

더 성가신 건 이게 비결정적이라는 점이다. 같은 카드를 5분 뒤에 다시 돌리면 통과할 수도 있다. 그래서 “우리 dev가 이 작업을 못 하는구나”와 “지금 순간 업스트림이 막혔구나”를 구분하는 데 시간이 걸렸다. 나는 처음에 dev 프로필 설정이 깨진 줄 알고 config를 뒤졌다. 헛수고였다.

교훈: 에이전트가 죽었을 때 “모델 탓”과 “가용성 탓”을 구분하는 로그가 없으면 엉뚱한 곳을 고친다. 429는 스택트레이스에 안 나온다. 나온 건 Event loop is closed뿐이었고, 그건 증상이지 원인이 아니었다.


5. 폴백을 걸다가 배운 것 — 같은 벤더는 폴백이 아니다

429를 맞고 나서 당연히 폴백을 걸기로 했다. 그리고 여기서 세 번 연속 틀렸다. 순서대로 남긴다.

첫 번째 실수 — 존재하지 않는 모델을 적었다.

dev 폴백으로 deepseek/deepseek-chat-v3-0324:free를 넣었다. “무료 코딩 모델로 유명하니까 있겠지” 하고 이름을 기억에서 꺼내 쓴 것이다. 실제로 조회해보니 없었다.

openrouter의 무료 모델을 전부 뽑아봤다. 총 14개였다.

cohere/north-mini-code:free
google/gemma-4-26b-a4b-it:free
google/gemma-4-31b-it:free
inclusionai/ling-3.0-tiny:free
nvidia/nemotron-3-nano-30b-a3b:free
nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free
nvidia/nemotron-3-super-120b-a12b:free
nvidia/nemotron-3-ultra-550b-a55b:free
nvidia/nemotron-3.5-content-safety:free
nvidia/nemotron-nano-12b-v2-vl:free
nvidia/nemotron-nano-9b-v2:free
openai/gpt-oss-20b:free
poolside/laguna-s-2.1:free
poolside/laguna-xs-2.1:free

이게 전부다. 그리고 이 중 절반이 nvidia 계열이다. 무료 티어의 선택지는 생각보다 훨씬 좁다. 폴백 설계 전에 이 목록을 먼저 뽑아야 했다.

두 번째 실수 — 같은 벤더로 폴백을 걸었다.

목록을 보고 poolside/laguna-xs-2.1:free로 바꿨다. laguna-s의 작은 버전이니 자연스러운 폴백처럼 보였다.

그런데 429 메시지를 다시 읽어보자. provider_name: 'Poolside'. rate limit은 모델 단위가 아니라 업스트림 벤더 단위로 걸린다. laguna-s가 막혔으면 laguna-xs도 같은 문에서 막힌다.

폴백의 목적은 “더 작은 모델로 버티기”가 아니라 “막힌 문을 피해 다른 문으로 나가기” 다. 그래서 최종적으로는 벤더를 갈랐다.

프로필 주 모델 폴백 1 폴백 2
dev poolside/laguna-s cohere/north-mini-code nous/hy3
infra nvidia/nemotron-super nous/hy3 nvidia/nemotron-nano
pm nvidia/nemotron-ultra nous/hy3
qa nous/hy3 google/gemma-4-26b

각 행에서 주 모델과 폴백의 벤더가 다르다. 이게 핵심이다. 모델 이름이 다른 것으로는 부족하다.

세 번째 실수 — 설정 키를 틀렸다. 그래서 폴백이 아예 동작하지 않았다.

이게 제일 부끄럽고, 제일 공유할 가치가 있다.

나는 폴백을 이렇게 넣었다.

model:
  default: poolside/laguna-s-2.1:free
  provider: openrouter
  fallback_model:          # ← model 블록 안쪽
    provider: openrouter
    model: cohere/north-mini-code:free

문법적으로 유효한 YAML이고, 린트도 통과하고, 눈으로 보면 완벽하다. 그런데 동작하지 않는다.

설정을 읽는 코드를 열어보니 이랬다.

for key in ("fallback_providers", "fallback_model"):
    for entry in _iter_fallback_entries(config.get(key)):

config.get(key)최상위(top-level)에서 찾는다. 내가 넣은 건 model: 블록 안쪽이었다. 그러니 영원히 안 읽힌다.

그래서 진짜 맞는 형태는 이거였다.

model:
  default: poolside/laguna-s-2.1:free
  provider: openrouter
fallback_providers:        # ← 최상위, 그리고 리스트
  - provider: openrouter
    model: cohere/north-mini-code:free
  - provider: nous
    model: tencent/hy3:free

무서운 건 틀렸다는 신호가 어디에도 없었다는 점이다. 에러도 경고도 없다. 그냥 폴백이 조용히 존재하지 않는 상태로 며칠 굴러간다. 실제로 나는 “폴백 걸었으니 이제 429 나도 넘어가겠지”라고 믿고 있었는데, 그 믿음이 근거가 없었다.

교훈: 안전장치를 설정한 뒤에는 “설정했다”가 아니라 “동작한다”를 확인해야 한다. 폴백·재시도·서킷브레이커처럼 평소엔 잠자는 장치는, 검증하지 않으면 정상인 척하는 부재가 된다. 실패 경로는 성공 경로보다 검증이 어렵고, 그래서 더 의도적으로 검증해야 한다.

여기서 하나 더. 이 배치에서 ops(비서 프로필) 폴백만은 끝내 스크립트로 못 넣었다. 해당 CLI가 대화형 터미널을 요구해서(requires an interactive terminal) 파이프로는 거부된다. 자동화 스크립트로 프로필을 찍어내려던 계획에 구멍이 하나 남았고, 그건 지금도 수동 작업으로 남아 있다. 완결된 자동화가 아니라 구멍이 남은 자동화라고 적어두는 게 정직하다.


6. 그래서 무료 5인조는 쓸 만한가 — 정직한 결산

된 것:

  • 역할 분리는 확실히 효과가 있었다. 특히 qa에게 반려 권한을 준 뒤로 “일단 통과” 패턴이 줄었다.
  • 판단 무거운 자리(pm)에 큰 모델, 실행 자리(dev)에 특화 모델을 배치하는 전략은 유효했다.
  • 검수자와 실행자의 모델 계보를 가른 것도 유효했다. 같은 계열이면 못 잡던 걸 잡았다.

안 된 것 / 대가:

  • 가용성이 가장 먼저 부러진다. 무료 티어의 대가는 품질이 아니라 간헐적 완전 불가용으로 청구된다.
  • 선택지가 좁다. openrouter 무료 14개, 그중 코딩용은 2벤더뿐. 폴백 체인을 길게 못 짠다.
  • provider 편중이 단일 장애점을 만든다. 계정 하나 막히면 함대 절반이 동시에 정지한다.
  • 폴백은 회피이지 해결이 아니다. 벤더를 갈라 우회했을 뿐, 무료 티어 자체의 rate limit은 그대로다. 부하가 올라가면 모든 폴백이 동시에 막히는 시나리오가 여전히 남아 있다.

결론:

무료 5인조는 실험과 학습에는 충분하고, 마감이 걸린 일에는 부족하다. “공짜니까 성능이 좀 떨어지겠지”라고 예상하면 오답이다. 실제로는 성능은 생각보다 쓸 만하고, 대신 언제 응답할지 모른다. 그리고 마감 앞에서는 후자가 훨씬 치명적이다.

그래서 내 다음 수는 모델을 업그레이드하는 게 아니라, 429가 났을 때 함대가 어떻게 버티는지를 설계하는 쪽이다. 카드를 자동 재시도할지, 다른 프로필에 재배정할지, 아니면 그냥 사람에게 넘길지. 이건 모델 선택 문제가 아니라 오케스트레이션 문제다.


마치며

멀티에이전트를 짜다 보면 자꾸 “어떤 모델이 제일 좋냐”로 관심이 쏠린다. 그런데 실제로 나를 멈춰 세운 것들은 순서대로 이랬다.

  1. 벤더 단위 rate limit (모델 성능 무관)
  2. 설정 키를 틀려서 조용히 죽어 있던 폴백 (모델 성능 무관)
  3. 존재하지 않는 모델명을 기억에 의존해 적은 것 (모델 성능 무관)

셋 다 모델이 아니라 배치와 검증의 문제였다. 함대를 굴릴 때 진짜 어려운 부분은 여기 있다.

다음 글에서는 이 함대에 가드레일(harness)을 어떻게 물렸는지 — 특히 “프롬프트로 부탁하는 규칙”과 “훅으로 강제하는 규칙”이 실제로 얼마나 다른지를 다룰 예정이다. 스포일러를 하자면, 텍스트로 적어둔 규칙은 모델이 바쁠 때 가장 먼저 잊는다.

Comments