Published on

Claude Tool Use 400 오류 - schema·json 진단 가이드

Authors

서버가 400을 돌려주는 순간 가장 괴로운 점은 요청이 네트워크까지는 정상적으로 도달했는데 애플리케이션 레벨에서 형식이 틀렸다는 뜻이라는 점입니다. Claude Tool Use(도구 호출)에서의 400은 특히 tools에 선언한 schema와 실제 모델이 생성하거나 우리가 전달한 tool_use 입력 JSON 사이의 불일치에서 자주 터집니다.

이 글은 “왜 400이 나는지”를 감으로 추측하는 대신, 스키마/JSON을 기계적으로 검증해서 원인을 재현·격리하는 실전 진단 흐름을 제공합니다.

1) Claude Tool Use 400의 전형적인 원인 지도

Tool Use에서 400이 나는 케이스는 대체로 아래 범주로 수렴합니다.

1.1 Tool schema(JSON Schema) 자체가 유효하지 않음

  • type 누락/오타 ("object" 대신 "obj")
  • properties가 객체가 아님
  • required가 배열이 아니거나, properties에 없는 키를 요구
  • additionalProperties 정책이 의도와 다름

1.2 schema는 맞는데, tool 입력 JSON이 schema를 위반

  • required 필드 누락
  • 타입 불일치(문자열 vs 숫자 vs 배열)
  • enum/패턴 위반
  • additionalProperties: false인데 여분 필드 포함

1.3 tool 명/형식 불일치

  • 모델이 tool_name을 다르게 호출(대소문자, 언더스코어 등)
  • tool_use 블록의 input이 문자열로 들어가거나(이중 JSON 인코딩) 객체가 아님

1.4 “JSON처럼 보이지만 JSON이 아닌” 문제

  • 작은따옴표 사용
  • trailing comma
  • NaN, Infinity 등 JSON에 없는 리터럴
  • 줄바꿈/이스케이프 문제로 문자열이 깨짐

이 중 가장 많이 보는 패턴은 additionalProperties: false + 모델이 습관적으로 reason, confidence 같은 필드를 덧붙여 스키마 위반 → 400 입니다.

2) 먼저 로그에서 확인할 3가지: 요청/응답/원본 tool_use

400을 디버깅할 때는 “무슨 요청이 나갔는지”를 원문 그대로 확보해야 합니다.

2.1 HTTP 레벨: raw request body 저장

  • 프록시(nginx)나 앱 서버에서 요청 바디를 샘플링 로깅
  • 민감정보(API 키, PII)는 마스킹

2.2 애플리케이션 레벨: tool schema와 tool 입력을 함께 저장

  • tools 배열(스키마 포함)
  • 모델이 생성한 tool_usenameinput

2.3 “이중 인코딩” 여부 확인

가장 흔한 실수 중 하나는 input을 객체가 아니라 **문자열(JSON 문자열)**로 넣는 것입니다.

{
  "type": "tool_use",
  "name": "search_docs",
  "input": "{\"query\":\"k8s timeout\"}" 
}

위는 얼핏 JSON이지만, input이 문자열입니다. 스키마는 보통 input을 object로 기대하므로 400을 유발합니다.

3) 스키마 설계에서 400을 줄이는 법: 엄격함의 균형

스키마를 너무 빡빡하게 만들면 모델이 조금만 다른 필드를 생성해도 400이 납니다. 반대로 너무 느슨하면 잘못된 입력이 도구로 흘러가 장애가 납니다. 균형을 잡는 핵심은 아래 4가지입니다.

3.1 additionalProperties 전략

  • 생산 환경 도구 호출: 원칙적으로 additionalProperties: false 권장
  • 단, 모델이 자주 덧붙이는 필드가 있다면
    • 스키마에 해당 필드를 명시적으로 추가하거나
    • 입력 정규화 레이어에서 제거(whitelist) 후 도구 실행

3.2 required는 최소로

모델이 항상 확실히 생성할 수 있는 필드만 required로 두세요. 예: query는 필수, top_k는 기본값 제공.

