CodeGym /행동 /ChatGPT Apps /오류, 멱등성과 '안전한' 도구 설계

오류, 멱등성과 '안전한' 도구 설계

ChatGPT Apps
레벨 4 , 레슨 4
사용 가능

1. ChatGPT App에서의 오류와 멱등성

전통적인 웹에서는 여전히 “사용자가 버튼을 클릭 → 하나의 HTTP 요청 → 하나의 응답”이라는 패러다임에 머무르는 경우가 많습니다. LLM 세계에서는 이미 오래전부터 그렇지 않습니다. 모델은 여러분의 도구를 여러 번 호출하기로 결정할 수도 있고, 사용자가 Regenerate를 누른 뒤 응답을 다시 생성할 수도 있으며, 되묻거나 경로 중간에서 네트워크 오류를 만날 수도 있습니다. 결과적으로 동일한 도구가 매우 비슷한 인자로 두세 번 호출되는 일이 충분히 일어납니다.

이때 모든 오류에는 갑자기 두 명의 소비자가 생깁니다. 한쪽에는 모델이 있습니다. 모델은 무엇이 잘못되었는지 기계가 읽기 쉬운 설명을 필요로 하며, 그걸 바탕으로 인자를 고쳐 다시 시도할 수 있어야 합니다. 다른 한쪽에는 사용자 UI(위젯과 채팅 자체)가 있습니다. 여기서는 사람에게 읽기 쉬운 메시지를 보여주고, “Error: 500 (see logs)” 대신 다음 행동을 제안해야 합니다.

또 하나 중요한 점: 전통적인 아키텍처는 누군가가 대량으로 “답변 다시 생성”을 눌러 재시도 수를 늘릴 거라고 거의 가정하지 않습니다. ChatGPT에서는 이 시나리오가 기본입니다. 게다가 플랫폼이 일시적인 네트워크 문제 시 자동으로 재호출을 수행할 수도 있습니다. 따라서 이 생태계에서 멱등성 개념은 선택이 아니라 기본 요구사항이며, 특히 실제로 무언가를 “진짜로” 수행하는 도구(주문 생성, 결제, 이메일 발송 등)에서 더욱 그렇습니다.

이 강의의 목적은 하나의 실패한 도구 호출(tool call) 때문에 사용자의 기분과 여러분의 프로덕션을 망치지 않도록 하는 방법을 설명하는 것입니다.

Insight

ChatGPT는 여러분의 함수에 인자를 “전달”하지 않고, 오히려 스키마에 맞춰 추정하여 인자 집합을 만듭니다. JSON Schema, 대화 컨텍스트를 보고 통계적으로 값을 고르며 — 꽤 자주 빗나갑니다. "잘못된 타입", "필수 필드를 누락", "모순되는 파라미터" 같은 오류는 tool call의 일상이지, 예외 상황이 아닙니다. 공개 데이터와 텔레메트리 기준으로 복잡한 스키마에서는 이런 빗나감이 최대 약 ~30%의 호출을 차지하기도 합니다.

모델에게는 문제가 아닙니다. 여러분의 응답을 “인자가 나빴다”는 신호로 받아들이고, 입력을 조금씩 바꿔서 두세 번 연달아 다시 시도할 수 있습니다. 그러나 여러분에게 의미하는 바는 다릅니다. 모든 도구는 거의 보장된 듯 여러 번, 매우 비슷한 파라미터로 호출될 것처럼 설계해야 합니다.

그래서 멱등성이 매우 중요합니다. ChatGPT는 어떤 파라미터로 함수를 호출해야 할지 계속해서 추정할 것입니다. 호출당 2~3회 시도는 흔한 일입니다.

2. 안전한 위젯 설정: text/html+skybridge_meta

서버 측 주제(오류, 재시도, 멱등성)로 들어가기 전에, Apps SDK 특유의 UI 보안 포인트 하나를 정리합시다. 즉, 위젯이 채팅 안에서 “인터넷의 무서운 페이지”처럼 보이지 않고 안전하게 렌더링되도록 만드는 방법입니다.

