CodeGym /행동 /ChatGPT Apps /도구 설명: JSON Schema, 타입 지정, 애너테이션

도구 설명: JSON Schema, 타입 지정, 애너테이션

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

1. 도구는 계약이다: 우리가 정확히 무엇을 기술하나

MCP 서버에 도구를 등록할 때는 작은 객체로 이를 기술합니다. TypeScript‑SDK의 단순화된 구조는 다음과 같습니다:

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    description: "수신자 프로필을 바탕으로 선물을 추천합니다.",
    inputSchema: {
      type: "object",
      // 이제 여기부터 자세히 파고듭니다
    },
  },
  async ({ input }) => {
    // 여기에 코드 작성
  }
);

모델은 async 처리기 내부({ input } => { ... })에서 무슨 일이 일어나는지 알지 못합니다. 모델이 보는 것은 단 3가지입니다:

  1. name/title — 도구의 이름.
  2. description — 언제 이 도구를 사용하는 것이 적절한지.
  3. inputSchema — 어떤 인자를 어떤 형식으로 전달해야 하는지.

이 강의에서 다루는 모든 내용은 3번 항목(그리고 _meta/annotations 같은 메타데이터 일부)에 해당합니다. 메타데이터는 뒤에서 다룹니다.

중요한 포인트: ChatGPT App 문맥에서의 JSON Schema는 지루한 유효성 검사기가 아니라, 모델을 위한 프롬프트의 일부입니다. 모델은 필드의 description을 실제로 읽고, enum이 무엇인지 이해하며, minItems, format 등을 인식합니다.

즉, 백엔드를 잘못된 데이터로부터 보호하는 것을 넘어, 여러분의 함수를 올바르게 호출하는 방법을 AI 모델에게 설명하는 것입니다.

2. 도구 suggest_gifts를 위한 기본 JSON Schema

간단한 것부터 시작해 봅시다. 다음과 같은 시나리오가 있다고 합시다:

사용자가 이렇게 말합니다:
“25살 형제에게 줄 선물 골라줘. 예산은 50–70달러이고, 비디오게임과 보드게임을 좋아해.”

도구 suggest_gifts는 대략 다음과 같은 인자를 받아야 합니다:

  • 수신자의 나이;
  • 관계 유형(형제, 동료, 파트너 등);
  • 최소 및 최대 예산;
  • 관심사 목록.

이를 Zod 없이 “가장 단순한 방식”으로, 순수 객체의 JSON Schema로 기술해 보겠습니다:

const suggestGiftsInputSchema = {
  type: "object",
  properties: {
    age: {
      type: "integer",
      minimum: 0,
      maximum: 120,
      description: "수신자의 나이(연).",
    },
    relationship: {
      type: "string",
      enum: ["friend", "partner", "sibling", "colleague", "parent"],
      description:
        "수신자와의 관계 유형: friend, partner, sibling(형제/자매), colleague, parent.",
    },
    minBudget: {
      type: "number",
      minimum: 0,
      description: "사용자 통화 기준의 최소 예산.",
    },
    maxBudget: {
      type: "number",
      minimum: 0,
      description: "사용자 통화 기준의 최대 예산.",
    },
    interests: {
      type: "array",
      items: {
        type: "string",
        description:
          "관심사를 나타내는 짧은 이름. 예: videogames, boardgames, books.",
      },
      minItems: 1,
      description: "수신자의 관심사 목록.",
    },
  },
  required: ["relationship", "maxBudget"],
};

여기서 바로 짚고 넘어가야 할 중요한 포인트 몇 가지.

첫째, 각 필드의 description입니다. 일반적인 API라면 굳이 쓰지 않아도 프런트엔드 개발자가 Swagger를 보고 이해할 수 있습니다. 하지만 여기서 “클라이언트”는 이름과 설명에서 의미를 추론하려는 모델입니다. “나이는 연 단위”, “예산은 사용자 통화 기준”, “enum은 고정된 값 집합”처럼 명확히 적을수록 런타임에서 이상한 인자를 볼 가능성이 줄어듭니다.