3.3 문자열 패턴/enum은 신중히

enum을 과하게 쓰면 모델이 맞추기 어렵습니다. 대신 정규화로 흡수하거나, 허용 범위를 넓히고 서버에서 매핑하세요.

3.4 숫자 타입: integer vs number

모델은 3 대신 3.0을 내기도 합니다. 정수만 받는다면 integer로 강제하되, 그렇지 않으면 number로 완화하세요.

4) 재현 가능한 진단: 로컬에서 JSON Schema 검증하기

가장 빠른 방법은 “Claude가 낸 tool input”을 그대로 가져와서 로컬에서 스키마 검증을 돌리는 것입니다. 아래는 Node.js 예시입니다.

4.1 Node.js + Ajv로 schema/input 검증

npm i ajv
// validate-tool-input.mjs
import Ajv from "ajv";

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

// 실제 400을 유발한 입력을 그대로 붙여넣기
const toolInput = {
  query: "kubernetes ingress timeout",
  top_k: 5,
  reason: "I think this is relevant" // additionalProperties:false 위반
};

const ajv = new Ajv({ allErrors: true, useDefaults: true });
const validate = ajv.compile(toolSchema);

const ok = validate(toolInput);
console.log("valid:", ok);
if (!ok) console.log(validate.errors);

이 스크립트로 Claude 호출 이전에(혹은 받은 직후) 동일한 검증을 수행하면, 400을 “원격 API의 모호한 에러”가 아니라 “구체적인 스키마 위반”으로 바꿀 수 있습니다.

5) 실전 패턴 6가지: 400을 만드는 스키마/JSON 불일치

5.1 required에 없는 키를 요구

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

top_kproperties에 없으면 스키마 자체가 이상하거나, 검증 단계에서 실패합니다.

5.2 additionalProperties:false + 모델의 습관 필드

모델이 자주 explanation, confidence, metadata 등을 덧붙이면 400이 납니다. 해결은 두 가지입니다.

  • 스키마에 허용 필드를 추가
  • 또는 서버에서 입력을 화이트리스트로 정리
function sanitizeInput(input) {
  const allowed = ["query", "top_k"]; 
  return Object.fromEntries(Object.entries(input).filter(([k]) => allowed.includes(k)));
}

5.3 타입 불일치: 숫자를 문자열로

{ "top_k": "5" }

스키마가 integer면 위 입력은 실패합니다. 모델이 문자열로 내는 경향이 있으면 서버에서 파싱하거나 스키마를 조정하세요.

5.4 배열/객체 혼동

{ "filters": { "tag": "aws" } }

스키마가 filters: { type: "array" }면 실패합니다. 모델에게 “filters는 배열”이라고 프롬프트로 강조하거나, 단일 객체를 배열로 감싸는 정규화를 두세요.

5.5 enum 불일치(대소문자)

{ "mode": "FAST" }

스키마 enum이 ["fast", "accurate"]면 실패합니다. 입력을 toLowerCase()로 정규화하거나 enum을 확장하세요.

5.6 JSON이 아닌 값이 섞임

  • NaN, Infinity
  • 날짜를 객체로 넣는 등(언어 런타임 직렬화 이슈)

특히 Python에서 float('nan')가 들어가면 JSON dump 단계에서 깨지거나, API가 거부할 수 있습니다.

6) “모델 출력이 문제” vs “내 요청이 문제”를 가르는 방법

Tool Use 400은 두 갈래로 나뉩니다.

  1. 내가 API에 보낸 tools schema가 잘못됨
  • 같은 schema로 어떤 입력도 통과하지 못함
  • 로컬 Ajv 검증에서도 스키마가 이상함
  1. 모델이 만든 tool input이 스키마를 위반
  • 스키마는 정상
  • 특정 대화/문맥에서만 400
  • additionalProperties, enum, 타입에서 주로 발생

