Alejandro Rioja.
AI Agents

AI 에이전트가 프로덕션에서 계속 실패하는 이유 (그리고 수정 방법)

Alejandro Rioja
Alejandro Rioja
5 분 읽기
TL;DR

대부분의 프로덕션 에이전트 실패는 다섯 가지 원인에서 옵니다: 엣지 케이스를 처리하지 못하는 취약한 프롬프트, 일시적 API 오류에 대한 재시도 로직 부재, 무엇이 고장났는지 볼 수 없는 관측 가능성 부재, 종료 조건 없는 폭주 루프, 그리고 모델이 잘못된 것을 선택할 만큼 모호한 도구 정의. 다섯 가지 모두 모델이나 프레임워크를 변경하지 않고 수정 가능합니다.

무료 뉴스레터

매주 수요일. 28,400명+ 구독자. 핵심만.

목차

2026년 6월 업데이트.

TL;DR: 대부분의 프로덕션 에이전트 실패는 다섯 가지 원인에서 옵니다: 엣지 케이스를 처리하지 못하는 취약한 프롬프트, 일시적 API 오류에 대한 재시도 로직 부재, 무엇이 고장났는지 볼 수 없는 관측 가능성 부재, 종료 조건 없는 폭주 루프, 그리고 모델이 잘못된 것을 선택할 만큼 모호한 도구 정의. 다섯 가지 모두 모델이나 프레임워크를 변경하지 않고 수정 가능합니다.

[운영자 관점] 프로덕션에서 30개 이상의 에이전트를 운영합니다. 이런 실패를 모두 경험했습니다. 가장 많은 시간을 소모한 것들은 신기한 문제가 아니었습니다——이미 처리했다고 생각했던 지루한 인프라 실패들이었습니다.

실패 1: 엣지 케이스 입력에서 깨지는 취약한 프롬프트

테스트 케이스에서 작동하는 프롬프트는 예상하지 못한 입력에서 실패합니다. 이것은 모델 한계가 아닙니다——명령 작성 문제입니다.

증상: 에이전트가 무의미한 출력을 생성하거나, 잘못된 도구를 호출하거나, 테스트한 것과 입력이 약간 다를 때 형식이 잘못된 JSON을 출력합니다.

근본 원인: 시스템 프롬프트가 해피 패스만 설명합니다. 데이터가 없거나, 형식이 잘못되거나, 모호할 때 모델에게 무엇을 해야 하는지 말하지 않습니다.

수정: 시스템 프롬프트에 명시적인 엣지 케이스 처리를 추가합니다:

code
If the input data is missing a required field, return:
{ "status": "error", "reason": "missing_field", "field": "<fieldname>" }
Do NOT attempt to infer or hallucinate missing values.

If you are uncertain which tool to call, call no tool and return:
{ "status": "clarification_needed", "question": "..." }

모델은 엣지 케이스에 대한 명시적 지침을 안정적으로 따릅니다. 실수는 해피 패스 지침이 지저분한 케이스를 처리하기 위해 일반화될 것이라고 가정하는 것입니다.

실패 2: 일시적 API 오류에 대한 재시도 로직 없음

에이전트가 호출하는 모든 외부 API는 언젠가 실패합니다. Claude API, Meta Graph API, 데이터베이스——모두 5xx 오류를 반환하거나, 타임아웃하거나, 속도 제한합니다. 에이전트에 재시도 로직이 없으면 하나의 일시적 오류가 전체 실행을 종료시킵니다.

증상: 에이전트 실행이 다른 단계에서 임의로 실패합니다. 로그에 503 또는 429가 표시되고 후속 시도가 없습니다.

수정: 모든 외부 호출을 지수 백오프 재시도로 래핑합니다:

typescript
async function withRetry<T>(fn: () => Promise<T>, retries = 3, baseDelayMs = 500): Promise<T> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      const isTransient = err.status === 429 || err.status >= 500 || err.code === "ECONNRESET";
      if (!isTransient || attempt === retries) throw err;
      const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * 100;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}

// Usage
const result = await withRetry(() => client.messages.create({ ... }));

지수 백오프를 포함한 세 번의 재시도는 일시적 실패의 약 99%를 처리합니다. 이것을 모든 외부 호출에 추가하면 임의 실패의 절반이 사라집니다.

실패 3: 관측 가능성 없음——무엇이 고장났는지 볼 수 없음

이것이 프로덕션에서 가장 일반적인 실패 모드이자 디버깅에 가장 많은 시간이 드는 것입니다: 에이전트가 조용히 실패하거나 잘못된 출력을 생성하는데, 체인의 어디서 잘못됐는지 알 수 없습니다.

증상: 뭔가 잘못됐다는 것은 알지만 단계를 식별할 수 없습니다. console.log 구문을 추가하고 수동으로 재실행하며 재현을 시도합니다.

수정: 모든 단계에서 구조화된 로깅, 전체 실행을 추적하는 실행 ID 포함:

typescript
function createLogger(runId: string, agentName: string) {
  return {
    step: (step: string, data: object) =>
      console.log(JSON.stringify({ runId, agent: agentName, step, ts: new Date().toISOString(), ...data })),
    error: (step: string, err: unknown) =>
      console.error(JSON.stringify({ runId, agent: agentName, step, error: String(err), ts: new Date().toISOString() })),
  };
}

const log = createLogger(crypto.randomUUID(), "newsletter-agent");
log.step("fetch_topic", { topicId: topic.id, topic: topic.name });
// ... do work ...
log.step("draft_complete", { subject: draft.subject, wordCount: draft.body.split(" ").length });

Cloudflare Workers를 사용하는 경우 이 로그는 Logpush 또는 Workers Tail로 갑니다. 로컬이나 VPS에서 실행 중이면 로그 집계기로 파이프합니다. 구조화된 JSON은 runId로 필터링하여 단일 실행에서 무슨 일이 있었는지 정확히 볼 수 있음을 의미합니다.

실패 4: 종료 조건 없는 폭주 루프

에이전트 루프——모델이 도구를 호출하고 조건이 충족될 때까지 반복하는——조건이 충족되지 않거나 모델이 잘못 식별하면 영원히 실행될 수 있습니다.

증상: 에이전트가 타임아웃 전에 수백 달러의 API 비용을 지출합니다. 또는 진전 없이 같은 도구 호출을 반복합니다.

수정: 항상 하드 반복 상한과 진행 확인을 갖습니다:

typescript
const MAX_ITERATIONS = 10;
let iterations = 0;
let lastToolCallName = "";
let sameToolCallCount = 0;

while (true) {
  iterations++;
  if (iterations > MAX_ITERATIONS) {
    log.error("loop", { reason: "exceeded_max_iterations" });
    break;
  }

  const response = await client.messages.create({ ... });

  // Detect stuck loops: same tool called 3x in a row
  const toolCall = response.content.find(b => b.type === "tool_use");
  if (toolCall?.name === lastToolCallName) {
    sameToolCallCount++;
    if (sameToolCallCount >= 3) {
      log.error("loop", { reason: "stuck_loop", tool: toolCall.name });
      break;
    }
  } else {
    sameToolCallCount = 0;
    lastToolCallName = toolCall?.name ?? "";
  }

  if (response.stop_reason === "end_turn") break;
}

이것은 “너무 오래 실행됨”과 “제자리에서 맴돔” 두 가지 실패 모드를 모두 포착합니다. 상한은 해피 패스에는 충분히 너그럽지만 폭발 반경을 제한할 만큼 타이트해야 합니다.

실패 5: 모델이 잘못 해결하는 모호한 도구 정의

모델에게 설명이 겹치는 두 가지 도구를 주면 때때로 잘못된 것을 호출합니다. 이것은 search_databaseget_record 또는 send_emailcreate_draft 같은 도구에서 특히 일반적입니다.

증상: 모델이 올바른 카테고리의 도구를 호출하지만 잘못된 특정 도구를 선택합니다. 또는 잘못된 컨텍스트에서 도구를 호출합니다 (읽기만 적절할 때 쓰기 도구 사용).

수정: 도구 설명을 상호 배타적으로 만들고 명시적으로 “언제 사용하지 말아야 하는지”를 추가합니다:

typescript
const tools = [
  {
    name: "get_subscriber",
    description: "Fetch a single subscriber record by email. Use ONLY when you have a specific email address. Do NOT use for searching or listing subscribers.",
    input_schema: { ... }
  },
  {
    name: "search_subscribers",
    description: "Search subscribers by tag, segment, or status. Use when you need to find subscribers matching a criteria — NOT when you have a specific email address.",
    input_schema: { ... }
  }
];

“X일 때 사용하지 말아야 한다” 조항은 대부분의 사람들이 건너뛰는 부분입니다. 이것이 가장 중요한 부분입니다. 모델은 긍정적인 설명에서 추론하는 것보다 명시적인 부정적 제약을 따르는 것을 더 잘합니다.

한 가지 더: 나쁜 입력으로 에이전트 테스트하기

대부분의 에이전트는 깨끗한 해피 패스 입력에서만 테스트됩니다. 프로덕션에는 지저분한 입력이 있습니다: 빈 문자열, null 필드, 유니코드 엣지 케이스, 200을 반환하지만 예상치 못한 스키마를 가진 API 응답.

다음을 명시적으로 테스트하는 테스트 스위트를 추가합니다:

에이전트가 이 중 어느 것에서 깨지면 라이브 전에 수정하세요. 프로덕션 환경이 당신이 한 모든 가정을 찾아낼 것입니다.

운영자의 최종 결론

프로덕션에서의 대부분의 에이전트 실패는 모델 문제로 위장한 인프라 문제입니다. 모델을 전환하기 전에 프롬프트에 재시도, 구조화된 로깅, 루프 상한, 명시적인 엣지 케이스 처리를 추가하세요. 모호한 도구 정의를 수정하세요. 그런 다음 나쁜 입력으로 테스트하세요. 모델을 탓하기 전에 이 모든 것을 하세요——내 경험상, 모델은 보통 마지막으로 변경이 필요한 것입니다.

계속 읽기

AI 플레이북을 받아보세요

매주 수요일. 28,400명+ 구독자. 핵심만.

↵ 전체 결과 보기 esc esc 닫기