
목표 입력, DAG 출력: Open-Multi-Agent가 목표를 작업 DAG로 변환하는 방법
요약
Open-Multi-Agent 프레임워크의 runTeam() 메서드가 목표를 작업 DAG(Directed Acyclic Graph)로 자동 변환하는 메커니즘을 설명합니다. 사용자가 직접 그래프를 설계할 필요 없이, 코디네이터 에이전트가 목표를 분해하고 작업을 할당하여 병렬 실행 및 합성을 수행합니다.
핵심 포인트
- 사용자가 직접 그래프를 설계할 필요 없이 자연어로 목표 전달 가능
- 코디네이터 에이전트가 목표를 JSON 형태의 작업 명세로 자동 분해
- 작업 간 의존성을 데이터(dependsOn)로 관리하여 DAG 구조 형성
- 에이전트별로 서로 다른 LLM 모델을 유연하게 설정 가능
당신은 그래프를 직접 손으로 그렸습니다. 그러다 요구사항이 바뀌었습니다.
대부분의 TypeScript 에이전트 프레임워크는 사용자가 직접 그래프를 그리도록 만듭니다. 노드(nodes)를 선언하고, 엣지(edges)를 연결하며, 무엇이 무엇 다음에 실행될지, 어디서 분기되고 어디서 합쳐질지를 결정해야 합니다. 목표가 바뀌기 전까지는 잘 작동하겠죠. 하지만 목표가 바뀌는 순간, 당신은 이미 한 번 구축했던 파이프라인을 다시 연결하기 위해 그래프 에디터로 돌아가야 합니다.
이를 모델링하는 또 다른 방법이 있습니다. 목표를 설명하면, 코디네이터(coordinator)가 당신을 대신해 그래프를 구축하도록 하는 것입니다.
그것이 바로 open-multi-agent의 runTeam()이 하는 일입니다. 당신은 팀과 문장 하나를 전달합니다. 그러면 결과가 돌아옵니다. 그 사이 과정에서 코디네이터 에이전트가 목표를 작업 DAG(Directed Acyclic Graph, 유향 비순환 그래프)로 분해하고, 작업을 에이전트들에게 할당하며, 독립적인 작업들을 병렬로 실행하고, 최종 답변을 합성합니다. 연결해야 할 엣지는 없습니다.
이 포스트는 그 "사이"에서 어떤 일이 일어나는지에 관한 것입니다. 왜냐하면 그 메커니즘이 핵심이기 때문입니다.
단 한 번의 호출
import { OpenMultiAgent } from '@open-multi-agent/core'
const orchestrator = new OpenMultiAgent({
...
내부 동작을 살펴보기 전에 주목해야 할 세 가지가 있습니다:
- 당신은 작업 그래프를 선언한 적이 없습니다. 평범한 영어로 목표를 작성했을 뿐입니다.
- 각 에이전트는 자체적인
model을 선언합니다. 오케스트레이터(orchestrator)의defaultModel은 코디네이터에 의해 사용되며, 워커(worker) 에이전트들은 각자의 모델을 가집니다. (deepseek를 Anthropic, OpenAI, Gemini, 로컬 모델 등 지원되는 다른 제공자로 교체할 수 있습니다.) - 목표는 의도적으로 구체적이어야 합니다. 짧고 단일 절로 이루어진 목표는 단순한 작업으로 취급되어 코디네이터를 완전히 건너뜁니다. 이에 대해서는 아래에서 더 자세히 다루겠습니다.
이를 실행하면 프레임워크는 일곱 가지 일을 수행합니다. 순서대로 나열하면 다음과 같습니다.
1단계: 코디네이터가 목표를 분해합니다
runTeam()은 coordinator라고 불리는 임시 에이전트를 가동합니다. 이는 당신의 명단에 포함된 에이전트가 아닙니다. 프레임워크는 이 실행을 위해 이를 생성하고 실행이 끝나면 폐기합니다. 코디네이터는 당신의 목표, 에이전트들의 이름, 그리고 하나의 지침을 전달받습니다:
다음 목표를 당신의 팀(researcher, writer)을 위한 작업들로 분해하세요.
json코드 펜스 안에 JSON 작업 배열만 반환하세요.
코디네이터(coordinator)는 작업 명세(task specs)가 담긴 JSON 배열로 응답합니다. 위 실행 결과에서 나온 실제 분해(decomposition) 예시는 다음과 같습니다:
[
{ "title": "Research stage-3 vs legacy experimental decorators",
"description": "Gather the syntax and behavioral differences ...",
...
각 작업은 title(제목), description(할당된 에이전트가 받게 될 실제 지시 사항), assignee(담당자), 그리고 반드시 기다려야 하는 작업 제목들의 목록인 dependsOn(의존성)을 포함합니다. 마지막 필드가 바로 DAG(Directed Acyclic Graph, 유향 비순환 그래프)이며, 이는 배선(wiring) 형태가 아닌 데이터로서 표현됩니다. 코디네이터가 연구(research)를 세 개의 독립적인 작업으로 나누고, 작성(write) 작업을 이 세 작업 모두에 의존하도록 설정한 점에 주목하세요. 코디네이터는 LLM(Large Language Model)이기 때문에 실행할 때마다 정확한 분할 방식은 달라질 수 있으며, 이것은 하나의 실제 계획 사례였습니다.
이 단계에서는 LLM 호출이 한 번 추가로 발생합니다. 코디네이터는 기본적으로 maxTurns가 3으로 설정되어 실행됩니다. 이러한 오버헤드(overhead)를 염두에 두시기 바랍니다. 이 내용은 마지막에 다시 다루겠습니다.
단계 2: 작업이 의존성 그래프(dependency graph)가 됨
명세(specs)는 TaskQueue(작업 큐)로 로드됩니다. 제목 기반의 dependsOn 참조는 실제 작업 ID로 해결(resolve)되므로, 큐는 그래프의 실제 형태를 파악할 수 있습니다. 작업은 그 작업이 의존하는 모든 작업이 완료된 후에만 "준비(ready)" 상태가 됩니다. 의존성이 없는 작업은 즉시 준비 상태가 됩니다.
만약 코디네이터가 사용 가능한 JSON을 반환하는 데 실패하더라도 실행이 중단(crash)되지는 않습니다. 프레임워크는 에이전트당 하나의 작업을 할당하는 방식으로 폴백(fallback)하며, 각 작업에는 원래의 목표가 설명(description)으로 전달됩니다. 예외(exception)가 발생하는 대신 성능이 저하된 실행 결과를 얻게 됩니다.
단계 3: 할당되지 않은 작업에 소유자 지정
코디네이터가 보통 assignee(담당자)를 채우지만, 반드시 그래야 하는 것은 아닙니다. 할당되지 않은 상태로 남은 모든 작업은 Scheduler(스케줄러)로 넘겨지며, 스케줄러가 이를 에이전트에게 할당합니다. 기본 전략은 dependency-first(의존성 우선)입니다. 또한 round-robin(라운드 로빈), least-busy(최소 작업량), 또는 각 에이전트의 이름과 시스템 프롬프트를 작업과 비교하여 점수를 매기는 capability-match(역량 매칭)를 선택할 수도 있습니다.
단계 4: 실행, 기본적으로 병렬 처리
작업은 AgentPool을 통해 실행됩니다. 독립적인 작업(dependsOn에 대기 중인 항목이 없는 작업)은 기본값이 5인 maxConcurrency까지 동시에 실행됩니다. 의존 작업(Dependents)은 입력이 완료될 때까지 기다린 후, 준비 상태가 되어 발송됩니다. 위의 실제 실행 사례에서 세 가지 연구 작업은 의존성이 없었으므로 모두 같은 순간에 시작되어 함께 실행되었습니다. 반면 쓰기(write) 작업은 세 가지 작업이 모두 완료될 때까지 기다렸습니다. 사용자가 이 과정을 직접 스케줄링한 것이 아닙니다. 그래프의 형태가 무엇이 중첩될 수 있는지를 결정하며, 풀(pool)은 제한 범위 내에서 최대한 많은 작업을 병렬로 실행합니다.
단계 5: 모든 결과는 공유 메모리에 저장됩니다
각 작업이 완료되면 해당 출력은 팀의 공유 메모리(shared memory)에 기록됩니다. 이것이 writer가 researcher의 조사 내용을 확인하는 방식입니다. 즉, 쓰기 작업이 준비될 때쯤이면 세 가지 연구 결과는 이미 메모리에 들어 있습니다. 에이전트들은 사용자가 한 호출의 출력을 다음 호출로 직접 전달(threading)하는 대신, 이 공유 저장소를 통해 통신합니다.
단계 6: 코디네이터(coordinator)가 종합합니다
큐(queue)가 비워지면 코디네이터가 두 번째로 실행됩니다. 이 단계에서는 모든 작업 출력을 읽어 목표에 대한 최종 답변을 작성합니다. 이것이 agentResults.get('coordinator')를 통해 읽게 되는 결과입니다.
최종 산문(prose) 대신 계획(plan) 자체를 검토하고 싶으신가요? 작업 기록은 결과의 result.tasks에 포함되어 있으며(각 작업은 title, assignee, status, dependsOn을 가짐), runTeam(team, goal, { planOnly: true })을 호출하면 아무것도 실행하지 않고 계획만 가져올 수 있습니다.
단계 7: 구조화된 결과를 얻습니다
runTeam()은 TeamRunResult로 해결(resolve)됩니다. 여기에는 에이전트 이름(여기서는 coordinator, researcher, writer)을 키로 하는 agentResults 맵, totalTokenUsage 수치, 그리고 상태와 메트릭이 포함된 tasks 기록 리스트가 포함됩니다. 발생한 모든 일은 사후에 검토할 수 있습니다.
실제 실행 모습
다음은 위의 코드를 DeepSeek(deepseek-v4-flash)를 대상으로 실행했을 때의 실제 출력 결과입니다:
코디네이터(coordinator)는 목표를 세 개의 병렬 연구 작업(research tasks)과 하나의 종속적인 작성 작업(write task)으로 분해하였고, 연구를 동시에 실행한 후 각 결과를 저장하고 최종 설명(explainer)을 합성했습니다. runTeam()은 success=true로 종료되었습니다. 아래의 명시적인 runTasks() 버전도 동일한 방식으로 실행되었습니다.
작업이 실패할 때
실패는 해당 작업의 종속 작업(dependents)을 넘어 연쇄적으로 발생하지 않습니다. 실패한 작업은 failed로 표시되며, 해당 작업에 종속된 모든 작업은 blocked 상태로 유지됩니다. 실패에 종속되지 않은 모든 작업은 완료될 때까지 계속 실행됩니다. 하나의 오류가 전체 그래프를 무너뜨리는 대신, 부분적인 결과와 함께 어떤 브랜치(branch)가 깨졌는지에 대한 명확한 기록을 남기며 실행이 종료됩니다.
코디네이터를 사용하지 말아야 할 때
목표 우선(Goal-first) 방식이 만능 해결책(silver bullet)은 아니며, 프레임워크는 이 점을 명시하고 있습니다.
단순한 목표는 코디네이터를 완전히 건너뜁니다. 목표가 짧고(200자 이하) 조정 지시 사항(coordination directives)이 포함되어 있지 않으면, runTeam()은 단축 실행(short-circuit)됩니다. 즉, 분해(decomposition)나 합성(synthesis) 단계 없이 가장 잘 매칭되는 에이전트(agent)를 선택하여 직접 실행합니다. "이 단락을 요약해줘"와 같은 요청을 위해 두 번의 추가적인 LLM 호출 비용을 지불할 이유는 없습니다. (이것이 바로 위의 퀵스타트 목표가 상세하게 기술된 이유입니다. 한 줄짜리 문장이었다면 단일 에이전트로 바로 라우팅되었을 것입니다.)
결정론(determinism)이 필요한 경우, 그래프를 직접 작성하세요. 코디네이터는 LLM이므로, 분해 결과가 실행할 때마다 달라질 수 있습니다 (위의 예시는 한 번의 실행에서는 세 개의 연구 작업을 생성했지만, 다른 실행에서는 단일 연구 작업을 생성했습니다). 매번 정확히 동일한 파이프라인이 필요한 경우(CI, 규제 대상 워크플로우, 또는 정밀하게 추론해야 하는 모든 경우), runTasks()를 사용하고 DAG를 직접 제공하십시오:
const result = await orchestrator.runTasks(team, [
{
title: 'Research decorator tradeoffs',
...
동일한 큐(queue), 동일한 스케줄러(scheduler), 동일한 병렬 실행(parallel execution). 단지 그래프를 요청하는 대신 당신이 직접 소유할 뿐입니다. (코디네이터(coordinator)가 생성한 계획을 고정하여 결정론적(deterministically)으로 재실행할 수도 있지만, 이는 별도의 포스팅에서 다룹니다.)
따라서 트레이드오프(tradeoff)는 명확합니다:
runTeam()은 목표 우선(goal-first) 방식입니다: 유연하지만, 계획 수립을 위한 두 번의 추가적인 LLM 호출 오버헤드가 발생하며, 실행 시마다 계획이 변경될 수 있습니다.runTasks()는 그래프 우선(graph-first) 방식입니다: 결정론적이며 실행당 비용이 더 저렴하지만, 당신이 직접 그래프를 유지 관리해야 합니다.
목표 우선(Goal-first) vs 그래프 우선(Graph-first)
이것은 프레임워크를 선택할 때 실제로 중요한 구분점입니다. 그래프 우선 도구(사용자가 노드를 연결함)는 제어력과 결정론을 얻는 대신 유지 관리의 부담을 감수합니다. 목표 우선 방식(사용자가 결과를 기술함)은 유연성을 얻는 대신 추가적인 계획 단계와 비결정론적인 계획을 감수합니다. open-multi-agent는 하나의 API 뒤에 이 두 가지를 모두 제공하므로, 처음에는 목표 우선 방식으로 시작하다가 반드시 고정되어야 하는 경로에서는 명시적인 그래프 방식으로 전환할 수 있습니다. 저는 이 차이점에 대해 Goal-Driven Agent Orchestration vs Explicit Graphs에서 더 자세히 작성했습니다.
시도해보기
npm install @open-multi-agent/core
team-collaboration 예제는 가장 작은 규모의 엔드 투 엔드(end-to-end) runTeam() 실행 사례입니다. 이것이 어디까지 가능한지 보고 싶다면, Gemma 4 로컬 예제를 확인해 보세요. 이 예제는 5B 로컬 모델을 코디네이터 자리에 배치하여, 당신의 로컬 머신에서 JSON 분해(decomposition)와 합성(synthesis)을 수행합니다.
한 가지 솔직한 주의사항을 말씀드리자면, 커뮤니티 및 프로덕션 검증 단계는 아직 초기 단계입니다. 만약 실제 워크로드(workload)에서 코디네이터를 실행해 보신다면, 계획이 잘 유지된 부분은 어디인지, 그리고 어느 부분에서 runTasks()로 전환해야 했는지에 대한 의견을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기