따라서 권장 흐름은:

  1. tools 스키마를 로컬에서 먼저 컴파일(검증기 생성)
  2. Claude가 생성한 tool_use.input을 그대로 검증
  3. 실패 시, (a) 스키마 완화 vs (b) 입력 정규화 vs (c) 프롬프트 개선 중 선택

7) 방어적 구현: Tool 입력 정규화 + 검증 + 재시도

400을 “그냥 실패”로 두지 말고, 아래처럼 입력 정규화 → 검증 → 필요 시 재요청의 파이프라인을 두면 운영 안정성이 크게 올라갑니다.

7.1 의사 코드(언어 불문)

onToolUse(name, input):
  input2 = normalize(input)
  if !validate(name, input2):
     return askModelToFixToolInput(validationErrors)
  return runTool(name, input2)

7.2 모델에게 스스로 수정하게 만들기(리페어 프롬프트)

검증 오류 메시지를 그대로 주면 모델이 스키마에 맞게 다시 생성하는 경우가 많습니다.

  • “다음 JSON Schema를 만족하도록 input만 다시 생성해라”
  • “추가 필드는 제거해라”
  • “타입을 integer로 맞춰라”

이때도 중요한 건 input만 재생성하게 제한하는 것입니다(불필요한 설명 금지).

8) 운영 관점 체크리스트: 400을 빠르게 줄이는 관측/테스트

8.1 스키마 변경은 버저닝

  • search_docs_v1, search_docs_v2처럼 tool name/version을 분리
  • 기존 대화 컨텍스트가 남아 있을 때도 안전

8.2 계약 테스트(contract test)

  • “모델이 자주 내는 입력 샘플”을 fixtures로 모아 스키마 검증 CI에 포함
  • 스키마 강화/변경 시 회귀를 즉시 발견

8.3 장애 분류를 400/429/5xx로 분리

400은 대개 재시도로 해결되지 않습니다. 반면 5xx/503은 재시도가 유효합니다. 재시도 설계는 아래 글의 접근이 참고가 됩니다.

8.4 “HTTP는 성공인데 스트리밍이 끊김”과 구분

LLM 서비스에서 400과 502/504는 원인도 대응도 완전히 다릅니다. 인그레스/타임아웃/스트리밍 이슈가 의심되면 아래 글처럼 네트워크 계층을 분리 진단하세요.

또한 400이 아닌데도 “요청이 실패한다”로 뭉뚱그리면, 403/정책 문제를 놓치기 쉽습니다. IAM/VPC/쿼터 이슈는 별도 축으로 보세요.

9) 권장 레퍼런스 스키마 템플릿

마지막으로, 운영에서 무난한 “검색 도구” 스키마 템플릿을 제시합니다. 핵심은 필수 최소화, 기본값, 추가 필드 차단, 문자열 최소 길이입니다.

{
  "name": "search_docs",
  "description": "Search internal docs by keyword query.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "minLength": 1 },
      "top_k": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 },
      "lang": { "type": "string", "enum": ["ko", "en"], "default": "ko" }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}

여기서도 lang enum은 모델이 종종 KR 같은 값을 낼 수 있으니, 실제 운영에서는 lang을 enum으로 강제하기보다 서버에서 매핑하는 편이 더 안정적일 수 있습니다.

10) 마무리: 400을 “스키마 계약 위반”으로 다루면 빨라진다

Claude Tool Use의 400은 대부분 스키마(계약)와 입력(JSON)의 불일치입니다. 해결의 핵심은 다음 3단계로 정리됩니다.

  1. raw payload 확보: tools 스키마 + tool_use input 원문 저장
  2. 로컬 스키마 검증: Ajv 같은 검증기로 즉시 재현
  3. 정규화/완화/리페어: additionalProperties, 타입, enum, required를 균형 있게 조정

이 흐름을 갖추면 400은 더 이상 “막연한 Bad Request”가 아니라, 정확한 규칙 위반을 고치는 작업이 됩니다. 운영 환경에서는 이 차이가 곧 MTTR(복구 시간)의 차이로 이어집니다.