registerResource와 MIME 타입 text/html+skybridge

ChatGPT 관점에서 여러분의 위젯은 사용자의 브라우저가 아닌 ChatGPT 클라이언트의 샌드박스로 들어가는 특수한 HTML 리소스입니다. 플랫폼이 이것을 단순 HTML이 아닌 위젯으로 인식하도록 하는 신호가 바로 MIME 타입 text/html+skybridge입니다.

MCP/서버 레벨에서는 대략 다음과 같이 리소스를 등록합니다(의사 TS):

// MCP 서버 설정 어딘가에서
registerResource({
  name: "giftgenius-widget",
  path: "/widget",
  mimeType: "text/html+skybridge", // 중요!
});

mimeType은 ChatGPT 클라이언트에게 보내는 신호입니다. “이것은 단순 HTML이 아니라, 내장 위젯용 컴포넌트 템플릿이며, 격리된 환경에서 실행해야 한다.” 만약 일반 text/html을 지정하면, 플랫폼이 원시 HTML을 보여주거나 렌더링 자체를 거부할 수 있습니다.

_meta와 보안 제어: CSP, 도메인, 테두리

그다음은 도구 또는 리소스의 응답과 함께 전달되는 메타데이터인 _meta입니다. 이를 통해 위젯이 불러올 수 있는 외부 리소스, 시각적 동작, 심지어 모델이 위젯을 어떻게 설명할지도 제어할 수 있습니다.

전형적인 구조 예:

const toolResult = {
  content: "<!-- 위젯 HTML -->",
  _meta: {
    "openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
    "openai/widgetDomain": "https://chatgpt.com",
    "openai/widgetPrefersBorder": true,
    "openai/widgetDescription": "GiftGenius는 선물 추천을 카드 형태로 보여줍니다."
  }
};

주요 필드를 살펴봅시다.

  • openai/widgetCSP는 위젯의 Content Security Policy를 설정합니다. 이는 ChatGPT 내부 브라우저를 위한 작은 파이어월과도 같습니다. 스크립트, 스타일, 이미지, XHR 등을 어디에서 불러올 수 있는지 명시합니다. 플랫폼은 와일드카드 * 없이 엄격한 정책을 기대하며, 사용되는 도메인(채팅, 자체 API, CDN 등)을 명시적으로 지정해야 합니다.
  • openai/widgetDomain은 위젯이 동작할 오리진을 지정합니다. 보통 ChatGPT 도메인이며, 이를 여러분의 사이트로 바꾸는 것이 아니라 격리된 환경에서 어떻게 보일지를 알리는 것입니다.
  • openai/widgetPrefersBorder는 순수한 시각적 플래그입니다. 위젯 주위에 테두리를 그릴지 여부를 나타냅니다. GiftGenius의 경우 추천 블록을 일반 채팅 메시지와 시각적으로 구분하기 위해 테두리를 두는 것이 합리적입니다.
  • openai/widgetDescription은 모델을 위한 텍스트 설명입니다. 모델이 임의로 설명을 만들어내기보다, 현재 어떤 인터페이스가 열렸는지 사용자에게 설명할 때 이 문자열을 사용할 수 있습니다. 이는 모델의 과도하거나 이상한 코멘트 위험을 줄입니다.

실용적 결론: 한 번 mimeType_meta를 신중히 설정해 두면, 허용된 범위 밖으로 벗어나지 않으며 사용자와 플랫폼 모두의 관점에서 예측 가능한 안전한 격리 UI를 얻게 됩니다. 프런트엔드 보안 측면은 이렇게 정리됩니다. 위젯은 샌드박스 안에서 여러분이 허용한 곳으로만 통신합니다. 이제 서버 측에 집중해 봅시다 — 오류의 유형, 오류 기술/반환 방법, 그리고 도구를 멱등적으로 만드는 방법입니다.

