Published on

AutoGPT 툴 호출 폭주? Rate Limit·큐로 안정화

Authors

AutoGPT 류의 에이전트(LLM + Tool/Function Calling)는 “생각-행동-관찰” 루프를 빠르게 반복합니다. 문제는 이 루프가 외부 툴 호출을 눈덩이처럼 증폭시키기 쉽다는 점입니다. 검색 API, 크롤러, DB, 결제/주문, 사내 마이크로서비스, 심지어 같은 툴을 같은 파라미터로 재호출하며 트래픽을 폭주시키기도 합니다.

이 글에서는 툴 호출 폭주(tool call storm) 를 시스템적으로 제어하는 방법을 다룹니다. 핵심은 두 가지입니다.

  • Rate Limit(속도 제한) 으로 “지금 당장”의 폭주를 막고
  • 큐(Queue) 로 “처리량”을 평탄화하며
  • 추가로 멱등성/중복 제거 로 “같은 일을 여러 번” 하지 않게 만드는 것입니다.

에이전트 기반 백엔드를 운영한다면, 아래 패턴을 조합해 장애를 크게 줄일 수 있습니다.

왜 AutoGPT는 툴 호출이 폭주하는가

1) 계획이 아니라 탐색(Exploration)이 기본값

에이전트는 목표를 달성하기 위해 여러 가설을 세우고 검증합니다. 이 과정이 곧 툴 호출의 반복입니다. 특히 웹 검색/브라우징/스크래핑 툴은 호출 비용이 낮아 보이지만, 네트워크·레이트리밋·파싱 실패가 겹치면 재시도가 연쇄적으로 일어납니다.

2) 관찰(Observation) 품질이 낮으면 재호출이 증가

툴 응답이 불완전하거나, 에이전트가 “성공/실패”를 판단하기 어렵다면 같은 툴을 다시 부릅니다.

  • 응답 스키마 불명확
  • 에러 코드/원인 누락
  • 타임아웃인데 성공으로 오인
  • 부분 성공(예: 10개 중 3개만 처리)인데 전체 실패로 인식

3) 동시성 제어가 없으면 한 번에 터진다

여러 세션/사용자/워크플로가 동시에 돌면, 각자가 “최적”이라고 생각하는 속도로 툴을 호출합니다. 결과적으로 시스템 전체는 순간적으로 임계점을 넘습니다.

증상: 무엇이 망가지나

  • API 제공자 429(Too Many Requests), 5xx 증가
  • 내부 서비스 CPU/DB 커넥션 고갈
  • 툴 호출 비용 폭증(특히 유료 API)
  • 에이전트 품질 저하(실패 관찰이 누적되어 더 많은 재시도)
  • 장애 시 재시도 폭풍 으로 복구가 더 늦어짐

에이전트 호출이 OpenAI 계열 API로 이어진다면, 파라미터/페이로드 문제로 400이 섞여 들어오며 재시도가 악화되기도 합니다. 입력 검증 체크리스트는 OpenAI Responses API 400 에러 원인 8가지도 함께 참고하면 좋습니다.

1단계: Rate Limit로 “즉시” 폭주를 막기

Rate Limit는 크게 2가지 축이 있습니다.

  • Per-user / Per-agent: 특정 사용자(또는 에이전트 인스턴스)가 폭주시키는 것을 제한
  • Global: 시스템 전체 툴 호출량을 제한(가장 안전장치로 중요)

토큰 버킷(Token Bucket) 또는 리키 버킷(Leaky Bucket)

가장 운영 친화적인 방식은 토큰 버킷입니다.

  • 버킷에 토큰이 일정 속도로 채워짐
  • 요청 1건당 토큰 1개(또는 가중치) 소모
  • 토큰이 없으면 대기하거나 거절

Node.js(Express) + Redis 기반 간단 예시

아래 예시는 “툴 호출 엔드포인트” 앞에서 글로벌 레이트를 제한합니다.

// rateLimit.ts
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

// 10초 윈도우에 50회 제한(글로벌)
const WINDOW_SEC = 10;
const LIMIT = 50;

export async function globalRateLimit(key: string) {
  const now = Math.floor(Date.now() / 1000);
  const windowKey = `rl:${key}:${now - (now % WINDOW_SEC)}`;

  const count = await redis.incr(windowKey);
  if (count === 1) {
    await redis.expire(windowKey, WINDOW_SEC + 1);
  }

  if (count > LIMIT) {
    const err = new Error("rate_limited");
    // MDX 빌드 에러 방지: 비교 연산 기호는 코드 블록 안이므로 안전
    (err as any).statusCode = 429;
    throw err;
  }
}
// toolsRoute.ts
import express from "express";
import { globalRateLimit } from "./rateLimit";

const router = express.Router();

router.post("/tool/execute", async (req, res) => {
  try {
    await globalRateLimit("tool-exec");
    // 실제 툴 실행 로직
    res.json({ ok: true });
  } catch (e: any) {
    const status = e.statusCode || 500;
    res.status(status).json({ ok: false, error: e.message });
  }
});

export default router;

