3줄 요약
- 0xCodez가 X에 올린 전체 강의 “Graph Engineering with Claude"의 정리다. 다단계 에이전트를 만들면 대부분 한 줄로 줄을 세우는데, 그 단계 중 절반은 앞 단계를 기다릴 이유가 없다는 관찰에서 출발한다.
- 일의 모양 자체가 그래프라는 관점으로 다시 그린다. 노드가 생각하고 엣지가 결과를 나른다. Claude Code의 dynamic workflows는 Claude가 평범한 자바스크립트 오케스트레이션 스크립트를 짜고, 그 스크립트가 서브에이전트 함대를 부린다. 조율 자체는 코드라서 모델 토큰이 들지 않는다.
- 노드와 엣지 정의부터 팬아웃·팬인, 라우터, 검증자, 격리, 수렴하는 순환, 모델 티어링, 토폴로지 선택, 그리고 Claude가 그래프를 스스로 그리는 self-routing까지 14단계로 쌓아 올린다.

왜 그래프인가
다단계 에이전트를 처음 만들면 십중팔구 곧은 직선이 나온다. 1단계, 2단계, 3단계가 앞 단계가 끝나기를 얌전히 기다렸다가 시작한다. 그런데 그 단계 중 절반은 애초에 기다릴 필요가 없었다. 라우팅도, 분기도, 병렬화도 없이 그저 줄만 선다. 머리 하나, 컨텍스트 하나, 한 번에 한 가지. 그러다 컨텍스트 창이 차면 에이전트는 자기가 뭘 하던 중이었는지 잊는다.
저자는 아무도 대놓고 말하지 않는 전환을 이렇게 옮긴다. 프롬프트는 문장이고, 루프는 순환이며, 하네스는 에이전트가 딛고 서는 바닥이다. 그러나 일 자체의 모양, 무엇이 무엇보다 먼저 도는지, 무엇이 동시에 돌 수 있는지, 무엇이 나머지 전부를 기다려야 하는지, 그 모양은 그래프다. 노드가 생각하고 엣지가 결과를 나른다.
Claude Code는 이 그래프를 직접 짓는 도구를 내놓았다. dynamic workflows다. Claude가 평범한 자바스크립트 오케스트레이션 스크립트를 쓰고, 그 스크립트를 실행할 서브에이전트 함대를 조율해 띄운다. 조율 자체는 대화가 아니라 코드이기 때문에 모델 토큰이 한 푼도 들지 않는다.
노드와 엣지: 그래프의 두 요소
01. 노드는 일이고, 엣지는 흐르는 것이다. 그래프에는 딱 두 가지만 있고, 이 둘을 헷갈리지 않는 것이 혼란의 대부분을 없앤다. 노드는 작업 단위다. 에이전트 하나, 경계가 분명한 일 하나, 입력 하나가 들어가 출력 하나가 나온다. 엣지는 의존이다. 이 노드의 출력이 저 노드의 입력으로 들어간다는 뜻, 그 이상은 아니다.

