Published on

Claude Tool Use 400 에러 - JSON Schema 디버깅

Authors

서버에서 Claude Tool Use를 붙여 놓고 개발하다 보면, 모델 응답이 아니라 API 호출 단계에서 400 Bad Request가 떨어지는 순간이 가장 당황스럽습니다. 특히 에러 메시지가 길고 추상적이거나, 어떤 필드가 문제인지 뚜렷하게 지목해주지 않으면 “대체 JSON Schema 어디가 잘못된 거지?”라는 상태로 시간을 태우기 쉽습니다.

이 글은 Claude Tool Use에서 발생하는 400 에러를 JSON Schema 관점에서 디버깅하는 실전 가이드입니다. 단순히 스키마 문법을 나열하기보다, 실제로 자주 발생하는 실패 패턴과 이를 빠르게 좁히는 절차, 그리고 로컬에서 검증 가능한 코드까지 포함합니다.

또한, 스키마 자체는 맞는데 트래픽/제한/재시도 설계가 섞여 문제를 더 어렵게 만드는 경우도 많습니다. 호출 실패가 400이 아닌 429/529라면 아래 글도 함께 보세요.

1) Tool Use 400의 본질: “스키마가 모델이 아니라 API에서 거절됨”

Tool Use는 대략 다음 흐름을 갖습니다.

  1. 클라이언트가 tools 배열에 도구 정의(이름, 설명, input_schema) 를 포함해 요청
  2. 서버(Claude API)가 요청 바디 및 스키마를 검증
  3. 통과하면 모델이 tool call을 생성하거나 일반 텍스트로 응답

여기서 400은 대개 2단계에서 발생합니다. 즉, 모델이 “이상한 tool call”을 만든 게 아니라, 요청에 포함된 tool 정의가 스펙/스키마 규칙을 위반했거나, 스키마는 그럴듯하지만 허용되지 않는 형태인 경우가 많습니다.

따라서 디버깅도 모델 프롬프트를 만지기 전에, 먼저 스키마를 독립적으로 검증하고 요청 JSON을 최소화해서 재현해야 합니다.

2) 가장 많이 터지는 JSON Schema 실수 TOP 패턴

아래는 현장에서 400을 가장 자주 유발하는 패턴들입니다. (각 항목은 “왜 문제인지”와 “어떻게 고치는지”에 초점을 둡니다.)

2.1 input_schema가 JSON Schema Draft 규칙과 어긋남

Tool의 input_schema는 보통 JSON Schema 형태를 따르지만, API가 허용하는 서브셋이 있습니다. 특히 다음이 흔한 함정입니다.

  • type 누락 (예: 최상위에 type: "object"가 없는데 properties만 있는 경우)
  • properties가 있는데 typeobject가 아님
  • required가 배열이 아닌 값으로 들어감
  • requiredproperties에 없는 키를 넣음

대응: 최상위는 거의 항상 type: "object"로 시작하고, properties/required의 정합성을 자동 검증하세요.

2.2 additionalProperties 처리 누락으로 인한 “모델 출력이 스키마를 자꾸 벗어남”

이건 400보다는 tool call 이후 검증에서 실패를 만들지만, 일부 구현에서는 요청 단계에서 더 엄격하게 보기도 합니다. 스키마를 엄격하게 만들고 싶다면:

  • additionalProperties: false를 넣고
  • 반드시 required를 정확히 지정

다만 너무 엄격하면 모델이 작은 변형만 해도 실패합니다. 초기에 디버깅 단계에서는 additionalProperties를 완화하고, 안정화 후에 강화하는 전략이 좋습니다.

2.3 oneOf/anyOf/allOf를 과하게 사용

복잡한 분기 스키마는 사람에게는 좋지만, 모델과 API 검증기 조합에서는 난이도가 급상승합니다.

  • oneOf 내부에 required가 서로 겹치거나 모호하면 모델이 중간 형태를 출력
  • anyOf는 너무 느슨해져서 의도치 않은 구조도 통과

대응: 처음에는 단일 object 스키마로 단순화하고, 분기가 필요하면 type 필드를 두고 enum으로 구분하는 방식이 실전에서 안정적입니다.