이 방식은 간단하지만, 레이트 초과 시 요청을 바로 실패시키므로 에이전트가 이를 재시도하며 더 시끄러워질 수 있습니다. 따라서 다음 단계(큐)와 함께 쓰는 게 보통 더 안정적입니다.

“거절” 대신 “대기”로 바꾸기

레이트 초과 시 429를 반환하는 대신, 서버 내부에서 짧게 대기시키거나(최대 대기 시간 제한) 큐로 넘기면 에이전트의 재시도 폭풍을 줄일 수 있습니다.

2단계: 큐로 처리량을 평탄화(Backpressure)

에이전트 트래픽은 스파이크가 큽니다. 큐는 스파이크를 흡수하고, 워커가 일정한 속도로 처리하게 만들어 시스템을 예측 가능하게 합니다.

큐 설계 체크리스트

  • 작업 단위(Job): 툴 호출 1건을 Job으로 모델링
  • 동시성(Concurrency): 워커에서 동시에 처리할 Job 수 제한
  • 재시도(Retry): 네트워크 오류/429/5xx 시 지수 백오프
  • DLQ(Dead Letter Queue): 반복 실패 Job 격리
  • 우선순위(Priority): 사용자 요청 기반 vs 배치 기반 분리

BullMQ(Redis) 예시: 툴 호출을 큐로 감싸기

// queue.ts
import { Queue, Worker } from "bullmq";
import IORedis from "ioredis";

const connection = new IORedis(process.env.REDIS_URL);

export const toolQueue = new Queue("tool-exec", {
  connection,
  defaultJobOptions: {
    attempts: 5,
    backoff: { type: "exponential", delay: 500 },
    removeOnComplete: 1000,
    removeOnFail: 1000
  }
});

// 워커: 동시에 10개만 실행
export const toolWorker = new Worker(
  "tool-exec",
  async job => {
    const { toolName, payload } = job.data;

    // 여기서 실제 외부 API 호출/DB 작업 수행
    // 타임아웃을 반드시 짧게 설정하고, 실패 원인을 구조화해서 반환
    return { toolName, ok: true, result: { echo: payload } };
  },
  { connection, concurrency: 10 }
);
// api.ts
import express from "express";
import { toolQueue } from "./queue";

const app = express();
app.use(express.json());

app.post("/tool/execute", async (req, res) => {
  const { toolName, payload } = req.body;

  // 즉시 실행하지 않고 큐에 적재
  const job = await toolQueue.add("call", { toolName, payload }, {
    // 우선순위: 숫자가 낮을수록 우선
    priority: 5
  });

  res.status(202).json({
    ok: true,
    jobId: job.id,
    status: "queued"
  });
});

app.get("/tool/jobs/:id", async (req, res) => {
  const job = await toolQueue.getJob(req.params.id);
  if (!job) return res.status(404).json({ ok: false, error: "not_found" });

  const state = await job.getState();
  const result = job.returnvalue;
  const reason = job.failedReason;

  res.json({ ok: true, state, result, reason });
});

export default app;

이 구조의 장점은 명확합니다.

  • API 서버는 빠르게 202를 반환
  • 워커가 정해진 동시성으로만 실행
  • 외부 API 레이트리밋에 걸려도 워커에서 백오프로 흡수

에이전트 UX: 폴링 vs 콜백

에이전트가 결과를 기다리는 방식은 두 가지가 흔합니다.

  • 폴링: GET /tool/jobs/:id 반복 호출(폴링 주기도 레이트리밋 필요)
  • 콜백: 작업 완료 시 Webhook 또는 SSE/WebSocket으로 전달

운영 안정성을 우선하면 폴링보다 콜백이 유리하지만, 구현 난이도는 더 높습니다.

3단계: 멱등성·중복 제거로 “같은 호출”을 줄이기

큐를 붙여도 에이전트가 같은 요청을 여러 번 넣으면 비용은 계속 듭니다. 특히 결제/주문/메일 발송 같은 툴은 중복 실행이 치명적입니다.

멱등 키(Idempotency Key) 기본 패턴

  • 요청 바디를 정규화한 뒤 해시 생성
  • idempotency_key로 Redis/DB에 실행 결과를 저장
  • 동일 키가 오면 이전 결과를 반환하거나 “이미 처리 중”으로 처리
import crypto from "crypto";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

function stableStringify(obj: any) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

export async function withIdempotency(toolName: string, payload: any, fn: () => Promise<any>) {
  const raw = `${toolName}:${stableStringify(payload)}`;
  const key = "idem:" + crypto.createHash("sha256").update(raw).digest("hex");

  // 1) 이미 결과가 있으면 반환
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // 2) 락(중복 실행 방지)
  const lockKey = key + ":lock";
  const locked = await redis.set(lockKey, "1", "NX", "EX", 30);
  if (!locked) {
    throw new Error("duplicate_in_flight");
  }

  try {
    const result = await fn();
    await redis.set(key, JSON.stringify(result), "EX", 60);
    return result;
  } finally {
    await redis.del(lockKey);
  }
}

이벤트 기반 시스템이라면 Outbox/사가도 고려