흔한 실수는 “그리고 나서"를 엣지로 착각하는 것이다. “파일을 요약하고 나서 날씨를 알려줘"에는 둘 사이에 엣지가 없다. 날씨는 요약을 소비하지 않는다. 데이터가 실제로 건너갈 때만 엣지가 존재한다. 에이전트 안의 모든 “그리고 나서"에 물어야 한다. 다음 단계가 앞 단계의 출력을 읽는가. 읽지 않는다면 엣지는 없고, 그 기다림은 낭비다.
02. 네 선형 스크립트는 퇴화한 그래프다. “A 하고, B 하고, C 하고, D 해"라고 쓰면 이미 그래프를 그린 것이다. 가지 없는 사슬 하나. 모든 노드가 들어오는 엣지 하나, 나가는 엣지 하나를 가진다. 맞게 돌긴 한다. 다만 느리고 취약하다. 사슬에는 여분이 없어서, C가 멈추면 D는 영영 오지 않고 A가 한 일은 갈 곳 없이 상류에 갇힌다. 그래프 엔지니어링의 첫 실전 기술은 이 사슬을 다시 그리는 것이다. 화살표마다 1단계의 질문을 던지면, 데이터를 나르지 않는 화살표 두세 개가 드러난다. 그것들을 끊으면 사슬은 더 넓은 무언가로 무너져 내린다. 동시에 돌 수 있는 독립 노드 몇 개가, 그것들을 모두 필요로 하는 노드 하나로 모이는 모양이다.
03. 모든 노드에 계약을 준다. 추론할 수 없는 노드는 병렬화할 수 없는 노드다. 해법은 계약이다. 경계가 분명한 입력, 경계가 분명한 출력, 정확히 한 가지 일. 입력은 노드가 읽는 것 전부이며, 공유 컨텍스트에서 넘겨받았다고 가정하지 않고 명시적으로 전달한다. 출력은 정해진 형태이며, 되도록 검증까지 거쳐 다음 노드가 추측 없이 소비한다.
워크플로우에서 이 계약은 스키마로 강제된다. agent() 호출에 JSON 스키마를 함께 주면, 서브에이전트는 검증된 구조화 데이터를 반환하도록 강제된다. 검증은 도구 호출 계층에서 일어나므로, 형태가 어긋나면 Claude가 알아서 재시도한다. 파싱하고 요행을 비는 자유 텍스트를 받는 것과는 다르다.
// 진짜 계약이 있는 노드: 입력·출력에 경계, 검증된 출력, 한 가지 일.
const ITEM = {
type: 'object', additionalProperties: false,
properties: {
title: { type: 'string' },
url: { type: 'string' },
impact: { type: 'string', enum: ['high', 'medium', 'low'] },
},
required: ['title', 'url', 'impact'],
};
const result = await agent(source.prompt, {
label: `research:${source.key}`,
schema: ITEM, // 검증된 구조화 출력을 강제
agentType: 'general-purpose',
});
04. 엣지를 데이터 계약으로 다룬다. 엣지는 “B가 A 다음에 온다"가 아니다. 무엇이 건너가는지에 대한 약속이다. A는 이 형태를 만들고, B는 이 형태를 소비하도록 지어졌다. 엣지를 순서가 아니라 데이터로 이름 붙이면, 엣지가 진짜인지(데이터가 실제로 움직이는가) 한눈에 보이고, 형태만 유지되면 양 끝의 노드를 갈아 끼워도 그래프가 깨지지 않는다. 실전에서 엣지는 평범한 자바스크립트로 산다. 팬아웃과 합성 사이의 리듀스 단계, 즉 평탄화·중복 제거·필터링은 노드가 반환한 형태를 다루는 코드일 뿐이다. 에이전트가 필요 없다. 그래프 사고의 조용한 승리 하나가 여기 있다. 사람들이 모델 토큰을 태우는 일의 상당량은 사실 엣지이고, 엣지는 공짜다.
부채꼴로 펼치고, 한 곳에 모은다
05. parallel()로 펼친다. 모든 값을 치르게 하는 수가 이것이다. 독립된 노드가 N개 있을 때, 즉 확인할 소스 N개, 리뷰할 파일 N개, 감사할 경로 N개가 있을 때는 사슬로 잇지 않는다. 한꺼번에 펼쳐 동시에 돌린다. 워크플로우에서는 parallel()이다. Claude가 thunk 배열을 받아 thunk마다 서브에이전트 하나를 띄우고, 모두 동시에 실행한 뒤 결과 배열을 돌려준다.

