Published on

Claude 3.5 Tool Use 400 오류 - tool_schema 해결

Authors

Claude 3.5에서 Tool Use를 붙이다 보면, 모델 응답이 아니라 API 레벨에서 바로 400 이 떨어지는 순간이 있습니다. 특히 에러 메시지에 tool_schema 또는 tools[].input_schema 같은 단어가 보이면, 원인은 거의 확정입니다. 도구 입력 스키마(JSON Schema)가 Claude가 요구하는 제약을 만족하지 못했거나, 모델이 생성한 tool input이 스키마 검증을 통과하지 못한 케이스입니다.

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

  • tool_schema 관련 400 의 대표 원인과 재현 패턴 정리
  • Claude 3.5 Tool Use에서 안전한 JSON Schema 작성 템플릿 제시
  • 운영에서 재발 방지용 검증/로깅/테스트 방법

이미 일반적인 400 케이스와 JSON 스키마 기본을 다룬 글이 있다면, 아래 글도 함께 보면 좋습니다.

1) tool_schema 400은 “스키마가 엄격해서” 터진다

Claude의 Tool Use는 대략 이런 흐름입니다.

  1. 클라이언트가 tools 배열로 도구 정의를 보냄
  2. 각 도구는 input_schema 로 입력 형태를 JSON Schema로 선언
  3. 모델이 tool_use 를 선택하면, input JSON을 생성
  4. 서버가 inputinput_schema 로 검증
  5. 검증 실패 시 요청 자체가 400 으로 실패하거나(플랫폼/버전에 따라), 혹은 tool 결과 단계에서 실패

즉, tool_schema 에러는 보통 둘 중 하나입니다.

  • 도구 정의(tools[].input_schema)가 유효하지 않음: 요청 즉시 400
  • 모델이 만든 도구 입력이 스키마에 맞지 않음: tool 호출 단계에서 400 또는 “tool input validation failed” 류 에러

이 글은 두 경우를 모두 다룹니다.

2) 가장 많이 터지는 원인 TOP 7

원인 1: input_schema.typeobject 가 아님

Tool input은 사실상 “파라미터 객체”로 취급됩니다. 따라서 input_schema 최상단은 거의 항상 type: "object" 여야 합니다.

잘못된 예:

{
  "type": "string"
}

올바른 예:

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

원인 2: required 에 있는데 properties 에 없음

스키마 작성 중 리팩터링하다가 required 만 남는 경우가 흔합니다.

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

위는 queryproperties 에 없어서 검증기에서 실패할 수 있습니다.

원인 3: additionalProperties 를 안 잠그고, 모델이 잡필드를 생성

Claude는 맥락에 따라 친절하게 필드를 더 만들어 넣습니다. 예를 들어 debug: true, notes: ... 같은 값이 섞이면 스키마에 걸립니다.

운영에서는 보통 아래 중 하나를 택합니다.

  • 엄격 모드: additionalProperties: false 로 잠그고 프롬프트로 “정해진 필드만 출력” 강하게 지시
  • 관대 모드: additionalProperties: true 또는 미지정(기본 허용)으로 두고 서버에서 필터링

대부분은 엄격 모드 + 프롬프트 강화 + 사전 검증 조합이 안정적입니다.

{
  "type": "object",
  "properties": {
    "user_id": { "type": "string" },
    "limit": { "type": "integer", "minimum": 1, "maximum": 100 }
  },
  "required": ["user_id"],
  "additionalProperties": false
}

원인 4: oneOf / anyOf / allOf 조합이 플랫폼 제약과 충돌

JSON Schema는 표현력이 강하지만, Tool Use 환경에서는 일부 조합이 제한되거나, 모델이 분기를 제대로 못 맞춰서 실패할 수 있습니다.

특히 다음 패턴이 위험합니다.

  • oneOf 로 분기했는데, 모델이 두 분기 필드를 섞어서 생성
  • anyOf 로 느슨하게 했는데도 내부 검증기가 기대와 달라 실패

가능하면 분기 대신 명시적 type 필드(디스크리미네이터) 를 두고, 서버에서 라우팅하는 방식이 더 실전적입니다.