Insight: 위젯 캐싱

ChatGPT는 앱 등록 시점의 위젯 HTML을 캐시합니다. ChatGPT의 HTML 위젯은 “살아 있는 프런트엔드”가 아니라 빌드 아티팩트로 고정됩니다. 앱을 게시(스토어 또는 Dev Mode)하면, 플랫폼이 HTML 리소스(text/html+skybridge)를 읽어 들이고 이후에는 항상 그 버전을 사용합니다. 한 줄의 텍스트나 카드의 들여쓰기 같은 아주 작은 변경도 사실상 새로운 릴리스를 의미합니다.

따라서 결론은 이렇습니다. HTML 구조, 슬롯, data-* 속성, structuredContentDOM 계약을 수정하는 일은 “빠른 핫픽스”가 아니라 온전한 프런트엔드 마이그레이션입니다. 오늘 items[]로 리스트를 렌더링하다가 내일 results[]로 바꾼다면, 오래된 위젯은 이를 알지 못합니다. 계속 이전 JSON을 받고 오동작하게 됩니다.

3. 도구 동작에서의 오류 유형

이제 본론으로 들어가서: 도구에 어떤 오류가 있으며, UX와 백엔드 관점에서 어떻게 다른지 살펴봅시다. 오류를 네 개의 층으로 생각하면 편합니다.

입력 검증 오류

가장 기초적인 레벨 — 입력 인자가 계약에 전혀 맞지 않는 경우입니다.

학습용 앱 GiftGenius와 그 도구 suggest_gifts(관심사와 예산으로 선물을 추천)의 예:

  • 나이가 0보다 작거나 120보다 큼;
  • 예산이 음수;
  • 필수 필드 relationship_type이 없음;
  • budget_min > budget_max.

스키마에 맞지 않는 단순한 JSON도 여기에 해당합니다. 이상적으로는 Apps SDK와 JSON Schema가 “아주 나쁜” 호출을 여러분의 코드 이전 단계에서 거를 수 있지만, budget_min/budget_max의 관계 같은 비즈니스 검증은 직접 해야 합니다.

비즈니스 로직 오류

입력은 겉보기엔 올바르지만, 도메인 규칙상 정상 결과를 낼 수 없는 경우입니다.

전형적 시나리오:

  • 주어진 관심사와 예산으로는 선물을 하나도 찾지 못함;
  • 사용자가 일일 추천 제한을 초과함;
  • 모델이 구매하라고 요청한 상품이 더 이상 판매되지 않음.

이는 “서버가 고장”난 것이 아니라 정상적이고 예상 가능한 상황입니다. 500 Internal Server Error가 아니라, 사용자와 모델이 이해할 수 있는 형태로 표현해야 합니다.

외부 인프라 오류

여기부터는 “기술적 지옥”입니다. 데이터베이스가 응답하지 않거나, 외부 API가 타임아웃이 나거나, 코드 내부에서 처리되지 않은 예외가 터지는 경우입니다.

예:

  • 선물 카탈로그 요청이 503을 반환하거나 응답하지 않음;
  • MongoDB가 갑자기 멈춤;
  • 선물 필터링 코드에서 0으로 나눔.

UX 관점에서는 “서비스가 일시적으로 이용 불가하니 나중에 시도하라”고 안내하거나, 때로는 보이지 않는 재시도를 시도하는 것이 적절합니다. 다만 조용히 실패하거나 원시 stack trace를 사용자에게 보여주는 일은 피해야 합니다.

플랫폼/네트워크 오류

마지막으로, 여러분의 코드 바깥에서 일어날 수 있는 층입니다. tool call이 도달하지 못했거나, 응답 도중 연결이 끊겼거나, 스트리밍 시나리오가 중단되는 경우입니다. 생각보다 자주 발생합니다. 예를 들어 무료 터널을 사용하면 피크 시간대 속도가 떨어져 ChatGPT tool call이 타임아웃으로 실패할 수 있습니다.

