iBetter Books
수정

프로덕션 도구 강건성

앞 절이 "언제 재시도하는가"라는 메커니즘이었다면, 이 절은 "에러를 어떻게 설계하고 시스템을 어떻게 무너지지 않게 만드는가"라는 아키텍처다. 프로덕션 에이전트의 강건성은 개별 도구의 견고함보다, 실패가 어떻게 전파되고 모델이 그 실패로부터 어떻게 회복하는지에 달려 있다. 이 절의 개념들은 시험에서 "왜 이 설계가 정답인가"를 묻는 의사결정 문제로 직결된다.

결과 수준 에러 대 프로토콜 에러 — 이 도메인의 최대 함정

가장 중요한 구분이다. 도구 실행 중 발생한 에러를 모델에게 전달하는 방식은 두 가지이고, 둘은 결과가 정반대다.

첫째, 결과 수준 에러(result-level error)다. 실패를 정상 결과의 일종으로 감싸 모델에게 되돌린다. Messages API에서는 tool_resultis_error: true를, MCP에서는 결과 객체에 isError: true를 넣는다. 이 방식에서 모델은 무엇이 실패했는지 보고, 다른 인자로 재시도하거나 대체 도구를 쓰거나 사용자에게 설명할 수 있다.

둘째, 프로토콜 에러(protocol-level error)다. 도구 함수가 예외를 던져 전송 계층의 JSON-RPC 에러로 빠져나간다. 이 경우 에러는 하네스 인프라가 처리하며, 모델은 무슨 일이 있었는지 보지 못한다. MCP 공식 문서가 명시하듯, 도구 실행 중 발생한 에러는 프로토콜 수준 에러가 아니라 결과 객체 안에서 보고되어야 한다(reported within the result object, not as MCP protocol-level errors). 그래야 모델이 에러를 보고 복구를 시도할 수 있기 때문이다.

// servers/screenshot_tool.tstry {  const result = performOperation();  return {    content: [{ type: "text", text: `Operation successful: ${result}` }],  };} catch (error) {  return {    isError: true,    content: [{ type: "text", text: `Error: ${error.message}` }],  };}

위는 MCP 공식 문서의 기본 패턴으로, 실행 실패를 예외로 던지는 대신 isError: true 결과로 감싼다. 다만 상위 SDK는 이 작업을 자동화하기도 한다. 파이썬 MCP SDK의 고수준 도구는 도구 함수가 던진 예외를 내부에서 잡아 모델이 볼 수 있는 에러 결과로 자동 변환하므로, 함수에서 그냥 예외를 던져도 결과 수준 에러가 된다.

언제 프로토콜 에러를 쓰는가. JSON-RPC 프로토콜 에러는 모델이 손쓸 수 없는 인프라 문제에 쓴다. 알 수 없는 도구 이름, 잘못된 요청 형식, 서버 내부 결함처럼 모델의 재시도가 의미 없는 경우다. 반대로 "API가 404를 냈다", "입력이 의미상 틀렸다", "타임아웃이 났다"처럼 모델이 판단·복구할 여지가 있는 실패는 결과 수준 에러로 보여 준다.

시험은 이 구분을 집요하게 묻는다. "도구가 외부 API에서 실패했을 때 올바른 처리는?"의 정답은 거의 항상 "isError를 true로 설정해 결과로 되돌려 모델이 보게 한다"이고, "예외를 던져 프로토콜 에러로 전파한다"는 오답이다. 이름의 대소문자 차이도 함정이다. MCP 결과 객체는 isError(카멜케이스), Anthropic Messages API의 tool_result 블록은 is_error(스네이크케이스)를 쓴다.

ToolError — 의도된 에러와 우연한 예외의 구분

Anthropic SDK의 도구 헬퍼를 쓸 때는 한 단계 더 세분된 구분이 있다. 도구 함수에서 일반 예외(plain exception)를 던지면 그 문자열 표현이 텍스트 에러로 모델에 전달되고 동시에 로깅된다. 반면 ToolError를 던지면 의도된 에러 응답으로 간주되어 로깅되지 않으며, 텍스트뿐 아니라 이미지 같은 구조화된 content 블록을 에러에 담을 수 있다.

# tools/validated_screenshot.pyfrom anthropic import beta_toolfrom anthropic.lib.tools import ToolError@beta_tooldef take_screenshot(url: str) -> str:    """Take a screenshot of a URL."""    if not is_valid_url(url):        raise ToolError(f"Invalid URL: {url}")    result = capture(url)    if result.error:        raise ToolError([            {"type": "text", "text": f"Failed to load page: {result.error}"},            {"type": "image", "source": {"type": "base64", "data": result.screenshot, "media_type": "image/png"}},        ])    return result.data

