Published on

Claude Tool Use 400 오류 - schema·JSON 해결 가이드

Authors

서버에서 Claude Tool Use를 붙이다 보면, 모델 응답이 아니라 HTTP 400 (Bad Request) 로 바로 떨어지는 순간이 있습니다. 특히 tools(또는 tool/tool_use) 기능을 켠 뒤부터는 “대충 JSON 맞겠지”가 통하지 않습니다. 400은 대개 요청 본문 구조가 API 규격과 다르거나, 더 흔하게는 tool schema(JSON Schema)와 실제 JSON이 불일치할 때 발생합니다.

이 글은 다음을 목표로 합니다.

  • 400을 유발하는 대표적인 schema/json 실수를 빠르게 분류
  • 재현 가능한 최소 예제와 함께 올바른 tool schema 작성법 제시
  • 런타임에서 JSON Schema 검증을 자동화해 “모델이 가끔 이상한 JSON을 내는 문제”까지 방어

> 참고로, 타입/스키마 불일치 디버깅은 TS/런타임 검증 이슈와 닮았습니다. 타입 선언과 실제 런타임 데이터가 엇갈릴 때의 감각은 아래 글들과도 유사합니다: Pydantic v2 FastAPI 응답 검증 에러 7종 해결법, TS 5.5 isolatedDeclarations 오류 해결 가이드

1) 400 오류를 먼저 “어디서” 터졌는지 나누기

Claude Tool Use에서 400은 크게 두 부류입니다.

A. 요청 자체가 API 스펙 위반

  • messages 배열 형태가 틀림
  • role/content 구조가 잘못됨
  • tools 필드 구조가 규격과 다름
  • JSON 파싱 불가(쉼표 누락 등)

이 경우는 모델이 호출되기 전에 API 게이트웨이/서버가 요청을 거절합니다.

B. tool schema와 tool input/output 불일치

  • tools[].input_schema가 JSON Schema 규격을 만족하지 않음
  • schema는 type: object인데 실제로는 문자열/배열이 들어옴
  • required/additionalProperties/enum 등 제약과 충돌

이 경우도 보통 400으로 떨어지며, 에러 메시지에 “schema”, “input_schema”, “invalid JSON” 같은 힌트가 섞여 나옵니다.

2) 가장 흔한 원인 TOP 10 (schema·json 중심)

아래는 실무에서 자주 보는 “400 만드는” 실수들입니다.

2.1 input_schema.type 누락 또는 object가 아님

Tool input은 보통 객체를 기대합니다. type이 없거나 string 등으로 되어 있으면 실패합니다.

2.2 properties는 있는데 type: object가 없음

JSON Schema에서 properties는 객체에만 의미가 있습니다.

2.3 required에 properties에 없는 키가 들어감

예: required: ["query"]인데 properties.query가 없음.

2.4 additionalProperties 기본값을 오해

일부 구현/검증기에서는 additionalProperties: false일 때 모델이 낸 “불필요한 키”가 있으면 즉시 실패합니다. 모델이 종종 reason 같은 키를 끼워 넣는다면 특히 위험합니다.

2.5 enum/const 제약이 너무 빡셈

모델이 대소문자/공백을 살짝 다르게 내면 바로 불일치.

2.6 oneOf/anyOf를 복잡하게 사용

검증기/호환성 문제로 400이 나는 경우가 있습니다. 가능하면 단순한 스키마로 시작하세요.

2.7 숫자 타입 혼동 (integer vs number)

모델이 1 대신 1.0 또는 문자열 "1"을 내면 실패.

2.8 날짜/시간을 format으로 강제

format: date-time을 강제했는데 모델이 2026-02-23 10:00처럼 내면 불일치.

2.9 중첩 객체에서 required 누락/과다

중첩 구조에서 required를 잘못 걸면 모델 출력이 조금만 달라도 실패합니다.

2.10 “JSON은 맞는데” 문자열로 감싸서 보냄

{"a":1}를 JSON 객체로 보내야 하는데 "{\"a\":1}" 형태(문자열)로 보내면 스키마가 object일 때 실패합니다.

3) 올바른 Tool 정의 예시 (안전한 최소 스키마)

아래는 가장 안전한 출발점입니다.

  • 입력은 항상 type: object
  • additionalProperties: false는 초기에는 끄거나, 켜더라도 모델이 낼 수 있는 키를 충분히 열어둠
  • 문자열 필드에 minLength 정도만 가볍게 적용

3.1 Tool schema 예시

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

여기서 핵심은 다음입니다.

  • required는 꼭 필요한 것만
  • limit는 optional + default
  • additionalProperties: false를 켰다면 모델이 다른 키를 넣지 않도록 프롬프트에서도 강하게 제한

4) 400 재현 케이스로 보는 “틀린 JSON” 패턴

4.1 required 불일치

{
  "type": "object",
  "properties": {"q": {"type": "string"}},
  "required": ["query"]
}
  • requiredquery가 있는데 properties.query가 없어서 실패.

4.2 object 스키마인데 실제 입력이 문자열

스키마:

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

모델/클라이언트가 보낸 값(잘못된 예):

"{\"query\":\"hello\"}"
  • JSON 문자열이지 객체가 아닙니다.

4.3 additionalProperties: false인데 모델이 키를 더 넣음

입력:

{"query":"hello","reason":"because"}
  • reason이 스키마에 없으면 실패.

5) 실전 디버깅 체크리스트 (10분 컷)

