
새벽 2시의 폭주하는 루프: 멈추지 않는 에이전트 탐지 및 종료하기
요약
AI 에이전트가 도구 호출 반복이나 무한 압축 루프에 빠져 토큰 사용량이 폭증하는 문제를 분석합니다. 에이전트 스스로는 루프를 인지할 수 없으므로, 모델 외부의 코드 영역에서 탐지 및 종료 메커니즘을 구축해야 함을 강조합니다.
핵심 포인트
- 에이전트의 루프는 개별 단계가 아닌 전체 실행 궤적(trajectory)에서 발생함
- 도구 호출 반복 및 무한 압축 루프로 인한 컨텍스트 윈도우 폭증 위험
- 탐지 및 킬 스위치는 모델 외부의 독립적인 코드 영역에 구현해야 함
- 도서: Agents in Production — Building, Tracing, and Shipping Multi-Step AI You Can Trust
- 저자의 다른 저서: Observability for LLM Applications — The AI Engineer's Library (2권 시리즈)의 동반 도서
- 내 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구를 사용하여 작업하는 개발자를 위한 IDE
- 저자 정보: xgabriel.com | GitHub
2025년 4월, 한 사용자가 Claude Code 리포지토리에 Issue #44726을 제출했습니다. 제목은 [BUG][Billing]이었고, 본문에는 토큰 수(token counts)가 담긴 표가 있었습니다. 일반적인 세션에서는 출력 토큰(output tokens)이 입력 토큰(input tokens)보다 훨씬 적습니다. 하지만 이 세션의 비율은 74:1이었습니다. 또 다른 첨부된 세션은 175:1로 나타났습니다. 계정 잔액은 마이너스로 돌아섰습니다.
보고서를 추적해 본 결과, 이는 복합적인 루프(compounding loop)로 인해 발생한 문제였습니다. 도구 호출(tool calls)이 반복됨에 따라 대화 기록(conversation history)과 파일 컨텍스트(file context)가 무제한으로 커진 것입니다. 에이전트는 무엇이 잘못되었는지 전혀 알지 못했습니다. 에이전트 자신의 관점에서는 정상적으로 작동하고 있었기 때문입니다.
이것이 문제의 핵심을 한 문장으로 요약한 것입니다. 루프 속에서 실행 중인 대상은 자신이 루프에 빠져 있다는 사실을 말해줄 수 없습니다. 왜냐하면 계속 실행할지 여부를 결정하는 주체가 바로 그 대상이기 때문입니다. 따라서 탐지(detection)와 킬 스위치(kill switch)는 모델 외부에, 즉 모델이 절대 건드릴 수 없는 코드 영역에 존재해야 합니다.
루프가 실제로 시작되는 방식
폭주는 단 한 번의 극적인 결정으로 발생하는 경우가 드뭅니다. 그것은 반복되는 작은 패턴입니다.
Issue #27281은 아주 전형적인 사례입니다. 에이전트가 실제로 Write 도구(tool)를 호출하지는 않은 채, 자신의 의도를 설명하며 "문서를 작성하겠습니다"라는 말을 턴(turn)마다 반복하며 갇혀버린 상황입니다. 수행하지도 않은 작업에 대해 떠드는 에이전트 때문에 컨텍스트 윈도우(context window)가 통째로 타버린 것입니다. Issue #6004는 "무한 압축 루프(infinite compaction loop)" 사례입니다. 에이전트가 자신의 히스토리를 압축하고, 진전이 없으면 다시 압축하기를 반복하여, 사용자가 예상보다 훨씬 빨리 사용량 제한(usage limit)에 도달하게 됩니다.
이들은 공통된 형태를 띱니다. 모델은 유효한 응답을 생성합니다. 도구를 호출할 수도 있습니다. 도구는 무언가를 반환할 수도 있습니다. 개별적인 단계 하나하나에는 문제가 없습니다. 로드 밸런서(load balancer)는 정상적인 엔드포인트(endpoint)로 인식합니다. 버그는 개별 단계가 아니라 궤적(trajectory)에 있으며, 궤적은 바로 요청 수준의 테스트 스위트(test suite)가 포착할 수 없는 영역입니다.
모델은 반복을 "인지"하지 못합니다. 순전파(forward pass)는 오직 현재 윈도우에 있는 내용만을 보기 때문입니다. 루프가 짧다면 윈도우에 반복되는 패턴이 나타나 모델이 루프를 탈출할 수도 있습니다. 하지만 루프가 요약(summary) 형태로 압축된다면, 반복은 마치 진전이 있는 것처럼 보입니다. 내부 시계는 존재하지 않습니다. 직접 구축하지 않는 한 배압(back-pressure)도 존재하지 않습니다.
우선 한계치 설정: 모델이 볼 수 없는 루프
영리한 탐지 기법을 도입하기 전에, 루프에 물리적인 한계치(ceiling)를 설정하십시오. 세 가지 축이 있습니다: 단계(steps), 토큰(tokens), 실제 경과 시간(wall-clock). 이것은 전체 시스템의 바닥이며, 모델이 결코 무시할 수 없는 단순한 for 루프입니다.
import time
MAX_STEPS = 12
...
step_fn은 단일 제공자 호출(provider call)입니다. 제한 사항은 이를 호출하는 코드에 존재해야 하며, 중단을 요청하는 프롬프트(prompt) 안에 있어서는 안 됩니다. 이 래퍼(wrapper)에 Issue #44726의 루프(결코 done을 반환하지 않는 모델)를 그대로 전달하면, 청구서가 날아오는 대신 세션이 종료됩니다:
stuck = lambda task: {
"done": False,
"tokens": 5000,
...
RuntimeError: step cap hit
경과 시간을 측정할 때는 time.time 대신 time.monotonic을 사용하세요. Wall-clock 타임스탬프(실제 시각)에 대한 NTP(Network Time Protocol) 보정이 발생하면, 장시간 실행되는 루프에서 결국 음수의 경과 값이 발생하게 되며, 이로 인해 킬 스위치(kill switch)가 조용히 작동을 멈추게 됩니다.
상한선(ceiling)은 필요하지만, 그것만으로는 충분하지 않습니다. 12단계 제한(12-step cap)은 루프가 멈추기 전까지 여전히 12번의 전체 회전을 수행하게 두며, 컴팩션 루프(compaction loop)는 12번의 회전 동안 실질적인 피해를 입힐 수 있습니다. 여러분은 폭주하는 루프를 더 일찍, 즉 진전(progress)이 멈추는 순간에 포착해야 합니다.
진전 없음 탐지 (No-progress detection): 상한선에 도달하기 전에 포착하기
상한선은 "너무 멀리 갔는가?"라고 묻습니다. 진전 없음 탐지(No-progress detection)는 더 나은 질문을 던집니다: "여전히 어딘가로 나아가고 있는가?"
정상적인 에이전트의 턴(turn)은 서로 다르게 보입니다: 새로운 도구(tool), 새로운 인자(argument), 새로운 결과(result). 하지만 갇힌 에이전트는 자기 자신을 반복합니다.
일반화가 잘 되면서도 비용이 가장 적게 드는 신호는 각 제안된 행동(action)의 해시(hash) 값입니다. 동일한 인자를 가진 동일한 도구가 연속해서 나타나는 것은 갇힌 루프의 지문(fingerprint)과 같습니다. 최근의 지문들을 추적하고, 하나가 너무 자주 반복되면 중단하세요.
import hashlib
import json
from collections import deque
...
check는 (ok, reason)을 반환합니다. 각 도구 호출(tool call)을 실행하기 전에 이를 호출하세요. 만약 False가 반환된다면, 동일한 행동을 반복하며 사이클을 돌고 있는 루프가 발생한 것이므로, 단계 제한(step cap)을 기다리는 대신 지금 즉시 중단해야 합니다.
Issue #27281에서 언급된 "의도 서술(narrating intent)" 루프에는 신호가 하나 더 필요합니다: 도구 호출을 전혀 생성하지 않는 턴들입니다. 행동 없이 세 번의 턴 동안 말만 하는 에이전트는 생각하고 있는 것이 아니라, 시간을 끌고(stalling) 있는 것입니다. 연속된 '도구 미사용 턴(no-tool turns)'의 횟수를 세고, 이를 진전 없음(no-progress)으로 취급하세요.
class StallDetector:
def __init__(self, max_idle_turns=3):
self.max_idle_turns = max_idle_turns
...
두 탐지기 모두 완벽할 수는 없습니다. 정당한 재시도(retry) 과정에서 동작이 한두 번 반복될 수 있기 때문에, repeat_limit을 1이 아닌 3으로 설정한 것입니다. 어떤 작업들은 진정으로 몇 번의 순수 추론(pure-reasoning) 턴을 필요로 하며, 그렇기에 max_idle_turns는 작동하기 전에 약간의 여유를 둡니다. 여러분의 실행 로그(traces)를 바탕으로 두 값을 튜닝하세요. 핵심은 모델이 이 두 가지 결정에 대해 투표권을 갖지 않는다는 점입니다.
공포 이야기를 끝내는 킬 스위치 (kill switch)
탐지는 업무의 절반일 뿐입니다. 무언가 탐지되었을 때, 실제로 중단해야 하며, 호출자(caller)가 논리적으로 파악할 수 있는 방식으로 중단해야 합니다. 아무런 정보도 없는 예외(bare exception)를 발생시켜 컨텍스트를 잃어버리지 마세요. 상위 레이어가 에이전트가 작업을 완료했는지 아니면 강제 종료되었는지 알 수 있도록 구조화된 중단 사유(structured stop reason)를 반환하여 사용자에게 올바른 정보를 보여줄 수 있게 해야 합니다.
다음은 상한선(ceiling), 두 탐지기, 그리고 깔끔한 중단을 함께 연결하는 외부 루프(outer loop)의 구현입니다.
from dataclasses import dataclass
@dataclass
...
model_call은 여러분의 프로바이더(provider) 호출입니다. Anthropic SDK를 사용하는 경우, 여러분의 messages와 tools를 사용하여 client.messages.create(...)를 래핑(wrap)하며, 루프가 실제 도구 사용(tool use)을 수행할 때는 Claude를 합리적인 기본값으로 설정합니다. 탐지기들은 모델의 제안(proposal)과 도구의 실제 실행 사이에 위치하는데, 이곳이 바로 킬 스위치가 존재해야 할 유일한 지점입니다. 즉, 모델이 결정한 후, 동작이 실행되기 전입니다.
구조화된 StopReason은 강제 종료를 사용 가능한 결과로 바꾸어 주는 역할을 합니다. done은 최종 답변을 보여줍니다. step_cap, no_progress, 그리고 stall은 부분적인 답변과 함께 "이 실행은 제한되었습니다 — 계속할까요?"라는 정직한 선택지를 제공합니다. 사용자가 몰래 36시간 동안 멈춰 있는 스피너(spinner)를 하염없이 바라보게 만드는 일은 결코 없어야 합니다.
새벽 2시의 상황이 반복되지 않도록 로그를 남기는 법
Issue #44726의 사용자가 결제 내역을 보고서야 문제를 알게 된 이유는 아무도 궤적(trajectory)을 모니터링하고 있지 않았기 때문입니다. 세션당 다음 네 가지 필드를 활용하면 더 나은 대응이 가능합니다:
session_idstop_reason.kind및stop_reason.detailsteps,input_tokens,output_tokens,wall_seconds- 도구별:
call_count, 그리고 탐지기가 작동했을 때의repeated_action플래그
시간 경과에 따른 중단 사유 (stop-reason)의 혼합 비율을 차트로 나타내세요. done이 지배적이어야 합니다. no_progress 또는 stall의 비중이 높아지는 것은 모델이 특정 유형의 작업에서 막히고 있다는 조기 경고입니다. 이는 재무팀에서 "이 숫자가 맞나요?"로 시작하는 메시지를 보내기 훨씬 전부터 나타납니다.
이것이 에이전트를 더 똑똑하게 만들어주지는 않습니다. 하지만 에이전트를 중단 가능하게 (stoppable) 만들어주며, 이는 새벽 2시에 관리자 없이 실행되는 모든 시스템에 있어 훨씬 더 중요합니다. 모델이 제어 흐름 (control flow)을 소유한다면, 여러분의 역할은 오프 스위치 (off switch)를 소유하는 것입니다.
폭주하는 루프 (Runaway loops)는 단일 요청에서는 절대 나타나지 않고 오직 궤적 (trajectory)에서만 나타나는 실패 모드 중 하나입니다. 이것이 바로 이미 구축해 놓은 테스트 피라미드를 통과해 버리는 이유입니다. _Agents in Production_은 이러한 안전장치 (rails)를 갖춘 다단계 에이전트를 구축하고 배포하는 과정을 다루며, _Observability for LLM Applications_는 청구서를 보고 추측하는 대신 궤적을 관찰할 수 있게 해주는 트레이싱 (tracing) 및 평가 (evals)를 다룹니다. 이 두 권은 합쳐져 _The AI Engineer's Library_를 구성하며, 이 포스트는 두 책이 공통적으로 주장하는 내용의 일부입니다: 여러분은 언어 모델이 저자인 시스템의 운영자라는 사실입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기