에이전트가 실패하는 방식들
이 장은 함정 나열이 아니라 사고 보고서에 가깝습니다. 여기 적은 것들은 전부 실제로 프로덕션에서 벌어지는 일이고, 대부분 로컬 테스트에서는 절대 안 보입니다. 테스트할 때는 사용자가 협조적인 질문만 던지고, 도구도 항상 정상 응답을 주기 때문입니다.
무한 루프 — 5장의 max_turns가 왜 필요했는가
가장 흔한 실패입니다. 모델이 같은 도구를 계속 조금씩 다른 인자로 호출하며 "이번엔 될까"를 반복하는 패턴입니다. 검색 도구가 빈 결과를 반환했을 때 특히 잘 생깁니다 — 모델이 쿼리를 바꿔가며 계속 재시도하고, 그때마다 API 호출 하나씩 청구됩니다.
# 실제로 관찰된 패턴 (턴 1~5)
# search("서울 맛집") -> 결과 없음
# search("서울 맛집 추천") -> 결과 없음
# search("서울 맛집 리스트") -> 결과 없음
# search("서울 음식점") -> 결과 없음
# search("Seoul restaurants") -> 결과 없음max_turns가 이 문제의 최종 방어선이지만, 더 나은 건 재시도 자체를 도구 결과 메시지로 억제하는 겁니다.
def search_docs(query: str, attempt_count: int) -> str:
results = vector_search(query)
if not results and attempt_count >= 2:
return "검색 결과가 없습니다. 다른 쿼리를 시도하지 말고, 사용자에게 결과가 없다고 답하세요."
return format_results(results)모델에게 "그만 시도하라"고 명시적으로 말해 주는 게 은근히 잘 먹힙니다. 모델은 사람과 비슷하게, 명확한 종료 신호가 없으면 계속 시도하는 쪽으로 편향되어 있습니다.
존재하지 않는 도구를 부른다
시스템 프롬프트나 대화 맥락에 옛날 도구 이름이 언급되어 있으면, 모델이 실제로 등록되지 않은 도구를 호출하려 시도하는 경우가 있습니다. 흔한 원인은 프롬프트를 리팩터링하면서 도구 이름을 바꿨는데 예시나 few-shot 텍스트를 안 고친 경우입니다. 5장의 execute_tool 코드가 else: return f"오류: 알 수 없는 도구 {name}"를 반환하도록 짠 이유가 이겁니다 — 여기서 프로그램이 죽으면 사용자는 그냥 먹통이 된 화면만 보게 됩니다.
비용 폭주 — 배포 후에야 터지는 종류의 버그
가장 무서운 실패는 기능적으로는 멀쩡한데 비용만 새는 경우입니다. 예를 들어 도구가 대용량 JSON을 반환하는데, 모델이 그 결과를 요약하지 않고 다음 프롬프트에 그대로 인용하며 되묻는 패턴이 반복되면, 토큰 사용량이 턴마다 선형이 아니라 거의 제곱으로 늘어납니다. 로컬 테스트에서는 턴 수가 2~3번에서 끝나니 안 보이다가, 실제 사용자가 복잡한 질문을 던져 턴이 15번쯤 이어지면 그제서야 청구서에 찍힙니다.
flowchart TD A["턴 1: 도구 결과 2000 토큰"] --> B["턴 2: 이전 메시지 전체 + 새 결과 2000 토큰"] B --> C["턴 3: 누적 메시지 + 새 결과 2000 토큰"] C --> D["턴 10: 컨텍스트 대부분이 과거 도구 결과"]
이걸 막는 유일한 방법은 사후 모니터링이 아니라 사전 설계입니다. 7장에서 다룬 도구 응답 압축과, 턴별 토큰 사용량을 로그로 남겨 알림 임계치를 거는 것 — 이 둘을 처음부터 넣지 않으면 문제를 인지하는 시점이 항상 청구서가 나온 뒤가 됩니다.
도구 결과를 모델이 무시한다
가끔은 도구가 정확한 값을 반환했는데도 모델이 최종 답변에서 그 값을 틀리게 말하는 경우가 있습니다. 도구 결과가 컨텍스트 안에서 다른 정보에 묻혀 있거나, 도구 결과 포맷이 모델이 파싱하기 애매한 형태(예: 중첩된 JSON을 문자열로 그대로 박아 넣은 경우)일 때 자주 생깁니다. 저는 이 문제를 겪은 뒤로 도구 결과를 사람이 읽기 쉬운 짧은 문장으로 가공해서 돌려주는 쪽으로 바꿨습니다 — {"temp": 24, "condition": "clear"}보다 "맑음, 24도"가 모델 입장에서도 더 안정적으로 인용됩니다.
정답은 없다, 방어선만 있다
이 장의 패턴들에 공통점이 있다면, 어느 것도 "이렇게 하면 100% 안 생긴다"는 해법이 없다는 겁니다. 모델의 판단은 확률적이고, 아무리 프롬프트를 다듬어도 드물게는 예상 밖의 행동이 나옵니다. 그래서 실무에서는 문제를 없애려 하기보다 턴 상한, 비용 알림, 도구 결과 검증 같은 여러 겹의 방어선을 두고, 방어선을 하나 뚫었을 때 다음 방어선이 막아주길 기대하는 식으로 설계합니다.