Published on

Claude Tool Use 400 오류, JSON Schema로 끝내기

Authors

서버에서 Claude Tool Use를 붙이다 보면, 가장 사람을 괴롭히는 에러가 400 Bad Request입니다. 특히 “툴 스키마는 맞게 쓴 것 같은데 왜 400이지?” 같은 상황이 반복됩니다. 이 글은 Claude Tool Use 400 오류의 실질적인 원인 대부분이 JSON Schema(혹은 그 해석 방식) 불일치에서 발생한다는 전제 아래, 자주 터지는 패턴을 모아 재현 → 원인 추적 → 수정 → 회귀 방지 순으로 정리합니다.

아래 내용을 따라가면 “일단 400을 없애는” 수준이 아니라, 툴 정의/툴 호출/검증을 자동화해서 같은 문제가 재발하지 않게 만들 수 있습니다.

400 오류가 의미하는 것: 네트워크가 아니라 요청 구조 문제

Tool Use의 400은 대개 다음 중 하나입니다.

  • tools 정의(JSON Schema)가 유효하지 않음
  • model이 생성한 tool input이 schema와 불일치
  • 요청 payload의 메시지 구조가 API 요구사항과 불일치
  • 스키마는 맞는데 “추가 속성/nullable/union” 같은 애매한 지점에서 플랫폼 해석과 충돌

즉, 5xx처럼 “서버가 불안정”한 문제가 아니라, 내 요청이 계약(스키마)을 어겼다는 신호입니다.

네트워크 계층 문제(예: 프록시/ALB/Cloudflare)로 4xx/5xx가 섞여 보일 때는 인프라 로그부터 보게 되는데, 그 경우는 아래 글의 접근이 도움이 됩니다.

하지만 Tool Use 400은 대부분 애플리케이션 레벨 계약 문제이므로, 요청 바디와 스키마를 먼저 의심하는 것이 정석입니다.

가장 흔한 원인 TOP 7

1) required 누락/불일치

스키마에서 required로 강제했는데 모델이 해당 필드를 빼먹으면 400이 납니다.

  • 스키마: required: ["query"]
  • 모델 출력: { "q": "..." }

해결: (1) 스키마를 현실적으로 완화하거나, (2) 시스템 프롬프트에서 필수 필드 누락 금지를 강하게 제약하거나, (3) 모델 출력 tool input을 서버에서 보정하지 말고 재요청(retry with correction) 하세요.

2) additionalProperties 기본값 함정

JSON Schema에서 additionalProperties를 명시하지 않으면 구현체에 따라 “허용”처럼 보이지만, 플랫폼/SDK가 엄격 모드로 검증하면 예상치 못한 필드가 들어오는 순간 실패합니다.

권장: 객체에는 가급적 additionalProperties: false를 명시하고, 정말 유연성이 필요하면 additionalProperties: { ... } 형태로 타입을 제한합니다.

3) 타입 불일치: number vs string

모델이 숫자를 문자열로 내보내는 경우가 잦습니다.

  • 기대: { "limit": 10 }
  • 실제: { "limit": "10" }

해결: 스키마에서 type: ["integer", "string"] 같은 union을 허용하고 서버에서 정규화하거나, 아예 프롬프트로 “반드시 정수로”를 강제합니다.

4) null 처리(Nullable) 충돌

JSON Schema 표준에선 type: ["string", "null"] 같은 방식으로 nullable을 표현합니다. 하지만 일부 환경은 nullable: true(OpenAPI 스타일)에 익숙해 혼용하다가 깨집니다.

권장: JSON Schema 방식으로 통일하세요.

5) oneOf/anyOf의 모호성

oneOf는 “정확히 하나만 매칭”이어야 합니다. 모델이 경계값을 만들어 두 스키마에 동시에 걸리면 실패할 수 있습니다.

권장: 가능하면 oneOf 대신 명확한 discriminator(예: kind)를 두고 if/then/else로 분기합니다.

6) 배열 아이템 구조가 느슨함

items를 빼거나 items: {}로 두면 모델이 제멋대로 구조를 만들어도 통과할 것 같지만, 플랫폼 검증이 더 엄격하면 실패합니다.

권장: 배열은 반드시 items에 정확한 스키마를 둡니다.

7) 메시지 구조 자체가 API 요구사항과 불일치

Tool Use는 보통 다음 요소들이 맞아야 합니다.

  • tools 정의가 요청에 포함됨
  • 모델이 tool call을 생성할 수 있는 모드/설정
  • tool 결과를 다시 모델에 전달할 때 역할/타입이 맞음

이 부분은 “스키마만 고치면 된다”가 아니라 요청/응답 루프 전체가 계약에 맞아야 합니다.

재현 가능한 최소 예제: 일부러 400을 만들어 보기

아래는 “모델이 tool input을 만들었는데 스키마에서 막히는” 대표 케이스입니다. (형식은 개념 예시이며, 사용하는 SDK/버전에 맞게 필드명은 조정하세요.)

잘못된 스키마 예시(추가 필드 허용/필수 불일치)

{
  "name": "search_docs",
  "description": "Search internal docs",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "limit": { "type": "integer", "minimum": 1, "maximum": 20 }
    },
    "required": ["query", "limit"],
    "additionalProperties": false
  }
}

모델이 아래처럼 생성하면 실패합니다.

{
  "query": "tool use 400",
  "limit": "10"
}
  • limit가 string이라 schema 위반 → 400

해결 전략 1: 스키마를 “모델 친화적으로” 설계

모델이 실수하기 쉬운 지점을 스키마에서 흡수하면 400이 급감합니다.

숫자 입력을 유연하게 받되 서버에서 정규화