둘째, enum은 모델 제어에 있어 가장 강력한 도구 중 하나입니다. relationship에 아무 문자열이나 허용하면 모델은 “bro”, “girlfriend”, “bestie”, “teammate” 같은 값이나 그보다 더 창의적인 것을 내놓을 수 있습니다. 반면 enum을 지정하면 모델은 매우 높은 확률로 해당 값 중에서만 선택합니다. 이는 인자에서의 “환각”을 직접적으로 줄여 줍니다.

셋째, 모든 것을 required로 만들 필요는 없습니다. 예를 들어 age는 선택적일 수 있습니다. 사용자가 나이를 말하지 않았다면, 설명을 그렇게 작성했을 때 모델은 “대략적인 나이”를 허공에서 지어내지 않을 것입니다. 여기서부터가 바로 예술입니다. 유연성과 엄격함의 균형입니다.

이제 이 스키마를 도구 등록에 사용해 봅시다:

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    description:
      "예산, 관계 유형, 수신자 관심사에 따라 선물 아이디어를 추천합니다.",
    inputSchema: suggestGiftsInputSchema,
  },
  async ({ input }) => {
    // 여기서 input은 이미 대략 스키마를 따릅니다
    // ...
  }
);

이런 “수동” 객체는 빠른 실험에는 좋지만, 앱이 커질수록 TypeScript 타입과 쉽게 어긋나기 시작합니다. 조금 후에 이 문제를 Zod와 타입으로부터 JSON Schema를 생성하는 방식으로 어떻게 해결할지 보겠습니다.

3. 프롬프트로서의 JSON Schema: 모델이 덜 힘들도록 description 쓰는 법

형식적으로 JSON Schema는 유효성 검사에 관한 것입니다. 하지만 LLM 세계에서는 구조화된 프롬프트이기도 합니다. 실용적인 규칙 몇 가지:

  1. 필드 description“여기에 무엇을 어떤 형식으로 넣을지”에 답해야 합니다.
    “날짜” 같은 설명은 도움이 되지 않습니다. “ISO 8601 날짜, 형식은 YYYY-MM-DD. 예: "2025-02-14"”는 큰 도움이 됩니다.
  2. 돈과 관련된 필드는 단위를 명시하세요.
    “사용자 통화 기준의 금액” 또는 “미국 달러 기준 금액”처럼 명확히 적는 편이 좋습니다. 그렇지 않으면 모델이 50을 적었을 때 그것이 50엔인지 50유로인지 헷갈릴 수 있습니다.
  3. 문자열 “카테고리”는 거의 항상 enum이 더 낫습니다.
    필드가 “카테고리” 문자열이라면 enum으로 만들고, 각 값을 도구의 description에 설명하는 편이 좋습니다. 예를 들어 relationship에 대해 도구 설명에 이렇게 쓸 수 있습니다: “relationship: friend(친구), partner(연인), sibling(형제·자매), colleague(직장 동료), parent(부모) 중 하나. 다른 값은 만들지 마세요.”
  4. 배열에는 minItems 를 지정하고, 이 리스트가 무엇인지 설명하는 것이 유용합니다.
    필드가 배열이라면 minItems를 명시하고, 이것이 정확히 어떤 리스트인지 간단히 설명하세요. 예를 들어 interests는 “사람을 산문처럼 묘사”하는 게 아니라 “짧은 태그들의 집합”입니다.

다소 번거롭게 들리겠지만, 실제로는 “설명이 있다”와 “설명이 없다”의 차이가 곧 안정적인 앱과 “오늘은 모델이 뭘 보낼지” 복불복의 차이입니다.

인사이트

MCP 도구에는 엄격한 크기 제한이 있으며 — 이것이 종종 “미스터리한” 크래시, 이상한 오류, 그리고 어시스턴트가 갑자기 여러분의 tools를 못 보는 이유가 됩니다.

