$47,000의 에이전트 루프: 왜 로깅, 모니터링, max_tokens 모두 이를 막지 못했는가
요약
LangChain 에이전트 간의 무한 루프로 인해 47,000달러의 비용이 발생한 사례를 분석합니다. 로깅과 모니터링이 존재함에도 불구하고, 실행(run) 단위의 강제 제한(circuit breaker)이 없어 발생한 구조적 실패를 다룹니다.
핵심 포인트
- 에이전트 간 핑퐁 현상으로 인한 무한 루프 위험성
- max_tokens는 단일 호출 제한일 뿐 전체 실행을 제어하지 못함
- 사후적 비용 대시보드는 실시간 폭주를 막을 수 없음
- 관측 가능성(Observability)과 회로 차단기(Circuit Breaker)의 차이
2025년 11월, 4개의 AI 에이전트가 11일 동안 작동하며 47,000달러의 청구서를 생성했습니다.
아마 이 이야기를 들어보셨을 것입니다. 시장 조사 파이프라인(market-research pipeline)에서 네 개의 LangChain 에이전트가 A2A(Agent-to-Agent)를 통해 협업하고 있었습니다. 그중 Analyzer(분석가)와 Verifier(검증가) 두 에이전트가 핑퐁(ping-ponging)을 시작했습니다. Analyzer는 분석 내용을 생성했고, Verifier는 더 많은 내용을 요청했으며, Analyzer는 더 많은 내용을 생성했습니다. 종료 조건(termination condition)도, 예산 제한(budget cap)도 없었습니다. 11일 후 청구서가 날아왔습니다.
사람들을 놀라게 하는 것은 바로 그 금액입니다. 하지만 금액은 흥미로운 부분이 아닙니다. 사후 분석(post-mortem)에서 나온 진짜 흥미로운 부분은 이것입니다:
그들에게는 로깅(logging)이 있었습니다. 모니터링(monitoring)도 있었습니다. 하지만 하드 리밋(hard limit, 강제 제한)은 없었습니다.
이 문장을 곱씹어 보십시오. 팀이 눈을 감고 작업한 것이 아니었습니다. 모든 데이터, 즉 모든 호출(call), 모든 토큰(token), 모든 달러(dollar)가 내내 대시보드로 스트리밍되고 있었습니다. 하지만 그중 어느 것도 의미가 없었습니다. 왜냐하면 관측 가능성(observability)은 목격자이지, 회로 차단기(circuit breaker)가 아니기 때문입니다. 관측 가능성은 건물이 불타고 있다는 사실을 알려줄 수는 있지만, 가스 밸브를 잠글 수는 없습니다.
저는 왜 일반적인 방어 기제들이 이를 잡아내지 못하는지, 그리고 실제로 이를 잡아내기 위해서는 어떤 형태를 갖춰야 하는지 살펴보고자 합니다. 제품 홍보가 아니라, 그 메커니즘에 대해서 말입니다. 만약 여러분이 프로덕션(production) 환경에서 에이전트를 운영하고 있다면, 이것은 여러분을 밤잠 설치게 만들 실패 모드(failure mode)이며, 보기보다 훨씬 더 구조적인 문제입니다.
실패 모드: 수천 개의 완벽하게 유효한 호출들
폭주하는 에이전트는 하나의 거대한 이상 요청(anomalous request)이 아닙니다. 그것은 개별적으로는 합리적인 수천 개의 작은 요청들입니다.
Analyzer와 Verifier가 수행한 모든 호출은 형식이 올바랐습니다. 각각은 max_tokens 제한 내에 있었습니다. 각각은 200 상태 코드를 반환했습니다. 각각은 고립된 상태에서 보았을 때, 자신의 일을 수행하는 건강한 에이전트와 정확히 일치했습니다. 병리적 현상은 오직 '실행(run)'의 수준, 즉 결코 닫히지 않는 루프(loop) 수준에서만 존재하며, 일반적인 스택(stack)에서는 그 수준을 감시하는 것이 거의 없습니다.
이것이 바로 명백한 방어 기제들이 이를 그냥 통과해 버리는 이유입니다:
max_tokens는 호출(call)당 기준이며, 실행(run)당 기준이 아닙니다. 이는 단일 응답의 크기를 제한할 뿐입니다. 만 번의 응답에 대해서는 아무런 제약을 가하지 못합니다. 루프는 트윗 한 줄 정도의 크기를 만 번 반복하는 것과 같습니다.- 비용 대시보드(Cost dashboards)는 사후적(post-hoc)입니다. 대시보드는 호출이 이미 완료되고 돈이 이미 빠져나간 후에 지출 내역을 보여줍니다. 설계상 현실보다 뒤처질 수밖에 없습니다. 화재 경보기가 무언가를 감지하려면, 먼저 불이 나야만 합니다.
- 알림(Alerts)에는 깨어 있는 사람이 개입(human in the loop)하여 지켜봐야 합니다. "지출이 $X를 초과했습니다"라는 알림이 토요일 새벽 3시에 Slack 채널로 전송됩니다. 하지만 아무도 이를 11일 동안 보지 못했습니다. 알림을 확인하는 것이 사람의 업무였고, 사람은 잠을 자고, 휴가를 가며, 문제가 없던 일은 여전히 괜찮을 것이라고 가정하기 때문입니다.
이 각각의 요소들은 유용합니다. 하지만 그 어느 것도 '중단(stop)' 장치는 아닙니다. 그것들은 모두 대시보드 위의 계기판일 뿐, 브레이크 페달이 아닙니다.
실제 중단을 위해 필요한 것
폭주하는 실행을 단순히 서술하는 것이 아니라 실제로 _중단(stop)_하고 싶다면, 강제 집행(enforcement)은 세 가지 속성을 충족해야 합니다. 이 중 하나라도 놓친다면 당신은 다시 사후 분석(post-mortem) 보고서를 작성하게 될 것입니다.
1. 결정론적(deterministic)이어야 합니다. 의사 결정 경로에 모델이 있어서는 안 됩니다. 문제의 본질은 경계가 없는 비결정론적 시스템(non-deterministic system)입니다. 비결정론적인 시스템을 사용하여 다른 비결정론적 시스템의 경계를 정하고 그것을 안전하다고 부를 수는 없습니다. "LLM이 언제 멈출지를 결정하는 LLM을 추가했습니다"라는 것은 제어(control)가 아니라, 실패할 수 있는 두 번째 요소를 추가한 것뿐입니다. 한계(limit)란 컴파일된 코드에서 total_cost > ceiling을 평가하는 것이어야 하며, 그렇지 않다면 그것은 한계가 아닙니다.
2. 호출 전(pre-call)에 이루어져야 합니다. 체크는 다음 요청이 프로세스를 떠나기 _전(before)_에 실행되어야 하며, 요청을 거부해야 합니다. 호출이 이루어진 후에 실행되는 모든 것은 정의상 이미 호출이 발생했고 달러가 빠져나간 상태입니다. 사후적 강제 집행(Post-hoc enforcement)은 모순입니다. 강제 집행과 지출이 경주를 벌이게 되며, 항상 지출이 승리합니다.
3. 호출(per-call) 단위가 아닌 실행(per-run) 단위여야 합니다. 문제가 발생하는 단위는 실행(run)입니다. 실행이 수행하는 모든 호출에 걸쳐 누적된 총 비용, 총 루프 반복 횟수(loop iterations), 총 실제 경과 시간(wall-clock), 그리고 외부에서 작동시킬 수 있는 킬 스위치(kill switch)가 포함됩니다. 병리적 현상이 발생하는 고도가 바로 그 지점이므로, 예산 또한 그 고도에 맞춰 설정되어야 합니다.
결정론적(Deterministic)이고, 호출 전(pre-call)에 이루어지며, 실행(per-run) 단위로 작동해야 합니다. 이것이 브레이크 페달의 형태입니다. 당연한 소리처럼 들리겠지만, 이것은 새로운 컴퓨터 과학이 아닙니다. 운영체제(OS), 데이터베이스, 트레이딩 시스템이 수십 년 동안 사용해 온 하드코딩된 리소스 거버너(resource governor)와 같은 것입니다. 참신한 점은 에이전트(agent) 세계가 이를 건너뛰었다는 사실뿐입니다.
저는 이전에 더 가혹한 영역에서 이를 구축해 본 적이 있습니다
저는 비결정론적(non-deterministic) 시스템을 감싸는 결정론적 리스크 엔진(risk engine)을 구축하는 데 수년을 보냈습니다. 실수가 실시간으로 실제 비용을 발생시키며 되돌릴 수 없는 그런 종류의 시스템 말입니다. 그리고 교훈은 항상 같았습니다. 해당 시스템을 안전하게 유지해 준 것은 결코 '똑똑한 부분'이 아니었습니다. 그것은 똑똑한 부분을 감싸고 있으며, '안 돼(no)'라고 말할 수 있는 권한을 가진, 멍청하고 하드코딩된 결정론적 계층이었습니다.
똑똑한 부분은 제안하고, 결정론적 계층은 결정합니다. 모든 되돌릴 수 없는 작업은 게이트(gate)를 통과해야 합니다. 영리하고 확률적인(probabilistic) 구성 요소가 킬 스위치를 쥐게 해서는 안 됩니다. 킬 스위치가 필요한 근본적인 이유는 바로 그 영리한 구성 요소가 잘못될 수 있는 존재이기 때문입니다.
에이전트는 정확히 이 패턴에 새 옷을 입힌 것에 불과합니다. LLM은 전략입니다. 리스크 엔진은 LLM이 말로 설득하여 통과할 수 없는, 컴파일된 정적 타입(statically-typed) 코드 안에 있어야 합니다.
실제 구현 모습
이러한 확신 때문에 저는 RiskKernel을 구축해 왔습니다. 이는 이미 보유하고 있는 에이전트 앞에 해당 결정론적 계층을 배치하는 오픈 소스 자가 호스팅(self-hosted) 런타임입니다. 영업적인 이야기보다는 구체적인 내용을 유지하겠습니다. 핵심은 그 '형태(shape)'이며, 여러분도 직접 구현할 수 있기 때문입니다.
하나의 환경 변수(environment variable)를 사용하여 기존 에이전트를 연결하기만 하면 됩니다:
OPENAI_BASE_URL=http://localhost:7070/v1
이제 모든 호출은 거버너(governor)를 통해 라우팅됩니다. 실행당 예산(budget) — 달러, 루프 횟수, 실제 경과 시간(wall-clock seconds) — 을 설정하면, 실행이 한도(ceiling)를 넘어서는 즉시 다음 호출이 전달되는 대신 HTTP 402 오류와 함께 거부됩니다. 이는 모델이 아닌 Go 언어로 강제됩니다. 본인의 프로바이더 키(provider key)를 사용하세요. 이미 수행 중이던 호출 외에는 아무것도 기기 외부로 나가지 않습니다.
당신이 무엇을 사용하든 훔쳐올 만한 가치가 있다고 생각하는 세부 사항 하나는 다음과 같습니다: 이미 프로바이더(provider)에 도달한 호출은 절대 조용히 폐기되지 않습니다. 당신은 그 호출에 대해 비용을 지불했으므로 결과가 반환되며, 거부되는 것은 그다음 호출입니다. 장부(ledger)는 정직하게 유지됩니다. 예산은 한도를 초과하게 만든 요청을 이중으로 계산하지 않습니다. 브레이크는 이미 비용을 지불한 작업을 버리는 방식이 아니라, 루프의 다음 회전 시점에 작동합니다.
그리고 제가 실제로 밤잠을 설치며 걱정하는 실패 모드는, 길고 정당한 실행이 중간에 죽어버리는 것이기 때문에, 이 시스템은 체크포인트(checkpoint)를 생성합니다. 실행을 kill -9로 종료하더라도 비용을 재지출하거나 처음부터 다시 시작할 필요 없이 재개할 수 있습니다. 이 부분이 긴 에이전트 실행을 안심하고 계속 돌려둘 수 있게 만드는 핵심이며, 이것이 게임의 전부입니다.
정직한 부분
현재의 한계점(edges)은 다음과 같습니다: SQLite를 사용하는 단일 인스턴스, 하나의 API 토큰, 아직 지원되지 않는 스트리밍(proxy는 stream: true에 대해 깔끔한 501 오류를 반환합니다. 스트리밍 중간에 강제하는 것은 진정으로 어려운 일이며, 저는 어설프게 출시하느니 제대로 구현해서 출시하고 싶습니다), 기본 지원 프로바이더는 Anthropic과 OpenAI이며, 업스트림 게이트웨이(upstream gateway)를 통해 그 외의 긴 꼬리(long tail) 프로바이더들을 지원합니다. 라이선스는 Apache-2.0이며, 어디에도 데이터를 전송(phone home)하지 않습니다. 유일한 아웃바운드 트래픽은 당신의 프로바이더와 당신이 지정한 백엔드로 향하는 트래픽뿐입니다. 한계점을 나중에 발견하기보다 미리 알고 계시는 편이 낫다고 생각합니다.
또한 이 도구는 당신의 관측성 스택(observability stack)이나 정책 방화벽(policy firewall)이 되려고 시도하지 않습니다. 당신이 이미 실행 중인 무엇이든 OpenTelemetry를 방출하며, 게이트웨이 및 대시보드와 상호 운용합니다. 이 도구는 정확히 한 가지 측면에서 경쟁합니다: 당신에게 피해를 주기 전에 결정론적(deterministically)으로 실행을 중단하는 것입니다.
요점
에이전트 (agents)를 실행한다면, 던져야 할 질문은 _"에이전트가 폭주할 때 내가 알게 될 것인가?"_가 아닙니다. 당신은 결국 알게 될 것입니다. 로그에서, 청구서에서, 혹은 사후 분석 (post-mortem) 과정에서 말이죠. 진짜 질문은 _"내가 알게 되기 전에 무엇이 그것을 막아줄 것인가?"_입니다.
로깅 (Logging)은 그것이 아닙니다. 모니터링 (Monitoring)도 그것이 아닙니다. max_tokens 역시 그것이 아닙니다. 실행 전(pre-call)에 결정론적 (deterministic)으로 작동하며, 킬 스위치 (kill switch)를 갖춘 실행당 제한 (per-run limit)이 바로 그것입니다. 제가 만든 것을 사용하든 직접 구현하든, 이미 보유하고 있는 에이전트 앞에 이를 구축하는 데는 불과 몇 시간의 작업이면 충분합니다.
11일. 47,000달러. 그리고 이 모든 과정을 지켜본 대시보드. 당신이 그 대시보드가 되지 마십시오.
RiskKernel은 오픈 소스 (Apache-2.0)이며 셀프 호스팅 (self-hosted)이 가능합니다 — pip install riskkernel 또는 docker run으로 설치하세요. 만약 에이전트 앞에 이를 배치했는데 가드레일 (guardrails)이 너무 엄격하거나 너무 느슨하다면, 어느 부분인지 진심으로 듣고 싶습니다. 그러한 피드백이 다음 릴리스를 만드는 밑거름이 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기