핵심은 의도된 에러(ToolError, 로깅 안 됨)와 예기치 못한 버그(일반 예외, 로깅됨)를 분리한다는 점이다. 입력 검증 실패처럼 "정상적으로 일어날 수 있는 에러"는 ToolError로, 코드 결함은 일반 예외로 두면, 로그가 실제 버그로만 채워져 관측이 깨끗해진다.

모델이 복구할 수 있는 에러 메시지 설계

에러를 모델에게 보여 주는 것만으로는 부족하다. 메시지가 모델의 복구를 도와야 한다. 나쁜 에러 메시지는 Error 500 또는 null처럼 행동 지침이 없다. 좋은 에러 메시지는 무엇이 왜 실패했고 모델이 다음에 무엇을 할 수 있는지를 담는다. 예를 들어 "date 인자가 'tomorrow'로 들어왔습니다. YYYY-MM-DD 형식이 필요합니다"는 모델이 즉시 인자를 고쳐 재호출하게 한다. 시험은 두 에러 메시지를 나란히 주고 "어느 쪽이 모델의 자기 복구를 돕는가"를 고르게 하거나, 모호한 메시지가 왜 에이전트 루프를 무한 반복시키는지를 묻는다. 행동 가능한(actionable) 메시지가 정답의 키워드다.

우아한 성능 저하와 폴백

도구 하나가 실패했다고 에이전트 전체가 멈추면 안 된다. 우아한 성능 저하(graceful degradation)는 일부 기능이 죽어도 시스템이 줄어든 기능으로라도 동작을 이어가게 한다. 실시간 재고 조회가 실패하면 캐시된 재고로 대답하되 "최신이 아닐 수 있다"는 단서를 붙이는 식이다. 폴백(fallback) 경로를 설계할 때는 폴백 결과임을 모델이 알 수 있게 메타데이터를 함께 주어, 모델이 신뢰도를 낮춰 응답하도록 해야 한다.

서킷 브레이커 — 실패하는 의존성을 격리하기

특정 외부 서비스가 계속 실패하는데 매 호출마다 타임아웃까지 기다리며 재시도하면, 그 지연이 누적되어 전체 시스템을 끌어내린다. 서킷 브레이커(circuit breaker)는 연속 실패가 임계치를 넘으면 회로를 "열어" 일정 시간 동안 그 의존성 호출을 즉시 차단(빠른 실패)한다. 일정 시간 뒤 "반쯤 열림" 상태로 시험 호출을 한 번 보내 성공하면 회로를 닫고 정상 복귀한다. 이는 재시도와 상호 보완 관계다. 재시도는 개별 일시 장애를 흡수하고, 서킷 브레이커는 지속 장애로부터 시스템을 보호한다. 시험은 "외부 의존성이 장시간 다운됐을 때 무한 재시도의 문제"를 제시하고 서킷 브레이커를 답으로 유도하거나, 재시도와 서킷 브레이커의 역할 차이를 구분하게 한다.

관측가능성 — 실패를 측정하지 못하면 고칠 수 없다

프로덕션 도구는 호출 횟수, 실패율, 지연 시간, 재시도 횟수, 서킷 상태를 로깅·계측해야 한다. 도구 호출에 상관관계 ID(correlation id)를 붙여 한 에이전트 턴에서 일어난 호출 사슬을 추적하면, 어느 도구가 루프를 망쳤는지 사후에 진단할 수 있다. 앞서 본 ToolError와 일반 예외의 로깅 차이가 여기서 의미를 갖는다. 의도된 에러를 로그에서 걸러 내면 진짜 버그가 묻히지 않는다. 시험은 "도구 신뢰성을 운영에서 어떻게 보장하는가"라는 시나리오에서 계측·로깅·추적의 필요성을 답으로 요구한다.

정리

  • 도구 실행 에러는 결과 수준 에러로 모델에게 보여 주는 것이 기본 정답이다. MCP는 isError, Messages API tool_result는 is_error를 쓰며, 둘은 대소문자가 다르다. 프로토콜·JSON-RPC 에러는 모델이 손쓸 수 없는 인프라 문제에만 쓴다.
  • ToolError는 의도된 에러로 로깅되지 않고 이미지 등 content 블록을 담을 수 있다. 일반 예외는 문자열로 전달되며 로깅된다. 입력 검증 실패는 ToolError, 코드 버그는 일반 예외로 분리한다.
  • 좋은 에러 메시지는 무엇이·왜 실패했고 다음에 무엇을 할지를 담아 모델의 자기 복구를 돕는다. 모호한 메시지는 무한 루프를 유발한다.
  • 우아한 성능 저하와 폴백으로 부분 실패를 흡수하고, 서킷 브레이커로 지속 장애를 격리한다. 재시도는 일시 장애를, 서킷 브레이커는 지속 장애를 다룬다.
  • 호출 수·실패율·지연·재시도·서킷 상태를 계측하고 상관관계 ID로 추적해야 신뢰성을 운영에서 보장할 수 있다.