핵심 규칙은 단순합니다: 도구 전체가 ~4 KB의 JSON 안에 들어와야 합니다. 이는 description 텍스트뿐 아니라 전체 구조를 포함합니다:

  • 도구 설명,
  • 인자 스키마(inputSchema),
  • 중첩 객체와 enum,
  • _meta 및 애너테이션.

도구가 비대해지면 플랫폼은 예측 불가능하게 동작하기 시작합니다: "Tool description is too long", "Schema validation failed", "Manifest exceeds size limits" 같은 오류가 나타나고, 가끔은 ChatGPT가 도구를 로드하지 않거나 아예 “존재를 잊어버리는” 일이 생깁니다.

권장 사항: description10002000자 정도로, 도구 전체는 “안전한” ~4 KB 이내로 유지하세요. 설명이 너무 길어진다면, 그 도구가 동시에 너무 많은 일을 하려는 신호일 때가 많습니다. 각 도구는 좁고 매우 명확해야 하며 — 그래야 모델이 경계를 더 잘 이해하고 입력 데이터에서 실수할 확률이 줄어듭니다.

4. TypeScript와 Zod: 두 개 대신 하나의 진실 소스

JSON 스키마를 수동으로 쓰는 일은 TypeScript 개발자에게 고통입니다. 두 개의 병렬 세계를 유지해야 하기 때문입니다:

  • TS 코드의 타입;
  • 모델을 위한 JSON Schema.

앱이 커질수록 둘은 어긋나기 시작합니다. 오늘 TypeScript 타입 필드를 바꾸고, 내일 스키마 업데이트를 잊으면 — 일주일 뒤 프로덕션에서 장애를 만납니다.

TS 세계의 사실상 표준 접근법은 Zod를 사용하고 Zod를 JSON Schema로 변환하는 것입니다(->).

아직 설치하지 않았다면 의존성을 설치합니다:

npm install zod zod-to-json-schema

suggest_gifts의 입력 스키마를 Zod로 기술해 봅시다:

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const SuggestGiftsInputZod = z.object({
  age: z
    .number()
    .int()
    .min(0)
    .max(120)
    .describe("수신자의 나이(연)."),
  relationship: z
    .enum(["friend", "partner", "sibling", "colleague", "parent"])
    .describe(
      "관계 유형: friend(친구), partner(파트너), sibling(형제/자매), colleague(동료), parent(부모)."
    ),
  minBudget: z
    .number()
    .min(0)
    .optional()
    .describe("사용자 통화 기준의 최소 예산."),
  maxBudget: z
    .number()
    .min(0)
    .describe("사용자 통화 기준의 최대 예산."),
  interests: z
    .array(
      z
        .string()
        .min(1)
        .describe(
          "짧은 관심사 태그. 예: videogames, boardgames, books."
        )
    )
    .min(1)
    .describe("수신자의 관심사 목록."),
});

이제 여러분은 다음을 갖게 됩니다:

  1. 런타임 유효성 검사: SuggestGiftsInputZod.parse(input);
  2. TypeScript 타입: type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;
  3. 모델을 위한 JSON Schema: zodToJsonSchema(SuggestGiftsInputZod).

도구를 등록할 때 사용합니다:

type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;

const suggestGiftsInputSchemaJson = zodToJsonSchema(
  SuggestGiftsInputZod,
  "SuggestGiftsInput"
);

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    description:
      "예산, 관계 유형, 수신자 관심사에 따라 선물 아이디어를 추천합니다.",
    inputSchema: suggestGiftsInputSchemaJson,
  },
  async ({ input }) => {
    // 여기서 input은 Zod로 추가 검증할 수 있습니다:
    const args = SuggestGiftsInputZod.parse(input) as SuggestGiftsInput;

    // 이후 타입이 보장된 args로 작업
  }
);

이 접근법은 바로 single source of truth — 단 하나의 진실 소스를 제공합니다: 스키마를 한 번만 기술하면 TypeScript 타입과 JSON Schema가 자동으로 생성됩니다.

