AI Agent의 두 가지 치명적 장애 패턴이 있습니다. 첫째는 무한 루프 — LLM이 도구 결과를 오해해서 같은 도구를 반복 호출하는 것. 둘째는 비용 폭탄 — 루프나 긴 대화로 토큰 사용량이 예산을 초과하는 것. 일반 서비스의 Circuit Breaker처럼 AI 전용 안전장치가 필요합니다.
// 시나리오: LLM이 도구 오류를 오해할 때
// Step 1: get_order_status("ORD-X") 호출
// → {"error": "DB 타임아웃"}
// LLM 판단: "정보가 부족하다. 다시 조회해야겠다."
// Step 2: get_order_status("ORD-X") 재호출
// → {"error": "DB 타임아웃"} (여전히 오류)
// LLM 판단: "여전히 부족. 한 번 더..."
// → 무한 반복 → 수십 번 호출 → 비용 폭발
// 실제 발생하는 또 다른 패턴:
// search("결과없음") → search("더 넓게") →
// search("더더 넓게") → 루프
public class AgentRunner {
private static final int MAX_STEPS = 10;
public AgentResult run(String userMessage) {
var messages = new ArrayList<Message>();
messages.add(userMsg(userMessage));
// ① 스텝 카운터
for (int step = 0; step < MAX_STEPS; step++) {
var response = llm.call(messages, tools);
if (response.isTextOnly()) {
return AgentResult.success(
response.getText(), step);
}
for (var tu : response.getToolUses()) {
messages.add(toolResult(
tu.getId(),
executeTool(tu)));
}
messages.add(assistantMsg(response));
}
// ② MAX_STEPS 초과 시 안전 종료
log.warn("Agent loop limit reached");
return AgentResult.limitExceeded(MAX_STEPS);
}
}
@Component
public class AiBudgetCircuitBreaker {
// 사용자당 일일 토큰 예산
private static final int DAILY_TOKEN_BUDGET = 50_000;
private final RedisTemplate<String, Integer> redis;
public void checkAndRecord(
String userId, int tokensUsed) {
String key = "ai:tokens:" + userId
+ ":" + LocalDate.now();
// ① 현재 사용량 조회
Integer used = redis.opsForValue().get(key);
int current = (used == null ? 0 : used);
// ② 예산 초과 체크
if (current + tokensUsed > DAILY_TOKEN_BUDGET) {
throw new BudgetExceededException(
"일일 AI 사용량 초과. 내일 다시 시도해주세요.");
}
// ③ 사용량 기록 (TTL 25시간)
redis.opsForValue().increment(key, tokensUsed);
redis.expire(key, Duration.ofHours(25));
}
}
public class LoopDetector {
// 최근 N번의 도구 호출 기록
private final Deque<String> recentCalls =
new ArrayDeque<>(5);
public void record(String toolName, JsonNode input) {
String key = toolName + ":" + input.toString();
recentCalls.addLast(key);
if (recentCalls.size() > 5)
recentCalls.removeFirst();
}
public boolean isLooping() {
if (recentCalls.size() < 3) return false;
// 마지막 3번이 같은 호출이면 루프
List<String> last3 = recentCalls.stream()
.skip(recentCalls.size() - 3)
.toList();
return last3.stream()
.distinct()
.count() == 1; // 모두 같은 키
}
}
// Agent Loop에 통합
if (loopDetector.isLooping()) {
return AgentResult.loopDetected();
}
① max_steps (스텝 제한): LLM 호출 횟수 상한. 보통 10~20 스텝. 이를 초과하면 "처리할 수 없는 요청입니다"로 안전 종료.
② 토큰 예산 (비용 제한): 사용자/팀별 일일·월별 토큰 한도를 Redis로 추적. 초과 시 Circuit Breaker로 차단.
③ 중복 호출 감지 (루프 감지): 최근 N번 도구 호출 기록 비교. 동일 호출이 반복되면 강제 종료.
세 가지를 모두 구현해야 합니다. 하나만으론 부족합니다 — 무한 루프가 짧은 호출을 빠르게 반복하면 스텝 제한만으론 비용이 폭발합니다.
세 계층으로 방어합니다. 첫째, max_steps로 LLM 호출 횟수를 제한합니다(10~20회). 둘째, 중복 호출 감지로 동일한 도구가 같은 인자로 반복 호출되면 루프로 판단해 종료합니다. 셋째, 타임아웃으로 단일 Agent 실행 전체에 시간 제한을 겁니다(예: 60초). 이 세 가지를 모두 구현하고, 루프 발생 시 사용자에게 명확한 오류 메시지를 반환합니다.
두 가지 방어선을 씁니다. 사전 제어: 사용자별 일일 토큰 예산을 Redis에 관리하고, 각 LLM 호출 전에 예산을 확인합니다. 초과 시 Circuit Breaker가 LLM 호출 자체를 막습니다. 사후 제어: 실제 사용된 토큰 수를 응답에서 파싱해서 기록하고, 이상 탐지(갑자기 10배 증가)가 되면 알람을 보냅니다. 쿠팡 실무에서도 Redis 카운터로 API 호출 수를 선제 제어했던 것과 같은 패턴입니다.
루프 방지 = max_steps + 중복 감지 + 전체 타임아웃. 비용 제어 = 토큰 예산(Redis) + AI Circuit Breaker. 이 다섯 가지 없이 프로덕션 Agent는 없다.