견고함을 만드는 세부가 둘 있다. 첫째, parallel()은 배리어라서 모든 thunk가 끝날 때까지 기다린 뒤 반환한다. 다음 단계가 완전한 집합을 본다. 둘째, 예외를 던지는 thunk는 배치 전체를 무너뜨리는 대신 null로 귀결한다. 그래서 불안정한 에이전트 하나가 실행 전체를 가라앉히지 못한다. 결과는 언제나 .filter(Boolean)으로 거른다. 동시성은 코어 수 언저리로 제한되고 초과분은 대기열에 들어가므로, thunk 백 개를 넘겨도 한 줌씩 차례로 다 끝난다.
phase('Research');
// 소스 아홉, 에이전트 아홉, 동시에.
const raw = await parallel(
SOURCES.map((s) => () =>
agent(s.prompt, {
label: `research:${s.key}`,
phase: 'Research',
schema: ITEM_SCHEMA, // 노드마다 검증된 JSON 반환
agentType: 'general-purpose',
}),
),
);
const collected = raw.filter(Boolean); // 실패한 에이전트의 null 제거
이 팬아웃은 모델 대화가 아니라 Claude가 쓴 코드 안에 산다. Claude 자신의 컨텍스트는 아홉 소스를 한꺼번에 담지 않는다. 서브에이전트가 저마다 제 컨텍스트를 지고, 최종 답만 돌아온다. 그래서 워크플로우를 수십, 수백 서브에이전트로 키워도 세션이 잠기지 않는다.
06. 배리어에서 모은다. 팬아웃은 무언가 모아야만 쓸모가 있다. 팬인은 엣지가 수렴하는 노드다. 에이전트 하나(또는 코드 한 조각)가 상류 결과 전부를 한꺼번에 보고, 집합 전체가 있어야 가능한 일을 한다. 소스를 가로질러 중복을 제거하거나, 임팩트로 순위를 매기거나, 전부 비어 돌아왔으면 일찍 빠져나가는 일이다. 배리어가 벽시계 비용을 치를 값어치를 하는 유일한 자리다. 그래프를 빠르게 유지하는 규칙은 이렇다. 배리어는 어떤 단계가 앞선 결과 전부를 정말로 한꺼번에 필요로 할 때만 쓴다. 그냥 리스트를 평탄화하는 정도라면 그건 엣지이니 인라인으로 처리한다.
07. 다이아몬드: 나누고, 일하고, 합친다. 팬아웃과 팬인을 붙이면 진지한 에이전트 그래프마다 등장하는 일꾼 토폴로지가 나온다. 다이아몬드다. 노드 하나가 일을 나누고, 여러 노드가 병렬로 일하고, 노드 하나가 합친다. 시장 조사든 의존성 감사든 코드 리뷰든 리서치 보고서든, 소스와 프롬프트만 바꾸면 같은 골격이 들어맞는다. 외워둘 만한 정식 이름이 있다. 펼치고(fan out), 줄이고(reduce), 합성한다(synthesize). 넓게 펼쳐 폭을 모으고, 평범한 코드로 줄여 압축하고, 마지막 에이전트로 합성해 답을 쓴다. 다이아몬드가 보이기 시작하면 “에이전트가 단계를 더 밟게 하려면"이 아니라 “스플릿은 어디, 머지는 어디"를 묻게 된다. 실제로 규모를 키우는 질문이 이것이다.
엣지에 논리를 얹는다
08. 조건으로 실행 시점에 엣지를 고른다. 모든 그래프가 고정은 아니다. 어떤 엣지를 탈지 노드가 발견한 것에 달릴 때가 있다. 라우터 노드가 결과를 살펴 어느 하류 경로를 켤지 정한다. 티켓을 분류한 뒤 맞는 핸들러로 분기하거나, 변경분 크기를 보고 빠른 리뷰냐 전면 감사냐를 가른다. 워크플로우에서는 노드의 검증된 출력에 대한 자바스크립트 if나 switch일 뿐이다. 제어 흐름이 코드 안에 살기 때문이다. 여기서 결정론이 한계가 아니라 기능이 된다. 라우터의 판단은 Claude가 내릴 수 있지만(서브에이전트가 분류), 라우팅 자체는 Claude가 쓴 코드다. 같은 분류에는 매번 같은 방식으로 돈다. “Claude가 감사를 건너뛰기로 했다” 같은 창발적 놀람은 없다. 건너뛰려면 그래프에 적혀 있어야 하는데, 적혀 있지 않기 때문이다.
09. 엣지에 검증자를 세운다. 그래프의 진짜 지렛대는 에이전트를 더 늘리는 것이 아니라, 그것들을 감싸 확신을 만들어내는 구조다. 검증자 노드는 결과가 하류로 내려가기 전 엣지에 앉아, 그 발견을 죽이려 애쓰는 것만이 임무다. 살아남으면 통과하고, 아니면 답에 닿지 못한다.

