본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 30. 06:36

Opus 4.8에서의 동적 워크플로우 (Dynamic Workflows): 자가 검증형 PR 리뷰어 구축하기

요약

Opus 4.8을 활용하여 단순 채팅을 넘어선 동적 워크플로우 구축 방법을 설명합니다. 오케스트레이터가 서브에이전트를 제어하며 병렬 및 순차 작업을 수행하는 그래프 기반의 에이전트 설계 패턴을 다룹니다.

핵심 포인트

  • 단순 프롬프트 입력을 넘어선 그래프 기반 워크플로우 설계
  • 오케스트레이터를 통한 서브에이전트의 병렬 및 순차 제어
  • 노드별 노력 제어(effort control)를 통한 비용 및 성능 최적화
  • 구조화된 출력 스키마를 활용한 에이전트 간 조합 가능성 확보
  • 자가 검증형 PR 리뷰어 구축을 통한 실전 패턴 학습

당신이 루프(loop)에서 벗어나는 방법

대부분의 사람들은 Opus 4.8을 이전의 모든 모델을 사용했던 방식 그대로 사용합니다. 채팅창을 열고, 요청을 입력하고, 커서가 움직이는 것을 지켜보고, 수정하고, 이를 반복하는 방식이죠. 그것은 대화(conversation)입니다. _동적 워크플로우 (Dynamic Workflow)_는 완전히 다른 것입니다.

변화의 핵심은 이것입니다: 당신이 루프(loop)가 되는 것을 멈추는 것입니다. 대신, 당신이 제어하는 일반적인 코드인 **오케스트레이터 (orchestrator)**가 당신이 설계한 서브에이전트 (subagents)를 생성합니다. 이들은 작업을 병렬로 분산시키고, 단계를 순차적으로 실행하며, 결과를 판단 및 병합하고, 전체 과정이 완료되면 보고합니다. Opus 4.8은 단일 워크플로우 내에서 수백 개의 병렬 서브에이전트를 구동할 수 있으며, 노드(node)별로 **노력 제어 (effort control)**를 적용하여 저렴한 단계는 저렴하게 유지하고, 어려운 단계는 더 깊이 생각하게 할 수 있습니다.

이 튜토리얼에서는 하나의 구체적인 결과물을 구축함으로써 핵심 패턴을 배우게 됩니다: 바로 정확성(correctness), 보안(security), 성능(performance) 전반에 걸쳐 작업을 분산시킨 후, 결과가 당신에게 도달하기 전에 모든 발견 사항을 **적대적 검증 (adversarially verifies)**하는 풀 리퀘스트 (pull-request) 리뷰어입니다.

// 당신은 형태를 설계합니다. 오케스트레이터가 이를 실행합니다.
const found    = await parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS })))
const deduped  = dedupeByFileLine(found.flatMap(r => r.findings))
...

이 과정을 마치면 언제 parallel()을 사용해야 하는지 아니면 pipeline()을 사용해야 하는지, 구조화된 출력 스키마 (structured output schemas)가 어떻게 서브에이전트를 조합 가능하게 (composable) 만드는지, 그리고 노드당 노력을 어디에 설정해야 하는지를 알게 될 것입니다.

사고 모델: 프롬프트가 아니라 그래프입니다

"프롬프트를 보내고, 완료된 응답을 받는다"라는 생각은 버리십시오. 대신 다음과 같이 생각하기 시작하십시오: 오케스트레이터가 워크플로우 그래프 (workflow graph)를 실행하며, 각 노드는 에이전트 호출 (agent call)이다. 오케스트레이터는 일반적인 코드입니다. 무엇을 어떤 순서로 실행할지, 그리고 각 결과로 무엇을 할지를 결정합니다. 서브에이전트는 말단 작업자(leaf workers)입니다. 각 에이전트는 집중된 프롬프트, 구조화된 출력 스키마, 그리고 고유한 노력 설정(effort setting)을 부여받습니다. 작업의 단위는 더 이상 프롬프트가 아니라 그래프입니다.

모든 그래프는 두 가지 기본 요소 (primitives)로 구성되며, 이 둘의 차이점은 전적으로 장벽 (barriers) — 즉, 오케스트레이터가 언제 차단(block)하고 기다리는가 — 에 달려 있습니다.

parallel()은 장벽입니다

