AI Sparkup

최신 AI 쉽게 깊게 따라잡기⚡

에이전트를 한꺼번에 굴리는 비동기 패턴, 함정은 패턴마다 다르다

에이전트 하나를 돌리는 건 어렵지 않습니다. 문제는 수십 개를 동시에, 이벤트 루프를 멈추거나 속도 제한 오류를 줄줄이 터뜨리지 않으면서 돌리는 순간부터 시작되죠.

사진 출처: MachineLearningMastery.com

머신러닝 전문 매체 Machine Learning Mastery가 파이썬 asyncio로 AI 에이전트를 동시에 실행하는 7가지 비동기 패턴을 정리했습니다. 각 패턴이 어떤 조율 문제를 푸는지, 프로덕션에서 어떤 함정이 따라오는지를 함께 짚은 글이죠. 핵심은 패턴마다 실패하는 방식이 다르다는 것.

출처: 7 Async Patterns for Running Agents Concurrently in Python – MachineLearningMastery.com

글이 다루는 7가지 패턴을 먼저 한눈에 정리하면 이렇습니다.

  1. fire-and-forget — 결과를 기다리지 않고 태스크를 백그라운드로 흘려보내는 방식
  2. scatter-gather — 여러 워커에 한꺼번에 뿌리고 전부 모일 때까지 기다리는 방식
  3. task group — 파이썬 3.11+에서 도입된, 범위가 명확한 구조적 gather
  4. producer-consumer — 큐를 사이에 두고 생산자와 소비자를 분리하는 방식
  5. 세마포어 백프레셔 — 자원에 동시 접근하는 에이전트 수에 상한을 거는 방식
  6. speculative execution — 같은 목표를 여러 에이전트에 경쟁시키고 먼저 끝난 걸 취하는 방식
  7. 파이프라인 체이닝 — 앞 에이전트의 출력을 뒤 에이전트의 입력으로 잇는 방식

각 패턴은 서로 다른 조율 문제를 푼다

파이썬의 asyncio는 동시 실행에 필요한 기본 도구를 전부 쥐여줍니다. 문제는 어떤 도구를 집느냐죠. 패턴마다 푸는 문제가 다르고, 엉뚱한 걸 고르면 뒤늦게야 드러나는 실패 모드가 생깁니다.

가장 단순한 건 fire-and-forget입니다. 태스크를 띄워놓고 결과를 기다리지 않은 채 다음 일로 넘어가는 방식. 로그를 남기거나 컨텍스트를 저장소에 비우는 것처럼, 결과가 뒤 단계에 영향을 주지 않는 작업에 잘 맞습니다.

함정은 따로 있죠. 분리된 태스크에서 터진 예외를 이벤트 루프가 조용히 삼켜버린다는 것. 에러 콜백을 명시적으로 붙이지 않으면 백그라운드 에이전트가 죽어도 아무런 알림이 오지 않죠.

여러 에이전트를 한꺼번에 굴려야 할 때는 scatter-gather로 넘어갑니다. 오케스트레이터 하나가 워커 여러 개에 작업을 부채꼴로 뿌리고, 전부 돌아올 때까지 기다렸다가 이어가는 구조죠. 다섯 개 에이전트가 서로 다른 데이터 소스를 병렬로 조회하는 상황을 떠올리면 됩니다.

여기서 걸리는 건 낙오자(straggler) 지연입니다. 기본 설정에서는 하나만 실패해도 나머지가 취소되고, 그걸 꺼도 전체 작업은 결국 가장 느린 에이전트를 기다리게 되죠. 느린 생성 하나가 전부의 발목을 잡는 겁니다.

메모리가 새는 큐, 토큰을 못 막는 세마포어

에이전트가 동시에 출발하지 않는 경우도 있죠. 한쪽이 일을 만들고 다른 쪽이 처리한다면, 그 사이에 큐를 완충지대로 두는 producer-consumer가 어울립니다. 생산자는 찾은 일을 큐에 넣고, 소비자는 독립적으로 꺼내 갑니다. 양쪽이 서로를 몰라도 되고, 생산자를 건드리지 않은 채 소비자만 늘리거나 줄일 수 있죠.

