llmfleet: 여러 에이전트의 턴을 하나의 Batch API 호출로 통합하여 50% 비용 절감하기
요약
Anthropic의 Batch API를 활용하여 여러 에이전트의 요청을 효율적으로 통합 관리하는 llmfleet 프레임워크를 소개합니다. 지연 시간 예산을 기반으로 동기 방식과 배치 방식을 자동으로 라우팅하여 비용을 50% 절감하면서도 개발 편의성을 높입니다.
핵심 포인트
- Anthropic Batch API를 활용한 비용 50% 절감
- latency_budget_ms 기반의 자동 라우팅 기능 제공
- 에이전트 군단(fleet) 단위의 효율적인 요청 풀링
- 동기/배치 호출 방식의 추상화를 통한 개발 복잡도 감소
Anthropic의 Batch API는 입력 토큰 비용을 50% 절감해 줍니다. 저는 이보다 비용 대비 효율(cost-to-effort ratio)이 더 좋은 기능을 떠올리기가 어렵습니다. 그리고 제가 만든 에이전트 중 실제로 이를 사용하는 것은 거의 없는데, 그 이유는 문서가 이를 오프라인 처리용 도구처럼 보이게 만들고, SDK가 이를 90~120초 동안 폴링(polling)하는 일회성 요청(one-shot request) 형태로 구성했기 때문입니다.
이러한 프레임워크는 한 명의 사용자에게는 잘못되었습니다. 하지만 에이전트 군단(fleet)에게는 정확합니다.
만약 당신이 각기 다른 코루틴(coroutine)에서 여러 에이전트를 병렬로 실행하며, 각 에이전트가 개별적으로 messages.create 호출을 하고 있다면, 배치(batching)의 적절한 단위는 한 사용자의 턴이 아닙니다. 그것은 사용자가 결코 볼 수 없는 레이어에 의해 함께 풀링(pooled)되고, 특정 윈도우(window) 단위로 플러시(flushed)되며, Future를 통해 대기 중인 코루틴으로 다시 라우팅되는 에이전트 군단의 턴입니다. Eran Sandler는 올해 초 왜 배치가 단일 에이전트에게 최악인지에 대해 글을 쓴 적이 있습니다. llmfleet는 그 반대입니다. 이는 "단일 에이전트"가 "20개의 에이전트"로 변할 때 수행하는 작업입니다.
문제점
Batch API는 SDK 호출 지점에 맞지 않는 형태로 구성되어 있습니다. 당신은 다음 중 하나를 선택해야 합니다:
- 그것이 존재하지 않는 척하며 전체 입력 비용을 지불합니다.
- 프로젝트당 작은 큐(queue)를 직접 구현하지만, 폴링(polling) 로직을 잘못 작성하고, 결국 요청의 절반에서 비용 절감 기회를 놓치며, 이제는 유지 관리해야 할 큐까지 떠안게 됩니다.
- 모든 것을 Batch로 밀어 넣지만, 대화형 채팅 경로가 90초나 걸릴 때 삶이 고통스러워집니다.
당신이 실제로 원하는 것은 호출당 라우팅 결정입니다: "이것은 빨리 돌아와야 하니 동기(sync) 방식으로 보내라. 이것은 기다려도 되니 풀링(pool)해라." 호출자에게는 두 경로가 모두 동일하게 보입니다. 디스패처(dispatcher)가 무엇이 무엇인지 판단합니다.
해결책의 형태
호출자는 latency_budget_ms를 전달하고 디스패처가 라우팅합니다.
import asyncio
from anthropic import AsyncAnthropic
from llmfleet import FleetDispatcher, RoutingPolicy
...
흥미로운 지점은 await fleet.submit(latency_budget_ms=...) 부분입니다. 호출자는 호출이 동기(sync)로 이루어졌는지 배치(batched)로 이루어졌는지 신경 쓰지 않습니다. 그저 응답을 기다릴(await) 뿐입니다. 디스패처가 라우팅을 해결했기 때문입니다.
라우팅을 강제하고 싶다면:
async def force_routes(fleet, kwargs):
await fleet.submit_sync(**kwargs)
await fleet.submit_batch(**kwargs)
무엇이 풀링(pooled)되었고 무엇이 되지 않았는지 알고 싶다면:
print(fleet.stats.sync_calls)
print(fleet.stats.batched_calls)
print(fleet.stats.batches_submitted)
이것이 API의 전부입니다.
수행하지 않는 작업
- 품질에 기반하여 제공자(provider)나 모델(model) 간의 라우팅을 수행하지 않습니다. 이를 위해서는 실제 라우터(router)를 사용하세요.
- 프로세스 간 풀링(cross-process pooling)을 수행하지 않습니다. Fleet은 프로세스 로컬(process-local)입니다. 프로세스 간 풀링이 필요하다면 Redis나 SQS를 앞에 두세요.
- 도구(tool)가 임계 경로(critical path)에 있는 도구 호출(tool-call) 턴을 풀링하려고 시도하지 않습니다. 그런 경우에는 (
submit_sync를 통해)force_sync=True를 전달하세요. - 기본적으로 실패한 배치(batch)를 재시도하지 않습니다. 배치가 에러를 발생시키면, 각 제출(submission)에 대해 에러를 받게 됩니다.
라이브러리 내부 (보여줄 만한 하나의 설계 선택)
디스패처(dispatcher)는 백그라운드 플러셔(flusher) 코루틴(coroutine)을 실행합니다. submit() 호출은 동기적으로 실행되거나, 큐(queue)에 쌓인 후 플러셔에 의해 해결(resolved)됩니다.
# 플러시 결정의 의사 코드(pseudo-shape)
async def _flusher(self):
while not self._stopped:
...
두 개의 임계값(threshold), 하나의 경합(race). 플러셔는 다음 중 하나라도 충족될 때 실행됩니다:
- 큐가
batch_min_size만큼 채워졌을 때, - 또는, 큐에 있는 가장 오래된 항목이
batch_window_ms동안 대기했을 때.
둘 중 먼저 도달하는 쪽이 승리합니다. 이것이 누구의 조정(coordinate) 없이도 Fleet이 독립적인 코루틴들 사이에서 배치를 공유할 수 있게 만드는 핵심 부분입니다. 각 submit() 호출은 단순히 큐에 추가하고 자신의 Future를 기다립니다(await). 플러셔는 배치가 돌아오면 해당 Future를 해결합니다.
_oldest_queued_at 체크는 트래픽이 폭증할 때 느린 경로(slow path)가 기아 상태(starvation)에 빠지는 것을 방지합니다. 이것이 없다면, 바쁜 시간 이후의 한산한 시간에는 항목들이 큐에 남아 다음 트래픽 폭증이 발생하여 개수가 batch_min_size를 넘길 때까지 기다려야 할 것입니다. 이것이 있음으로써, 모든 항목은 도착 후 batch_window_ms 이내에 플러시(flush)됨을 보장받습니다.
이것이 유용한 경우
- 다수의 에이전트를 동시에 실행하며, 그중 대부분이 채점(grading), 요약(summarization), 또는 추출(extraction)과 같은 오프라인 작업을 수행하는 경우.
- 리더보드(leaderboard)나 평가(eval)를 실행 중이며, 다음 1초가 아닌 다음 1시간 이내에 1,000개의 생성(generation)이 완료되어도 되는 경우.
- 동일한 프로세스 내에 채팅 경로(chat path)와 백그라운드 경로(background path)가 존재하며, 두 경로 모두를 위한 단일 클라이언트를 원하는 경우.
- 입력 토큰(input tokens)이 크고(긴 시스템 프롬프트, RAG 컨텍스트), 50% 할인이 의미 있는 경우.
- 라우팅 결정(routing decision)이 별도의 코드 경로가 아닌 매개변수(parameter)가 되기를 원하는 경우.
이것이 원하지 않는 경우
- 단일 에이전트를 실행하는 경우. 동기(sync) API를 사용하세요. 배치(Batch)는 단일 사용자를 위한 적절한 형태가 아닙니다.
- 모든 호출에서 1초 미만의 p99 지연 시간(latency)이 필요한 경우. 동기 라우팅(sync routing)을 사용하더라도 디스패처(dispatcher)가 미세한 오버헤드를 추가합니다.
- 프로세스 간 풀링(cross-process pooling)이 필요한 경우. llmfleet은 프로세스 내(in-process)에서만 작동합니다.
설치
pip install llmfleet
Repo: https://github.com/MukundaKatta/llmfleet
형제 라이브러리 (Sibling libraries)
| 라이브러리 | 역할 |
|---|---|
| cachebench | 호출당 캐시 히트 비율(cache hit ratio) + 절감된 비용 |
| ... |
cachebench와 llmfleet은 함께 사용하기 좋습니다. 인내심 있는 호출(patient calls)을 배치(batches)로 풀링한 다음, 호출당 캐시 히트 비율을 추적하여 배치가 조용히 캐시를 놓쳤을 때를 알 수 있습니다.
다음 단계
v0.1.0은 Anthropic 전용입니다. OpenAI Batch API와 Bedrock async-invoke는 로드맵에 포함되어 있습니다. 디스패처(dispatcher)는 제공자 중립적(provider-agnostic)이지만, 각 새로운 벤더를 위한 제공자별 클라이언트 어댑터(per-provider client adapter)가 현재 구현되어야 할 부분입니다. PR(Pull Request)을 환영합니다.
만약 수십 개의 에이전트를 동시에 실행하면서 배치를 사용하지 않고 있다면, 이번 분기에 절약할 수 있는 가장 저렴한 50%의 비용이 바로 여기에 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기