5.1 API 요청 본문을 “원문 그대로” 로그로 남기기

  • 프록시/SDK가 직렬화하면서 바꾸는 경우가 있습니다.
  • 반드시 전송 직전 JSON 문자열을 로깅하세요.

5.2 에러 메시지에서 키워드 분류

  • invalid_request_error: 최상위 구조 문제 가능성
  • schema/input_schema: tool 스키마 문제 가능성
  • json: 파싱/직렬화 문제 가능성

5.3 tool schema를 최소화해서 통과시키기

  1. type: object + properties 1개 + required 1개만 남김
  2. 통과하면 제약을 하나씩 추가

5.4 additionalProperties: false는 마지막에 켜기

모델이 “불필요한 키”를 넣는 습관이 있어 초기에 디버깅을 방해합니다.

5.5 enum/format 강제는 단계적으로

  • 먼저 문자열로 받고
  • 서버에서 정규화/검증 후
  • 정말 필요할 때만 enum/format을 스키마에 올리세요.

6) Node.js에서 JSON Schema로 사전 검증(권장)

400을 줄이는 가장 확실한 방법은 Claude에 보내기 전에

  • tool schema 자체가 유효한지
  • tool input이 schema를 만족하는지

를 로컬에서 검증하는 것입니다.

여기서는 가장 널리 쓰이는 AJV로 예시를 듭니다.

6.1 설치

npm i ajv ajv-formats

6.2 스키마 및 입력 검증 코드

import Ajv from "ajv";
import addFormats from "ajv-formats";

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

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

const validate = ajv.compile(inputSchema);

function assertValidToolInput(input) {
  const ok = validate(input);
  if (!ok) {
    const details = validate.errors?.map(e => ({
      instancePath: e.instancePath,
      schemaPath: e.schemaPath,
      message: e.message
    }));
    throw new Error("Tool input schema validation failed: " + JSON.stringify(details));
  }
}

// 예: 실제 모델이 생성했거나 클라이언트가 전달한 tool input
const toolInput = { query: "kubernetes", limit: 5 };
assertValidToolInput(toolInput);
console.log("valid");

이 검증을 넣으면, 문제의 80%는 Claude 호출 전에 잡힙니다. 특히 additionalProperties: false를 켰을 때 어떤 키가 튀는지 즉시 확인할 수 있습니다.

7) 프롬프트/시스템 설계로 “모델이 스키마를 어기지 않게” 만들기

스키마를 엄격히 해도 모델이 가끔 어길 수 있습니다. 다음을 함께 적용하면 안정성이 올라갑니다.

7.1 Tool 호출 규칙을 시스템 메시지에 박기

  • “tool input은 반드시 JSON 객체이며, 스키마 외 키를 포함하지 말 것”
  • “문자열로 JSON을 감싸지 말 것”

7.2 모델 출력에 설명(reasoning)을 섞지 못하게 하기

  • tool input에 reason을 넣지 말라고 명시
  • 필요하면 아예 스키마에 notes 같은 필드를 허용하고 서버에서 무시

7.3 실패 시 재시도 전략

  • 검증 실패하면: 모델에게 “스키마 위반, 다시 생성”을 명확히 피드백
  • 단, 같은 프롬프트로 무한 재시도하지 말고 위반 항목을 구체적으로 전달

8) 스키마 설계 패턴: 엄격함과 관용의 균형

8.1 초반에는 관용적으로, 후반에 엄격하게

  • 개발 초기: additionalProperties: true 또는 생략
  • 안정화 단계: additionalProperties: false + 허용 키 확정

8.2 문자열 정규화는 서버에서

  • enum으로 강제하기보다 서버에서 trim(), toLowerCase() 후 매칭
  • 날짜/시간도 서버에서 파싱 후 검증

8.3 “버전 필드”로 진화 가능하게

스키마가 바뀌면 구버전 클라이언트/프롬프트가 400을 만들기 쉽습니다.

{
  "type": "object",
  "properties": {
    "schema_version": { "type": "integer", "enum": [1] },
    "query": { "type": "string" }
  },
  "required": ["schema_version", "query"],
  "additionalProperties": false
}

9) 운영에서의 관찰 포인트

  • 400 비율이 갑자기 증가했다면: 스키마 변경/배포가 원인일 가능성이 큼
  • 특정 입력에서만 400이 난다면: enum/format/minLength 같은 제약이 과도할 수 있음
  • 도구가 많아질수록: 스키마 간 일관성(필드명, 타입, required 정책)이 중요

런타임에서의 “검증 실패”는 결국 애플리케이션 안정성 문제로 번집니다. FastAPI/Pydantic에서 응답 검증을 걸어두는 것과 같은 맥락으로, Tool Use에서도 요청 전 검증 + 실패 시 재생성 루프가 사실상 필수입니다. (관련: Pydantic v2 FastAPI 응답 검증 에러 7종 해결법)

10) 결론: 400을 없애는 가장 현실적인 접근

  • 스키마는 단순하게 시작하고, 제약은 단계적으로 강화
  • additionalProperties: false는 강력하지만, 프롬프트/검증/재시도까지 세트로 가져가기
  • AJV 같은 검증기를 붙여 Claude 호출 전에 불일치를 잡기

이 3가지만 지켜도 “가끔씩 터지는 400”의 대부분은 재현 가능하고, 로그로 설명 가능해집니다. 다음 단계로는 스키마를 TS 타입과 동기화하거나(예: zod→json schema), tool 호출 실패를 자동으로 복구하는 리트라이 정책까지 붙이면 운영 안정성이 크게 올라갑니다.