원인 5: 숫자 타입 혼동(integer vs number)

모델이 "10" 같은 문자열을 내거나, 소수로 내는 경우가 있습니다. 스키마가 integer 인데 10.5 가 오면 실패합니다.

  • 정수만 받으면 integer
  • 소수 가능하면 number
  • 문자열로 받아서 서버에서 파싱할 거면 string 으로 받고 패턴 검증

원인 6: 날짜/시간 포맷을 과하게 강제

format: "date-time" 같은 제약은 편하지만, 모델이 2026-2-3 처럼 느슨한 값을 내면 실패합니다.

운영에서는 다음 중 하나를 선택하세요.

  • format 을 빼고 서버에서 파싱/검증
  • 프롬프트로 ISO-8601 예시를 강하게 주고, 실패 시 재시도

원인 7: 도구 이름/설명/스키마가 너무 길거나 불명확

이건 명세 위반이라기보다 “모델이 스키마를 잘 못 지켜서” 간접적으로 400 을 유발합니다.

  • name 은 짧고 동사 중심
  • description 에 입력 규칙을 명확히
  • properties 에 각 필드 description 을 꼼꼼히

3) 안전한 input_schema 템플릿

아래 템플릿은 실전에서 가장 덜 터지는 형태입니다.

  • 최상단 object
  • 명확한 propertiesrequired
  • additionalProperties: false
  • 가능한 범위 제약(minLength, minimum, enum)을 넣어 모델을 가이드
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1,
      "description": "검색할 키워드. 공백만 있는 값은 금지."
    },
    "top_k": {
      "type": "integer",
      "minimum": 1,
      "maximum": 20,
      "default": 5,
      "description": "반환할 최대 결과 수(1~20)."
    },
    "lang": {
      "type": "string",
      "enum": ["ko", "en"],
      "default": "ko",
      "description": "언어 코드."
    }
  },
  "required": ["query"],
  "additionalProperties": false
}

프롬프트에도 “스키마 준수”를 중복으로 박아라

스키마가 엄격할수록, 프롬프트에도 다음을 명시하는 게 좋습니다.

  • 도구 호출 시 properties 에 정의된 키만 사용
  • 문자열/정수 타입 주의
  • 날짜는 ISO-8601

이런 중복은 과해 보이지만, 운영 안정성은 확실히 올라갑니다.

4) Node.js(Anthropic SDK) 예제: 올바른 Tool 정의

아래 예시는 Claude 3.5 계열 모델에서 Tool Use를 호출하는 전형적인 형태입니다. (SDK 버전에 따라 필드명이 다를 수 있으니, 핵심은 tools[].input_schema 구조를 참고하세요.)

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const tools = [
  {
    name: "search_docs",
    description: "사내 문서에서 키워드로 검색한다.",
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", minLength: 1 },
        top_k: { type: "integer", minimum: 1, maximum: 10, default: 5 }
      },
      required: ["query"],
      additionalProperties: false
    }
  }
] as const;

const msg = await client.messages.create({
  model: "claude-3-5-sonnet-latest",
  max_tokens: 512,
  tools,
  messages: [
    {
      role: "user",
      content: "내부 보안 가이드에서 JWT 키 회전에 대한 문서를 찾아줘."
    }
  ]
});

console.log(JSON.stringify(msg, null, 2));

여기서 400 이 난다면, 우선은 다음을 의심하세요.

  • input_schema 가 JSON Schema로서 유효한지
  • required / properties 불일치
  • additionalProperties: false 인데 모델이 다른 키를 만들었는지

JWT/JWKS 같은 운영 이슈를 다루는 API 게이트웨이 환경이라면, 아래 글처럼 “캐시/키회전” 문제도 함께 엮여 장애로 보일 수 있어, 원인 분리에도 도움이 됩니다.

5) 모델이 스키마를 어기는 경우: “검증 + 재시도” 패턴