실제 환경에서는 zodToJsonSchema가 기대하는 구조를 내는지 확인하는 테스트를 추가하게 될 텐데, 이는 테스트 모듈에서 더 자세히 다룹니다.

인사이트: ChatGPT는 선택적 파라미터에 약합니다

프로덕션에서 가장 고통스러운 실무 중 하나: 도구 스키마에서 optional 필드를 적극 사용하기 시작하는 순간, tool-call 품질이 눈에 띄게 떨어집니다. 모델은 이론상 선택적 파라미터를 “이해”하지만, 실제로는 — 심지어 비즈니스 로직상 매우 중요할 때에도 — 대부분 아예 보내지 않습니다.

Response API는 이를 우아하게 해결했습니다. optional 필드를 제거하고 — 모든 도구 파라미터를 required로 만들었습니다. 그러나 문제의 본질은 같습니다: “필드 절반을 선택적으로 두고, 모델이 스스로 채울지 판단하게 하자”는 아이디어는 현실과 부딪힙니다. 보통 모델은 그냥 아무 것도 보내지 않습니다.

5. “스키마”가 끝나고 “UI 설계”가 시작되는 지점

지금까지는 inputSchema, 즉 도구 실행을 위해 모델이 생성해야 하는 인자에 대해서만 이야기했습니다. 하지만 도구 호출 이후에도 일은 끝나지 않습니다. 결과를 UI에 렌더링해야 합니다.

여기서 두 레벨을 분리하는 것이 유용합니다:

  • 도구의 스키마는 모델이 생성해야 하는 입력 인자를 기술합니다. 이는 항상 MCP / tool‑call 영역에서 살아가는 JSON입니다.
  • UI 컴포넌트(위젯)는 toolOutput.structuredContent를 읽고 그에 기반해 인터페이스를 구성합니다. structuredContent 형식 역시 여러분이 설계하지만, 이것은 더 이상 모델을 위한 JSON Schema는 아닙니다(물론 내부적으로 형식을 정리할 수는 있습니다).

가끔 개발자들이 하나의 JSON 객체로 두 마리 토끼 — 모델 입력과 UI 데이터 포맷 — 를 잡으려 합니다. 이 접근은 잘 되지 않는 경우가 많습니다. 분리하는 편이 더 편리합니다:

  • inputSchema — 모델이 도구를 실행하기 위해 필요한 것;
  • structuredContent — UI가 결과를 렌더링하기 위해 필요한 것.

예를 들어, suggest_giftsinputSchema에는 어떤 선물의 id도 들어있지 않습니다. 반면 structuredContent에는 id, title, price, 구매 링크 등으로 구성된 카드 리스트가 들어갑니다.

6. 애너테이션과 _meta: UX와 보안에 영향 주기

파라미터 스키마와 응답 구조 외에도 또 하나의 레이어가 있습니다 — 플랫폼이 도구를 사용자에게 어떻게 보여주고, 도구를 어떻게 취급할지에 관한 것입니다. 이는 메타데이터와 애너테이션이 담당합니다.

표준 필드 title, description, inputSchema 외에도, 도구에는 추가 메타데이터와 애너테이션이 있을 수 있습니다. Apps SDK와 MCP에서는 일부가 _meta(예: securitySchemes)에 있고, 다른 일부는 readOnlyHint, destructiveHint 같은 OpenAI‑specific 힌트 필드에 있습니다.

여기서 중요한 점: 이 애너테이션은 JSON Schema를 바꾸지는 않지만, ChatGPT가 도구를 사용자에게 어떻게 보여주고 그 호출을 어떻게 취급할지에 영향을 줍니다.

예시: readOnlyHintdestructiveHint

두 개의 도구가 있다고 가정해 봅시다:

  • list_gifts — 선물 목록을 가져오기(안전함);
  • create_order — 주문 생성(잠재적으로 위험: 돈, 주소 등 민감).

대략 다음과 같이 표시할 수 있습니다(의사코드):

