2026년에 다중 에이전트 시스템을 구축하는 모든 개발자는 여전히 직선으로 작업하고 있습니다. 1단계, 다음 2단계, 그리고 3단계 - 각 단계는 이전 단계가 끝나기를 기다립니다. 이것이 왜 비효율적인지, 그리고 어떻게 해결할 수 있는지 알아보겠습니다.

아무도 확인하지 않는 문제
여러분은 다단계 에이전트를 구축했습니다. 작동은 잘 됩니다. 하지만 느리기도 합니다.
모델이 병목이라고 생각할 것입니다. 하지만 아닙니다.
병목은 여러분이 그린 구조입니다. 체인 형태 - 1단계가 2단계를 기다리고, 2단계가 3단계를 기다리는 방식 - 은 절반의 단계가 서로 아무 관련이 없어도 순차적으로 실행되도록 강제합니다.
"이 문서를 요약한 다음, 날씨를 확인하세요"는 하나의 워크플로우로 위장한 두 개의 독립적인 작업입니다. 날씨 작업은 요약이 필요하지 않습니다. 원래부터 그랬습니다. 하지만 체인으로 작성했다면 어쨌든 기다려야 합니다.
이러한 불필요한 대기 시간이 수십 단계에 걸쳐 누적되면, 실행 시간의 대부분을 차지하게 됩니다.
1장 - 루프 vs 그래프
루프는 자기 개선의 한 단위입니다:
1시도해 보기 → 결과 확인 → 조정 → 다시 시도
이것이 기본 단위입니다. 하나의 에이전트, 하나의 지표, 수렴할 때까지 순환합니다.
루프에는 알려진 실패 패턴이 있습니다: 측정하는 것만 최적화하고 나머지는 무시합니다. 티켓을 빨리 마감하도록 조정된 지원 봇은 티켓을 빨리 마감하겠지만, 만족도는 조용히 추락합니다. 루프는 자신의 지표 밖을 볼 수 없습니다. 이것이 바로 에이전트 아키텍처에 나타난 구드하트의 법칙입니다.
그래프는 설계 자체로 이 문제를 해결합니다. 하나의 숫자를 쫓는 하나의 루프 대신, 서로를 감시하고 교정하는 루프 네트워크를 구축합니다. 노드 A의 출력이 노드 B로 전달됩니다. 노드 C는 독립적으로 실행되어 둘 다 확인합니다. 단일 지표가 전체 시스템을 움직이는 것이 아니라, 구조 자체가 그 역할을 합니다.

에이전트 시스템에서 이는 한 가지 구체적인 변화를 의미합니다: 모든 것을 위에서 아래로 처리하는 하나의 에이전트를 작성하는 것을 중단하세요. 먼저 작업의 구조를 설계하세요 - 무엇이 무엇보다 먼저 실행되어야 하는지, 무엇을 동시에 실행할 수 있는지, 무엇이 실제로 기다려야 하는지.
2장 - 노드, 엣지, 그리고 그것들을 구분하는 테스트
그래프는 정확히 두 가지 구성 요소로 이루어집니다:
노드 - 하나의 작업 단위. 하나의 에이전트, 하나의 작업, 하나의 입력, 하나의 출력.
엣지 - 실제 의존성. 노드 B의 입력에는 노드 A의 출력이 필요합니다.
거의 모든 사람이 저지르는 실수: "그리고 나서"를 기본적으로 엣지로 간주하는 것입니다.
1"이 코드베이스를 읽고 변경 로그를 작성하세요"2"가격 페이지를 가져와서 경쟁사 기능을 요약하세요"
워크플로우의 모든 "그리고 나서"에 대해 한 가지 질문을 해보세요:
다음 단계가 실제로 이전 단계의 출력을 읽나요?
예 → 실제 엣지입니다. 순차적 순서를 유지하세요. 아니오 → 엣지가 아닙니다. 대기 시간은 낭비입니다. 병렬로 실행하세요.
두 작업 사이에 데이터가 교차되지 않는다면, 두 작업은 독립적입니다 — 그리고 순차적으로 실행하는 모든 독립적인 쌍은 공짜로 버리는 실행 시간입니다.
코드에 적용된 테스트는 다음과 같습니다:
1from dataclasses import dataclass23@dataclass4class TaskNode:5 id: str6 prompt: str7 depends_on: list[str] # 이 노드가 실제로 필요로 하는 노드의 ID89def has_real_edge(node_a: TaskNode, node_b: TaskNode) -> bool:10 """11 핵심 그래프 엔지니어링 테스트:12 node_b의 프롬프트가 실제로 node_a의 출력을 필요로 하는가?13 """14 return node_a.id in node_b.depends_on1516# 예시: 대부분의 "체인"은 2-3개의 실제 의존성 그룹으로 축소됩니다17nodes = [18 TaskNode("audit_routes", "모든 API 라우트 파일 나열", []),19 TaskNode("check_auth", "인증 미들웨어 적용 범위 확인", []),20 TaskNode("fetch_weather", "오늘 날씨 가져오기", []),21 TaskNode("summarize", "라우트 + 인증 결과 요약",22 depends_on=["audit_routes", "check_auth"]),23]2425# audit_routes, check_auth, fetch_weather 사이에는 엣지가 없습니다26# 병렬로 실행됩니다. "summarize"만 실제 엣지를 가지고 있습니다 -- 기다립니다.
현재의 "A를 하고, B를 하고, C를 하세요" 에이전트는 기술적으로 이미 그래프입니다. 하지만 가장 나쁜 형태의 그래프입니다 - C가 중단되면 다운스트림에서 아무것도 실행되지 않는 단일 체인입니다.
3장 - 첫 번째 그래프 구축