이를 완전히 제어할 수는 없지만, 재호출과 중단이 시스템을 혼란으로 몰아넣지 않도록 도구와 위젯을 설계할 수는 있습니다. 그래서 try/catch만으로 끝내지 않고, 멱등성과 신중한 오류 처리를 강조하는 것입니다.

4. 오류를 모델과 UI 모두에 이해되게 설계하고 반환하기

중요한 관점 전환: 오류는 console.error에 로깅한 것으로 끝나지 않습니다. 도구 계약의 일부이며, 모델과 UI가 함께 다루게 됩니다.

오류 구조

보통은 다음과 같은 단순 구조를 따르기 좋습니다.

type ToolError = {
  code: string;        // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
  message: string;     // 사람이 읽기 쉬운 메시지 혹은 모델을 위한 간결한 메시지
  retryable: boolean;  // 다시 시도할 가치가 있는지
};

그리고 도구의 결과는 판별 가능한 유니온으로 감싸면 편합니다.

type SuggestGiftsResult =
  | { ok: true; gifts: GiftCard[] }
  | { ok: false; error: ToolError };

MCP 프로토콜에는 “이것은 오류”라는 별도 플래그가 있지만, 내부적으로는 자체 포맷을 유지하는 것이 유용합니다. 그래야 UI와 모델이 동일하게 해석할 수 있습니다.

'fail gracefully' 전략

모든 불편한 상황을 “강한” 오류로 만들 필요는 없습니다. 때로는 오류 대신 빈 결과를 돌려주고 간단한 설명을 제공하는 편이 훨씬 유용합니다.

예를 들어 선물을 찾지 못했다면, ok: true, 빈 배열 gifts: []을 반환하고 UI와 모델을 위한 noResultsReason 같은 필드를 함께 넣는 편이 "NO_RESULTS" 오류를 주는 것보다 낫습니다. 그러면 모델은 이렇게 대화를 이어갈 수 있습니다. “이 예산에서는 찾지 못했습니다. 예산을 올리거나 관심사를 더 구체화할까요?”

반면 외부 API가 완전히 다운됐다면, ok: false, code: "UPSTREAM_UNAVAILABLE", retryable: true가 적절합니다. 모델이 나중에 혹은 다른 파라미터로 다시 시도할 수 있도록요.

섹션 3에서 본 바와 같이 오류에는 네 가지 층이 있습니다. 검증 오류는 보통 ok: false, retryable: false로 처리합니다 — 동일 인자로 재시도할 필요가 없으니까요. “아무것도 찾지 못함” 같은 비즈니스 상황은 대개 ok: true에 빈 결과와 설명을 함께 주는 편이 낫습니다. 외부 서비스의 인프라 장애는 ok: false, retryable: true로 처리해 모델이 안전하게 다시 시도할 수 있게 합니다. 플랫폼/네트워크 오류는 여러분의 코드 이전이나 이후에도 발생할 수 있고, 실무에서는 종종 도구의 재호출로 나타납니다 — 그래서 멱등성이 더욱 중요합니다. 이어서 설명합니다.

내부 세부정보를 외부로 누출하지 않기

서버 코드에서는 error.toString()을 그대로 응답에 넣고 싶은 유혹을 느끼기 쉽습니다. 그러나 LLM 도구에서는 좋지 않습니다. 대화에 잡음이 늘고, 내부 URL, stack trace, 테이블 이름 같은 민감한 정보가 노출될 수 있습니다. 예외를 잡아 간결한 오류 코드와 정돈된 메시지로 변환하는 것을 권장합니다.

간단한 래핑 예:

try {
  const gifts = await loadGiftsFromCatalog(input);
  return { ok: true, gifts };
} catch (err) {
  console.error("suggest_gifts failed", err);
  return {
    ok: false,
    error: {
      code: "UPSTREAM_ERROR",
      message: "Catalog service is unavailable",
      retryable: true
    }
  };
}