server.registerTool(
  "list_gifts",
  {
    title: "List gift suggestions",
    description: "지정된 필터로 사용 가능한 선물 목록을 가져옵니다.",
    inputSchema: listGiftsInputSchema,
    _meta: {
      readOnlyHint: true,
    },
  },
  async ({ input }) => { /* ... */ }
);

server.registerTool(
  "create_order",
  {
    title: "Create gift order",
    description:
      "사용자 명의로 특정 선물에 대한 주문을 생성합니다. 명시적 확인 이후에만 사용하세요.",
    inputSchema: createOrderInputSchema,
    _meta: {
      destructiveHint: true,
    },
  },
  async ({ input }) => { /* ... */ }
);

의미론은 다음과 같습니다. readOnlyHint는 도구가 아무 것도 변경하지 않는 안전한 도구임을 ChatGPT에 신호합니다. 모델과 UI는 더 자유롭게 호출할 수 있습니다. destructiveHint는 도구가 되돌릴 수 없거나 중요한 작업을 수행함을 의미하므로, 사용자에게 더 자주 확인을 요구하고 모델도 더 신중해집니다.

여러분의 Gift 앱에서 suggest_gifts는 명확히 read‑only입니다. 반면, 주문 생성, 결제, 사용자 데이터 변경 같은 도구는 잠재적으로 destructive로 표시하는 편이 좋습니다.

openWorldHint 및 유사 필드

때때로 도구가 “오픈 월드”에서 동작한다는 점, 즉 그 결과가 완전하지 않다는 점을 모델에 힌트하고 싶을 때가 있습니다. 예를 들어 search_products는 존재하는 모든 상품을 절대 반환하지 않고, 관련된 것만 반환합니다.

이런 애너테이션은 모델이 “search_products에 없으면 존재하지 않는다” 같은 강한 결론을 내리지 않도록 돕습니다. 미묘한 UX 포인트이지만, 프로덕션 애플리케이션에서는 차이가 분명합니다.

_meta와 UI 표시

도구가 결과를 반환할 때, _meta에서 위젯에 영향을 주는 설정을 추가로 지정할 수 있습니다. 예: 어떤 HTML 템플릿을 output‑template으로 사용할지, 테두리 필요 여부, 호출 중 어떤 문구를 보여줄지 등.

예컨대 공식 예시에서는 서버가 위젯의 HTML을 MCP 리소스로 별도로 등록하고, _meta["openai/outputTemplate"]를 통해 이를 참조합니다.

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    description: "선물 아이디어를 추천합니다.",
    inputSchema: suggestGiftsInputSchemaJson,
    _meta: {
      "openai/outputTemplate": "ui://widget/gifts.html", // MCP 리소스의 id입니다: server.registerResource(...)
      "openai/toolInvocation/invoking": "선물을 찾는 중…",		// 검색 중에 표시됩니다
      "openai/toolInvocation/invoked": "선물 후보를 찾았습니다",   // 검색이 끝났을 때 표시됩니다
    },
  },
  async ({ input }) => {
    // ...
    return {
      content: [],
      structuredContent: { items: gifts },
    };
  }
);

이렇게 한 곳에서 다음을 함께 기술할 수 있습니다:

  • 모델을 위한 입력 데이터 형태(inputSchema);
  • 도구가 UI에서 어떻게 보이고 동작할지(_meta).

7. 스키마 설계: 모델에 무엇을 요구하고, 무엇은 요구하지 말아야 하는가

흔한 함정 중 하나는 모든 일을 모델에 떠넘기려는 시도입니다. 예컨대 inputSchemagiftId 필드를 두고, description에 “우리 데이터베이스의 선물 UUID”라고 쓰는 것입니다. 모델은 "0f21b5f0-5a3a-4d1b-8f0b-9f1a6e3c1234" 같은 UUID를 그럴듯하게 생성하겠지만, 문제는 그런 선물이 실제로 존재하지 않을 가능성이 높다는 점입니다.

좋은 규칙: 모델에게 내부 세계에 묶인 기술적 식별자나 데이터를 생성하도록 요구하지 마세요.

