23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
나의 에이전트가 순서대로 일하는 대신,
JavaScript 스크립트가 여러 서브에이전트를 지휘하는 방식. 반복·분기·병렬 같은 제어 흐름을 코드로 확정해 두고, 실제 작업만 에이전트에게 맡깁니다.
Claude Code의 Dynamic Workflow 기능이 이 방식을 실행합니다.
아래에 나오는 agent()·pipeline() 같은 함수는 그 스크립트 안에서 쓰는 API이고, Claude Code가 이를 읽어 서브에이전트를 띄웁니다.
큰 작업을 에이전트 하나에게 통째로 맡기면 두 가지 한계가 생깁니다.
컨텍스트가 넘치고(한 머리에 다 안 들어감), 흐름이 모델의 즉흥 판단에 좌우됩니다(매번 다르게 행동).
Dynamic Workflow는 이 문제를 "흐름은 코드로 고정, 작업은 에이전트에 위임" 으로 풉니다. 네 가지를 동시에 얻습니다:
🔭 빠짐없이 (Comprehensive) | 작업을 쪼개 병렬로 커버. 50개 파일도 한 번에 훑습니다. |
✅ 확신 있게 (Confident) | 독립적인 관점으로 교차 검증하고, 적대적으로 반박한 뒤 확정합니다. |
📦 큰 규모로 (Scale) | 한 컨텍스트에 안 담기는 마이그레이션·감사·전수 작업을 처리합니다. |
🎯 결정적으로 (Deterministic) | 반복·분기·팬아웃이 코드로 확정됩니다. 모델의 즉흥 판단이 아닙니다. |
무엇이 달라지는지부터 보겠습니다.
일반 에이전트 호출 | Dynamic Workflow | |
|---|---|---|
흐름을 정하는 주체 | 에이전트가 "다음에 뭘 할지" 매번 스스로 판단 | 루프·조건·팬아웃을 스크립트가 확정 |
재현성 | 모델 판단에 달려 있어 매번 다르게 동작 | 같은 스크립트면 같은 흐름 |
중간 결과 | 전부 Claude의 컨텍스트에 쌓임 | 스크립트 변수에 남고, 컨텍스트에는 최종 결과만 |
에이전트의 역할 | 계획과 실행을 함께 떠맡음 | "이 한 가지 작업"만 하고 결과 반환 — 조립은 코드가 |
구현은 단순합니다. 워크플로우는 meta 블록으로 시작하는 평범한 JavaScript 스크립트이고, 본문에서 agent()를 호출하면 서브에이전트가 떠서 작업하고 그 결과를 다시 JS 코드로 다룹니다.
export const meta = {
name: 'review-changes',
description: '변경된 파일을 차원별로 리뷰하고 각 발견을 검증',
phases: [{ title: 'Review' }, { title: 'Verify' }],
}
// ↑ meta는 반드시 순수 리터럴 (변수·함수 호출 금지)
// ↓ 여기서부터 스크립트 본문 — await를 바로 쓸 수 있음
phase('Review')
const result = await agent('이 디렉토리에서 버그를 찾아라', { schema: BUGS_SCHEMA })
// schema를 주면 검증된 객체를 반환 — 파싱 불필요기억할 점: 서브에이전트에게는 "너의 마지막 텍스트가 곧 반환값"이라고 전달됩니다. 사람에게 보내는 메시지가 아니라 데이터를 반환합니다.
schema를 주면 검증까지 자동입니다.
위 코드는 뼈대만 보여주는 조각입니다. 스키마 정의부터 결과 반환까지 그대로 돌아가는 전체 스크립트는 6장에 있습니다.
함수 | 역할 | 핵심 |
|---|---|---|
| 서브에이전트 하나를 띄움 | schema 있으면 검증된 객체, 없으면 텍스트 반환. 죽으면 |
| 각 항목을 여러 단계에 독립적으로 통과시킴 | 단계 사이에 장벽 없음 — A가 3단계일 때 B는 1단계 가능. 기본 선택 |
| 여러 작업을 동시에 실행 | 장벽(barrier) — 전부 끝날 때까지 대기. 모든 결과가 한꺼번에 필요할 때만 |
| 진행 단계 구분 / 진행 메시지 출력 | 사용자에게 보이는 진행 트리를 구성 |
그 외에 budget (토큰 예산), args (외부 입력), workflow() (다른 워크플로우를 하위 단계로 실행) 등이 있습니다.
런타임이 걸어두는 상한은 두 개입니다.
동시에 도는 에이전트는 최대 16개이고(CPU 코어가 적은 머신에서는 그보다 적게), 한 번의 실행에서 띄울 수 있는 에이전트는 총 1,000개입니다.
그래서 항목을 100개 넘겨도 슬롯이 비는 대로 순서대로 전부 처리됩니다.
이 둘의 차이가 워크플로우 설계의 핵심입니다. 기본은 항상 pipeline이고, parallel은 "정말로 모든 결과를 한꺼번에 모아야 할 때"만 씁니다.
그림 1. 항목 A와 항목 B가 각각 리뷰·검증 단계를 독립적으로 통과하는 흐름도
A가 검증 중일 때 B는 아직 리뷰 중일 수 있습니다. 빠른 항목이 느린 항목을 기다리지 않으므로, 전체 시간은 가장 느린 한 체인이 결정합니다.
그림 2. finder 3개가 장벽에서 전부 완료를 기다린 뒤 결과 병합으로 이어지는 흐름도
이전 단계의 결과가 전부 모여야 다음 단계로 갈 수 있습니다. 전체 결과에서 중복을 제거한 뒤 검증하거나, 0건이면 조기 종료하는 경우가 여기 해당합니다.
판별 기준: "다음 단계가 이전 단계의 모든 항목을 교차 참조하는가?"
— 예(중복 제거/병합/조기 종료)면
parallel, 아니면pipeline. "그냥 flatten/map/filter가 필요해서"는 장벽의 이유가 되지 못합니다.변환은 pipeline 단계 안에서 하세요.
// 표준 패턴 — 리뷰가 끝나는 즉시 그 차원만 검증 시작 (pipeline)
const results = await pipeline(
DIMENSIONS,
d => agent(d.prompt, { phase: 'Review', schema: FINDINGS }),
review => parallel(review.findings.map(f => () =>
agent(`적대적으로 검증: ${f.title}`, { phase: 'Verify', schema: VERDICT })
.then(v => ({ ...f, verdict: v }))
))
)
const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)아래는 자주 쓰는 형태들입니다. 작업 성격에 맞게 조합하면 됩니다.
🛡️ 적대적 검증 (Adversarial verify)
발견마다 독립적인 회의론자 N명이 반박을 시도. 과반이 반박하면 폐기. 그럴듯하지만 틀린 결과를 걸러냄.
🧭 관점 다양화 검증
같은 검증자 N명 대신 각자 다른 렌즈(정확성·보안·성능·재현성)를 줌. 중복으로는 못 잡는 실패 모드를 포착.
⚖️ 심사위원단 (Judge panel)
서로 다른 각도로 N개 시도를 생성 → 병렬 심사로 채점 → 우승안 기반으로 종합. 해법 공간이 넓을 때 강력.
🔁 마를 때까지 반복 (Loop-until-dry)
크기를 모르는 발견 작업(버그·엣지케이스)에서, K번 연속 새 결과가 없을 때까지 finder를 계속 투입. 꼬리를 놓치지 않음.
🔀 멀티모달 스윕
컨테이너별·내용별·엔티티별·시간별 등 서로 다른 방식으로 동시 검색. 한 가지 각도로는 다 못 찾을 때.
🔍 완전성 비평가 (Completeness critic)
마지막에 "빠진 게 뭐냐 — 안 돌린 방식, 미검증 주장, 안 읽은 출처?"를 묻는 에이전트. 답이 다음 라운드 작업이 됨.
지금까지 나온 조각을 하나로 합친 완결된 스크립트입니다.
모듈별로 버그를 찾고, 찾은 즉시 각 발견을 적대적으로 검증해 확정된 것만 반환합니다.
MODULES 경로만 자기 저장소에 맞게 바꾸면 그대로 돌아갑니다.
export const meta = {
name: 'audit-modules',
description: '모듈별로 버그를 찾고 각 발견을 적대적으로 검증',
phases: [
{ title: 'Find', detail: '모듈별 병렬 탐색' },
{ title: 'Verify', detail: '발견마다 반박 시도' },
],
}
// 스키마 — agent()가 이 형태로 검증된 객체를 돌려준다. 파싱·재시도 불필요.
const FINDINGS = {
type: 'object',
properties: {
findings: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' },
file: { type: 'string' },
detail: { type: 'string' },
},
required: ['title', 'file', 'detail'],
},
},
},
required: ['findings'],
}
const VERDICT = {
type: 'object',
properties: {
isReal: { type: 'boolean' },
reason: { type: 'string' },
},
required: ['isReal', 'reason'],
}
// 대상 목록 — 외부에서 args로 넘기거나 기본값 사용
const MODULES = args?.modules ?? ['src/auth', 'src/billing', 'src/api']
log(`${MODULES.length}개 모듈 감사 시작`)
const results = await pipeline(
MODULES,
// 1단계: 모듈마다 버그 탐색
(mod) =>
agent(`${mod} 디렉토리에서 실제로 터질 수 있는 버그를 찾아라.`, {
label: `find:${mod}`,
phase: 'Find', // pipeline 안에서는 phase()가 아니라 opts.phase를 쓴다
schema: FINDINGS,
}),
// 2단계: 그 모듈의 발견만 즉시 검증 — 다른 모듈을 기다리지 않는다
(found, mod) => {
if (!found?.findings.length) return [] // agent가 죽으면 null이 온다
return parallel(
found.findings.map((f) => () =>
agent(
`다음 주장을 반박해 보라: "${f.title}" (${f.file})\n${f.detail}\n` +
`확신이 없으면 isReal=false로 판단하라.`,
{ label: `verify:${mod}`, phase: 'Verify', schema: VERDICT },
).then((v) => ({ ...f, verdict: v })),
),
)
},
)
const confirmed = results
.flat()
.filter(Boolean) // 죽은 에이전트는 null로 떨어진다
.filter((f) => f.verdict?.isReal)
log(`확정 ${confirmed.length}건`)
return { confirmed }읽을 때 짚어볼 지점 네 곳입니다.
2단계의 두 번째 인자 mod — pipeline의 각 단계는 (이전 결과, 원본 항목, 인덱스)를 받습니다. 그래서 1단계 반환값에 모듈명을 끼워 넣어 나르지 않아도 됩니다.
pipeline 안의 parallel — 겉은 장벽 없이 흐르고, 안에서 한 모듈의 발견들만 동시에 검증합니다. 4장의 판별 기준이 그대로 적용된 형태입니다.
phase() 대신 opts.phase — pipeline·parallel 안에서는 전역 phase() 상태가 경쟁할 수 있어 옵션으로 직접 지정합니다.
필터가 두 번 — 단계 자체가 실패한 항목은 null로 떨어지므로 filter(Boolean)이 걸러내고, 검증 에이전트가 죽어 verdict가 null인 항목은 뒤의 verdict?.isReal이 걸러냅니다. 에이전트는 죽을 수 있다고 전제하고 씁니다.
✅ 적합 | ✋ 부적합 |
|---|---|
전수 감사·리뷰·리서치 (병렬 팬아웃) | 대화형 한 턴짜리 응답 |
대규모 마이그레이션 (사이트 발견 → 변환 → 검증) | 사소한 기계적 편집 |
독립 관점의 교차/적대적 검증이 필요한 작업 | 흐름이 단순해 단일 에이전트로 충분한 일 |
한 컨텍스트에 안 담기는 규모 | 사용자가 명시적으로 요청하지 않은 대규모 팬아웃 |
중요한 전제: Dynamic Workflow는 한 번에 수십에서 수백 개의 에이전트를 띄우고 그만큼 토큰을 씁니다. 그래서 기본값은 직접 요청했을 때만 실행되는 쪽입니다
— 프롬프트에
ultracode키워드를 넣거나 "워크플로우로 처리해줘"처럼 말하거나, 저장해 둔 워크플로우를/이름으로 부르는 방식입니다.반대로
/effort ultracode를 켜 두면 매 작업마다 Claude가 알아서 판단해 워크플로우를 씁니다.
비용이 문제라면 깊이를 예산에 맡길 수 있습니다.
스크립트 안에서 budget으로 남은 토큰을 조회할 수 있어서, 몇 개를 돌릴지 미리 정하는 대신 예산이 마를 때까지 돌리는 식으로 쓸 수 있습니다.
const bugs = []
// budget.total이 없으면 remaining()은 Infinity → 가드 필수
while (budget.total && budget.remaining() > 50_000) {
const r = await agent('이 코드베이스에서 버그를 찾아라', { schema: BUGS })
bugs.push(...r.bugs)
log(`${bugs.length}개 발견, ${Math.round(budget.remaining()/1000)}k 남음`)
}루프 대신 고정 규모를 정하는 방식도 같은 요령입니다. budget.total을 나눠 finder 수를 계산하면, 예산이 클수록 더 많이 투입됩니다.
먼저 인라인으로 정찰(파일 나열, 범위 파악)해 작업 목록을 발견한 뒤, 그 목록을 워크플로우로 파이프라인 처리하는 형태가 많습니다.
작업 전에 모든 걸 알 필요는 없고, 오케스트레이션 단계 전에만 알면 됩니다.
직접 해보기: 가장 빠른 길은 기본 내장 워크플로우입니다.
/deep-research <질문>을 실행하면 여러 에이전트가 출처를 교차검증해 리포트를 만들어내는 과정을 그대로 볼 수 있습니다.
내 작업에 쓰려면 프롬프트에 ultracode를 넣거나 "워크플로우로 처리해줘"라고 요청하면 됩니다.
진행 상황은 /workflows에서 실시간으로 볼 수 있고, 마음에 든 실행은 그 화면에서 s 키로 내 명령어로 저장할 수 있습니다.
Claude Code v2.1.154 이상, 유료 플랜에서 동작합니다(Pro는 /config의 Dynamic workflows를 켜야 합니다).
Anthropic API, Amazon Bedrock, Google Cloud Agent Platform, Microsoft Foundry에서도 지원됩니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.