필요 사항:
- Claude Code (Dynamic Workflows를 지원하는 최신 버전).
- Max, Team 또는 Enterprise 플랜 - 워크플로우가 기본적으로 활성화되어 있습니다. Pro에서는 수동으로 활성화하세요.
실제 저장소를 여세요. 장난감 예제가 아닙니다 - 이점은 실제 규모에서만 나타납니다.
첫 번째 그래프를 시작하는 프롬프트:
1이 코드베이스의 모든 라우트 파일을 감사하는 워크플로우를 생성하세요.23각 라우트 파일에 대해 독립적으로 확인하세요:4- 인증 미들웨어 존재 여부5- 모든 매개변수에 대한 입력 검증6- 속도 제한 설정 여부7- 오류 처리에서 스택 추적이 유출되지 않는지89모든 라우트 파일에 대해 이러한 검사를 병렬로 실행하세요 —10서로 의존하지 않습니다.1112모든 파일이 확인된 후, 심각도별로 그룹화된 하나의 통합13보고서를 생성하세요: 치명적, 경고, 정보.1415통합 단계는 모든 검사가 완료될 때까지 기다려야 합니다.16그 이전의 모든 단계는 기다리면 안 됩니다.
프롬프트 자체에 구조가 내장되어 있음에 주목하세요: 병렬 작업이 명시적으로 언급되고, 하나의 실제 의존성(통합이 모든 검사를 기다리는 것)이 명시적으로 명명되었습니다. 에이전트가 그래프를 추론하도록 기대하는 것이 아니라, 직접 설명하고 있는 것입니다.
내부에서 일어나는 일 - 오케스트레이션의 간소화된 버전:
1import asyncio2from anthropic import Anthropic34client = Anthropic()56async def audit_route_file(filepath: str) -> dict:7 """하나의 노드. 다른 모든 라우트 파일과 독립적으로 실행됩니다."""8 response = await client.messages.create(9 model="claude-sonnet-5",10 max_tokens=1000,11 messages=[{12 "role": "user",13 "content": f"""이 라우트 파일을 감사하세요:14 - 인증 미들웨어, 입력 검증,15 속도 제한, 오류 처리1617 파일: {filepath}1819 JSON 반환: {{"file": "", "issues": [], "severity": ""}}"""20 }]21 )22 return {"file": filepath, "result": response.content[0].text}2324async def consolidate(results: list[dict]) -> str:25 """하나의 실제 엣지 -- 모든 감사 노드가 완료될 때까지 기다립니다."""26 response = await client.messages.create(27 model="claude-opus-4-8",28 max_tokens=2000,29 messages=[{30 "role": "user",31 "content": f"""다음 {len(results)}개의 라우트 감사 결과를32 심각도별로 그룹화된 하나의 보고서로 통합하세요:3334 {results}"""35 }]36 )37 return response.content[0].text3839async def run_graph(route_files: list[str]):40 # 확산 -- 모든 독립 노드가 동시에 실행됩니다41 audit_tasks = [audit_route_file(f) for f in route_files]42 results = await asyncio.gather(*audit_tasks)4344 # 수렴 -- 실제 의존성이 있는 하나의 노드45 report = await consolidate(results)46 return report4748# 40개의 라우트 파일, 하나의 프롬프트, 하나의 병렬 패스49results = asyncio.run(run_graph([50 f"routes/{f}.py" for f in ["auth", "users", "billing", "orders"]51 # ...36개 더
각각 약 8초가 소요되는 40개의 순차적 API 호출은 5분이 넘습니다. 동일한 40개 호출을 병렬로 확산하면: 15초 미만으로, 가장 느린 단일 파일에 의해 제한되며 모든 파일의 합계에 의해 제한되지 않습니다.
4장 - 그래프가 실제로 실패하는 지점
그래프 엔지니어링은 세 가지 예측 가능한 지점에서 실패합니다. 발생하기 전에 알아두세요.
컨텍스트 붕괴. 1,000개의 노드를 확산하고 모든 1,000개의 출력을 하나의 통합 단계에 공급하려고 하면, 합성조차 시작하기 전에 컨텍스트 창을 초과하게 됩니다. 수정 방법: 수렴을 계층화하세요. 노드를 20-50개씩 배치로 그룹화하고, 각 배치를 요약한 다음, 원시 출력이 아닌 요약을 통합하세요.
1async def layered_consolidate(results: list[dict], batch_size: int = 30):2 """계층별 수렴 -- 대규모 원시 출력을 절대 합성하지 않습니다."""3 batches = [results[i:i+batch_size]4 for i in range(0, len(results), batch_size)]56 batch_summaries = await asyncio.gather(*[7 summarize_batch(batch) for batch in batches8 ])910 # 최종 통합은 1,000개의 원시 결과가 아닌 요약을 대상으로 작업합니다11 return await consolidate(batch_summaries)
거짓 독립성. 두 노드의 프롬프트가 서로를 참조하지 않기 때문에 독립적이라고 가정하겠지만, 두 노드가 모두 동일한 파일에 쓰거나 동일한 속도 제한 API를 호출할 수 있습니다. 이것은 숨겨진 엣지입니다. 수정 방법: 공유 데이터뿐만 아니라 공유 리소스도 감사하세요. 쓰기 충돌이 있는 두 노드는 데이터 의존성이 전혀 없더라도 엣지가 필요합니다.
무음 노드 실패. 체인에서는 하나의 실패가 모든 것을 중단시킵니다 - 짜증나지만 명백합니다. 그래프에서는 200개 중 하나의 실패한 노드가 완료된 것처럼 보이는 보고서 속으로 사라질 수 있습니다. 수정 방법: 모든 수렴 단계는 합성하기 전에 노드 수를 예상 수와 비교하고, 부분 데이터로 조용히 작업하는 대신 격차를 명시적으로 표시합니다.
1async def safe_consolidate(results: list[dict], expected_count: int):2 if len(results) < expected_count:3 missing = expected_count - len(results)4 print(f"경고: {missing}개 노드가 무음 실패했습니다. "5 f"보고서가 불완전할 수 있습니다.")6 return await consolidate(results)
5장 - 실제 규모의 시스템으로 확장

