- Published on
Claude Tool Use 400 invalid_tool_schema 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Claude Tool Use를 붙이다 보면 가장 당황스러운 오류가 400 invalid_tool_schema입니다. 메시지 자체는 “툴 스키마가 잘못됐다”는 뜻인데, 실제로는 JSON Schema의 사소한 규칙 위반, SDK가 기대하는 형태와의 불일치, 모델이 생성한 tool call 인자 검증 실패 등 여러 경우가 한 에러 코드로 뭉쳐져 나타납니다.
이 글은 “왜 이 에러가 나오는지”를 감으로 추측하는 대신, Claude가 요구하는 tool schema(=input_schema)의 제약을 기준으로 빠르게 원인을 좁히는 방법을 정리합니다. 특히 운영 환경에서는 단순히 스키마를 고치는 것보다, 재발하지 않도록 검증/로깅을 체계화하는 게 중요합니다. (비슷한 디버깅 접근이 필요한 사례로는 gRPC 타임아웃 지옥 탈출 - 데드라인 전파 설계도 참고할 만합니다.)
invalid_tool_schema가 의미하는 것
Claude Tool Use에서 툴은 보통 다음 3요소로 정의됩니다.
name: 툴 식별자description: 모델이 툴을 언제 써야 하는지 이해하도록 돕는 설명input_schema: 툴 입력 인자의 JSON Schema
400 invalid_tool_schema는 대체로 아래 중 하나입니다.
- 요청에 포함한 tool 정의 자체가 스키마 규칙을 위반
- tool 정의는 맞는데, 모델이 생성한 tool call의
input이 스키마 검증에서 탈락 - SDK/버전에 따라 요구하는 필드명이 다른데(예:
input_schemavsparameters), 형식이 어긋남
즉 “툴 스키마”는 tool 정의를 의미하기도 하고, 넓게는 tool 호출 인자까지 포함해 검증하는 경우도 있습니다. 그래서 디버깅은 반드시 (A) tool 정의 검증과 (B) tool 호출 인자 검증을 분리해서 봐야 합니다.
가장 흔한 원인 TOP 8
1) input_schema.type이 object가 아님
Tool Use 입력은 사실상 “인자 객체”를 받는 형태라, input_schema는 type: "object"가 기본입니다.
{
"name": "search_docs",
"description": "Search internal docs",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
type: "string" 같은 단일 타입을 루트로 두면 실패하는 경우가 많습니다.
2) properties 누락 또는 required가 잘못된 키를 참조
다음은 겉으로는 그럴듯하지만 required가 properties에 없는 키를 요구해서 깨집니다.
{
"type": "object",
"properties": {
"q": { "type": "string" }
},
"required": ["query"]
}
이 경우 모델이 query를 채워도/안 채워도 스키마 자체가 모순이라 거절될 수 있습니다.
3) additionalProperties 정책이 너무 엄격한데 모델 출력이 흔들림
additionalProperties: false는 매우 유용하지만, 모델이 종종 여분 필드를 붙이면 바로 검증 실패합니다.
- 운영 초기에 스키마 안정화 전:
additionalProperties: true또는 생략 - 안정화 후:
false로 잠그고, 프롬프트/예시로 모델 출력을 고정
{
"type": "object",
"properties": {
"query": { "type": "string" },
"top_k": { "type": "integer", "minimum": 1, "maximum": 20 }
},
"required": ["query"],
"additionalProperties": false
}
4) oneOf/anyOf/allOf 조합이 복잡해서 SDK/게이트웨이가 제한
JSON Schema는 강력하지만, 일부 런타임/게이트웨이/SDK에서 지원이 제한되거나, 모델이 그 조건을 맞추기 어렵습니다.
가능하면 초반에는 단순하게 시작하세요.
- 피할 것: 깊은 중첩의
oneOf+discriminator유사 패턴 - 권장:
type: object+ 명확한required+enum
5) enum 타입 불일치
enum은 문자열 enum인데 모델이 숫자를 내거나, 반대로 숫자 enum인데 문자열로 내면 실패합니다.
{
"type": "object",
"properties": {
"sort": { "type": "string", "enum": ["relevance", "date"] }
},
"required": ["sort"]
}
프롬프트에 “sort는 relevance/date 중 하나”를 반복해서 고정해 주세요.
6) 날짜/시간 포맷(format) 기대치 문제
format: date-time은 표준이지만, 모델이 2026/02/23 10:00 같은 비표준을 내면 실패합니다.
- 스키마를 느슨하게: 단순
type: string - 또는 프롬프트에서 RFC3339 예시를 강제
{
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "RFC3339 date-time, e.g. 2026-02-23T10:00:00Z"
}
}
}
7) 툴 이름/설명/필드명에 허용되지 않는 문자 또는 과도한 길이
플랫폼/SDK마다 제약이 다를 수 있지만, 다음이 안전합니다.
name: 소문자/숫자/언더스코어 위주 (search_docs,get_user)- 공백/특수문자 최소화
description: 짧고 명확하게 (모델이 “언제 호출해야 하는지” 중심)
8) “스키마는 맞는데” 모델이 만든 tool input이 스키마를 못 맞춤
이 케이스가 실제로 가장 많습니다. 예를 들어 모델이 top_k를 문자열로 내거나, 누락하거나, 여분 필드를 붙입니다.
이건 스키마 수정만으로는 해결이 안 되고, 프롬프트 설계 + 런타임 검증 + 재시도 전략이 필요합니다. RAG에서 출력이 흔들릴 때 체크리스트를 두는 것처럼, 툴 입력도 고정 장치가 필요합니다. 관련 디버깅 접근은 LangChain LlamaIndex RAG에서 답변이 반복되고 환각될 때... 글의 “원인 분리 → 관측 → 고정” 흐름이 그대로 적용됩니다.
재현 가능한 최소 예제 (정상/비정상)
정상 스키마 예제
{
"name": "get_weather",
"description": "Get weather by city name.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name in English" },
"unit": { "type": "string", "enum": ["c", "f"], "default": "c" }
},
"required": ["city"],
"additionalProperties": false
}
}
비정상 스키마 예제 (루트 타입 오류)
{
"name": "get_weather",
"description": "Get weather by city name.",
"input_schema": {
"type": "string"
}
}
Node.js에서 스키마/인자 검증을 로컬에서 먼저 돌리기
운영에서 400을 맞고 로그만 보고 추측하지 말고, 요청 직전 로컬 검증을 넣으면 해결 속도가 급격히 빨라집니다. AJV로 input_schema와 tool input을 검증하는 패턴이 가장 실용적입니다.
1) AJV로 tool input 검증
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true, strict: false });
const inputSchema = {
type: "object",
properties: {
city: { type: "string" },
unit: { type: "string", enum: ["c", "f"], default: "c" }
},
required: ["city"],
additionalProperties: false
};
const validate = ajv.compile(inputSchema);
function assertToolInput(input) {
const ok = validate(input);
if (!ok) {
const err = new Error("Tool input schema validation failed");
err.details = validate.errors;
throw err;
}
}
// 예: 모델이 만들어낸 tool input이라고 가정
const toolInput = { city: "Seoul", unit: "c" };
assertToolInput(toolInput);
2) 실패 시 “자동 수정” 대신 “재질문(repair)” 프롬프트로 재시도
스키마 불일치가 나면 코드에서 억지로 캐스팅하기보다(예: "10" → 10) 모델에게 스키마에 맞춰 다시 tool input만 출력하게 하는 편이 안정적입니다.
- 실패 로그에
validate.errors를 붙여 원인을 모델에 전달 - “다른 텍스트 없이 JSON만” 같은 출력 제약
이런 방식은 네트워크/서비스 장애에서 타임아웃을 설계하듯, 실패를 전제로 한 경로를 만드는 것과 같습니다. (관점은 다르지만 회복 설계는 Go gRPC context deadline exceeded 9가지 원인처럼 “실패 유형을 분류하고 관측 지점을 만든다”가 핵심입니다.)
Python에서 흔히 하는 실수와 해결
Python에서도 동일하게 jsonschema로 검증을 걸어두면 좋습니다.
from jsonschema import Draft202012Validator
schema = {
"type": "object",
"properties": {
"query": {"type": "string"},
"top_k": {"type": "integer", "minimum": 1, "maximum": 10}
},
"required": ["query"],
"additionalProperties": False
}
validator = Draft202012Validator(schema)
tool_input = {"query": "kubernetes", "top_k": 5}
errors = sorted(validator.iter_errors(tool_input), key=lambda e: e.path)
if errors:
raise ValueError([{"path": list(e.path), "message": e.message} for e in errors])
여기서 중요한 점은 “Draft 버전”입니다. 환경에 따라 기본 draft가 다르고, format 처리나 일부 키워드 동작이 달라질 수 있습니다. 운영에서는 validator 버전을 고정하고, 테스트에서도 같은 버전을 쓰는 게 안전합니다.
실전 디버깅 체크리스트 (30분 안에 끝내기)
1단계: tool 정의 자체를 의심
-
input_schema.type == "object"인가? -
properties가 존재하는가? -
required가properties의 키와 정확히 일치하는가? -
additionalProperties: false를 켰다면 모델이 여분 필드를 내지 않도록 프롬프트를 고정했는가? -
enum,minimum/maximum,format이 과도하게 엄격하지 않은가?
2단계: 모델이 생성한 tool input을 의심
- tool call 직전/직후의 raw 응답을 저장하고 있는가?
- 스키마 검증 에러를 구조화해서 로깅하는가? (
path,message) - 실패 시 재시도(repair) 루프가 있는가?
3단계: SDK/게이트웨이 차이를 의심
- 사용 중인 SDK가 요구하는 필드명이
input_schema가 맞는가? - tool 정의를 어디선가 직렬화/역직렬화하면서 타입이 깨지지 않았는가? (예: 숫자가 문자열로)
- 프록시/게이트웨이가 payload를 변형하지 않는가?
권장 패턴: “단순 스키마 + 강한 예시 + 검증 + repair”
Tool Use를 안정화시키는 가장 현실적인 조합은 아래입니다.
- 스키마는 단순하게 시작한다 (
object+properties+required) - 모델에게 정확한 예시를 1~2개 준다 (특히 enum/날짜/정수)
- 런타임에서 스키마 검증을 강제한다 (AJV/jsonschema)
- 실패하면 repair 프롬프트로 tool input만 재생성한다
이 흐름을 적용하면 invalid_tool_schema를 “가끔 터지는 미스터리”에서 “관측 가능하고 자동 복구되는 이벤트”로 바꿀 수 있습니다.
마무리
400 invalid_tool_schema는 결국 스키마와 실제 입력의 불일치 문제입니다. 해결의 핵심은 (1) 스키마를 단순하고 명확하게 만들고, (2) 모델 출력이 그 스키마를 따르도록 예시/제약을 주며, (3) 애플리케이션 레벨에서 검증과 repair 루프를 갖추는 것입니다.
이 3가지만 갖추면, 툴이 늘어나도 운영 안정성이 급격히 좋아지고 디버깅 시간도 크게 줄어듭니다.