parallel() 팬 아웃(fan-out)은 동시에 여러 서브에이전트(subagents)로 작업을 분산하며, 모든 에이전트가 결과를 반환할 때만 해결(resolve)됩니다. 가장 느린 노드가 완료될 때까지 이후의 단계는 실행되지 않습니다. 다음 의사결정을 내리기 전에 반드시 모든 결과가 수집되어야 하는 독립적인 작업에 사용하세요. 예를 들어 리뷰 차원(dimension)당 하나의 서브에이전트 할당, N-way 검증, 수백 개의 동시 체크 등이 이에 해당합니다.

// FAN-OUT: 차원들이 독립적임 → 함께 실행
const found = await parallel(
  DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS, effort: "medium" }))
...

() => 형태의 쏜크(thunks)에 주목하세요. parallel()이 직접 이들을 호출합니다. 즉, parallel()은 작업을 스케줄링하는 것이지, 이미 시작된 프로미스(promises)를 전달받는 것이 아닙니다.

pipeline()은 순서를 강제합니다

pipeline()은 단계 $N+1$이 단계 $N$의 출력에 의존하는 스테이지(stages)들을 체이닝(chaining)합니다. 각 단계는 입력값이 존재할 때까지 차단(block)되므로, 단계들은 엄격하게 순차적으로 실행되며 지연 시간(latencies)이 합산됩니다. 진정한 데이터 의존성이 있을 때 이를 사용하세요. 예를 들어, 발견 사항(findings)이 존재하기 전에는 리뷰를 합성할 수 없으며, 발견 사항이 중복 제거(deduplicated)되기 전에는 이를 검증할 수 없습니다.

const review = await pipeline(
  () => parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS }))),
  (found)   => dedupeByFileLine(found.flatMap(r => r.findings)),
...

dedupeByFileLine은 에이전트가 아니라는 점에 유의하세요. 결정론적(deterministic)인 작업은 코드 내에 유지됩니다. 판단(judgment)이 필요한 경우에만 서브에이전트를 사용합니다.

전체 문법 요약: 독립성을 위해서는 parallel, 의존성을 위해서는 pipeline을 사용합니다. 실제 워크플로우는 이 두 가지를 교차하며 사용하며, 폭을 넓히기 위해 팬 아웃(fanning out)하고 순서가 중요한 곳에서는 체이닝(chaining)합니다.

구조화된 출력(Structured outputs): 파싱이 아닌 타입 지정

위의 모든 agent() 호출은 schema를 전달합니다. 모델은 해당 계약(contract)에 맞춰 형성된 데이터 — FINDINGS, VERDICT, REVIEW — 를 반환하므로, 산문(prose)을 정규 표현식(regex)으로 처리하는 대신 필드를 인덱싱할 수 있습니다. 이 덕분에 중복 제거 및 필터링 단계가 또 다른 LLM 호출이 아닌 *일반 코드(plain code)*로 동작할 수 있습니다.

const real = verified.filter(v => v.refuted === false)

스키마(Schemas)는 하위 에이전트(subagents)를 조합 가능하게(composable) 유지하는 이음새 역할을 합니다. 노드의 출력은 기계가 읽을 수 있는 형태이므로, 다음 노드(에이전트 또는 코드)는 중간에 파싱 레이어(parsing layer)를 거칠 필요 없이 이를 바로 소비할 수 있습니다.

실전 예제: 자가 검증형 PR 리뷰어

대부분의 "AI 코드 리뷰"는 하나의 모델, 하나의 프롬프트, 한 번의 패스(one pass)로 이루어집니다. 이는 그럴듯한 버그를 찾아내지만, 실제로는 존재하지 않는 버그를 포함하여 모두 동일한 확신을 가지고 보고합니다. 동적 워크플로우(Dynamic workflows)를 사용하면 더 나은 결과를 얻을 수 있습니다. 리뷰 차원(dimensions)에 따라 병렬로 확장(fan out)한 다음, 모델이 결과를 보고하기 전에 _자신의 발견 사항을 스스로 공격(attack its own findings)_하도록 만드는 것입니다. 전체 파이프라인은 다음과 같습니다.

1단계: 차원에 따른 확장 (Fan out across dimensions)

리뷰 차원당 하나의 하위 에이전트를 실행합니다. 이들은 서로 의존하지 않으므로 배리어(barrier) 뒤에서 동시에 실행됩니다.

