- Published on
Claude Tool Use 400 오류 - schema·JSON 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 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 + defaultadditionalProperties: false를 켰다면 모델이 다른 키를 넣지 않도록 프롬프트에서도 강하게 제한
4) 400 재현 케이스로 보는 “틀린 JSON” 패턴
4.1 required 불일치
{
"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["query"]
}
required에query가 있는데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를 최소화해서 통과시키기
type: object+properties1개 +required1개만 남김- 통과하면 제약을 하나씩 추가
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 호출 실패를 자동으로 복구하는 리트라이 정책까지 붙이면 운영 안정성이 크게 올라갑니다.