AI 청구서는 경고를 보내지 않습니다: laravel-ai-tasks를 이용한 Laravel에서의 예산 제어
요약
Laravel 환경에서 AI API 호출 비용을 효율적으로 관리할 수 있는 laravel-ai-tasks 패키지를 소개합니다. 사전 점검과 호출 후 점검이라는 이중 체크 메커니즘을 통해 테넌트별 예산 초과를 방지합니다.
핵심 포인트
- 사전 점검(Pre-flight)과 호출 후 점검(Post-call)의 이중 예산 확인
- 테넌트별로 독립적인 월간 예산 설정 및 관리 가능
- BudgetExceededException을 통한 실시간 비용 통제
- 실제 사용된 토큰 비용을 정확히 기록하여 예산 계산 오류 방지
첫 번째 AI 호출에는 비용 가드레일(guardrail)이 없습니다. 그럴 필요도 없습니다. 단 한 번의 요청이고, 테스트 중이니 괜찮습니다.
하지만 서비스가 배포되면 상황이 달라집니다. 재시도(Retries)가 쌓이고, 큐 워커(queue worker)가 밤새 백로그(backlog)를 처리하며, 한 테넌트(tenant)가 다른 모든 테넌트를 합친 것보다 10배 많은 트래픽을 보낼 수도 있습니다. 그리고 월말에 서비스 제공업체의 인보이스(invoice)가 도착할 때까지 아무도 이를 알아차리지 못합니다. 요청 라이프사이클(request lifecycle) 중 어디에서도 "중단"을 말할 수 있는 지점이 없었기 때문입니다.
laravel-ai-tasks는 이러한 중단 지점을 사후에 확인하는 보고서가 아니라, 디스패치(dispatch) 흐름의 일급 시민(first-class part)으로서 내장합니다.
한 번이 아닌 두 번의 체크
호출 "전"에만 예산을 확인하는 것은 충분하지 않습니다. 서비스 제공업체가 토큰 사용량(token usage)과 함께 응답을 보내기 전까지는 호출의 실제 비용을 알 수 없기 때문입니다. 예산에서 2달러가 남은 테넌트가 5달러가 드는 요청을 보낼 수 있으며, 사전 점검(pre-flight)만 수행한다면 이를 허용하게 됩니다.
따라서 이 패키지는 두 번 확인합니다:
| 체크 유형 | 시점 | 사용 데이터 |
|---|---|---|
| 사전 점검 (Pre-flight) | 제공업체 호출 전 | 이번 달 현재까지의 지출액 |
| 호출 후 점검 (Post-call) | 응답 직후 | 이번 응답의 실제 비용 |
두 경우 모두 BudgetExceededException을 발생시킵니다. 두 체크 모두 AI::send(), AI::stream(), 그리고 큐 작업(queued job)의 모든 경로에서 실행됩니다.
만약 호출 후 점검(post-call check) 단계에서 예외가 발생한다면, 이미 제공업체에 비용이 지불된 상태입니다. 이 경우 실행 결과는 버려지는 대신, 실제 cost와 함께 ok로 기록됩니다. 그렇지 않으면 해당 지출이 다음 달 예산 계산에서 사라지게 되어, 테넌트가 이미 사용한 금액을 여유분으로 오해하게 될 것입니다.
테넌트별 제한 설정
// config/ai-tasks.php
'budgets' => [
'default' => ['monthly_usd' => 100.0],
...
테넌트에 대한 항목이 없으면 → default를 사용합니다. default도 없다면 → 제한이 없습니다. 예산 설정은 선택 사항(opt-in)입니다.
테넌트 ID는 TenantResolver에서 가져옵니다:
class TenantResolver
{
public function id(): string
...
X-Tenant-Id 헤더 → 인증된 사용자(authenticated user) → config 기본값. 자체 테넌시 모델을 사용하고 있습니까? 서비스 프로바이더에서 리졸버를 직접 바인딩하세요. 한 줄이면 충분합니다:
$this->app->singleton(
\Fomvasss\AiTasks\Support\TenantResolver::class,
fn () => new MyTenantResolver()
...
현실적인 사례: 지원 티켓 분류(triaging support tickets)
실제 트래픽이 뒤에 붙는다면 예산은 중요해집니다. 흔한 시나리오가 있습니다. 들어오는 모든 지원 티켓이 사람이 보기 전에 세 번의 AI 호출을 거칩니다. 요약, 검색용 키워드 추출, 카테고리 분류입니다. 세 개의 태스크 클래스가 같은 구조를 가집니다:
class SummarizeTextTask extends AiTask
{
public function __construct(
...
ExtractKeywordsTask와 ClassifyContentTask는 구조화된 JSON을 위한 schema() 클로저를 사용하여 동일한 패턴을 따릅니다. 실제 편집되지 않은 하나의 티켓(
+---------+-------------------------+-------------+-------------+------------------+
| Tenant | Period | Spent (USD) | Limit (USD) | Remaining (USD) |
+---------+-------------------------+-------------+-------------+------------------+
...
운영(Production) 환경의 수치는 이와 전혀 다르게 보이겠지만, 테이블과 그 이면의 체크 로직은 동일합니다.
옵션:
--month=YYYY-MM— 지난 달 확인--from=YYYY-MM-DD --to=YYYY-MM-DD— 임의의 사용자 지정 범위 (지출액만 확인 — 사용자 지정 범위는 비교할 단일 월간 한도가 없습니다)
테넌트(Tenant)의 월간 예산이 소진되면, 명령은 0이 아닌 종료 코드(non-zero exit code)로 종료됩니다. 이를 cron 작업에 등록하면 별도의 알림 코드를 작성하지 않고도 예산 알림 시스템을 구축할 수 있습니다.
이것이 중요한 이유
예산 집행(Budget enforcement)은 보통 예상치 못한 청구서를 받은 _이후_에 추가되며, 실행 도중 중단되도록 설계되지 않은 코드에 임시방편으로 덧붙여지곤 합니다. 하지만 여기서는 구조적입니다. 모든 디스패치(dispatch) 모드는 동일한 두 가지 체크를 거치며, 모든 실행은 테넌트(tenant)에 귀속됩니다. 또한 상태 확인은 별도로 존재함을 기억해야 하는 쿼리 대신 하나의 명령어로 이루어집니다.
GitHub: fomvasss/laravel-ai-tasks
Packagist: packagist.org/packages/fomvasss/laravel-ai-tasks
댓글로 질문해 주시면 기꺼이 답변해 드리겠습니다. Star와 Issue 모두 환영합니다 🙂
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기