2.4 format/pattern/minimum 등 제약이 “현실 데이터”와 충돌

예를 들어 날짜를 format: "date-time"로 걸어두면 모델이 2026-02-24 10:00처럼 공백 포함 형태를 내는 순간 실패합니다.

대응:

  • 초기에 제약을 최소화
  • 반드시 필요한 제약만 남기기
  • pattern을 쓸 경우, 테스트 케이스를 충분히 확보

2.5 숫자 타입 혼동: integer vs number

모델은 11.0을 섞어 내기 쉽습니다. integer를 강제하면 예상치 못한 실패가 날 수 있습니다.

대응: 정말 정수만 허용해야 하는 게 아니라면 number로 두고, 서버에서 후처리로 반올림/검증하는 편이 안정적입니다.

2.6 enumnull을 섞거나 nullable 표현을 잘못함

JSON Schema에서 nullable을 표현하는 방식은 다양합니다.

  • type: ["string", "null"]
  • 또는 anyOfnull 포함

하지만 일부 서브셋 구현에서는 특정 형태가 제한될 수 있습니다.

대응: 가능하면 nullable을 줄이고, 값이 없으면 필드를 생략하는 쪽으로 설계합니다. 즉, required에서 빼고 optional로 두는 방식이 가장 단순합니다.

3) “최소 재현”으로 400을 잡는 절차

스키마 디버깅은 한 번에 다 보려 하면 실패합니다. 아래 순서가 가장 빠릅니다.

3.1 요청 바디를 최소화

  • tool을 1개만 남기기
  • input_schema를 가장 단순한 형태로 바꾸고 호출이 되는지 확인
  • 그 다음 필드를 하나씩 추가

3.2 스키마를 로컬에서 먼저 검증

API가 내는 400은 원인 메시지가 제한적일 수 있습니다. 로컬에서 JSON Schema validator로 먼저 잡으면 훨씬 빠릅니다.

아래는 Python에서 jsonschematool 입력값을 검증하는 예시입니다. (스키마 문법 오류 검증 + 샘플 데이터 검증)

from jsonschema import Draft202012Validator

schema = {
    "type": "object",
    "properties": {
        "query": {"type": "string"},
        "top_k": {"type": "integer", "minimum": 1, "maximum": 20},
    },
    "required": ["query"],
    "additionalProperties": False,
}

# 1) 스키마 자체가 유효한지
Draft202012Validator.check_schema(schema)

# 2) 샘플 입력이 통과하는지
validator = Draft202012Validator(schema)

def validate_input(data: dict):
    errors = sorted(validator.iter_errors(data), key=lambda e: e.path)
    if errors:
        for e in errors:
            path = ".".join([str(p) for p in e.path])
            raise ValueError(f"Invalid at `{path}`: {e.message}")

validate_input({"query": "hello", "top_k": 5})

주의할 점은, Claude API가 정확히 Draft 2020-12를 쓴다고 단정할 수는 없다는 것입니다. 하지만 로컬 검증은 최소한 명백한 스키마 불일치를 빠르게 제거하는 데 매우 유용합니다.

3.3 모델이 실제로 생성할 법한 입력 샘플로 테스트

사람이 만드는 “완벽한 JSON”이 아니라, 모델이 흔히 만드는 형태를 샘플로 넣어보세요.

  • 숫자를 문자열로 내는 경우: "5"
  • 불필요한 필드 추가: {"query":"x","foo":"bar"}
  • 날짜 포맷 흔들림

이 단계에서 스키마가 너무 엄격한지 판단할 수 있습니다.

4) 안전한 Tool Schema 템플릿(권장 기본형)

아래 템플릿은 실무에서 가장 사고가 적은 형태입니다.

  • 최상위 object
  • properties는 모두 명시
  • required는 최소한만
  • additionalProperties는 초기에는 true로 두고, 안정화 후 false로 전환
{
  "name": "search_docs",
  "description": "Search internal documentation by keyword.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Search query string"
      },
      "top_k": {
        "type": "number",
        "description": "Number of results to return",
        "minimum": 1,
        "maximum": 20
      }
    },
    "required": ["query"]
  }
}

