내 AI 에이전트의 병목 현상은 모델이 아니라 아키텍처였다
요약
AI 에이전트의 성능 병목 현상은 모델의 성능보다 아키텍처 설계에 달려 있습니다. 단일 에이전트의 순차적 처리 방식에서 벗어나, 역할을 분리하고 병렬로 실행하는 멀티 에이전트 오케스트레이션 패턴을 통해 처리량을 획기적으로 개선할 수 있습니다.
핵심 포인트
- 단일 에이전트의 순차적 호출은 데이터 증가 시 처리량이 급격히 저하됨
- 역할별로 에이전트를 분리하고 asyncio 등을 활용해 병렬 실행하는 것이 핵심
- 작업 간 의존성이 있는 경우나 작업 단위가 너무 작은 경우에는 병렬화가 비효율적임
- I/O 바운드 문제가 RAG 검색에서 발생한다면 LLM 병렬화보다 검색 최적화가 우선임
3개월 전, 저는 고객 워크플로우를 위해 문서 분류, 태깅, 요약 생성을 처리하는 단일 에이전트를 운영하고 있었습니다. 하루 50개의 문서를 처리할 때는 잘 작동했습니다. 그러다 물량이 500개로 늘어났습니다. 에이전트는 배치당 40분이 걸리기 시작했습니다. 처리량(Throughput)이 확장되는 것이 아니라, 붕괴되었습니다.
해결책은 더 큰 모델을 사용하는 것이 아니었습니다. 에이전트를 병렬로 실행되는 세 가지 전문화된 역할로 분리하는 것이었습니다. 처리량은 40분에서 4분으로 줄어들었습니다. 모델은 전혀 바뀌지 않았습니다. 아키텍처(Architecture)가 바뀌었습니다.
이것은 AI 에이전트 개발에서 사람들이 충분히 이야기하지 않는 부분인 **에이전트 오케스트레이션 아키텍처 (agent orchestration architecture)**입니다. "작동한다"와 "확장 가능하다" 사이의 간극는 거의 항상 모델의 문제가 아니라 아키텍처의 문제입니다.
순차적 함정 (The Sequential Trap)
개발자들이 처음 에이전트 시스템을 구축할 때, 거의 항상 모든 것을 순차적으로 수행하는 하나의 에이전트로 시작합니다:
# 함정: 하나의 에이전트, 모든 것이 순차적임
class DocumentPipeline:
def process(self, doc):
...
각 .run() 호출은 전체 컨텍스트(Context)가 전달되는 별도의 LLM 호출입니다. 문서 하나라면 괜찮습니다. 하지만 500개라면, 1,500번의 LLM 호출을 순차적으로 수행하게 됩니다. 호출당 2초가 걸린다고 해도, 이는 3,000초, 즉 50분에 달합니다.
모델은 거의 작동하고 있지 않습니다. 그 시간의 대부분을 기다리는 데 사용하고 있습니다.
실제로 작동하는 멀티 에이전트 패턴 (The Multi-Agent Pattern That Actually Works)
해결책은 역할별로 분리하고, 서로 의존하지 않는 에이전트들을 병렬로 실행하는 것입니다:
import asyncio
from dataclasses import dataclass
from typing import List
...
세 개의 에이전트, 하나의 asyncio.gather() 호출. 각 에이전트는 더 작고 집중된 시스템 프롬프트(System Prompt)를 받습니다. 이는 컨텍스트 윈도우(Context Window)가 더 좁아지고 모델이 생각해야 할 내용이 줄어들기 때문에 더 빠른 추론(Inference)을 의미하기도 합니다.
병목 현상은 "모델 속도"에서 "얼마나 빨리 작업을 할당하고 병합할 수 있는가"로 이동합니다.
병렬화를 하지 말아야 할 때
병렬 처리가 항상 더 나은 것은 아닙니다. 제가 너무 일찍 병렬화를 시도하여 시간을 낭비했던 경우는 다음과 같습니다:
두 번째 작업이 첫 번째 작업의 출력을 필요로 할 때. 만약 태깅(Tagging)이 요약(Summary)에 의존한다면, 이 두 작업을 병렬화할 수 없습니다. 의존성 그래프(Dependency Graph)가 중요합니다.
오버헤드를 정당화하기에는 작업이 너무 작은 경우. 에이전트를 디스패칭(Dispatching)하는 데는 시스템 프롬프트(System Prompt) 로딩, 컨텍스트(Context) 설정과 같은 오버헤드가 발생합니다. 실제 LLM 실행 시간이 100ms 미만인 작업의 경우, 직렬(Serial) 방식이 조정 비용(Coordination cost)보다 더 빠를 수 있습니다.
연산(Compute)이 아니라 검색(Retrieval)에서 I/O 바운드(I/O bound)가 발생하는 경우. 만약 에이전트가 시간의 90%를 RAG 조회(Lookup)를 기다리는 데 소비한다면, LLM 호출을 병렬화하는 것은 도움이 되지 않습니다. 검색(Retrieval)을 먼저 최적화하세요.
# 반드시 직렬화해야 하는 경우 (의존성이 존재하는 경우)
async def run_dependent_pipeline(doc: str) -> str:
# 단계 1이 완료되어야 단계 2가 시작될 수 있음
...
라우팅 문제: 언제 팬아웃(Fan Out)하고, 언제 큐(Queue)에 넣을 것인가
에이전트의 수가 많아질수록 라우팅(Routing) 문제는 더욱 흥미로워집니다. 저는 다음과 같은 간단한 디스패처(Dispatcher) 패턴을 사용합니다:
class AgentDispatcher:
def __init__(self):
self.agents = {
...
이 디스패처는 독립적인 작업에 대한 팬아웃(Fan-out)을 처리하며, 호출 코드를 변경하지 않고도 새로운 에이전트 역할(Role)을 추가할 수 있게 해줍니다. 이제 병목 현상은 "속도 제한(Rate limits)에 걸리지 않고 동시에 얼마나 많은 에이전트를 실행할 수 있는가"로 바뀌었습니다. 이는 이전과는 다른 문제이며, 직면하기에 좋은 문제입니다.
내가 배운 것들
1. 병렬화하기 전에 프로파일링(Profile) 하세요. 시간이 실제로 어디에 소비되는지 측정하세요. LLM 추론(Inference)인가요, 아니면 검색(Retrieval)인가요? 잘못된 것을 병렬화하는 것은 복잡성만 가중시키는 낭비일 뿐입니다.
2. 전문화된 에이전트가 범용 에이전트보다 빠릅니다. gpt-4o-mini에서 3줄 정도의 타이트한 시스템 프롬프트(System Prompt)를 사용하는 요약 에이전트는 요약 작업에서 gpt-4o를 사용하는 범용 에이전트를 일관되게 이길 것입니다. 더 작은 모델, 집중된 역할, 그리고 더 빠르고 저렴한 비용을 제공합니다.
3. 아키텍처 문제는 곧 제품의 문제입니다. 멀티 에이전트(Multi-agent)로 넘어가기 전에 자문해 보세요: 각 역할이 활성 상태(Warm)를 유지할 만큼 충분한 작업을 가지고 있는가? 만약 5개의 에이전트를 실행하고 있지만 각 에이전트가 10분에 한 번씩만 작업을 처리한다면, 병렬화의 이점 없이 오케스트레이션(Orchestration) 오버헤드에 대한 비용만 지불하고 있는 것입니다.
4. 도구 사용(Tool use)은 의존성 그래프(Dependency graph)를 복잡하게 만듭니다. 에이전트가 도구(웹 검색, 데이터베이스 쿼리, 파일 I/O)를 사용하기 시작하면, 의존성 그래프(Dependency graph)는 더욱 복잡해집니다. 코드를 작성하기 전에 이를 명시적으로 그려보세요.
저에게 가장 큰 돌파구(Unlock)가 되었던 깨달음은 "AI 에이전트를 구축하는 것"이 두 가지 별개의 문제라는 점이었습니다. 즉, 에이전트가 무엇을 하는가(프롬프팅(Prompting), 도구, 지식)와 에이전트가 다른 에이전트 및 시스템과 어떻게 통합되는가입니다. 대부분의 튜토리얼은 전자에만 완전히 집중합니다. 하지만 프로덕션 시스템(Production systems)의 성패는 후자에 달려 있습니다.
만약 처리량(Throughput) 한계에 부딪혔을 때 가장 먼저 드는 생각이 "더 큰 모델을 써보자"라면, 먼저 오후 시간을 할애하여 시스템의 의존성 그래프(Dependency graph)를 그려보라고 제안하고 싶습니다. 그러면 더 큰 모델이 전혀 병목 현상(Bottleneck)의 원인이 아니었다는 사실을 발견하게 될지도 모릅니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기