모델은 정돈된 신호를, UI는 이해하기 쉬운 텍스트를 보며, 자세한 정보는 로그에 남습니다.

위젯에서의 오류 표시

React 위젯 관점에서는 간단합니다. ok를 확인하고, false라면 친근한 메시지와(가능하다면) 다음에 할 수 있는 행동을 제시하면 됩니다.

function GiftResults({ result }: { result: SuggestGiftsResult }) {
  if (!result.ok) {
    return (
      <div>
        <p>선물을 추천하지 못했습니다: {result.error.message}</p>
        {result.error.retryable && <p>매개변수를 바꾸거나 다시 시도해 보세요.</p>}
      </div>
    );
  }

  if (result.gifts.length === 0) {
    return <p>해당 조건에 맞는 선물을 찾지 못했습니다. 예산이나 관심사를 변경해 보세요.</p>;
  }

  return <GiftCardsList gifts={result.gifts} />;
}

이처럼 단순하고 솔직한 메시지가 “무언가 잘못되었습니다”보다 UX를 훨씬 좋게 만듭니다.

이미 합의했듯, 일부 오류는 retryable: true로 명시하고 “다시 시도”를 제안할 수 있습니다. 시스템에 이러한 재시도(UI에서의 명시적 재시도 또는 플랫폼 차원의 숨은 재시도)가 생기면 다음 질문이 따라옵니다. 동일한 데이터를 가지고 도구가 두 번 호출되면 어떻게 되는가? 이것이 바로 멱등성의 이야기입니다.

5. 멱등성: '같은 호출이 한 번 더'에 대한 보호

이제 가장 재미있는 부분입니다. 형식적으로 멱등성이란 동일한 입력으로 반복 호출해도 시스템의 상태와 결과가 변하지 않는 연산의 성질입니다. 엄밀히는 동일한 응답과 부작용의 부재 모두를 의미하지만, ChatGPT Apps 실무에서는 무엇보다도 부작용 방지가 중요합니다. 즉, 반복 호출이 데이터를 망치거나 새로운 엔티티를 만들지 않게 하는 것입니다. 응답이 조금 달라질 수는 있어도요.

ChatGPT Apps 맥락에서 멱등성은 재시도, Regenerate, 예측 불가능한 LLM 로직으로 인한 여러 현상을 막는 보호막입니다.

멱등성이 특히 중요한 곳

읽기 전용 도구는 기본적으로 안전한 경우가 많습니다. 동일 파라미터로 suggest_gifts를 몇 번 호출해도 그냥 또 하나의 선물 목록을 받을 뿐입니다. 목록이 조금 달라져도 시스템 상태는 바뀌지 않으며 부작용도 없습니다.

반대로 외부 시스템의 상태를 변경하는 도구는 매우 중요합니다.

  • 주문 생성(create_order);
  • 결제 처리(charge_card, submit_payment);
  • 이메일/알림 발송(send_email, send_sms);
  • 부작용을 동반한 엔티티 생성(예: 예약).

이런 도구가 거의 동일한 인자로 연속 두 번 호출되면, 주문 중복, 이중 결제 등 회계 악몽이 생길 수 있습니다.

idempotency_key 패턴

고전적인 접근은 도구에 추가 파라미터 idempotency_key를 넣는 것입니다. 이는 작업을 식별하는 문자열입니다. 동일 키의 요청이 이미 성공적으로 처리된 경우, 서버는 작업을 다시 수행하지 않고 저장된 결과를 반환합니다.

GiftGenius의 가상의 도구 create_checkout_session에 대한 확장 스키마 예:

const CreateCheckoutSchema = {
  type: "object",
  properties: {
    giftId: {
      type: "string",
      description: "선택한 선물의 ID"
    },
    idempotency_key: {
      type: "string",
      description: "중복을 방지하기 위한 고유 작업 키"
    }
  },
  required: ["giftId", "idempotency_key"]
} as const;