운영에서 가장 현실적인 해법은 모델의 tool input을 서버에서 JSON Schema로 검증하고, 실패하면 다음 중 하나를 수행하는 겁니다.

  • 같은 요청을 “스키마 준수” 문구를 더 강하게 해서 재시도
  • 모델에게 “방금 입력이 스키마를 위반했으니 수정해라”라고 피드백 후 재생성
  • 혹은 tool 호출을 포기하고 일반 답변으로 degrade

Python 예제: jsonschema 로 tool input 검증

from jsonschema import validate, ValidationError

TOOL_SCHEMA = {
    "type": "object",
    "properties": {
        "query": {"type": "string", "minLength": 1},
        "top_k": {"type": "integer", "minimum": 1, "maximum": 10}
    },
    "required": ["query"],
    "additionalProperties": False
}

def validate_tool_input(tool_input: dict) -> tuple[bool, str | None]:
    try:
        validate(instance=tool_input, schema=TOOL_SCHEMA)
        return True, None
    except ValidationError as e:
        return False, e.message

bad = {"query": "jwt rotation", "top_k": "10"}  # top_k 타입 불일치
ok, err = validate_tool_input(bad)
print(ok, err)

이 검증 로직을 넣으면, “Claude가 이상한 값을 냈다”를 감이 아니라 명확한 에러 메시지로 수집할 수 있습니다.

6) 디버깅 체크리스트: 15분 안에 원인 좁히기

다음 순서로 보면 대부분 빠르게 해결됩니다.

1단계: 요청에서 tools 를 제거하고 정상 응답 확인

  • tools 없이도 응답이 오면 네트워크/인증 문제가 아니라 “tool schema” 문제일 확률이 큼

2단계: input_schema 를 최소 형태로 축소

예를 들어 필수 키 하나만 남깁니다.

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

이 상태에서 성공하면, 원인은 대개 아래 중 하나입니다.

  • oneOf 같은 복잡한 제약
  • format 강제
  • enum 값 불일치

3단계: 모델이 실제로 생성한 tool input을 원문 그대로 로깅

  • 공백, 숫자 문자열, 누락 키, 추가 키가 흔한 원인
  • 로깅할 때 개인정보/토큰은 마스킹

4단계: 스키마 검증기를 로컬에 붙여 재현

  • Node: ajv
  • Python: jsonschema

이 단계에서 “재현 가능한 실패”로 바뀌면 해결은 거의 끝입니다.

파이프라인에서 에러가 삼켜져 원인 파악이 늦어지는 경우도 많습니다. CI나 배치에서 비슷한 상황을 겪는다면 아래 글의 pipefail/에러 전파 패턴이 디버깅에 도움이 됩니다.

7) 실전 팁: 스키마를 “모델 친화적으로” 만들기

스키마는 검증기용이기도 하지만, 동시에 모델에게 주는 인터페이스 문서입니다. 다음 원칙이 효과가 좋습니다.

  • 필드명은 짧고 의미가 명확하게: userId vs uid 는 팀 컨벤션에 맞추되 일관성 유지
  • description 을 적극 활용: 무엇을 넣어야 하는지 예시까지 적기
  • enum 은 최소화: 정말 필요한 곳에만
  • 숫자는 범위를 주기: minimum/maximum
  • 문자열은 minLength 로 빈 문자열 방지
  • additionalProperties: false 를 쓰면, 프롬프트에 “정의된 키만”을 반드시 명시

8) 결론: tool_schema 400은 스키마/검증 체계로 끝낸다

Claude 3.5 Tool Use의 400 tool_schema 는 운이 나빠서가 아니라, 보통 아래 중 하나로 설명됩니다.

  • input_schema 자체가 JSON Schema로 유효하지 않음
  • 모델이 생성한 tool input이 스키마를 위반함(타입/누락/추가 키)

해결 전략은 명확합니다.

  1. input_schema 를 단순하고 엄격하게(object + required + additionalProperties: false)
  2. 서버에서 JSON Schema 검증을 반드시 수행
  3. 실패 시 재시도 또는 수정 루프를 설계

이 3가지만 갖추면, Tool Use는 “가끔 터지는 기능”이 아니라 “운영 가능한 인터페이스”로 바뀝니다.