패턴이 40개 노드에서 작동한다면, 수백 개로 확장하는 것은 2장부터 그래프를 올바르게 구축했다면 재설계가 아닌 설정 변경입니다.
전체 프로덕션 구조:
1 오케스트레이터2 |3 +--------+-------+-------+--------+4 v v v v v5 노드 1 노드 2 노드 3 ... 노드 N6 (병렬, 노드 간 엣지 없음)7 | | | |8 +--------+-------+-------+-------+9 v10 배치 요약 <- 계층별 수렴11 (30개씩 그룹화)12 v13 최종 보고서 <- 하나의 진정한 엣지
오케스트레이터의 유일한 임무: 작업을 노드로 분해하고, 실제 엣지를 식별하고, 전달하는 것입니다. 자체적으로는 아무 작업도 수행하지 않습니다 - 그래프를 그릴 뿐입니다.
1async def orchestrate(task: str, resources: list[str]):2 """3 오케스트레이터 노드 -- 분해만 하고 실행하지 않습니다.4 """5 plan = await client.messages.create(6 model="claude-opus-4-8",7 max_tokens=2000,8 messages=[{9 "role": "user",10 "content": f"""작업: {task}11 사용 가능한 리소스: {resources}1213 그래프로 분해:14 - 각 독립 노드 나열 (공유 엣지 없음)15 - 노드 간 실제 의존성 나열16 - 개수가 50개를 초과하는 경우 노드를 수렴 배치로 그룹화1718 다음을 포함하는 JSON 반환: nodes, edges, batch_groups"""19 }]20 )2122 graph = parse_plan(plan.content[0].text)2324 # 독립 노드를 병렬로 실행25 node_results = await asyncio.gather(*[26 execute_node(n) for n in graph["nodes"] if not n["depends_on"]27 ])2829 # 그런 다음 실제 엣지만 존중하여 의존 노드 실행30 final = await execute_dependent_chain(graph["edges"], node_results)3132 return final
이것이 그래프 엔지니어링이 나타내는 실제 변화입니다: 모든 단계를 직접 작성하는 사람에서 의존성 구조를 설계하는 사람으로 바뀌는 것입니다. 에이전트가 노드를 채웁니다. 여러분은 엣지를 소유합니다.
선 대신 그래프로 생각할 때 달라지는 것
40단계의 선형 에이전트는 40개의 순차적 실패 지점과 가장 느린 단일 단계의 40배 지연 시간을 가집니다.
동일한 40개의 작업 단위를 가진 그래프는 실제 의존성 수만큼의 병렬 실패 지점을 가지며(일반적으로 대부분의 워크플로우에서 3~5개), 지연 시간은 총 단계 수가 아닌 가장 느린 계층에 의해 제한됩니다.
이는 미미한 속도 향상이 아닙니다. 정확히 동일한 기본 작업을 실행하면서 5분이 걸리는 워크플로우와 15초가 걸리는 워크플로우의 차이입니다.
모델은 결코 병목이 아니었습니다. 여러분이 그린 선이 병목이었습니다.
이 글은 2026년 7월 기준 다중 에이전트 오케스트레이션 패턴에 대한 기술적 분석입니다. 코드 예제는 설명을 위한 것입니다 — 대규모 배포 전에 프로덕션 환경에 맞게 오류 처리, 속도 제한 및 재시도 로직을 조정하세요.
읽어주셔서 감사합니다.