서버에서는 처리기가 대략 다음을 수행합니다.

async function createCheckoutSession(input: CreateCheckoutInput) {
  const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
  if (existing) {
    return existing; // 기존 결과를 반환
  }

  const session = await paymentProvider.createSession({ giftId: input.giftId });
  await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
  return session;
}

모델이 어떤 이유로 동일한 idempotency_key로 도구를 다시 호출하더라도, 사용자는 두 번째 결제를 받지 않고 동일한 체크아웃을 보게 됩니다.

preparecommit의 분리

특히 민감한 작업(결제, 되돌릴 수 없는 변경)에는 2단계 접근을 자주 씁니다. 준비 단계(prepare_*)와 커밋 단계(commit_*)를 별도 도구로 나눕니다.

예:

  • prepare_order — 재고 확인, 비용 계산, “주문 초안” 반환;
  • commit_order — 초안 ID로 실제 주문을 만들고 결제를 시작.

이런 설계는 여러 장점을 줍니다. 첫째, 첫 단계는 완전히 멱등적으로 만들 수 있습니다. 동일 파라미터의 반복 prepare_order는 동일한 초안을 반환합니다. 둘째, commit_order는 사용자의 명시적 확인 이후에만 허용할 수 있어, UX와 보안 모두에 유리합니다.

6. 안전한 도구 설계

멱등성은 필수지만 전부는 아닙니다. 모델에 제공하는 도구의 설계 자체가 많은 것을 좌우합니다.

최소 권한 원칙

아이디어는 단순합니다. 각 도구는 시나리오에 필요한 것만 딱 할 수 있어야 합니다. 다음과 같은 하나의 만능 함수 do_anything_with_user_account는 만들지 마세요.

  • 읽기/수정/삭제를 모두 수행할 수 있고;
  • operation 문자열과 행운을 비는 수준의 JSON payload를 받는.

대신 다음처럼 분리된, 명확히 설명된 도구를 만드세요.

  • get_user_profile;
  • update_user_preferences;
  • create_order;
  • cancel_order.

GiftGenius에도 같은 논리가 적용됩니다. suggest_gifts는 후보만 고릅니다. create_checkout_session은 주문 취소나 사용자 이메일 변경을 알지 못합니다.

읽기(read)와 쓰기(write) 도구의 분리

읽기만 하는 도구와 상태를 변경하는 도구를 명확히 나누는 것이 좋은 패턴입니다. 선물 카탈로그 조회(search_products, suggest_gifts)는 모델이 과하게 사용해도 안전합니다. 반면 create_ordercharge_payment는 더 신중히 다뤄야 합니다.

이런 도구의 설명에는 무엇을 하고 어떤 컨텍스트에서 호출 가능한지를 명시적으로 적어두는 것이 좋습니다. 예:

{
  "name": "create_checkout_session",
  "description": "단일 선물에 대한 새로운 결제 세션을 생성합니다. 사용자 승인이 명확히 확인된 이후에만 호출하세요.",
  "parameters": { /* ... */ }
}

물론 이것이 100% 보호를 보장하지는 않습니다(LLM은 여전히 실수할 수 있습니다). 하지만 최소한 위험에 대한 명확한 신호를 줍니다.

Human-in-the-loop와 확인 절차

정말 “위험한” 작업에는 확인 절차가 있는 시나리오가 유용합니다. 예를 들어 모델은:

  1. 먼저 구매 준비 데이터를 생성해 UI가 사용하기 편한 형태(선물 이름, 가격, 배송 주소)로 반환하는 도구를 호출합니다.
  2. 플랫폼은 사용자에게 “구매 확인” 버튼이 있는 위젯을 보여줍니다.
  3. 사용자가 버튼을 클릭한 뒤에야 커밋 도구가 호출되어 실제 결제를 수행합니다.

