- Published on
Claude Tool Use 400 에러 - JSON 스키마 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Claude Tool Use를 붙이다 보면, 모델 응답이 아니라 HTTP 400 (Bad Request) 로 바로 떨어지는 순간이 있습니다. 대개는 인증/쿼터 문제가 아니라, 요청 바디에 포함된 tool 정의의 JSON Schema가 Claude가 기대하는 형태와 어긋났을 때 발생합니다.
이 글에서는 “왜 400이 나는지”를 로그에서 빠르게 판별하고, 가장 자주 터지는 스키마 실수(특히 input_schema)를 검증 가능한 형태로 고치는 방법을 정리합니다. 마지막에는 운영에서 재발을 막는 테스트/가드레일까지 다룹니다.
> 외부 API 장애(503)처럼 재시도로 해결되는 유형이 아니라, 400은 대부분 요청 자체가 잘못된 것이라 재시도해도 계속 실패합니다. 장애 대응 관점은 OpenAI Responses API 503 멈춤 - 재시도·폴백 설계 글의 “재시도 가능한 실패 vs 불가능한 실패” 구분이 그대로 적용됩니다.
1) Claude Tool Use에서 400이 의미하는 것
Claude Tool Use 흐름은 간단히 말해:
- 클라이언트가
tools배열에 도구 이름/설명 + 입력 JSON Schema 를 전달 - 모델이 필요 시
tool_use를 생성 - 서버가 tool을 실행하고
tool_result를 다시 모델에 전달
여기서 400은 보통 1단계(요청)에서 터집니다. 흔한 원인은:
tools[].input_schema가 JSON Schema 규격을 만족하지 않음- 스키마 타입/필드가 Claude가 허용하는 subset과 불일치(예:
type누락) required가 배열이 아닌 문자열, 또는 존재하지 않는 프로퍼티를 가리킴additionalProperties/oneOf/anyOf등 고급 키워드를 잘못 사용tools[].name규칙 위반(공백/특수문자/중복 등)
핵심은 “모델이 이해 못 해서”가 아니라 “서버가 요청을 거절” 한다는 점입니다.
2) 가장 흔한 스키마 실수 Top 7
2.1 input_schema.type 누락
JSON Schema에서 객체를 받으려면 보통 아래가 필요합니다.
{
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
type이 빠지면(혹은 properties만 있고 type이 없으면) 400이 나는 케이스가 많습니다.
2.2 required 타입 오류
required는 반드시 문자열 배열이어야 합니다.
{
"type": "object",
"properties": { "q": { "type": "string" } },
"required": "q"
}
위처럼 문자열로 주면 실패합니다. 아래처럼 고치세요.
{
"type": "object",
"properties": { "q": { "type": "string" } },
"required": ["q"]
}
2.3 required에 없는 프로퍼티를 넣음
{
"type": "object",
"properties": { "q": { "type": "string" } },
"required": ["q", "page"]
}
page가 properties에 없으면 거절될 수 있습니다. 스키마를 정합하게 맞추세요.
2.4 properties 값이 스키마 객체가 아님
{
"type": "object",
"properties": {
"q": "string"
}
}
각 프로퍼티는 { "type": "string" } 같은 스키마 객체여야 합니다.
2.5 enum 타입 불일치
{
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["fast", 1] }
}
}
type이 string이면 enum도 문자열로 통일해야 합니다.
2.6 배열 스키마에서 items 누락/오류
{
"type": "object",
"properties": {
"ids": { "type": "array" }
}
}
items를 명시하는 편이 안전합니다.
{
"type": "object",
"properties": {
"ids": { "type": "array", "items": { "type": "string" } }
}
}
2.7 additionalProperties 처리 미흡
운영에서는 “모델이 스키마에 없는 필드를 넣어도 허용할 것인지”를 결정해야 합니다.
- 엄격 모드:
additionalProperties: false - 관대 모드:
additionalProperties: true또는 생략
엄격 모드로 갈 때는 required/properties 정합성이 더 중요해집니다.
3) (실전) 400을 재현하는 잘못된 Tool 정의 vs 정상 정의
아래는 의도적으로 400을 유발하기 쉬운 예시입니다.
3.1 잘못된 예
{
"name": "search_docs",
"description": "Search internal docs",
"input_schema": {
"properties": {
"query": { "type": "string" }
},
"required": "query"
}
}
문제점:
type: object누락required가 문자열
3.2 올바른 예
{
"name": "search_docs",
"description": "Search internal docs",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색어"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 5
}
},
"required": ["query"],
"additionalProperties": false
}
}
이 정도 형태가 가장 예측 가능하고 운영 친화적입니다.
4) Node.js(Typescript)에서 요청 구성: 최소 예제
아래는 “도구 정의 + 메시지”를 구성할 때 스키마를 안전하게 넣는 최소 예제입니다. (SDK 버전에 따라 필드명은 다를 수 있으니, 핵심은 tool input_schema가 유효한 JSON Schema여야 한다는 점입니다.)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const tools = [
{
name: "search_docs",
description: "Search internal docs",
input_schema: {
type: "object",
properties: {
query: { type: "string" },
limit: { type: "integer", minimum: 1, maximum: 10, default: 5 }
},
required: ["query"],
additionalProperties: false
}
}
] as const;
async function main() {
const msg = await client.messages.create({
model: "claude-3-5-sonnet-latest",
max_tokens: 512,
tools,
messages: [
{
role: "user",
content: "문서에서 'rate limit' 관련 항목을 찾아줘"
}
]
});
console.log(JSON.stringify(msg, null, 2));
}
main().catch(console.error);
여기서 400이 난다면, 90%는 tools 정의(특히 input_schema)가 원인입니다.
5) Python에서 스키마를 사전 검증하기 (jsonschema)
운영에서 가장 좋은 해법은 “Claude에 보내기 전에” 스키마를 검증하는 것입니다. Python이라면 jsonschema로 최소한의 구조 검사를 할 수 있습니다.
from jsonschema import Draft202012Validator
TOOL_INPUT_SCHEMA = {
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "minimum": 1, "maximum": 10}
},
"required": ["query"],
"additionalProperties": False,
}
# 스키마 자체가 유효한지(메타 스키마 기준) 확인
Draft202012Validator.check_schema(TOOL_INPUT_SCHEMA)
print("schema ok")
이 검증은 “스키마 문법이 JSON Schema로서 타당한가”를 확인합니다. 다만 Claude가 지원하는 subset/제약은 별개일 수 있으니, 아래 섹션의 가드레일도 같이 두는 게 좋습니다.
> Python 런타임/패키지 문제로 검증 코드가 깨질 때가 있는데, 특히 3.12에서 의존성이 꼬이면 원인 파악이 어려워집니다. 환경 이슈가 의심되면 Python 3.12에서 pkg_resources 에러 근본 해결처럼 먼저 툴체인부터 안정화하세요.
6) Claude가 싫어하는(혹은 위험한) 스키마 패턴과 대안
6.1 oneOf/anyOf/allOf 남용
고급 조합 스키마는 모델/서버 구현에 따라 예외가 생기기 쉽습니다. 가능하면 단순화하세요.
- 나쁜 방향:
oneOf로 복잡한 분기 - 좋은 방향:
type: object+modeenum + mode별 optional 필드 + 서버에서 추가 검증
6.2 깊게 중첩된 객체
중첩이 깊어질수록 모델이 생성하는 입력이 흔들리고, 스키마 불일치로 실패할 확률이 올라갑니다.
- 대안: 1-depth 평탄화, 또는 문자열(JSON)로 받지 말고 구조를 단순화
6.3 날짜/시간 포맷을 format에만 의존
format: date-time을 넣더라도 모델이 항상 ISO-8601로 주지 않을 수 있습니다.
- 대안: 스키마는 string으로 두고, 서버에서 엄격 파싱(예:
datetime.fromisoformat) 후 에러 메시지를 명확히 반환
7) 400을 “디버깅 가능한 에러”로 바꾸는 로깅/테스트 전략
7.1 요청 바디에서 tools 정의를 그대로 남기기
개인정보/비밀정보를 제외하고, 최소한 아래는 로깅하세요.
- model
- tools[].name
- tools[].input_schema (또는 해시 + 원문은 안전 저장소)
400은 재현성이 높기 때문에, 그 시점의 tool schema 스냅샷이 있으면 해결 시간이 급감합니다.
7.2 CI에서 “스키마 린트”를 돌리기
추천 체크리스트:
- 모든 tool에
name/description/input_schema존재 input_schema.type == objectrequired는 배열이며, 모든 항목이 properties에 존재additionalProperties정책이 팀 컨벤션과 일치
간단한 Typescript 런타임 체크 예:
type JSONSchema = any;
function assertToolSchema(schema: JSONSchema) {
if (schema?.type !== "object") throw new Error("input_schema.type must be object");
if (schema?.required && !Array.isArray(schema.required)) {
throw new Error("input_schema.required must be an array");
}
if (Array.isArray(schema?.required)) {
for (const k of schema.required) {
if (!schema.properties?.[k]) throw new Error(`required key not in properties: ${k}`);
}
}
}
7.3 “모델이 만든 tool input”도 별도 검증
요청 스키마가 정상이어도, 모델이 생성한 tool input이 스키마를 어길 수 있습니다. 이건 400이 아니라 tool 실행 단계에서 터지므로, 서버에서 입력 검증을 반드시 하세요.
- JSON Schema validator로 tool input 검증
- 실패 시: tool_result로 명확한 에러 메시지 + 기대 형태를 반환
8) 운영 팁: 실패를 줄이는 스키마 설계 규칙
- 필드는 적을수록 좋다(최소 입력)
- 숫자는
minimum/maximum으로 범위를 좁힌다 - 문자열은 가능하면
enum으로 제한한다 additionalProperties: false를 쓸 거면, optional 필드도properties에 모두 선언한다- 에러 메시지는 “어떤 필드가 왜 잘못됐는지”를 한 줄로 끝내지 말고 예시를 포함한다
이 규칙은 분산 시스템의 “입력 계약”을 단단히 하는 접근과 비슷합니다. 실제로 MSA에서 중복 실행/보상 처리 버그를 막는 것처럼, 입력/상태 계약을 명확히 해야 장애가 줄어듭니다. 관련해서는 MSA 사가(Saga) 패턴 - 중복 실행·보상처리 버그 해결 글의 ‘계약/보상 설계’ 관점이 참고가 됩니다.
9) 체크리스트: Claude Tool Use 400이 나면 이것부터
-
tools[].input_schema에type: object가 있는가? -
required는 문자열 배열인가? -
required의 모든 키가properties에 존재하는가? -
properties의 각 값이{ type: ... }형태의 스키마 객체인가? - 배열이면
items가 있는가? - enum 타입이
type과 일치하는가? -
additionalProperties정책이 일관적인가? - tool name이 중복/규칙 위반이 아닌가?
마무리
Claude Tool Use의 400 에러는 대부분 “모델이 이상하다”가 아니라 “내가 보낸 tool schema가 계약을 어겼다”에 가깝습니다. 해결의 핵심은:
input_schema를 단순하고 엄격하게 작성- 전송 전 스키마 자체를 검증
- CI/런타임 가드레일로 재발을 차단
이 3가지만 갖춰도, 400 때문에 개발 흐름이 끊기는 일이 눈에 띄게 줄어듭니다.