루프, 오케스트레이션, 동적 워크플로

🏷️ 정보 LLM

이 글은 @Av1dlive의 아티클을 참고하여 작성했습니다.

프롬프트는 더 이상 병목이 아니다!

에이전트 하나를 한 창에서 굴릴 때는 프롬프트가 거의 전부였습니다. 에이전트 수가 늘어날수록 프롬프트 한 줄 보다는 멈추는 조건과 검증 게이트를 제대로 잡아두는 게 훨씬 효과적입니다.

오케스트레이터

30개의 에이전트를 관리하려고 30개의 창에서 대화하지 않습니다. 하나하고만 대화합니다. 그 하나가 오케스트레이터고, 나머지를 관리합니다. 좋은 오케스트레이터는 딱 세 가지만 합니다.

  1. 목표를 경계가 분명하고 각각 검증 가능한 하위 작업으로 쪼갠다.
  2. 각 하위 작업을 작업자에게 위임한다. 좁은 브리프와 명확한 파일 소유권을 함께.
  3. 결과를 모아 다음에 뭘 할지 결정한다.

중요한 것은, 오케스트레이터는 절대 직접 일하지 않습니다. 리드 에이전트가 구현 코드를 쓰기 시작하는 순간, 컨텍스트가 세부사항으로 가득 차고 전체 작업의 큰 그림을 놓칩니다. 생각하고, 쪼개고, 할당하고, 검사만 합니다. 그 외엔 아무것도 하지 않습니다.

또 하나, 넓게 가지 말고 깊게 가야 합니다. 오케스트레이터가 작업자 여덟 명을 직접 띄우면 컨텍스트가 산산조각 납니다. 대신 리드 두세 명을 띄우고, 각 리드가 다시 자기 전문가 두세 명을 띄우게 합니다. 같은 컨텍스트 비용으로 분해 깊이를 세 배로 가져가는 구조입니다. 실제 조직이 한 사람이 모든 일을 배분하는 게 아니라 계층으로 확장하는 겁니다.

기계가 검증할 수 있는가

검증 가능한 작업이면 루프를 돌릴 수 있습니다. 테스트가 통과하거나 안 하거나, 타입체커가 초록색이거나 아니거나, 벤치마크가 임계값을 넘거나 못 넘거나. 이런 작업에는 에이전트가 밤새도록 일할 수 있습니다.

검증 불가능한 작업이면 루프만으로는 안 됩니다. 좋은 평가(eval)를 설계하는 일, API가 자연스러운지 판단하는 일, 어떤 연구 방향이 추구할 가치가 있는지 고르는 일. 여기엔 취향을 주입해야 합니다. 멈출 게이트가 없으니 목표를 넘겨주고 손 떼면 안 됩니다.

그래서 오케스트레이션 엔지니어링의 대부분은 사실 모호한 목표를 검증 가능한 것으로 바꾸는 작업입니다. "잘 만들어"를 "이 구체적인 체크들을 통과시켜"로 바꾸는 겁니다. 사람들이 "에이전트가 자율적으로 일 못 한다"고 이야기하는 대부분은 검증 불가능한 작업을 검증 가능한 것처럼 다뤘기 때문입니다.

골 루프

골 루프(goal loop)는 지속적인 목표 + 결정론적 체크입니다. 에이전트가 한 턴 돌고 멈추는 게 아니라, 목표를 향해 한 걸음씩 계속 일합니다. 시스템 전체가 에이전트와 검증기를 둘러싼 루프 하나입니다.

i=0
until npm test -s; do
  agent -p "목표: 테스트 스위트를 초록색으로 만들어라.
            바꾸기 전에 코드를 읽고 전체 그림을 먼저 파악하라.
            'npm test'가 실패 중이다. 통과에 가장 가까운 최소 변경을 하라."
  (( ++i > 20 )) && { echo "stop: 20 iterations"; exit 1; }
done

검증기는 종료 코드가 게이트인 명령이면 뭐든 됩니다. 테스트, 타입체크, 린트, 커스텀 스크립트. 여기서 타협 불가능한 두 가지가 있습니다.

저지