이렇게 하면 모델이 “조용히” 주문을 진행할 수 없으며, 설령 모델이 그렇게 하는 것이 아주 똑똑하다고 판단하더라도 사용자 참여 없이는 진행되지 않습니다.

설명과 주석에서의 위험 시맨틱

일부 플랫폼 버전에서는 destructiveHint 같은 특수 주석이 등장해 도구가 되돌릴 수 없는 작업을 한다는 신호를 제공합니다. 그런 필드가 없거나 아직 불안정하더라도, description과 파라미터 이름 안에 이 시맨틱을 녹여둘 수 있습니다.

예를 들어 다음 대신:

{
  "name": "delete_user_data",
  "description": "사용자 데이터를 삭제합니다."
}

다음처럼 만듭니다:

{
  "name": "request_user_data_deletion",
  "description": "서비스 정책에 따라 사용자의 개인 데이터 삭제를 위해 계정을 표시합니다. 사용자가 명시적으로 삭제를 요청한 후에만 사용하세요."
}

그리고 그 주변에 사용자 확인 UX도 함께 구축합니다.

7. GiftGenius에 대한 작은 실전 개선

이제까지의 내용을 선물 추천 앱 GiftGenius에 연결해 봅시다. GiftGenius에 create_checkout_session이라는 도구를 하나 더 추가해, 사용자가 선물을 고르는 것뿐 아니라 결제 단계로 진행할 수 있게 한다고 가정해 봅니다.

JSON Schema와 보안의 관점에서 다음을 수행합니다.

첫째, idempotency_key를 추가하고 설명을 신중히 작성합니다.

const CreateCheckoutTool = {
  name: "create_checkout_session",
  description:
    "선택한 단일 선물에 대한 결제 세션을 생성합니다. " +
    "사용자가 해당 선물을 구매하겠다고 명확히 확인한 이후에만 호출하세요.",
  parameters: {
    type: "object",
    properties: {
      gift_id: {
        type: "string",
        description: "suggest_gifts 결과에 있는 선물 식별자."
      },
      idempotency_key: {
        type: "string",
        description: "고유 작업 키. 재호출 시 동일한 키를 사용하세요."
      }
    },
    required: ["gift_id", "idempotency_key"]
  }
} as const;

둘째, 서버에서 멱등 처리기를 구현합니다.

async function handleCreateCheckout(input: CreateCheckoutInput) {
  const existing = await db.checkout.findOne({ idempotencyKey: input.idempotency_key });
  if (existing) {
    return { ok: true, checkout: existing };
  }

  const checkout = await payments.createSession({ giftId: input.gift_id });
  await db.checkout.insert({ idempotencyKey: input.idempotency_key, ...checkout });

  return { ok: true, checkout };
}

셋째, 오류를 고려합니다.

try {
  return await handleCreateCheckout(input);
} catch (err) {
  console.error("create_checkout_session failed", err);
  return {
    ok: false,
    error: {
      code: "PAYMENT_PROVIDER_ERROR",
      message: "결제 세션을 생성하지 못했습니다. 나중에 다시 시도해 주세요.",
      retryable: true
    }
  };
}

그리고 위젯에서는 이해하기 쉬운 오류 상태를 보여주고, 필요하다면 UI 레벨에서 “다시 시도” 버튼을 제공해 모델과의 새 대화를 유도합니다.

이렇게 한 걸음씩 나아가면, 우리의 귀여운 학습용 프로젝트는 “데모용 장난감”을 넘어 이론상 프로덕션에 출시할 수 있는 무언가에 가까워집니다.

8. 오류와 멱등성에서 흔한 실수