const DIMENSIONS = [
  { name: "correctness", prompt: correctnessPrompt(diff) },
  { name: "security",    prompt: securityPrompt(diff) },
...

agent() 호출은 고유한 컨텍스트 윈도우(context window)를 가진 격리된 하위 에이전트입니다. 즉, 보안 리뷰어는 성능 리뷰어의 노이즈를 절대 볼 수 없습니다. { schema: FINDINGS }는 구조화된 출력(structured output)을 강제합니다. 즉, 나중에 정규 표현식(regex)으로 처리해야 하는 산문(prose)이 아니라 { file, line, severity, claim } 형태의 배열을 반환합니다.

2단계: 중복 제거 (에이전트가 아닌 일반 코드 사용)

세 명의 리뷰어가 동일한 라인을 지적할 수 있습니다. 병합은 결정론적인 집합 논리(set logic)이므로, 이를 위해 모델을 낭비하지 마세요.

const deduped = dedupeByFileLine(found.flatMap(r => r.findings));

flatMap은 차원별 배열을 하나의 리스트로 평탄화(flatten)하며, dedupeByFileLine(file, line) 키를 공유하는 항목들을 하나로 합칩니다. 정답이 기계적인 작업이라면 어디에서든 코드를 사용하세요. 에이전트는 판단을 위한 것이지, 조인(joins)을 위한 것이 아닙니다.

3단계: 적대적 검증 (Adversarially verify)

이 단계가 오탐(false positives)을 제거하는 단계입니다. 살아남은 각 발견 사항에 대해, 오직 그것을 **반박(refute)**하는 것만을 임무로 하는 회의론자(skeptic) 하위 에이전트를 생성합니다.

const verified = await parallel(
  deduped.map(f => () => agent(refutePrompt(f), { schema: VERDICT }))
);
...

refutePrompt(f)는 서브 에이전트(subagent)에게 다음과 같이 지시합니다: "여기에 버그라고 주장되는 내용이 있습니다. 이것이 틀렸음을 증명하십시오. 이를 안전하게 만드는 가드(guard), 호출자(caller), 또는 타입을 찾아내십시오." VERDICT{ refuted: boolean, reason: string } 형식입니다. 전담 공격자(dedicated attacker)의 검증을 견뎌낸 발견 사항은 보고할 가치가 있지만, 그렇지 못한 것은 가치가 없습니다.

더 높은 위험도가 따르는 발견 사항의 경우, 발견 사항당 _N_명의 회의론자(skeptics)를 배치하고 과반수가 반박하지 못하는 내용만 남깁니다. 이를 통해 검증(verification)은 리뷰(review)와 독립적으로 확장됩니다:

async function survivesQuorum(f, n = 3) {
  const verdicts = await parallel(
    Array.from({ length: n }, () => () => agent(refutePrompt(f), { schema: VERDICT }))
...

이것은 판사 패턴(judge pattern)입니다. 반박(refutation)은 1단계의 생성(generation) 단계와 분리된 판결(adjudication) 과정입니다. 모델에게 단순히 자신의 발견 사항을 다시 요약하라고 요청하는 것은 약한 발견 사항들을 보고서에 세탁하여 포함시키는 결과를 초래합니다. 반박은 합의(agreement)보다 더 날카로운 필터입니다.

4단계: 합성 (Synthesize)

하나의 에이전트가 확인된 발견 사항들을 사람이 읽을 수 있는 리뷰로 변환합니다.

const review = await agent(synthesisPrompt(real), { schema: REVIEW });

전체 연결 (Wiring it together)

const review = await pipeline(
  ()        => parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS }))),
  (found)   => dedupeByFileLine(found.flatMap(r => r.findings)),
...

pipeline()은 순차적(sequential)입니다. 즉, 각 단계의 출력이 다음 단계의 입력으로 들어갑니다. parallel()은 1단계와 3단계 내부의 배리어(barrier) 역할을 합니다.

노드별 노력 제어 (Effort control per node)

모든 노드에 동일한 연산 자원(compute)을 할당할 필요는 없습니다. 호출당 노력(effort)을 설정하십시오. 반박은 범위가 좁은 질문이므로 회의론자들은 낮은 비용으로 실행하며, 합성은 사람이 신뢰하는 결과물(artifact)이므로 높은 노력으로 실행합니다.