{
  "name": "search_docs",
  "description": "Search internal docs",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "minLength": 1 },
      "limit": {
        "oneOf": [
          { "type": "integer", "minimum": 1, "maximum": 20 },
          { "type": "string", "pattern": "^[0-9]+$" }
        ]
      }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}
  • limit를 optional로 바꾸고
  • 숫자/숫자문자열을 모두 허용

이후 서버에서 limit를 정수로 캐스팅하고 범위를 재검증하면 안정적입니다.

해결 전략 2: “검증 실패 시 재요청”을 루프에 넣기

가장 나쁜 패턴은 검증 실패를 서버에서 임의 보정하는 것입니다. 보정은 빠르게 보이지만, 시간이 지나면 데이터 품질이 망가지고 디버깅이 어려워집니다.

권장 루프:

  1. 모델이 tool call 생성
  2. 서버가 JSON Schema로 검증
  3. 실패하면 에러 메시지(어떤 필드가 왜 틀렸는지) 를 모델에게 전달하고 재생성

Node.js(Ajv)로 tool input 검증 + 피드백 생성

import Ajv from "ajv";

const ajv = new Ajv({ allErrors: true, strict: true });

const schema = {
  type: "object",
  properties: {
    query: { type: "string", minLength: 1 },
    limit: { type: "integer", minimum: 1, maximum: 20 }
  },
  required: ["query"],
  additionalProperties: false
};

const validate = ajv.compile(schema);

export function validateToolInput(input) {
  const ok = validate(input);
  if (ok) return { ok: true };

  // 모델에게 주기 좋은 형태로 요약
  const details = (validate.errors ?? []).map(e => ({
    path: e.instancePath,
    message: e.message,
    keyword: e.keyword,
    params: e.params
  }));

  return {
    ok: false,
    details,
    hint: "Input must match the JSON Schema exactly. Fix the fields and retry."
  };
}

이 검증 결과를 그대로 모델에게 전달하면, 다음 시도에서 tool input이 스키마를 맞추도록 유도할 수 있습니다.

해결 전략 3: 프롬프트에 ‘스키마 준수 규칙’을 명문화

스키마만으로 해결이 안 되는 경우가 있습니다. 예를 들어 모델이 계속 “추가 필드”를 넣거나, 날짜를 제멋대로 포맷하는 경우입니다.

시스템/개발자 프롬프트에 아래 규칙을 넣으면 효과가 큽니다.

  • “tool input은 JSON만 출력하고, 스키마에 없는 키를 절대 추가하지 마라.”
  • “정수 필드는 따옴표 없이 숫자로만 출력하라.”
  • “날짜는 ISO-8601(YYYY-MM-DD)만 허용한다.”

이때 중요한 점은 규칙을 길게 설명하기보다, 금지/허용을 단문으로 주는 것입니다.

해결 전략 4: 스키마를 OpenAPI가 아닌 JSON Schema로 일관되게

실무에서 자주 보는 사고는 다음 혼용입니다.

  • OpenAPI 스타일: nullable: true
  • JSON Schema 스타일: type: ["string", "null"]

도구/SDK가 JSON Schema만 기대하는데 OpenAPI 속성이 섞이면, “조용히 무시”되거나 “엄격 모드에서 실패”합니다.

권장 체크리스트:

  • nullable 같은 OpenAPI 전용 키를 쓰지 않는다
  • format을 쓰더라도, 실제 검증기가 format을 강제하는지 확인한다(Ajv는 옵션 필요)
  • oneOf는 모호하지 않게 설계한다

운영 관점: 400을 “관측 가능”하게 만들기

400은 금방 지나가면 재현이 어렵습니다. 아래 3가지만 해도 디버깅 시간이 크게 줄어듭니다.

  1. 요청 payload 원문 저장(PII 마스킹)
  2. tool schema 버전 태깅: schema_version을 코드/로그에 남기기
  3. 검증 에러를 구조화 로깅: 어떤 키, 어떤 타입에서 실패했는지

인프라 레벨에서 문제가 섞여 보일 때(예: 재시도 폭증으로 NAT 비용 급증, egress 실패)도 있으니, 트래픽/재시도 정책까지 같이 점검하세요.

실전 팁: 스키마 설계 패턴(안전한 기본값)

1) 객체는 기본적으로 닫아라

{
  "type": "object",
  "properties": {
    "query": { "type": "string" }
  },
  "required": ["query"],
  "additionalProperties": false
}

2) 문자열은 minLength를 넣어라

빈 문자열은 모델이 자주 만들어내는 “형식상 채움”입니다.

3) enum은 되도록 짧게, 의미를 프롬프트에 병기

enum 값이 길거나 애매하면 모델이 오타를 냅니다.

4) 날짜/시간은 format만 믿지 말고 pattern도 고려

검증기마다 format 강제 여부가 다릅니다.

{
  "type": "string",
  "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}

마무리: 400을 없애는 핵심은 “계약을 자동화”하는 것

Claude Tool Use 400 오류는 대부분 “모델이 이상하다”가 아니라 스키마/검증/재시도 루프가 준비되지 않았다는 신호입니다.

정리하면,

  • 스키마는 JSON Schema로 일관되게, 객체는 additionalProperties: false
  • 모델이 실수하기 쉬운 타입은 유연하게 받거나(권장) 재요청 루프를 구축
  • Ajv 같은 검증기를 붙여 실패를 관측 가능하게 만들기

이 3가지만 적용해도 Tool Use 기반 기능의 안정성이 눈에 띄게 올라갑니다. 다음 단계로는 스키마 버전관리와 회귀 테스트(샘플 tool input 고정)를 붙여, 배포 시마다 400이 재발하지 않게 만드는 것을 추천합니다.