실수 №1: 오류 = 그냥 throw와 500.
모든 실패에서 tool이 예외를 던지고 그게 “무언가 잘못됨”으로 끝나면, 모델과 UI는 정보를 얻지 못합니다. 모델은 동일 인자로 재시도해야 하는지 모르고, 사용자는 다음에 무엇을 해야 할지 모릅니다. 코드, 짧은 메시지, retryable 여부를 포함한 구조화된 오류를 반환하고, 내부에서는 상세 정보를 로깅하는 편이 훨씬 낫습니다.

실수 №2: 오류 유형을 구분하지 않음.
검증, 비즈니스, 인프라 오류를 한데 섞는 것은 좋지 않습니다. 그 결과 “아무것도 찾지 못함”이 “데이터베이스 다운”과 동일하게 보입니다. 이는 UX를 해치고 모델의 적절한 반응을 어렵게 만듭니다. 요청 변경을 제안할 상황에서 “죄송, 서비스 고장” 모드로 빠질 수 있습니다. 특히 섹션 3에서 말한 비즈니스 오류와 인프라 오류를 섞을 때 더 아픕니다.

실수 №3: 재시도 세계에서 비멱등 연산.
create_order를 항상 한 번만 호출된다고 가정하고 설계하면, 사용자가 Regenerate를 자주 누르거나 연결이 중간에 끊기는 환경에서 주문 중복으로 직행합니다. 부작용이 있는 도구라면 거의 항상 idempotency_key를 추가하고 결과를 저장하여 재호출이 새 엔티티를 만들지 않도록 해야 합니다.

실수 №4: 하나의 거대한 “만능” 도구.
개발자가 action 파라미터 하나로 검색/생성/수정/삭제를 모두 하는 슈퍼 도구를 만들려는 경우가 있습니다. LLM에게는 거의 확실하게 예측 불가능한 행동을 야기합니다. 모델이 언제 무엇을 호출할지 학습하기 어려워지고, 오류의 결과도 훨씬 더 무거워집니다. 대신 가능한 한 작고 명확히 설명된 read-only 도구와, 별도로 신중히 설계된 변경(mutation) 도구(확인 절차 포함)로 나누는 것이 바람직합니다.

실수 №5: 내부 세부정보가 응답에 새어 나옴.
모델과 UI에 raw stack trace나 예외 전문을 던지는 것은 전형적인 엔지니어의 게으름입니다. 사용자에게 불친절하고, 내부 구조를 노출할 수 있으며, 모델이 수정하는 데에도 도움이 되지 않습니다. 예외를 잡아 간결한 코드와 단순한 메시지로 매핑하고, 모든 세부정보는 로그와 모니터링 시스템에 남기세요.

실수 №6: 오류를 위젯 UX와 연결하지 않음.
서버 측은 오류 코드를 깔끔히 반환하지만, UI 위젯이 영원한 spinner나 빈 블록으로 떨어지는 경우가 종종 있습니다. 사용자는 “아무 일도 일어나지 않음”을 보게 되고, 모델은 tool-call이 끝났다고 보고 평소처럼 대화를 이어갑니다. 별도의 errorempty 상태를 설계하고, 사람이 이해하기 쉬운 메시지와 가능한 행동(파라미터 변경, 나중에 다시 시도)을 제안하는 편이 훨씬 낫습니다.

실수 №7: 최소 권한 원칙 무시.
멱등성과 좋은 오류 처리를 구현했다 하더라도, execute_sql_anywhere 같은 무엇이든 가능한 도구를 노출하면 위험은 여전히 큽니다. LLM은 잘못된 컨텍스트나 잘못된 파라미터로 그것을 호출할 수 있습니다. 각 도구는 가능하면 최대한 좁고, 한 가지 명확한 작업만 수행해야 합니다 — 특히 돈이나 개인 데이터가 관련된 경우에는 더욱 그렇습니다.

1
설문조사/퀴즈
App 도구 및 <code><span class="text-user">callTool</span></code>, 레벨 4, 레슨 4
사용 불가능
App 도구 및 callTool
App 도구 및 callTool: UI ↔ 백엔드 연결
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION