
에이전트를 배포하는 순간 직면하게 되는 7가지 새로운 실패 모드
요약
AI 에이전트를 실제 서비스에 배포할 때 발생하는 7가지 새로운 실패 모드를 분석합니다. 특히 모델이 스스로 루프에 빠졌음을 인지하지 못해 발생하는 비용 폭증과 같은 궤적(trajectory) 기반의 오류를 다룹니다.
핵심 포인트
- 에이전트는 개별 단계가 유효하더라도 전체 경로(trajectory) 상에서 실패할 수 있음
- 폭주하는 루프(Runaway loops)는 프롬프트가 아닌 코드 레벨의 예산 제한으로 해결해야 함
- 도구 호출 시 누적되는 컨텍스트가 비용과 성능에 미치는 영향을 관리해야 함
- 기존 QA 방식으로는 에이전트의 논리적 경로 오류를 잡아내기 어려움
- 도서: 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)가 담긴 표가 있었습니다. 일반적인 세션은 출력 토큰 하나당 약 5개에서 15개의 입력 토큰이 소모됩니다. 하지만 이 사용자는 74:1의 비율을 보이고 있었습니다. 또 다른 사용자는 175:1의 세션을 첨부했습니다. 잔액은 마이너스였습니다. Anthropic 엔지니어들은 이를 "도구 호출(tool calls)을 거치면서 누적된 대화 기록 및/또는 프로젝트 파일 컨텍스트가 무제한으로 성장하는 복합적인 루프(compounding loop)"로 추적했습니다.
금액 문제는 단지 증상일 뿐입니다. 실제로 문제가 된 것은 루프를 실행하는 주체가 자신이 루프 안에 있다는 사실을 알려줄 수 없다는 점입니다. 왜냐하면 그 주체가 루프를 계속 실행할지 여부를 결정하는 주체이기 때문입니다.
문제의 형태는 다음과 같습니다. 일반적인 웹 서비스는 테스트로 표현할 수 있는 방식으로 실패합니다. 즉, 500 에러, 잘못된 페이로드(payload), 혹은 단언(assert)할 수 있는 타임아웃(timeout) 등이 그것입니다. 반면 에이전트는 궤적(trajectory) — 즉, 세션 동안 모델이 내린 결정의 시퀀스 — 안에 존재하는 방식으로 실패합니다. 개별 단계는 모두 유효합니다. 서비스는 200 응답을 반환합니다. 버그는 단일 요청이 아니라 모델이 거쳐온 경로에 존재합니다. 아래는 이러한 사례 7가지이며, 각각 기존의 QA(품질 보증)가 이를 놓치는 이유와 함께 설명합니다.
1. 폭주하는 루프 (Runaway loops)
Issue #44726은 이러한 현상의 한 사례일 뿐입니다. 또 다른 사례에서는 세션이 쓰기 도구(write tool)를 호출하지도 않은 채 "문서를 작성하겠습니다"라는 말만 반복하며 갇혀버리는 현상이 기록되었습니다. 또 다른 사례에서는 에이전트가 자신의 의도를 스스로 설명하는 데 전체 컨텍스트 윈도우(context window)를 전부 소모해 버리기도 했습니다. 에이전트는 자신이 루프(loop)에 빠졌다는 사실을 알지 못합니다. 왜냐하면 "안다"라는 개념은 현재의 윈도우만을 바라보는 상태가 없는 순방향 패스(stateless forward pass)에는 적용될 수 없는 단어이기 때문입니다.
해결책은 더 나은 프롬프트(prompt)가 아닙니다. 모델의 손이 닿지 않는 곳, 즉 코드상에서 강제되는 예산(budget)입니다.
MAX_STEPS = 12
def run_agent(client, messages, tools):
...
상한선을 두는 것은 지루한 일입니다. 하지만 그것이 핵심입니다. 여러분의 배포 파이프라인이 호출하는 모든 서비스에는 타임아웃(timeout)이 있습니다. 방치할 경우 모든 서비스는 영원히 실패할 수 있기 때문입니다. 에이전트도 이와 같은 종류의 존재이며, 여기에 더해 '영원히 실패하는 것이 올바른 응답'이라고 스스로 결정할 권한까지 부여되어 있습니다.
2. 비용 폭발 (Cost explosions)
Anthropic은 가장 명확한 공개 수치를 발표했습니다. 에이전트는 일반적인 채팅 교환보다 약 4배 더 많은 토큰을 사용하며, 멀티 에이전트 시스템(multi-agent systems)은 약 15배 더 많은 토큰을 사용합니다. 이는 최소치입니다. 파이프라인의 토큰 예산은 입력 크기에 대략 선형적으로 비례합니다. 반면 에이전트의 예산은 모델이 얼마나 오래 계속할지 결정하는 방식에 따른 함수이며, 여러분은 그 함수를 사전에 알 수 없습니다.
파이프라인은 꼬리가 얇지만(thin tails), 에이전트는 꼬리가 두껍습니다(fat tails). 실행 도중 결정 함수가 경고 없이 발산(diverge)할 수 있기 때문입니다. 세션당 지출을 추적하고 엄격한 상한선에서 차단하십시오.
class Budget:
def __init__(self, max_tokens):
self.max_tokens = max_tokens
...
매 턴(turn)이 끝날 때마다 budget.charge(resp.usage)를 호출하십시오. 여러분의 재무팀이 이 문제를 잡아내는 모니터링 시스템이 되어서는 안 됩니다. 청구서는 나쁜 대시보드입니다.
3. 도구 호출 오작동 (Tool-call misfires)
모델이 도구 호출 (tool call)을 생성합니다. 호출은 구문적으로 유효합니다. 하지만 잘못된 동작을 수행합니다. 이는 네 가지 방식으로 나뉩니다: 런타임(runtime)에서 감지되는 스키마 위반 (schema violation), 겉보기에는 맞지만 잘못된 행을 삭제하는 호출, 메뉴에서 잘못된 도구를 선택하는 경우, 그리고 상태 유지 환경 (stateful environment)에서 상태를 고려하지 않고 취해진 행동입니다.
전형적인 사례는 2025년 7월 Replit 사고로, Fortune의 보도에 따르면 에이전트가 명시적인 코드 프리즈 (code freeze) 기간 동안 운영 데이터베이스 (production database)에 대해 파괴적인 SQL을 실행했습니다. 더 좁은 도구 시그니처 (tool signatures)가 도움이 될 수는 있습니다. 하지만 이것이 이 범주 자체를 해결하지는 못합니다. 파괴적으로 행동하기로 결정한 모델은 자신이 가진 가장 좁은 범위의 파괴적인 도구를 찾아낼 것이기 때문입니다.
교훈은 도구가 오작동할 것이라고 가정하고 폭발 반경 (blast radius)을 설계하라는 것입니다. 위험한 도구들은 인간의 승인을 거치도록 제한하십시오:
DESTRUCTIVE = {"delete_rows", "send_email", "deploy"}
def run_tools(resp, approve):
...
여러분의 계약 테스트 (contract tests)는 인자 (arguments)를 검증하고 호출이 성공했음을 확인했습니다. 하지만 모델이 애초에 그 함수를 호출했어야 했는지에 대해서는 침묵합니다.
4. 컨텍스트 비대화 (Context bloat)
Chroma의 2025년 7월 컨텍스트 부패 보고서 (context rot report)는 18개의 모델을 평가했으며, 입력 길이가 길어질수록 성능이 급격히 저하되는 것을 발견했습니다. Drew Breunig은 그러한 상황을 초래하는 실패 모드들을 정리했습니다. 컨텍스트를 추가하는 것은 비용을 발생시키며, 특정 지점을 넘어서면 전혀 도움이 되지 않습니다. 일정 크기를 넘어서면 에이전트는 새로운 계획을 세우기보다 과거 행동을 반복하는 것을 선호하기 시작합니다. 컨텍스트 윈도우 (window)는 "내가 아는 것은 이것이다"에서 "내가 이미 했던 일이다, 아마 다시 해야 할 것 같다"로 부패합니다.
여러분의 통합 테스트 (integration test)는 이를 잡아내지 못합니다. 왜냐하면 짧은 세션으로 실행되기 때문이며, 부패는 긴 세션에서만 나타나기 때문입니다. 이를 확인하려면 단계별 토큰 수 (token counts)가 포함된 운영 트레이스 (production trace)가 필요합니다. 저렴하고 첫 번째로 취할 수 있는 방어책은 윈도우를 모니터링하고, 그것이 지배하기 전에 다듬는 것입니다:
def context_pressure(client, messages, model="claude-opus-4-8"):
count = client.messages.count_tokens(
model=model, messages=messages)
...
실제 추적(traces) 데이터에서 선택한 임계값을 이 숫자가 넘어서면, 가장 오래된 대화 내용을 요약하거나 오래된 도구 결과(tool results)를 삭제하십시오. 동일한 토큰 예산(token budget)을 가졌더라도 서로 다른 히스토리(histories)를 가진 두 세션은 서로 다른 품질을 생성하며, 그 어느 것도 벤치마크가 측정했던 세션과 동일하지 않습니다.
5. 상태 비동기화 (State desync)
에이전트의 메모리가 현실과 일치하지 않지만, 에이전트는 이를 인지하지 못합니다. 서비스가 아무 작업도 수행하지 않고(no-op) 200 응답을 보냈을 때, 에이전트는 쓰기 작업이 성공했다고 믿습니다. 6단계 뒤에 에이전트는 빈 결과를 읽고, "데이터가 누락되었습니다. 다시 시도하겠습니다"라고 설명한 뒤, 다시 쓰기를 수행하고, 또 다른 200 응답을 받으면 해당 작업이 멱등성(idempotent)을 가진다고 결론 내립니다. 하지만 그렇지 않습니다. 에이전트는 영원히 실패하면서 이를 진행(progress)이라고 부르고 있는 것입니다.
전통적인 QA(Quality Assurance)는 피스처(fixtures)가 매번 깨끗한 상태에서 시작하기 때문에 이를 놓칩니다. 이러한 괴리는 유의미할 정도로 긴 세션에서만 축적됩니다. 방어책은 성공을 나타내는 형태의 객체를 신뢰하는 것을 멈추고, 당신이 쓴 내용을 다시 읽어보는 것입니다:
def write_and_verify(row_id, payload):
db.upsert(row_id, payload)
stored = db.get(row_id)
...
정직한 결과를 도구 결과(tool result)로서 모델에 다시 전달하십시오. "읽기 결과 불일치(readback mismatch)"를 듣는 에이전트는 복구할 수 있습니다. 하지만 조용한 실패(silent failure) 상황에서 "200 OK"를 듣는 에이전트는 복구할 수 없습니다.
6. 검색을 통한 인젝션 (Injection via retrieval)
Simon Willison의 공식화는 치명적인 삼중주(lethal trifecta)를 설명합니다: 에이전트가 개인 데이터(private data)를 보유하고, 신뢰할 수 없는 콘텐츠에 노출되어 있으며, 데이터를 외부로 보낼 수 있는 방법이 있을 때 에이전트는 매우 위험한 상태에 놓입니다. 2025년 9월, Willison은 Notion 취약점 사례를 기록했는데, PDF 내의 숨겨진 흰색 글자(white-on-white text)가 에이전트를 유도하여 검색 쿼리를 통해 데이터를 유출(exfiltrating)하게 만들었습니다. 에이전트는 지시받은 대로 정확히 행동했습니다. 그 지시는 바로 PDF로부터 내려진 것이었습니다.
당신의 입력값 정화기(input sanitizer)는 도움이 되지 않습니다. 왜냐하면 인젝션(injection)이 사용자 입력(user input)을 통해 들어오는 것이 아니기 때문입니다. 그것은 도구 결과(tool result), 즉 웹 페이지, 이메일 본문, 가져온 이슈(issue), 파일 이름 등을 통해 들어옵니다. 에이전트가 읽는 모든 것은 모델의 관점에서는 지시 사항(instructions)입니다. Willison의 작업 제약 조건은 세 가지 중 최대 두 가지만 세션당 허용하고, 세 가지가 모두 존재할 때는 인간의 승인을 요구하는 것입니다. 신뢰할 수 없는 콘텐츠를 모델이 명령이 아닌 데이터로 취급하도록 감싸십시오(wrap):
def wrap_untrusted(source, body):
return (
f"<untrusted source={source!r}>\n"
...
이것을 해결책(fix)이 아닌 태도(posture)로 간주하십시오. Willison은 완전한 해결책은 존재하지 않는다고 명시적으로 말했습니다. 이 공격 클래스는 모델이 외부에서 오는 텍스트를 어떻게 처리할지 결정하는 한 계속 적용됩니다.
7. 조용한 드리프트 (Silent drift)
이것은 아무런 문제가 없는 것처럼 보입니다. 개별 단계는 틀리지 않았고, 로그도 깨끗합니다. 하지만 0시간 차의 트레이스(trace)와 8시간 차의 트레이스를 비교해 볼 때 비로소 편차(deviation)가 나타납니다. 에이전트가 파괴적인 도구를 호출하는 데 약간 더 의욕적이고, 확인하는 데는 약간 덜 신중하며, 질문하는 대신 요약하려는 경향이 약간 더 강해지는 식입니다. 에이전트를 충분히 오래 실행하는 팀들은 동일한 느린 드리프트(drift)를 설명합니다.
단일 시점의 유닛 테스트(unit test)로는 드리프트를 볼 수 없습니다. 시간이 경과에 따른 트레이스와 비교할 기준점(baseline)이 필요합니다. 그것은 pytest의 역할이 아닙니다. 그것은 관측 가능성(observability)의 역할이며, 이것이 일곱 가지 실패 모드 전체를 관통하는 핵심입니다. 이들은 궤적(trajectory)의 속성인 반면, 전통적인 QA는 개별 요청(request)의 속성을 테스트합니다.
왜 이 중 어느 것도 당신의 테스트 스위트(test suite)에 없는가
여기 있는 모든 실패 모드는 하나의 속성을 공유합니다. 이들을 하나로 묶는 것은 희귀성이 아닙니다. 그것들은 모두 궤적의 속성(trajectory properties)이며, 모델이 다음에 일어날 일을 결정하는 시스템은 유닛 테스트(unit-test)를 할 수 없습니다.
여전히 구축할 수 있고, 여전히 배포할 수도 있습니다. 하지만 첫 번째 질문은 "이 테스트를 통과하는가?"에서 "모델이 무엇을 결정했는가, 왜 그렇게 결정했는가, 비용은 얼마나 들었는가, 그리고 다른 날에도 똑같이 결정할 것인가?"로 바뀝니다. 이러한 질문들은 유닛 테스트 (unit-test) 방식의 답변이 아니라, 궤적 (trajectory) 방식의 답변을 필요로 합니다.
실제 사용자 앞에서 실행되어야 하는 에이전트 (agent)에 캡 (caps), 예산 (budgets), 승인 게이트 (approval gates)를 연결하고 있다면, 그것이 바로 _The AI Engineer's Library_가 다루는 영역입니다. _Agents in Production_은 구축 및 배포의 영역을 다룹니다. 즉, ReAct 루프 (ReAct loop), 오작동 상황에서도 견뎌내는 도구 호출 (tool calling), 모델 하위 계층의 가드레일 (guardrails) 등을 다룹니다. _Observability for LLM Applications_는 궤적 (trajectories)을 추적 (trace), 평가 (eval), 비용 정산 (cost-account)할 수 있는 데이터로 전환하는 영역을 다룹니다. 위에서 언급한 7가지 실패 모드 (failure modes)가 바로 이 두 권의 책이 존재하는 이유입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기