여기서 top_knumber로 둔 이유는, 모델이 5 대신 5.0을 내도 통과시키기 위함입니다. 서버에서 int()로 캐스팅하고 범위를 재검증하는 편이 전체 안정성이 높습니다.

5) 400을 만드는 “요청 구조” 실수도 함께 점검

스키마만 맞추면 끝이라고 생각하기 쉽지만, 실제로는 요청 JSON의 형태가 스펙과 미세하게 어긋나서 400이 나는 경우도 많습니다.

체크리스트:

  • tools가 배열인지
  • 각 tool에 name이 유니크한지
  • name이 허용 문자/길이 규칙을 위반하지 않는지
  • input_schema가 객체인지 (문자열로 직렬화된 JSON을 넣지 않았는지)
  • content에 tool 관련 블록을 넣는 방식이 SDK 요구사항과 일치하는지

특히 “스키마를 JSON 문자열로 넣는 실수”는 자주 봅니다. 예를 들어 아래처럼 input_schema가 문자열이면 API는 객체를 기대하다가 400을 낼 수 있습니다.

{
  "name": "bad_tool",
  "description": "Wrong",
  "input_schema": "{\"type\":\"object\"}"
}

반드시 아래처럼 JSON 오브젝트로 넣어야 합니다.

{
  "name": "good_tool",
  "description": "Correct",
  "input_schema": {"type": "object", "properties": {}}
}

6) 서버 로그/관측: 400을 “재현 가능한 오류”로 바꾸기

스키마 디버깅은 관측이 없으면 운빨 게임이 됩니다. 다음을 권장합니다.

  • 요청 바디를 그대로 저장하되, 민감 정보는 마스킹
  • tool 정의(name, input_schema)를 별도 로그로 남기기
  • 400 응답 바디(에러 메시지, 필드 경로)를 원문 그대로 저장
  • 동일 요청을 curl로 재실행할 수 있도록 request_id와 함께 보관

그리고 400이 아니라 간헐적으로 끊기는 문제(스트리밍/프록시/버퍼)라면, 네트워크 계층 이슈가 섞여 디버깅이 더 어려워집니다. 이 경우 아래 체크리스트가 도움이 됩니다.

7) 실전 디버깅 예시: required 불일치로 400 유발

아래 스키마는 얼핏 정상처럼 보이지만, requiredq를 넣어놓고 properties에는 query만 정의했습니다. 이런 불일치는 흔히 400의 원인이 됩니다.

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

수정은 간단합니다.

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

이런 류의 실수는 사람이 눈으로 보면 지나치기 쉬워서, 로컬 validator와 테스트 케이스가 사실상 필수입니다.

8) 운영 단계 권장안: “스키마는 단순하게, 검증은 서버에서”

Tool Use를 안정적으로 운영하려면 다음 타협이 효과적입니다.

  • 스키마는 모델이 지키기 쉬운 수준으로 단순화
  • 타입/범위/필수 여부는 최소만 스키마에 반영
  • 나머지 강한 검증은 서버 코드에서 수행
  • 실패 시 모델에게 재시도 프롬프트를 주기보다, 서버가 에러를 구조화해 다시 tool call을 유도

특히 레이트리밋/과부하가 섞인 환경에서는, 스키마 문제(400)와 제한 문제(429/529)를 분리해서 관측해야 대응이 가능합니다.

9) 마무리: 400을 “스키마 체크리스트”로 끝내기

Claude Tool Use의 400은 대부분 “모델이 이상해서”가 아니라 “스키마가 미세하게 어긋나서” 발생합니다. 해결의 핵심은 다음 3가지입니다.

  1. 요청을 최소화해서 재현
  2. 로컬 JSON Schema 검증으로 명백한 오류 제거
  3. 스키마를 단순하게 유지하고, 강한 검증은 서버로 이동

이 과정을 루틴으로 만들면, 400은 더 이상 공포의 에러가 아니라 몇 분 안에 정리되는 체크리스트 항목이 됩니다.