agent(refutePrompt(f),       { schema: VERDICT, effort: "low"  });
agent(synthesisPrompt(real), { schema: REVIEW,  effort: "high" });

판단이 어려운 곳에는 추론(reasoning) 자원을 집중하고, 작업이 기계적인 곳에서는 자원을 아낍니다. 그리고 최종 리뷰가 게시되기 전에는 여전히 사람이 승인합니다.

함정과 모범 사례 (Pitfalls and best practices)

의존성에 맞춰 프리미티브(primitive)를 매칭하기

parallel()은 가장 느린 노드가 완료될 때 반환되며, pipeline()은 스테이지를 순차적으로 실행하고 그 지연 시간 (latency)을 누적합니다. 이 둘을 잘못 매칭하는 것이 가장 흔한 비용 실수입니다. 리뷰 차원 (dimensions)들은 서로 독립적이므로, 이를 체이닝 (chaining)하지 말고 팬 아웃 (fan out) 하세요.

// Good: 3개의 차원이 동시에 실행되며, 실제 소요 시간 (wall-time) ≈ 가장 느린 차원
const found = await parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS })))

...

pipeline()은 진정한 데이터 의존성이 있는 경우를 위해 남겨두세요. 예를 들어, 중복 제거 (dedup)의 출력이 반드시 필요하여 해당 엣지 (edge)가 순차적이어야 하는 경우에 사용합니다.

검증하기 전에 중복을 제거하세요 (Dedup before you verify)

검증 (Verification)은 비용이 많이 드는 단계입니다. 발견된 사항(finding) 하나당 N개의 회의론자 (skeptics)를 생성할 수 있기 때문입니다. 만약 정확성 (correctness)과 보안 (security) 모두에서 auth.js:42를 지적했다면, 두 번 검증하는 것은 아무런 이득 없이 예산만 낭비하는 것입니다. 에이전트 (agent)를 사용하지 말고 일반적인 코드로 먼저 중복을 통합하세요.

머지 (merge) 단계에는 사람이 개입하게 하세요

합성 (synthesize) 단계는 인간 참여형 (human-in-the-loop) 체크포인트입니다. 확인된 발견 사항들은 권장 사항일 뿐 자동 커밋 (auto-commit)이 아닙니다. 어떤 것이 반영되기 전에 반드시 사람이 승인해야 합니다.

노이즈가 아닌 신호를 증폭하세요

팬 아웃 (fan-out)은 베이스 노드 (base node)가 생성하는 모든 것을 배수로 늘리기 때문에, 베이스 노드의 신뢰성이 중요합니다. Anthropic의 보고에 따르면 Opus 4.8은 이전 모델보다 침묵하는 코드 버그 (silent code bugs)를 약 4배 적게 발생시킵니다. 각 리프 리뷰어 (leaf reviewer)가 더 신뢰할 수 있을수록, 많은 리뷰어를 병렬로 실행하는 것이 더 안전합니다.

언제 워크플로우를 사용해야 하는가

단일 에이전트 (single agent)가 기본값으로 적절합니다. 작업에 "이름을 붙일 수 있는 구조"가 있을 때만 동적 워크플로우 (dynamic workflow)를 고려하세요. 즉, 병렬로 팬 아웃되는 독립적인 차원들, 자기 평가 (self-graded) 방식이 아닌 적대적 (adversarial) 방식이어야 하는 검증 단계, 또는 확인된 입력값에 의존하는 합성 패스 (synthesis pass)가 있는 경우입니다.

PR 리뷰 예시는 각 단계가 서로 다른 형태를 띠고 있기 때문에 워크플로우를 사용할 가치가 있습니다. 즉, 팬 아웃 (fan out), 코드를 통한 통합 (collapse in code), 반박을 위한 재차 팬 아웃 (fan out again to refute), 그리고 합성 (synthesize)의 과정을 거칩니다. parallel()은 장벽 역할을 하고, pipeline()은 순서를 강제하며, 스키마 (schemas)는 연결 부위를 기계가 읽을 수 있는 상태로 유지합니다. 노력은 합성 단계에 집중하고, 기계적인 통과 단계에는 최소한으로 투입합니다.

열린 질문 (Open question): 당신의 "나를 믿으세요 (trust me)"라고 말하는 에이전트 단계 중 실제로 회의론자가 기다리고 있는 검증되지 않은 주장 (unverified claim)은 무엇입니까?

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0