- Published on
Claude Tool Use 400 에러, JSON Schema로 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Claude Tool Use를 붙이다 보면, 어느 날 갑자기 400 Bad Request가 터지면서 도구 호출이 전부 실패하는 순간이 옵니다. 로그를 보면 대개 invalid_request_error 류의 메시지와 함께 tools 또는 input_schema 근처가 문제라고 나오지만, 원인이 한 가지로 딱 떨어지지 않아 삽질을 유발합니다.
이 글은 Claude Tool Use에서 발생하는 400 에러를 JSON Schema 관점에서 재현 가능한 형태로 분해하고, 실전에서 바로 적용 가능한 해결 패턴을 정리합니다. 특히 스키마를 “그럴듯하게” 작성해 두고 런타임에서 깨지는 경우가 많기 때문에, TypeScript로 스키마와 객체를 함께 검증하는 방법도 같이 소개합니다.
400 에러의 본질: 스키마와 실제 입력의 불일치
Tool Use에서 400이 나는 케이스는 대부분 다음 중 하나입니다.
tools[].input_schema가 JSON Schema 규칙에 맞지 않음- 모델이 생성한 tool arguments가 스키마를 만족하지 못함
- 서버가 tool 결과를 다시 모델에 전달할 때 포맷이 깨짐
- 도구 정의와 실제 실행 결과의 타입이 어긋남
핵심은 “Claude가 이해하는 스키마”와 “내가 의도한 스키마”가 다를 때 발생한다는 점입니다. 특히 JSON Schema는 자유도가 높아 보이지만, 플랫폼이 지원하는 서브셋이 존재하고, 그 범위를 벗어나면 400으로 떨어지는 일이 흔합니다.
가장 흔한 스키마 실수 10가지 체크리스트
아래 항목은 실제로 400을 가장 자주 만드는 패턴입니다. 하나씩 점검하면 원인 파악 속도가 크게 빨라집니다.
1) type 누락 또는 잘못된 위치
JSON Schema에서 properties를 쓰려면 상위에 보통 type: "object"가 필요합니다. type이 빠지면 일부 구현체는 관대하지만, Tool Use에서는 엄격하게 실패할 수 있습니다.
2) required가 배열이 아니거나, 존재하지 않는 필드를 요구
required는 반드시 문자열 배열이어야 하고, 배열에 들어간 키는 properties에 존재해야 합니다.
- 잘못된 예:
required: "query" - 잘못된 예:
required: ["query", "notExist"]
3) additionalProperties 처리 미스
도구 입력을 엄격히 통제하려고 additionalProperties: false를 켜는 경우가 많습니다. 이때 모델이 예상치 못한 키를 하나라도 생성하면 즉시 스키마 불일치가 됩니다.
운영에서는 다음 전략이 현실적입니다.
- 초기에는
additionalProperties: true로 관대하게 운영 - 안정화되면
false로 전환하고, 모델 프롬프트와 예시를 강화
4) oneOf / anyOf / allOf 남용
복잡한 스키마 조합자는 플랫폼마다 지원 범위가 다릅니다. 지원이 불완전하면 스키마 자체가 invalid로 판단되어 400이 납니다.
가능하면 1차 버전에서는 단순한 type, properties, required, enum 위주로 구성하세요.
5) format에 대한 과신
format: "email" 같은 힌트는 검증기로서 동작하지 않거나, 반대로 특정 구현에서는 엄격하게 동작할 수 있습니다. Tool Use에서 400을 피하려면 format은 “문서화 목적” 정도로만 사용하고, 실제 제약은 pattern 또는 enum으로 명시하는 편이 안전합니다.
6) integer vs number 혼동
모델은 숫자를 생성할 때 1과 1.0을 섞어 내기도 합니다. 스키마가 integer로 제한되어 있으면 1.2 같은 값이 들어오는 순간 불일치가 됩니다.
- 정말 정수만 허용해야 한다면
integer - 애매하면
number로 열어 두고 서버에서 반올림 또는 검증
7) nullable을 JSON Schema 표준처럼 쓰지 않기
OpenAPI 스타일의 nullable: true는 JSON Schema 표준 키가 아닙니다. 안전한 방식은 다음 중 하나입니다.
type: ["string", "null"]- 또는
anyOf를 쓰되, 지원 범위를 확인
8) 배열 아이템 스키마 누락
type: "array"만 있고 items가 없으면 모델이 어떤 구조로 채워도 된다고 해석될 수 있고, 일부 검증에서는 스키마가 불완전하다고 판단할 수 있습니다.
9) enum에 타입이 섞임
enum: ["1", 1]처럼 문자열과 숫자가 섞이면 모델이 어떤 타입으로 출력할지 흔들리고, 검증도 애매해집니다. enum은 한 가지 타입으로 통일하세요.
10) 도구 입력 스키마는 맞는데, 모델이 tool arguments를 문자열로 감싸는 경우
모델이 { "query": "..." }를 만들어야 하는데, 실수로 "{ \"query\": \"...\" }" 같은 문자열 JSON을 만들어 버리면 스키마 불일치가 납니다.
이 경우는 프롬프트에서 “arguments는 JSON 오브젝트”임을 명확히 하고, 서버에서도 문자열이면 JSON.parse를 시도하는 방어 로직을 넣는 편이 좋습니다.
안전한 기본 스키마 템플릿
가장 많이 쓰는 형태는 “object 입력 + required + 추가 키 제한” 조합입니다. 아래 템플릿을 기준으로 시작하면 문제를 줄일 수 있습니다.
export const searchTool = {
name: "search_docs",
description: "문서에서 키워드를 검색합니다.",
input_schema: {
type: "object",
additionalProperties: false,
properties: {
query: {
type: "string",
description: "검색어",
minLength: 1
},
limit: {
type: "integer",
description: "최대 결과 수",
minimum: 1,
maximum: 20,
default: 5
}
},
required: ["query"]
}
} as const;
포인트는 다음과 같습니다.
type: "object"를 명시additionalProperties: false로 키를 통제required는 최소로 시작- 숫자는 범위를 걸어 모델이 튀는 값을 만들 확률을 낮춤
400을 재현하고 원인을 빠르게 찾는 디버깅 루프
400이 발생했을 때 “스키마가 잘못인지” vs “모델이 스키마를 못 맞췄는지”를 분리해서 봐야 합니다.
1) 먼저 스키마 자체를 로컬에서 검증
Node.js 환경이라면 ajv 같은 JSON Schema validator로 input_schema 자체와 샘플 arguments를 검증해 두면, 400을 절반은 예방할 수 있습니다.
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(searchTool.input_schema);
const sampleArgs = { query: "claude", limit: 5 };
if (!validate(sampleArgs)) {
console.error(validate.errors);
}
strict: false는 초기 도입 시 현실적인 선택입니다. 나중에 스키마가 안정화되면 엄격 모드로 올리세요.
2) 실제 요청 페이로드를 그대로 로깅
Tool Use는 요청 구조가 조금만 깨져도 400이 납니다. 운영 환경에서는 개인정보를 마스킹하되, 최소한 다음은 남겨야 합니다.
tools배열의 각name과input_schema- 모델이 반환한 tool call arguments 원문
- 서버가 도구 결과를 모델에 다시 전달하는 payload
3) 모델이 생성한 arguments를 “검증 후 실행”으로 바꾸기
도구를 바로 실행하지 말고, 스키마 검증을 통과한 경우에만 실행하세요. 실패하면 모델에게 재시도를 유도하거나, 서버가 안전한 기본값으로 보정합니다.
function coerceArgs(raw: unknown) {
// 모델이 문자열 JSON을 뱉는 경우 방어
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
return raw;
}
const args = coerceArgs(toolCall.arguments);
if (!validate(args)) {
// 여기서 모델에게 "스키마에 맞게 다시"를 요청하거나
// 사용자에게 오류를 설명하는 메시지를 구성
throw new Error("Tool arguments schema validation failed");
}
// 통과한 경우에만 도구 실행
const result = await runSearch(args.query, args.limit ?? 5);
이 패턴을 넣으면 “400은 안 나는데 결과가 이상함” 같은 2차 문제도 함께 줄어듭니다.
TypeScript로 스키마-객체 불일치 줄이기
스키마를 손으로 쓰면, 시간이 지나면서 구현과 문서가 어긋납니다. 특히 required나 enum 같은 부분은 리팩터링 때 가장 먼저 깨집니다.
이때 TypeScript의 satisfies를 활용하면, “스키마를 담는 객체가 최소한의 형태를 만족하는지”를 컴파일 타임에 잡을 수 있습니다. 관련 내용은 TS 5.x satisfies로 타입 안전 유지하며 객체 검증도 함께 보면 좋습니다.
아래 예시는 도구 정의 형태를 강제하는 간단한 타입입니다.
type JsonSchemaObject = {
type: "object";
properties: Record<string, unknown>;
required?: string[];
additionalProperties?: boolean;
};
type ClaudeTool = {
name: string;
description?: string;
input_schema: JsonSchemaObject;
};
export const searchTool2 = {
name: "search_docs",
description: "문서에서 키워드를 검색합니다.",
input_schema: {
type: "object",
additionalProperties: false,
properties: {
query: { type: "string", minLength: 1 },
limit: { type: "integer", minimum: 1, maximum: 20 }
},
required: ["query"]
}
} satisfies ClaudeTool;
이 방식은 “완전한 JSON Schema 타입 모델링”은 아니지만, 최소한 다음을 방지합니다.
required를 문자열로 잘못 쓰는 실수input_schema에서type을 빼먹는 실수- 도구 정의의 키 이름을 잘못 쓰는 실수
더 강하게 가려면, properties의 키와 required의 관계까지 타입으로 엮는 제네릭을 만들 수 있는데, 이때는 복잡도가 올라가므로 팀 규모와 유지보수 여력에 맞춰 선택하세요.
운영에서 자주 만나는 케이스별 처방
케이스 A: 특정 프롬프트에서만 400이 터진다
- 모델이 특정 문맥에서만 추가 필드를 생성하거나
- 문자열 JSON 형태로 arguments를 만들거나
enum밖의 값을 뱉는 경우가 많습니다.
해결책:
additionalProperties: false를 잠시 풀고 어떤 키가 나오는지 관찰- tool arguments가 문자열이면 파싱 방어 로직 추가
- 프롬프트에 “도구 입력은 스키마를 반드시 준수” 문장을 넣고, 예시를 제공
케이스 B: 로컬에서는 되는데 배포 환경에서만 400
이 경우는 스키마보다는 “요청 페이로드가 변형”되는 문제가 섞여 있을 수 있습니다.
- 프록시나 미들웨어가 JSON body를 변형
Content-Type누락- 로그 마스킹 과정에서 payload를 잘못 재조립
네트워크 계층 이슈가 섞이면 5xx로도 튈 수 있으니, 장애 대응 관점에서는 재시도 전략도 같이 챙겨두는 편이 좋습니다. 예를 들어 외부 API 호출 전반의 재시도/중복 방지 패턴은 OpenAI 429·5xx 재시도, Idempotency 키로 중복 결제 막기에서 설명한 접근을 Tool 실행에도 응용할 수 있습니다.
케이스 C: 스키마는 맞는데도 도구 호출 자체가 안 나온다
이건 400이 아니라 “모델이 tool call을 선택하지 않은” 문제일 가능성이 큽니다.
- 도구 설명이 너무 짧거나 모호함
- 사용자 질문과 도구 목적의 연결이 약함
- 도구를 써야만 답할 수 있는 제약이 프롬프트에 없음
해결책:
- description을 구체적으로 작성
- tool 사용 예시를 system 또는 developer 메시지에 추가
- “필요하면 도구를 사용하라”가 아니라 “정확한 값을 위해 도구를 사용하라”처럼 강제력을 높임
실전 권장 아키텍처: 스키마 검증을 파이프라인에 고정
운영에서 가장 튼튼한 구조는 아래 순서입니다.
- 모델 응답에서 tool call 추출
- arguments를 정규화하고, 스키마로 검증
- 실패 시 재질문 또는 보정
- 성공 시 도구 실행
- 도구 결과를 다시 모델에 전달하기 전에 결과도 스키마 형태로 정규화
특히 2번을 빼면, 문제는 언젠가 반드시 터집니다. “지금은 잘 된다”는 상태는 보통 프롬프트가 우연히 안정적이어서 그렇지, 모델 업데이트나 입력 분포 변화로 쉽게 깨집니다.
마무리: 400은 대부분 스키마 문제이고, 예방이 가능하다
Claude Tool Use의 400 에러는 무작위가 아니라, 대부분 JSON Schema의 작은 불일치에서 시작합니다. 다음 3가지만 적용해도 재발률이 크게 줄어듭니다.
input_schema를 단순하게 시작하고, 조합자 사용을 최소화- 모델 arguments를 실행 전에 반드시 로컬 검증
- TypeScript
satisfies로 도구 정의의 형태를 컴파일 타임에 고정
스키마를 “문서”가 아니라 “계약”으로 취급하면, Tool Use는 훨씬 예측 가능해지고 운영 비용도 내려갑니다.