손에 쥘 만한 패턴이 셋이다.
- 적대적 검증: 발견마다 그것을 반박하도록 지시받은 독립 회의론자 N명을 띄우고, 과반이 살아남을 때만 유지한다.
- 다양한 렌즈 검증: 검증자마다 다른 렌즈를 준다. 정확성, 보안, 재현 여부. 같은 점검을 N번 하는 것으로는 못 잡는 실패 양상을 다양성이 잡는다.
- 심사 패널: 서로 다른 각도로 시도 N개를 만들고, 병렬 심사로 점수를 매긴 뒤, 승자를 바탕으로 합성하면서 차점자들의 좋은 대목을 접붙인다.
한 팀이 Bun 런타임을 포팅할 때 루프 안에 적대적 코드 리뷰를 넣어 돌린 것이 바로 이 패턴이다.
10. 노드를 격리해 한 실패가 그래프를 오염시키지 못하게 한다. 사슬에서는 실패가 연쇄한다. C가 죽으면 D는 안 돌고 전체가 멈춘다. 그래프에서는 실패가 제 노드 안에 갇혀야 한다. 일부는 이미 그렇다. parallel() 안에서 예외를 던진 thunk는 null로 귀결하므로, 나쁜 에이전트 하나가 빠져도 좋은 에이전트 여덟은 여전히 반환한다. .filter(Boolean)이 그 격리 장치다. 팬인은 언제나 입력이 빠질 수 있음을 견디도록 설계하고, 집합이 다 찼다고 가정하지 않는다. 더 미묘한 실패는 노드끼리 밟는 것이다. 에이전트들이 병렬로 파일을 쓰면 충돌할 수 있다. 해법은 격리다. isolation: "worktree"를 주면 에이전트마다 제 git worktree에서 샌드박스로 일하고 깔끔하게 병합한다. 노드가 실제로 병렬로 쓸 때만 꺼낸다. 모든 실행에 붙는 기본 세금이 아니라, 그것이 필요한 단 하나의 토폴로지를 위한 안전벨트다.
수렴하는 순환
11. 순환을 넣되, 반드시 수렴시킨다. 일이 얼마나 큰지 안에 들어가 봐야 아는 때가 있다. 크기를 모르는 발견, 버그 하나를 찾으면 셋이 더 튀어나오는 버그 청소 같은 것이다. 그런 일에는 순환, 곧 앞 노드로 되돌아가는 통제된 엣지가 필요하다. 위험은 뻔하다. 수렴하지 않는 순환은 예산이 바닥날 때까지 에이전트를 띄우는 무한 루프다. 수렴하는 패턴은 loop-until-dry다. 새로 나오는 게 없는 라운드가 K번 연속될 때까지 파인더를 띄우다가 멈춘다. 성패를 가르는 세부 하나, 그리고 거의 모두가 처음에 저지르는 실수는 무엇을 기준으로 중복을 제거하느냐다. 확인된 결과만이 아니라 지금까지 본 것 전부를 기준으로 중복을 제거해야 한다. 그러지 않으면 기각된 발견이 매 라운드 다시 나타나고, 루프는 영영 마르지 않으며, 같은 막다른 길을 돈 내고 재발견하는 기계를 지은 꼴이 된다.
const seen = new Set(); const confirmed = []; let dry = 0;
while (dry < 2) { // 빈 라운드 2번이면 멈춘다
const found = (await parallel(
FINDERS.map((f) => () => agent(f.prompt, { schema: BUGS }))
)).filter(Boolean).flatMap((r) => r.bugs);
const fresh = found.filter((b) => !seen.has(key(b)));
if (!fresh.length) { dry++; continue; } // 새것 없음 → 마름으로
dry = 0;
fresh.forEach((b) => seen.add(key(b))); // 확인된 것이 아니라 '본 것'으로 중복 제거
const judged = await parallel(fresh.map((b) => () =>
parallel(['correctness', 'security', 'repro'].map((lens) => () =>
agent(`${lens} 관점에서 이 발견이 진짜인가?`, { label: b.desc, schema: VERDICT })))
.then((v) => ({ b, real: v.filter(Boolean).filter((x) => x.real).length >= 2 }))));
confirmed.push(...judged.filter((v) => v.real).map((v) => v.b));
}
비용과 지연을 정하는 것
12. 노드마다 모델을 계층으로 나눈다. 모든 노드가 최고 모델을 필요로 하지는 않는다. 그래프는 이걸 단일 에이전트가 못 하는 방식으로 드러낸다. 경계가 분명하고 반복적인 노드(이 필드를 추출해라, 이 티켓을 분류해라)가 있고, 진짜 판단을 지는 노드(보고서를 합성해라, 발견을 판정해라)가 있다. 지루한 노드는 값싼 모델로 돌리고, 비싼 토큰은 판단이 실제로 사는 곳에 쓴다. 워크플로우에서 서브에이전트는 스크립트가 덮어쓰지 않는 한 세션 모델을 물려받는다. 그래서 기본값으로 큰 실행이 통째로 세션 등급으로 청구된다. agent() 호출 하나에 준 model 옵션이 그 노드만 다른 곳으로 라우팅한다. 큰 실행 전에 모델을 확인하고, 팬아웃의 반복 노드는 값싼 모델로 내리되 머지 노드는 높게 유지한다. 토큰을 많이 먹는 그래프를 모양은 그대로 둔 채 비싼 것에서 경제적인 것으로 바꾸는 지렛대다.
13. 토폴로지가 곧 비용이자 지연이다. 그래프의 모양은 장식이 아니라 벽시계 시간을 좌우하는 가장 큰 단일 지렛대다. 모두가 걸려 넘어지는 선택은 parallel()이냐 pipeline()이냐다. parallel() 배리어는 다음 단계가 시작되기 전에 가장 느린 노드를 모두가 기다리게 한다. pipeline()은 배리어 없이 항목마다 모든 단계를 독립으로 흘려보낸다. 항목 A가 3단계에 있는 동안 항목 B는 아직 1단계에 있어도 된다. 빠른 항목은 느린 항목 뒤에서 놀지 않고 일찍 끝난다.