툴 호출이 “외부 시스템 상태 변경”을 동반한다면 단순 멱등성만으로는 부족합니다. 예를 들어 “주문 생성 후 결제 요청 후 배송 등록” 같은 흐름은 부분 실패 시 보상 트랜잭션이 필요합니다.

이때는 Outbox + 사가 패턴이 실전적인 선택입니다. 중복 처리와 일관성 문제는 Kafka EOI 중복처리 해결 - Outbox+사가 패턴, 그리고 사가 구현 관점은 MSA 사가(Saga) 패턴 구현으로 중복결제 방지하기에서 더 깊게 다룹니다.

4단계: 툴 자체를 “에이전트 친화적”으로 만들기

레이트리밋/큐는 인프라 레벨 처방이고, 툴 설계는 애플리케이션 레벨 처방입니다. 둘 다 해야 재발이 줄어듭니다.

1) 응답 스키마에 “판단 가능한 상태”를 넣기

에이전트가 재호출하지 않게 하려면, 응답이 명확해야 합니다.

  • status: success, retryable_error, fatal_error
  • error_code: timeout, rate_limited, invalid_input
  • retry_after_ms: 재시도 권장 시간
  • partial: 부분 성공 여부

2) 툴 타임아웃을 짧게, 실패를 빠르게

에이전트 루프에서는 “느린 성공”보다 “빠른 실패 + 재계획”이 낫습니다.

  • 외부 HTTP 호출 타임아웃을 예: 3초~10초로 제한
  • 타임아웃은 retryable_error로 분류
  • 워커 레벨에서만 재시도하고, 에이전트 레벨 재시도는 줄이기

3) 캐시를 적극적으로

검색/조회형 툴은 캐시로 호출량을 크게 줄일 수 있습니다.

  • 동일 쿼리 30초 캐시
  • 동일 URL 스크랩 결과 5분 캐시
  • 실패도 캐시(짧게)해서 실패 폭풍 방지

5단계: 운영 관측(Observability) 없으면 다시 터진다

툴 호출 폭주는 “원인”보다 “패턴”이 중요합니다. 패턴을 잡으려면 지표가 필요합니다.

필수 지표

  • 툴별 QPS, 성공률, p95/p99 지연
  • 429/5xx 비율
  • 큐 대기 시간(Enqueue 시각부터 Start까지)
  • 워커 동시 실행 수, 재시도 횟수
  • 사용자/에이전트별 호출량 상위 N

로그에 반드시 남길 필드

  • agent_run_id (에이전트 실행 단위)
  • tool_call_id (툴 호출 단위)
  • idempotency_key
  • job_id (큐 사용 시)
  • error_code, retryable 여부

이렇게 해두면 “어떤 에이전트가 어떤 툴을 왜 반복 호출했는지”를 추적할 수 있습니다.

6단계: 쿠버네티스에서의 함정: HPA로는 해결이 안 되는 경우

툴 호출 폭주가 오면 보통 HPA로 워커/서버를 늘리고 싶어집니다. 하지만 외부 API 레이트리밋이 병목이면, 스케일 아웃은 오히려 429를 더 늘립니다.

  • 워커는 늘리되, 툴별 동시성 상한을 유지
  • 큐 기반으로 “처리량”을 통제
  • 필요하면 툴 제공자별로 별도 큐/별도 워커 풀을 분리

또한 워커가 자주 죽거나 재시작하면(메모리 급증, OOM, 프로브 실패) 큐가 쌓이며 지연이 악화됩니다. 쿠버네티스 운영 이슈가 함께 보이면 EKS Pod 1분마다 재시작? livenessProbe 실패 해결 같은 체크리스트로 안정성을 먼저 확보하세요.

추천 아키텍처 조합(현업에서 가장 자주 쓰는 형태)

  1. API Gateway/서버
  • 간단한 입력 검증
  • Per-user 레이트리밋
  • 요청을 큐에 적재하고 202 반환
  1. Queue
  • 툴별 큐 분리(예: search, scrape, payments)
  • 우선순위/재시도/DLQ
  1. Worker
  • 툴별 동시성 상한
  • 외부 API 호출에 지수 백오프 + retry_after_ms 준수
  • 멱등성 키로 중복 실행 방지
  1. Storage
  • 결과 캐시(조회형)
  • 멱등성 저장소(상태 변경형)
  1. Observability
  • 툴 호출 지표/큐 지표/에이전트 런 지표

마무리: “레이트리밋 + 큐 + 멱등성”이 세트다

AutoGPT 툴 호출 폭주는 단일 기법으로 해결되지 않습니다.

  • Rate Limit는 폭주를 즉시 차단하지만, 거절이 재시도를 부를 수 있고
  • 큐는 처리량을 평탄화하지만, 중복 요청 자체를 줄이진 못하며
  • 멱등성/중복 제거는 비용과 사고를 줄이지만, 동시성 제어가 없으면 여전히 터집니다.

따라서 운영 환경에서는 이 3가지를 세트로 두고, 툴 응답 스키마와 관측 지표를 정비해 “에이전트가 덜 호출해도 되게” 만들어야 합니다. 그렇게 하면 에이전트 품질은 오히려 좋아지고, 비용과 장애는 체감될 정도로 줄어듭니다.