에이전트는 일찍 그만둡니다. '최선'을 다했고, 여기서 멈춘다고 이야기합니다. 에이전트가 자기 숙제를 채점하면 부정행위를 합니다. 악의는 아니고, 그냥 자기가 끝났다고 스스로를 설득하는 겁니다.

곤란한 일이지만, 별도의 저지(judge)를 두어 해결할 수 있습니다. 일의 결과를 구체적인 루브릭에 대고 채점하는 게 유일한 임무인 두 번째 에이전트입니다. "끝났나, 안 끝났나, 뭐가 빠졌나" 같은 것에만 답합니다. 코딩 에이전트와 이해관계가 없으니 봐주지 않습니다.

while :; do
  agent --model "$BUILD" -p "작업: $TASK. 먼저 읽고, 구현하고, 테스트를 돌려라."
  verdict=$(agent --model "$JUDGE" -p "엄격한 리뷰어. rubric.md의 모든 항목에 대해 채점하라.
            정확히 'PASS' 또는 'FAIL: <무엇이 빠졌는지>'로만 답하라.")
  [ $verdict == PASS* ](403eb55d.html) && break
done

빌더와 저지를 다른 모델 패밀리로 돌리세요. 다른 패밀리는 서로 상관 없는 실수를 하니까, 저지가 빌더의 맹점을 봅니다. Claude Code 빌더 + 코덱스 저지 조합은 훌륭합니다. 같은 모델로 채점하면 둘이 똑같은 착각을 공유할 수 있습니다.

루브릭은 구체적이고 이진적이어야 합니다. 좋은 루브릭은 "모든 테스트 초록, 마이그레이션 포함, 디버그 로깅 없음."

나쁜 루브릭은 "잘 만들어."

실제로 쓰게 될 루프들

골 루프가 기본 패턴이고, 몇 가지 변형이 실무의 대부분을 커버합니다.

1. 검증 루프. 실행하고, 검증기 돌리고, 실패를 다시 먹이고, 게이트가 초록될 때까지 반복. 회귀 테스트 스윕, 타입체크 등 결정론적 통과/실패에 씁니다.

2. 큐 앤 리셋 루프. 작업을 작고 원자적인 목록으로 쪼개 하나씩 처리합니다. 각 작업이 끝나면 에이전트를 깨끗한 컨텍스트로 리셋하고 다음 걸 집습니다. 몇 시간 돌아간 컨텍스트는 혼란으로 가득 찹니다. 기억은 에이전트 밖, 작업 파일과 커밋 히스토리에 둡니다.

while read -r task; do
  agent -p "작업: $task. 먼저 읽어라. 구현하고 테스트를 돌려라."
  npm test -s && git commit -aqm "auto: $task"   # 초록일 때만 커밋
done < tasks.txt

3. 모니터 루프. 신호 스트림(열린 이슈, 실패한 빌드, 에러 로그, 새 피드백)에 에이전트를 붙여 중요한 걸 떠올리게 합니다. 묻기를 기다리지 않고, 읽고 분류하고 보고하거나 초안 수정을 엽니다.

while sleep 300; do
  agent -p "지난 5분의 에러 로그를 읽어라. 새 패턴이 나타나면
            최소 재현과 함께 이슈를 열어라. 아니면 'nothing new'라고 답하라."
done

4. 플랜 후 빌드 루프. 루프를 두 단계로 나눕니다. 먼저 계획만 만들고 멈추는 패스. 사람이 계획을 검토한 뒤, 승인된 계획을 따르는 실행 패스. 계획 수정은 저렴하지만 코드 수정은 비쌉니다. 나쁜 방향은 계획 단계에서 잡습니다.

다섯 프롬프트

프롬프트는 소원이 아닙니다. 에이전트한테는 명세(spec)입니다. 평범한 출력과 훌륭한 출력의 차이는 거의 전부 이 명세의 품질에서 나옵니다. 재사용 가능한 작은 프롬프트 세트를 두는데, 다섯 개가 대부분의 무게를 짊어집니다.

1. 분해 프롬프트 (오케스트레이터용):

너는 오케스트레이터다. 코드를 쓰지 마라.
이 목표를 경계가 분명한 하위 작업 3~6개로 쪼개라.
각각: 한 줄 브리프, 소유할 정확한 파일들, 완료 체크.
다른 작업에 의존하는 게 있으면 표시하라. 목록을 출력하고 멈춰라.
목표: <goal>

2. 작업자 브리프 (전문가용):

너는 오직 이 파일들만 소유한다: <files>.
작업: <한 줄>. 완료 조건: <검증 가능한 체크>.
편집 전에 그 파일들과 호출자들을 읽어라. 네 파일 밖은 아무것도 바꾸지 마라.
끝나면 <name>.md에 5줄 보고를 쓰고 테스트를 돌려라.

3. 저지 프롬프트 (다른 모델 패밀리용):

너는 엄격하고 인색한 리뷰어다. 이 코드를 네가 쓰지 않았다.
아래 모든 항목에 대해 채점하라. 하나라도 빠지면 FAIL이다.
<rubric>
정확히 한 줄로 답하라: 'PASS' 또는 'FAIL: <무엇이 빠졌는지>'.

4. 플랜 우선 프롬프트 (플랜 모드용):

<goal>에 대한 서면 계획을 만들어라. 접근법, 건드릴 파일,
엣지 케이스, 테스트 전략, 그리고 하지 않을 것을 다뤄라.
아직 코드를 쓰지 마라. 내가 검토하게 계획 후 멈춰라.

5. 성찰 혹은 종료 프롬프트 (에이전트가 막혔을 때):

너는 같은 체크를 3번 실패했다. 같은 접근을 반복하지 마라.
3줄로 답하라: 정확히 뭐가 실패했는지, 어떤 가정이 틀렸는지,
시도할 가장 작은 다른 것 하나. 그러고 나서 오직 그것만 시도하라.

모든 프롬프트에는 공통점이 있습니다. 역할, 경계, 완료 체크, 하지 말 것을 명시하세요

모델 라우팅

질문은 보통 양자택일로 던져집니다. 비싼 모델을 계획에 쓸까, 구현에 쓸까? 글쓴이는 이게 틀린 프레임이라고 합니다. 실수의 폭발 반경(blast radius)으로 라우팅하라는 거죠. 실수가 가장 비싼 곳이 어디인지를 물으라는 겁니다.

나쁜 계획은 프로젝트 전체에 퍼집니다. 경계가 분명하고 테스트된 함수 하나의 나쁜 줄은 몇 분이면 잡힙니다. 그러니 실수가 비싸고 되돌리기 어려운 곳에 돈을 쓰고, 싸고 격리된 곳에서 아낍니다.

티어별 라우팅을 정리하면 이렇습니다.

단계

모델 티어

이유

계획·아키텍처·분해

최상위, 항상

레버리지 최고, 토큰량은 적음. 여기서 아끼는 게 가장 비싼 실수

구현 (타이트한 명세)

중간 티어, 병렬

명세가 이미 사고를 했음. 테스트로 검증

구현 (느슨한 명세)

최상위

빈틈 메우기는 추론. 싼 모델은 열두 방향으로 틀리게 추측

리뷰·저지

다른 패밀리, 싸지 않게

비싼 실수를 잡는 곳

탐색·검색·요약·분류

가장 싸고 빠른 모델

추론 제로, 볼륨 큼. grep에 프리미엄 요금 내지 말 것

모델 패밀리마다 성격이 다르다는 점도 라우팅에 영향을 줍니다. 어떤 패밀리는 빈틈을 메웁니다. 명세가 느슨하면 합리적인 가정을 하고 계속 나아갑니다. 또 다른 패밀리는 시킨 것만 하고 그 이상은 거의 안 합니다.

싼 라우팅은 호출당 가격을 최적화하지만, 병합 가능한 출력을 내는 토큰 비중을 조용히 망칠 수 있습니다. 다섯 번 재시도하고 병합 못 할 코드를 내는 싼 모델은, 프리미엄 모델 한 번 깔끔한 패스보다 비쌉니다. 물론 상황마다 다릅니다. 각자의 작업에서, 직접 호출당 비용이 아닌 쓸모 있는 출력의 비용을 측정하세요.

스킬

같은 프롬프트를 계속 붙여넣거나 같은 워크플로를 반복 실행하고 있다면, 그걸 스킬로 만듭니다.

좋은 스킬을 만드는 방법이 몇 가지 있습니다. 스킬을 수백 줄 이하로 짧게 유지하기, 긴 참고 자료는 별도 파일로, 언제 쓰는지를 또박또박 쓰기 같은 것들이죠.

에이전트가 스킬을 다시 쓰게 두지 마세요. 사람이 모든 줄을 큐레이션해야 합니다. 에이전트가 멋대로 고치면 무너집니다.

동적 워크플로

루프가 작동하면 서브 에이전트로 확장합니다. 서브 에이전트에게는 작업 충돌 방지를 위한 git worktree와 자기만의 터미널 창을 줍니다.

tmux new-session -d -s fleet
for name in hilbert gauss poincare; do
  git worktree add -B "agent/$name" "../wt-$name" main
  tmux new-window -t fleet -n "$name" -c "../wt-$name" "$AGENT"
done

에이전트에 이름을 붙이면 좋습니다. agent_1과 agent_15를 구분하기는 어렵겠죠. 추적할 수 없다면 조종할 수 없습니다.

그리고 협력하게 만듭니다. 기본적으로 에이전트는 서로를 무시합니다. 형제가 보낸 것을 소음으로 취급합니다. 모델은 사용자에게 응답하고 주변 신호는 대체로 무시하도록 훈련됐으니까요. 그래서 에이전트 사이 메시지를 사용자 턴으로 전달합니다.

원래 루프는 조율을 모델 안에 둡니다. 모델이 매 스텝을 결정하니까 토큰을 태우고 컨텍스트를 채웁니다. 뭔가 좀 더 나은 방법이 없을까요?

좋은 돌파구가 있습니다. 동적 워크플로입니다. 오케스트레이터가 작업자들을 조율하는 스크립트를 쓰고, 별도 런타임이 그 스크립트를 백그라운드에서 돌립니다. 상태가 모델의 기억이 아닌 스크립트의 변수에 있습니다. 조율에 모델 토큰을 쓰지 않습니다, 메인 컨텍스트는 깨끗하게 유지되고, 수십에서 한 번에 천 개까지 펼쳐집니다.

// 오케스트레이터가 이걸 한 번 쓰고, 런타임이 돌린다. 모델이 아니라.
const files = await glob("src/**/*.ts");

const results = await mapLimit(files, 16, async (file) => {
  const r = await subagent(`${file}을 새 API로 마이그레이션하라. 테스트를 돌려라.`);
  return { file, ok: r.testsPassed };
});

const failed = results.filter(r => !r.ok);
return `migrated ${results.length - failed.length} of ${results.length}. ` +
       `retry: ${failed.map(f => f.file).join(", ")}`;

코드와 런타임이 컨텍스트를 책임집니다. 통과/실패를 평범한 배열에 기록하고, 모델은 짧은 요약만 돌려줍니다. 중간 트랜스크립트는 모델 컨텍스트를 건드리지 않습니다. 모델은 스크립트를 쓰느라 한 번 사고했고, 조율은 스크립트가 공짜로 했습니다.

다만 이건 패턴이 알려져 있고 검증이 객관적일 때만 쓰는 겁니다. "아직 뭘 할지 알아내는 중"이거나 "하나의 일관된 추론 사슬이 필요한" 작업엔 강한 에이전트 하나가 맞습니다.

가드레일, 토폴로지, 컨트롤 파일

사람 병목은 사실 진짜 일을 하고 있었습니다. 병목이 아니고 사람이 원래 그런 겁니다. 그렇다고 사람을 완전히 빼버리면 작은 오류가 복리로 쌓입니다.

그래서 가드레일이 필요합니다. 사람 병목이 제공하던 교정을 대체합니다.

이제 병목은 생성이 아니라 검증입니다. 에이전트는 내가 확인할 수 있는 속도보다 빠르게 그럴듯한 출력을 냅니다. 그럴듯한 건 정확한 게 아닙니다.

토폴로지도 비슷한 결입니다. 에이전트가 많다고 출력까지 많을 필요는 없습니다. 오히려 줄여야 하죠. 조율은 공짜가 아니며, 점점 복리로 늘어납니다. 출력도 규칙입니다. 실제로 대화가 필요한 에이전트는 소수입니다. 조율이 필요 없는 에이전트는 하위 작업으로 위임하세요.

컨트롤 파일은 오케스트레이션을 위한 문서입니다. CLAUDE.md와 조금 다릅니다. orchestration.md를 만드는 겁니다. 어떤 작업에 어떤 모드를 쓸지, 어떤 모델 티어가 어디로 갈지, 가드레일이 뭔지, 언제 사람에게 요청할지. 사람이 쓴 계약이고, 모든 에이전트가 세션 시작마다 읽고 스스로 접근법을 고릅니다.


<!-- -->
# orchestration.md. 여기서 일이 어떻게 RUN되는가. 사람이 큐레이션. 에이전트는 편집 금지.

모드를 골라라:
- 골 루프 (감독형): 모호하거나 설계 작업. 완료 체크를 정의하라.
- 검증 루프: 결정론적 게이트. 항상 반복과 비용에 상한.
- 빌드 + 저지: 작업자가 만들고, 다른 패밀리 저지가 승인.
- 함대 (3~5): 상호의존 하위 작업, 격리된 worktree, 동료 메시징.
- 큐 앤 리셋: 작고 원자적인 작업 다수, 매번 깨끗한 컨텍스트.
- 동적 워크플로: 알려진 패턴, 객관적 게이트, 넓고 반복적. 조율은 코드로, 무인.

모델을 폭발 반경으로 라우팅하라:
- 계획·아키텍처: 최상위, 항상.
- 빌드, 타이트한 명세: 중간 티어, 병렬, 테스트로 검증.
- 빌드, 느슨한 명세: 최상위. 빈틈 메우기는 추론이니까.
- 리뷰·저지: 다른 패밀리. 절대 구현자의 모델이 아님.

가드레일:
- 에이전트당 토큰 예산. 85% 근처 자동 일시정지. 3번 막히면 죽이고 재배정.
- 1시간 넘는 런은 별도 저지 필수. 작업자는 절대 스스로 완료 보고 안 함.
- 가설 세우기 전에 코드 읽기. 첫 편집 전에 서면 계획.

다시 한 번 강조하겠습니다. 짧게 유지할 것, 에이전트가 건드리지 못하게 할 것.

끝까지 사람의 몫으로 남은 것

오케스트레이션은 믿을만한 에이전트를 만듭니다. 하지만 에이전트는 문제를 고르지 못합니다. 무엇이 좋은지도 모릅니다. 사람은 끝까지 다음과 같은 일을 해야 할 겁니다.

판단은 위임하지 않습니다.

명확한 통과/실패 기준이 있는 범위 작업은 넘깁니다.

아키텍처와 "무엇을 만들지 않을지"의 결정, 전체 맥락 리뷰는 사람이 쥡니다.

명세가 핵심입니다.

모호한 사고는 최악입니다. 뛰어난 엔지니어는 뾰족하고 명확한 명세를 가장 중요하게 생각합니다.

적게 투입합니다.

4인 작업에 2명을 배치합니다. 제약이 원하는 행동을 강제합니다. 손으로 하는 대신 루프를 만들게 되고, 다음번엔 그 일이 이미 자동화돼 있습니다. 예산을 수작업에서 토큰으로 옮기는 거죠.

소프트웨어를 쓰는 게 아니라, 소프트웨어를 쓰는 공장을 짓는 겁니다. 공장엔 정밀한 입력, 매 공정의 품질 관리, 그리고 제품이 무엇이어야 하는지 아는 주인이 필요합니다.


이 글은 @Av1dlive의 관점에서 작성되었습니다. 그리고 그 뼈대에 대체로 동의합니다. 특히 컨트롤 파일을 사람이 큐레이션하고 에이전트가 못 고치게 하라는 부분은 정말로 중요하다고 생각합니다.

프롬프트 한 줄을 더 다듬는 시간보다, 멈추는 조건과 검증 게이트를 설계하는 데 시간을 쓰는 편이 결과를 더 좌우한다는 것. 에이전트 하나를 손으로 굴리던 습관에서 벗어나려는 분이라면, 작은 골 루프 하나에 저지 하나를 붙여보는 것부터 시작해보면 좋겠습니다.