다만 큐에 상한이 없으면 조용히 메모리가 샙니다. 생산 속도가 소비 속도를 앞지르면 큐가 RAM이 바닥날 때까지 부풀어 오르죠. 최대 크기를 정해 생산자 쪽에 backpressure를 걸어야 합니다.

프로덕션에서 가장 실용적인 패턴을 하나 꼽자면 세마포어 백프레셔입니다. 한 자원에 동시에 접근할 수 있는 에이전트 수에 단단한 상한을 걸고, 넘치는 에이전트는 순서를 기다리게 하는 방식. 외부 API, DB 커넥션 풀, 내부 서비스처럼 처리량 천장이 있는 자원에 잘 맞죠.

여기엔 놓치기 쉬운 함정이 있습니다. 세마포어가 제한하는 건 연결 수이지 토큰이 아니라는 것. 동시 요청을 10개로 묶어도, 그 10개가 전부 큰 출력을 뿜어내면 분당 토큰 한도(TPM)는 그대로 넘어갑니다. 엄격한 API 준수가 필요하면 토큰을 인식하는 스로틀링을 함께 걸어야 하죠.

실패는 천천히, 나중에 드러난다

속도를 위해 계산 효율을 포기하는 패턴도 있죠. speculative execution은 같은 목표를 여러 에이전트에 경쟁시키고, 먼저 유효한 결과를 낸 쪽을 취하면서 나머지를 취소합니다. 빠른 작은 모델과 느린 큰 모델을 나란히 달리게 하는 식이죠.

문제는 태스크를 취소해도 provider 서버에서는 생성이 멈추지 않는다는 것. 진 에이전트도 계정의 토큰을 계속 태웁니다. 매번, 진 에이전트 전부에 대해.

여기에 하나 더. 네트워킹을 완벽하게 비동기로 짜도, JSON 파싱이나 토크나이저처럼 CPU를 붙잡는 동기 작업이 끼면 이벤트 루프 자체가 멈춥니다. 루프가 막히는 순간 진행 중이던 요청들이 타임아웃 하트비트를 놓치고, 비동기로 잘 짜둔 구조 전체에 연쇄 실패가 번지죠. 무거운 연산은 스레드 풀로 넘기라는 게 원문의 조언입니다.

결국 대부분의 프로덕션 시스템은 이 패턴들을 두세 개씩 조합해 씁니다. 파이프라인 각 단계 안에 세마포어를 두거나, 소비자 풀 안에서 gather를 쓰는 식으로. 에이전트를 어떤 구조로 배치하고 어디로 라우팅하느냐는 그 자체로 하나의 설계 축이죠. 멀티에이전트 패턴별로 모델 호출 비용을 비교한 글도 함께 보면 선택 기준이 잡힙니다.

멀티에이전트 4가지 패턴, LangChain 성능 데이터로 본 선택 기준

해보려면

환경: 파이썬 3.11 이상(task group 사용 시), 표준 라이브러리 asyncio. 별도 설치 없이 gather·Queue·Semaphore·TaskGroup을 바로 씁니다.

출발점: 동시 접근 상한과 구조적 실행을 결합하는 뼈대는 이 정도입니다. 전체 구현은 원문 참고.

sem = asyncio.Semaphore(10)  # 동시 접속 상한
async with asyncio.TaskGroup() as tg:
    for job in jobs: tg.create_task(run_agent(sem, job))

확인: task group 블록을 빠져나올 때 모든 태스크가 완료 또는 취소 상태로 정리되는지, 세마포어가 동시 요청을 상한 이하로 유지하는지 봅니다.

걸리는 지점: 세마포어는 연결 수만 막지 토큰은 못 막아 TPM 초과가 납니다. fire-and-forget 태스크의 예외는 콜백을 안 붙이면 조용히 사라집니다. speculative execution은 취소해도 서버 측 생성이 이어져 토큰 비용이 계속 발생하고, CPU 작업이 끼면 이벤트 루프가 통째로 멈춥니다.


AI Sparkup 구독하기

최신 게시물 요약과 더 심층적인 정보를 이메일로 받아 보세요! (무료)

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다