기본은 pipeline()으로 둔다. 배리어는 어떤 단계가 앞선 결과 전부를 정말로 한꺼번에 필요로 할 때만 꺼낸다. 집합 전체를 가로지르는 중복 제거, 총량에 따른 조기 종료, “다른 발견들"과 비교하는 프롬프트 같은 경우다. “코드가 더 깔끔해서”, “단계들이 분리돼 보여서"는 이유가 못 된다. 배리어 지연은 실재하고 측정되는 낭비 시간이다. 분리된 것과 동기화된 것은 같지 않다.
Claude가 그래프를 그린다
14. self-routing: Claude에게 그래프를 그리게 한다. 마지막 수는 미리 계획할 수 없는 일에 대해 손으로 그래프 그리기를 그만두는 것이다. dynamic workflows에서는 목표를 서술하면 Claude가 오케스트레이션 스크립트를 직접 쓴다. 작업을 분해하고, 팬아웃을 고르고, 조율된 서브에이전트 함대를 띄우고, 결과를 합성한다. 들어맞기를 바란 고정 그래프가 아니라, 이번 실행에 맞춘 그래프를 얻는다. 들어가는 문이 셋이다. 프롬프트에 “workflow"라는 단어를 넣으면 Claude가 그 작업에 맞는 워크플로우를 쓴다. 저장했거나 번들된 것을 실행한다. /deep-research는 프로덕션에 실제로 실린 그래프로, 범위 설정에서 병렬 검색, 페치, 적대적 검증, 합성으로 이어지는 이 강의 그대로의 골격이다. 또는 ultracode를 켜면 Claude가 세션의 모든 실질적 작업에 워크플로우를 계획한다. 실행이 마음에 들면 s를 눌러 스크립트를 .claude/workflows/에 저장한다. 버전 관리되고, 이름으로 다시 돌릴 수 있으며, 리포를 클론한 누구나 띄울 수 있는 그래프가 된다.
이번 주에 지어볼 여섯 그래프
저자가 실습으로 권하는 여섯 가지다.
| 그래프 | 골격 |
|---|---|
| 모든 경로의 보안 청소 | 경로 파일마다 서브에이전트 하나가 누락된 인증 점검을 사냥하고, 검증자 패스가 보고 전에 모든 발견을 확인한다. |
/deep-research로 인용 있는 보고서 | 질문을 여러 각도로 분해하고, 병렬 검색, 소스 중복 제거, 세 표 회의론자의 적대적 검증을 거쳐 쓴다. |
| 모듈을 파일 단위로 포팅 | 파일별로 번역을 펼치고, 테스트 스위트를 각 파일의 게이트로 걸며, 실패를 되돌려 순환시킨다. |
| 변경분의 적대적 리뷰 | 변경분 크기로 라우팅한다. 작으면 빠른 한 번, 크면 정확성·보안·성능 렌즈의 전면 병렬 감사 뒤 심사 패널이 합성한다. |
| 정기 생태계 스캔 | 한 번 저장해 두고 영원히 다시 돌린다. 여러 소스를 병렬로 확인해 배리어에서 임팩트로 순위 매기고 다이제스트를 쓴다. |
| 크기를 모르는 발견 | 파인더를 병렬로 돌려 새 발견마다 본 것 전부와 대조하고, 생존자를 검증하며, 두 라운드가 빈손일 때까지 순환하다 멈춘다. |
가장 눈여겨본 것
이 강의에서 되풀이해 돌아온 문장은 “조율은 대화가 아니라 코드라서 토큰이 들지 않는다"였다. 멀티에이전트를 논할 때 흔히 조율자 에이전트를 하나 더 두고 그 에이전트가 나머지를 지휘하게 하는데, 그 지휘 자체가 매 턴 모델을 태운다. 저자의 관점은 조율을 모델 바깥, 결정론적 자바스크립트로 밀어내고 모델은 노드 안의 판단에만 쓴다. 팬아웃과 리듀스, 라우팅, 루프 종료 조건이 전부 공짜 엣지가 된다.
그래서 “combine the results” 같은 걸 에이전트에게 시키고 싶은 유혹을 참으라는 조언이 인상 깊었다. 합치는 일이 평탄화와 중복 제거라면 그건 results.flatMap(...)과 Set이면 끝난다. 결정론적이고, 즉각적이고, 토큰이 0이다. 에이전트는 배관이 아니라 판단에 아껴 쓴다. 모든 엣지가 에이전트인 그래프는 제 배선에 세를 무는 그래프라는 비유가 이 원칙을 잘 눌러 담는다.
저자는 이 도구들을 명령형으로 소개하지만, 대부분은 Claude Code라는 특정 하네스의 기능(dynamic workflows, parallel(), pipeline(), isolation: "worktree", ultracode)에 묶여 있다. 그래프라는 사고틀 자체는 어느 오케스트레이션 환경에도 옮겨지지만, 코드 예시를 그대로 쓰려면 이 하네스가 전제된다는 점은 감안하고 읽는 게 좋겠다.
맺는 문장은 짧다. “프롬프터는 질문을 던지고, 아키텍트는 그래프를 그린다.” 대부분은 계속 한 줄로 단계를 쌓을 것이고, 그래프를 그릴 줄 아는 사람은 함대를 부리면서 나머지가 갇혀 있는 천장을 알아채지도 못하리라는 것이 저자의 닫는 말이다.
출처
0xCodez, “Graph Engineering with Claude: 14-Step roadmap from 0 to graph architect (Full Course)”, X, 2026-07-20. 원문: https://x.com/0xcodez/status/2079165300625330317
본문 도식은 모두 원문 아티클에 포함된 저자의 다이어그램을 인용했다.