대신 다단계 시나리오로 구성하는 편이 좋습니다:

  1. suggest_giftsid, title, price 등을 포함한 선물 목록을 반환한다;
  2. UI/모델이 사용자로 하여금 제안된 옵션 중 하나를 선택하게 한다;
  3. create_order는 이미 존재하는 집합에서의 giftId를 입력으로 받는다.

스키마 관점에서 이는 다음을 의미합니다:

  • 사용자 “바깥”을 보는 도구의 inputSchema는 사용자가 합리적으로 입력할 수 있는 것만 기술합니다: 검색 파라미터, 필터, 기준 등;
  • 내부 엔티티를 조작하는 도구의 inputSchema는 이미 알려진 id에 의존하며, 이를 모델이 지어내도록 요구하지 않습니다.

여러분의 Gift 앱에서는 suggest_gifts에서 모델에게 “SKU 코드를 만들어 달라”고 요청하지 않고, 오직 쿼리 파라미터만 요구하면 됩니다. SKU는 백엔드에서 붙이고, UI가 이를 사용자에게 보여주면 됩니다.

참고: SKU는 상품의 재고 관리용 코드입니다. 예: "GFT-CHC-500-BS".

8. 작은 실습 블록: 전부 모아보기

위에서 다룬 내용을 한곳에 모아 봅시다: Zod 스키마, JSON Schema 생성, _meta를 포함한 도구 등록, 그리고 비즈니스 로직에서의 스키마 사용. Gift 앱을 위한 최소하지만 연결된 예제를 만들어 봅니다.

먼저 Zod 스키마와 타입입니다:

import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const SuggestGiftsInputZod = z.object({
  relationship: z
    .enum(["friend", "partner", "sibling", "colleague", "parent"])
    .describe("선물 수신자와의 관계 유형."),
  maxBudget: z
    .number()
    .min(0)
    .describe("사용자 통화 기준의 최대 예산."),
  interests: z
    .array(
      z
        .string()
        .min(1)
        .describe("짧은 관심사 태그. 예: videogames.")
    )
    .min(1)
    .describe("수신자의 관심사 목록."),
});

type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;

const suggestGiftsInputSchemaJson = zodToJsonSchema(
  SuggestGiftsInputZod,
  "SuggestGiftsInput"
);

다음은 UI를 위한 _meta와 함께 도구를 등록하는 예입니다:

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    description:
      "예산, 관계, 관심사에 따라 선물 아이디어가 필요할 때 사용하세요.",
    inputSchema: suggestGiftsInputSchemaJson,
    _meta: {
      "openai/outputTemplate": "ui://widget/gifts.html",
      "openai/toolInvocation/invoking": "선물을 찾는 중…",
      "openai/toolInvocation/invoked": "선물 후보를 찾았습니다",
      readOnlyHint: true,
    },
  },
  async ({ input }) => {
    const args = SuggestGiftsInputZod.parse(input) as SuggestGiftsInput;

    const gifts = await findGifts(args); // 비즈니스 로직

    return {
      content: [],
      structuredContent: {
        items: gifts,
      },
    };
  }
);

어딘가에는 타입이 지정된 비즈니스 함수가 있을 것입니다:

async function findGifts(input: SuggestGiftsInput) {
  // 여기서 input.relationship, input.maxBudget, input.interests를 사용할 수 있고
  // Gift 타입의 객체 배열을 반환합니다
  return [
    {
      id: "gift-1",
      title: "비디오게임 테마의 보드게임",
      price: 45,
      currency: "USD",
    },
  ];
}

위젯 측에서는 window.openai.toolOutput.structuredContent.items를 가져와 카드를 렌더링하면 됩니다. 이에 대해서는 두어 강의 뒤에 더 자세히 다룹니다.

9. 도구를 기술할 때의 흔한 실수

