Handoff와 Routine (무상태 오케스트레이션)
OpenAI의 Swarm(2024년 10월)은 멀티 에이전트 오케스트레이션(multi-agent orchestration)을 두 가지 기본 요소(primitive)로 압축했습니다. 하나는 시스템 프롬프트(system prompt) 안에 지시사항과 도구를 묶은 루틴(routine)이고, 다른 하나는 또 다른 에이전트(Agent)를 반환하는 도구인 핸드오프(handoff)입니다. 상태 기계(state machine)도, 분기 전용 도메인 특화 언어(DSL)도 없습니다. LLM은 알맞은 핸드오프 도구를 호출해 직접 라우팅(routing)을 수행합니다. OpenAI Agents SDK(2025년 3월)는 이 아이디어를 잇는 프로덕션용 후속 구현(successor)입니다. 다만 Swarm 자체는 전체 소스가 몇백 줄 안에 들어갈 정도로 단순하기 때문에, 지금도 가장 깔끔한 개념적 기준으로 남아 있습니다. 이 패턴이 빠르게 확산된 이유는 API 표면이 대략 "agent = prompt + tools; handoff = function returning agent"로 요약될 만큼 작기 때문입니다. 한계도 분명합니다. Swarm은 무상태(stateless) 구조이므로 메모리(memory)는 호출자(caller)가 직접 책임져야 합니다.
유형: Learn + Build
언어: Python (stdlib)
선수 지식: Phase 16 · 04 (멀티 에이전트 기본 모델)
예상 시간: 약 60분
문제
대부분의 멀티 에이전트 프레임워크(multi-agent framework)는 먼저 자기만의 DSL을 배우라고 요구합니다. LangGraph에는 노드(node)와 엣지(edge)가 있고, CrewAI에는 크루(crew)와 태스크(task)가 있으며, AutoGen에는 GroupChat과 매니저(manager)가 있습니다. 이런 DSL은 분명히 의미 있는 추상화이지만, 문제를 필요 이상으로 무겁게 느껴지게 만들 때가 많습니다.
Swarm은 정반대 방향으로 밀고 갑니다. 모델이 이미 갖추고 있는 도구 호출(tool calling) 능력을 그대로 사용합니다. 핸드오프는 곧 도구 호출이 됩니다. 오케스트레이터(orchestrator)는 현재 대화를 쥐고 있는 에이전트 자신입니다. 상태 기계는 에이전트의 시스템 프롬프트 안에 암묵적으로 녹아 들어갑니다.
개념
두 가지 기본 요소
루틴(Routine). 에이전트의 역할과 사용할 수 있는 도구를 정의하는 시스템 프롬프트입니다. 범위가 정해진 지시사항 묶음이라고 생각하면 됩니다. 예를 들면 "당신은 분류(triage) 에이전트입니다. 사용자가 환불을 묻는다면 환불(refund) 에이전트로 넘기세요." 같은 형태입니다.
핸드오프(Handoff). 에이전트가 호출할 수 있는 도구이며, 새 Agent 객체를 반환합니다. Swarm 런타임(runtime)은 반환값이 Agent임을 감지하고, 다음 턴부터 활성 에이전트(active agent)를 그 에이전트로 전환합니다.
추상화는 이것이 전부입니다.
def transfer_to_refunds():
return refund_agent
triage_agent = Agent(
name="triage",
instructions="Route the user to the right specialist.",
functions=[transfer_to_refunds, transfer_to_sales, transfer_to_support],
)
분류 에이전트의 시스템 프롬프트는 사용자 메시지를 기준으로 올바른 핸드오프를 선택하도록 유도합니다. 라우팅 자체는 LLM의 도구 호출이 수행합니다.
왜 널리 퍼졌는가
- 작은 API. 배워야 할 개념이 두 개뿐입니다.
- 모델이 이미 잘 하는 일을 그대로 활용합니다. 도구 호출은 이미 여러 모델 제공자(provider)에서 프로덕션 수준으로 사용되고 있습니다.
- 상태 기계를 직접 다룰 부담이 없습니다. 그래프 구조를 따로 기술하지 않아도, 에이전트의 프롬프트가 자신이 누구에게 넘길 수 있는지를 함께 설명합니다.
무상태라는 대가
Swarm은 실행(run) 사이에서 명시적으로 무상태입니다. 프레임워크는 한 번의 실행 동안 메시지 이력(message history)을 유지하지만, 그 어떤 것도 영속화(persist)하지 않습니다. 메모리, 연속성, 장기 실행 작업은 모두 호출자의 책임입니다.
프로덕션 환경(OpenAI Agents SDK, 2025년 3월)에서는 바로 이 점이 주요 변경점 중 하나였습니다. SDK는 핸드오프라는 기본 요소(primitive)를 유지하면서도 내장 세션 관리(session management), 가드레일(guardrail), 추적(tracing)을 추가했습니다.
Swarm과 핸드오프가 잘 맞는 경우
- 분류(Triage) 패턴. 최전선 에이전트가 사용자를 알맞은 전문 에이전트(specialist)로 라우팅합니다.
- 기술(Skill) 기반 핸드오프. "작업에 코드가 필요하면 코더(coder) 에이전트를 호출하고, 조사가 필요하면 리서처(researcher) 에이전트를 호출한다" 같은 구조입니다.
- 짧고 경계가 분명한 대화. 고객 지원, FAQ에서 티켓(ticket)으로 연결하기, 단순한 업무 흐름(workflow)에 적합합니다.
Swarm이 어려워지는 경우
- 공유 메모리가 필요한 긴 세션. 핸드오프는 대화 상태를 새 에이전트의 프롬프트와 기존 이력의 조합으로 다시 구성합니다. 호출자가 직접 관리하는 메모리 없이는 에이전트 사이를 가로지르는 지속 상태가 존재하지 않습니다.
- 병렬 실행. 핸드오프는 한 번에 하나씩만 일어납니다. 활성 에이전트가 바뀌는 직렬 구조이기 때문에, 병렬성이 필요하면 호출자가 여러 Swarm 실행을 직접 오케스트레이션해야 합니다.
- 감사와 재현. 무상태 실행은 정확히 똑같이 재현하기 어렵습니다. LLM의 핸드오프 선택은 결정적(deterministic)이지 않기 때문입니다.
OpenAI Agents SDK (2025년 3월)
프로덕션용 후속 구현은 다음을 추가합니다.
- 세션 상태(Session state). 실행 사이에서 유지되는 영속 스레드(thread)입니다.
- 가드레일(Guardrail). 입력과 출력을 검사하는 검증 훅(validation hook)입니다.
- 추적(Tracing). 모든 도구 호출과 핸드오프가 로그로 남습니다.
- 핸드오프 필터(Handoff filter). 핸드오프 시 어떤 문맥(context)을 넘길지 제어합니다.
핸드오프라는 기본 요소는 살아남았고, 그 주변에 프로덕션 사용성을 위한 장치들이 더해진 형태입니다.
Swarm과 GroupChat 비교
둘 다 LLM 기반 라우팅을 사용하지만, 다음 순서를 누가 결정하는가가 다릅니다.
- GroupChat: 선택자(selector; 함수 또는 LLM)가 바깥에서 다음 발화자(speaker)를 고릅니다.
- Swarm: 현재 에이전트가 핸드오프 도구를 호출해 자신의 후속 에이전트를 직접 고릅니다.
Swarm은 "에이전트가 다음 일을 결정한다"에 가깝고, GroupChat은 "매니저가 다음 일을 결정한다"에 가깝습니다. Swarm의 결정은 활성 에이전트의 도구 호출 안에 들어 있고, GroupChat의 결정은 GroupChatManager 안에 들어 있습니다.
직접 만들기
code/main.py는 Swarm을 처음부터 구현합니다. Agent 데이터클래스(dataclass), 핸드오프 메커니즘(도구가 Agent를 반환), 에이전트 전환을 감지하는 실행 루프(run loop)가 포함되어 있습니다.
데모에서는 분류(triage) 에이전트가 환불(refund), 영업(sales), 지원(support) 전문 에이전트(specialist)로 사용자를 라우팅합니다. 각 전문 에이전트는 자기만의 도구를 갖고 있습니다. 실행 루프는 매 핸드오프마다 그 사실을 출력합니다.
다음과 같이 실행합니다.
python3 code/main.py
사용해보기
outputs/skill-handoff-designer.md는 주어진 작업(task)에 맞는 핸드오프 토폴로지(handoff topology)를 설계합니다. 어떤 에이전트가 존재하는지, 어떤 핸드오프를 호출할 수 있는지, 어떤 문맥(context)이 전달되는지를 정리합니다.
배포 전 확인
체크리스트:
- 핸드오프 로깅(Handoff logging). 모든 핸드오프는 출발 에이전트(from-agent), 도착 에이전트(to-agent), 문맥 스냅샷(context snapshot)을 포함한 추적 이벤트(trace event)를 남기도록 합니다.
- 문맥 전달 규칙(Context transfer rules). 핸드오프 때 무엇을 옮길지 결정합니다. 전체 이력은 비용이 크고, 마지막 N개 메시지를 넘기거나 요약(summary)을 넘기는 선택지가 있습니다.
- 핸드오프에 대한 가드레일(Guardrail on handoff). 도구 권한이 다른 전문 에이전트로 넘어가는 핸드오프는 반드시 인증되어야 합니다. 그렇지 않으면 프롬프트 인젝션(prompt injection)이 원치 않는 핸드오프를 강제로 유발할 수 있습니다.
- 루프 감지(Loop detection). 두 에이전트가 서로 계속 핸드오프를 주고받는 상황은 흔한 실패 모드입니다. 최근 K개 이동을 링 버퍼(ring) 형태로 검사하는 단순한 방식으로도 감지할 수 있습니다.
- 폴백 에이전트(Fallback agent). 핸드오프 대상이 존재하지 않을 때는 안전한 기본 에이전트로 되돌립니다.
연습문제
- (쉬움)
code/main.py를 실행하고, 환불 에이전트로 분류되는 흐름을 확인합니다. 두 번째 턴의 활성 에이전트가 환불 에이전트인지 검증합니다.
- (중간) 루프 감지 규칙을 추가합니다. 같은 두 에이전트가 연속으로 세 번 핸드오프를 주고받았다면 강제로 종료하도록 하고, 폴백(fallback)을 설계합니다.
- (중간) OpenAI Agents SDK 문서에서 핸드오프 필터(handoff filter)에 대해 읽습니다. "핸드오프 시점에 요약(summarize-on-handoff)" 버전을 구현합니다. 떠나는 에이전트(outgoing agent)가 문맥을 글머리표 요약(bullet summary)으로 압축한 뒤, 새로 들어오는 에이전트(incoming agent)가 이를 이어받게 합니다.
- (어려움) Swarm의 핸드오프와
GroupChatManager의 선택자(selector)를 비교합니다. 두 패턴 중 어느 쪽이 프롬프트 인젝션 위험을 더 키울 수 있으며, 그 이유는 무엇인가요?
- (어려움) Swarm 쿡북(https://developers.openai.com/cookbook/examples/orchestrating_agents)을 읽습니다. Swarm이 명시적으로 내린 설계 결정 중 OpenAI Agents SDK가 바꾸었거나 그대로 유지한 것을 하나 찾아 정리합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 루틴(Routine) | "에이전트 프롬프트" | 시스템 프롬프트와 도구 목록을 함께 묶은 것이다. 역할과 호출 가능한 핸드오프를 정의한다. |
| 핸드오프(Handoff) | "다른 에이전트로 넘기기" | 활성 에이전트가 호출할 수 있는 도구이며, 새 Agent를 반환한다. 런타임은 이 반환값을 보고 활성 에이전트를 전환한다. |
| 무상태(Stateless) | "실행 사이에 메모리가 없음" | Swarm은 아무것도 영속화하지 않는다. 메모리는 호출자의 책임이다. |
| 활성 에이전트(Active agent) | "지금 말하는 에이전트" | 현재 대화를 쥐고 있는 에이전트를 뜻한다. 핸드오프가 이 값을 바꾼다. |
| 문맥 전달(Context transfer) | "핸드오프 때 옮겨지는 것" | 새로 들어오는 에이전트가 볼 이력을 정하는 정책이다. 전체 이력, 마지막 N개, 요약 중 하나를 선택할 수 있다. |
| 핸드오프 루프(Handoff loop) | "에이전트가 핑퐁(ping-pong)을 함" | 두 에이전트가 서로 계속 넘겨 주는 실패 모드다. |
| OpenAI Agents SDK | "프로덕션 버전 Swarm" | 2025년 3월의 후속 구현이다. 핸드오프라는 기본 요소 위에 세션, 가드레일, 추적을 더한다. |
| 핸드오프 필터(Handoff filter) | "전환 시점의 관문" | 핸드오프 경계에서 문맥을 검사하고 수정할 수 있게 해 주는 SDK 기능이다. |
더 읽을거리