오류 №1: 필드 설명이 지나치게 일반적이거나 무의미함.
description: "날짜" 또는 description: "필터 파라미터"처럼 쓰면 모델은 유용한 정보를 거의 얻지 못합니다. “메서드가 중요한 무언가를 수행합니다” 같은 문서와 다를 바 없습니다. “무엇을 넣을지”와 “어떤 형식으로 넣을지”에 답하는 설명을 사용하세요. 예: “ISO 8601 날짜, 형식은 YYYY-MM-DD, 예: "2025-02-14"” 또는 “사용자 통화 기준의 금액, 예: 49.99”.

오류 №2: enum이 필요해 보이는 곳에 enum이 없음.
개발자들은 종종 문자열을 enum으로 바꾸는 걸 귀찮아하며 type: "string"으로 남겨둡니다. 그 결과 모델은 제멋대로 값을 만들고, 백엔드는 당황하며, UI는 깨집니다. 고정된 옵션 집합(relationship, 상태 타입, 정렬 방식 등)이 있다면 — 거의 항상 enum으로 만드는 것이 좋습니다. 이것만으로도 tool‑call의 예측 가능성이 크게 올라갑니다.

오류 №3: 스키마와 타입에 서로 다른 진실 소스가 존재함.
클래식 사례: TypeScript에서 maxBudgetpriceMax로 바꾸었는데, JSON Schema는 업데이트하지 않은 경우입니다. 모델은 계속 maxBudget를 보내고, 코드는 priceMax를 기대하며, 모든 것이 붕괴합니다. 보통 이런 오류는 프로덕션에서야 발견됩니다. 따라서 처음부터 Zod나 유사 도구를 사용해 하나의 선언으로 타입과 JSON Schema를 생성하는 것이 좋습니다.

오류 №4: 내부 식별자 생성을 모델에 요청함.
userId, giftId, orderId 같은 필드를 “우리 시스템의 사용자 UUID”로 기술하면, 모델은 필연적으로 꾸며낸 값을 채웁니다. 심지어 UUID에 대한 pattern을 추가하더라도, 모델은 “그럴듯해 보이는” UUID를 생성할 뿐 실제와는 무관합니다. 이런 필드는(인증, 이전 tool‑call 등) 컨텍스트에 근거하여 백엔드에서 채우게 하고, 모델에 이를 요구하지 않는 편이 낫습니다.

오류 №5: 모든 경우를 아우르는 거대한 “신급” 스키마.
때로는 do_everything 같은 하나의 도구로 만들고 싶어집니다. 엄청난 객체에 절반은 nullable, 절반은 optional인 식으로요. 모델은 그 안에서 허우적됩니다. 기능을 몇 개의 도구로 나누고, 더 좁고 이해하기 쉬운 스키마를 만드는 편이 좋습니다: 하나는 선물 검색, 다른 하나는 특정 선물 상세, 또 다른 하나는 주문 생성처럼요.

오류 №6: _meta와 애너테이션을 무시함.
많은 개발자가 name, description, inputSchema까지만 작성하고, _metaopenai/outputTemplatedestructiveHint 같은 힌트를 놓칩니다. 결과적으로 “조용히” 위험한 행동을 하는 도구에 대해 UI 힌트나 확인 절차가 없게 됩니다. 이는 사용자 신뢰를 떨어뜨리고 예기치 않은 작업의 위험을 만듭니다. 애너테이션을 사용해 read‑only와 위험한 도구를 명확히 표시하고, 실행 상태 메시지도 친절히 설정하세요.

오류 №7: 서버 측 입력 유효성 검사를 생략함.
JSON Schema와 Zod가 모든 것을 기술한다고 해도, 모델만 믿는 것은 위험합니다. 때로는 모델이 부분적으로만 유효한 데이터를 내놓거나, 여러분이 스키마를 바꾸면서 비즈니스 제약을 잊을 수도 있습니다. 핸들러를 try { parse } catch { ... }로 감싸 사용자 친화적 에러를 제공하면, 모델이 인자를 수정할 기회를 얻고, 여러분은 불완전한 tool‑call 하나 때문에 전체 서비스를 떨어뜨리는 일을 피할 수 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION