- Published on
Claude 3 API tool_use 400 에러 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 400 Bad Request를 반환한다는 건, 네트워크나 일시 장애보다는 요청 본문(payload) 자체가 스펙과 맞지 않는다는 신호인 경우가 대부분입니다. Claude 3 API에서 특히 tool_use를 도입하면, 텍스트만 주고받던 때보다 요청/응답의 구조적 제약이 크게 늘어납니다. 그 결과, 사소한 필드 누락이나 타입 불일치가 곧바로 400으로 이어집니다.
이 글에서는 Claude 3 API에서 tool_use 관련 400이 발생하는 가장 흔한 원인 7가지를, “어디를 보면 바로 잡을 수 있는지” 중심으로 정리합니다. (SDK별 세부 필드명은 다를 수 있으니, 핵심은 구조와 매칭 규칙이라고 생각하시면 됩니다.)
또한 운영 환경에서 400을 빠르게 분류하는 방법도 함께 다룹니다. 재시도 설계 자체는 429나 503에서 중요하지만, 400은 재시도해도 계속 실패하므로 로그에서 즉시 원인 특정이 핵심입니다. 재시도/폴백 관점은 OpenAI Responses API 503 멈춤 - 재시도·폴백 설계 글의 접근을 참고하면, “재시도하면 안 되는 오류”를 분리하는 데 도움이 됩니다.
400을 빨리 잡는 기본 진단 루틴
- 서버가 준 에러 메시지에
path또는field힌트가 있는지 확인 - 요청 JSON을 그대로 저장해 스키마 검증(로컬에서 JSON Schema로 검증하거나 최소한 타입/필수필드 체크)
tools정의와tool_use호출이 이름/입력 스키마 기준으로 1:1로 맞는지 확인- 멀티턴이라면, 과거 메시지의
tool_use_id와 이번tool_result가 정확히 매칭되는지 확인
운영에서 이 루틴을 자동화하면, 400은 대부분 몇 분 안에 잡힙니다.
원인 1) tools 배열 스키마가 잘못됨 (name, input_schema 누락/오타)
tool_use를 쓰려면 모델에게 “사용 가능한 도구 목록”을 tools로 제공해야 합니다. 여기서 가장 흔한 실수는 다음입니다.
tools가 배열이 아닌 객체로 들어감- 도구 정의에서
name누락 input_schema가 JSON Schema 형태가 아님input_schema의type이 빠짐(대부분object)
잘못된 예
{
"model": "claude-3-...",
"messages": [{"role": "user", "content": "날씨 알려줘"}],
"tools": {
"name": "get_weather"
}
}
올바른 예(개념)
{
"model": "claude-3-...",
"messages": [{"role": "user", "content": "서울 날씨 알려줘"}],
"tools": [
{
"name": "get_weather",
"description": "도시의 현재 날씨를 조회합니다.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
]
}
체크 포인트: tools의 각 원소가 “함수 시그니처”라고 생각하고, name과 input_schema를 엄격히 맞추세요.
원인 2) tool_use의 name이 tools에 정의된 이름과 불일치
모델이 tool_use를 생성할 때, 호출하는 도구 이름은 반드시 tools[].name 중 하나여야 합니다. 애플리케이션 코드에서 중간에 매핑을 바꾸거나, 프롬프트에서 도구 이름을 다르게 유도하면 불일치가 생깁니다.
tools에는getWeather로 정의- 모델 출력 또는 후처리에서
get_weather로 호출
이 경우는 대개 “도구가 존재하지 않는다” 류의 메시지로 400이 납니다.
예방 팁
- 도구 이름은 스네이크 케이스로 고정하고, 코드 상수로 관리
- 프롬프트에 도구 이름을 “자연어로” 설명하지 말고, 정확한 식별자를 그대로 노출
원인 3) tool_use.input이 JSON Schema를 만족하지 못함 (타입/필수값 불일치)
가장 빈번한 400 원인입니다. 예를 들어 input_schema.required에 city가 들어있는데, 모델이 {"location": "Seoul"}처럼 다른 키를 내거나, 숫자/배열 타입을 넣으면 바로 실패합니다.
재현 예
input_schema는 city를 요구하는데 요청이 다음처럼 들어오는 케이스입니다.
{
"type": "tool_use",
"id": "toolu_...",
"name": "get_weather",
"input": {
"location": "Seoul"
}
}
해결 전략
input_schema를 너무 빡빡하게 만들지 말고, 초기에 관용적으로 설계- 필수값이 많다면, 모델이 놓치기 쉬우니
required를 최소화 - 키 이름을 짧고 명확하게(
city,unit,date등) - 서버에서 도구 실행 전에 입력 검증 실패를 모델에게 되돌려(tool_result로 에러를 전달) 스스로 수정하게 만들기
원인 4) messages 구조에서 content 블록 타입이 잘못됨 (텍스트/툴 블록 혼합 규칙 위반)
Claude 계열 API는 메시지의 content가 단순 문자열이거나, 또는 블록 배열(예: text, tool_use, tool_result) 형태인 경우가 있습니다. 여기서 흔한 실수는 다음과 같습니다.
content를 문자열로 보내야 하는데 배열로 보냄(또는 반대)- 블록 배열을 쓰면서 각 블록에
type이 누락됨 tool_result를 user가 아닌 assistant로 넣는 등 역할 규칙 위반
잘못된 예(개념)
{
"role": "assistant",
"content": [
{"text": "도구를 호출할게요"}
]
}
위처럼 블록 객체에 type이 없으면 스키마 에러로 400이 날 수 있습니다.
올바른 예(개념)
{
"role": "assistant",
"content": [
{"type": "text", "text": "도구를 호출할게요"},
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Seoul"}
}
]
}
체크 포인트: 한 번이라도 블록 배열을 쓰기 시작하면, 이후 턴에서도 동일한 규칙을 유지해야 하는 경우가 많습니다. “어떤 SDK는 문자열 허용, 어떤 SDK는 블록만 허용” 같은 차이도 있으니, 팀 내에서 포맷을 표준화하세요.
원인 5) tool_result가 tool_use.id와 매칭되지 않음 (id 누락/오타/턴 꼬임)
tool_use는 보통 id를 포함하고, 다음 턴에서 애플리케이션은 그 id를 tool_result의 tool_use_id로 다시 넘겨야 합니다. 여기서 다음 문제가 자주 발생합니다.
tool_use_id를 누락- 다른 요청에서 나온
tool_use.id를 섞어서 전달(동시성/비동기 처리에서 흔함) - 하나의 응답에 도구 호출이 여러 개였는데 결과를 일부만 보냄
잘못된 예
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_WRONG",
"content": "맑음 3도"
}
]
}
해결 전략
tool_use.id를 요청 단위 trace id와 함께 저장- 도구 실행 파이프라인에서
tool_use.id를 키로 결과를 조립 - 병렬 호출을 허용한다면, 결과를 모아서 한 번에
tool_result블록들을 전달
이 문제는 “스키마는 맞는데 400”으로 보일 수 있어 더 까다롭습니다. 특히 서버리스나 컨테이너 환경에서 동시성이 올라가면 빈도가 증가합니다. 콜드 스타트/동시성 이슈로 타이밍이 꼬일 때는 GCP Cloud Run 503·콜드스타트 폭증 해결 가이드처럼 런타임 특성을 점검하는 것도 간접적으로 도움이 됩니다(비동기 워커/큐로 분리 등).
원인 6) tools 정의의 JSON Schema가 “표준 JSON”이 아님 (직렬화 깨짐)
의외로 많이 나오는 케이스입니다. 코드에서 스키마를 만들 때 다음이 섞이면, JSON 직렬화 단계에서 깨지거나 API가 거부합니다.
undefined값 포함(자바스크립트)NaN,Infinity같은 JSON 비표준 숫자- 함수/클래스/Date 객체를 그대로 넣음
- trailing comma 자체는 JSON에 없는데, 문자열로 직접 작성하다가 섞임
Node.js에서 흔한 실수 예
const tools = [
{
name: "get_weather",
input_schema: {
type: "object",
properties: {
city: { type: "string" },
// default: undefined, // 이런 값이 섞이면 직렬화/검증에서 문제
},
},
},
];
해결 전략
- 요청 직전에
JSON.stringify(payload)를 한 번 찍어보고, 그 결과가 기대대로인지 확인 - 스키마와 payload를 생성하는 코드에 타입을 강하게 부여
- TypeScript를 쓴다면 스키마 빌더를 만들고,
satisfies로 형태를 고정하는 방식이 효과적입니다. 타입 안정화는 TS 5.x satisfies로 타입 오류 줄이는 실전 글의 패턴을 그대로 적용할 수 있습니다.
원인 7) tool_choice 또는 강제 호출 설정이 잘못되어 모순된 요청이 됨
일부 SDK/설정에서는 “도구를 반드시 호출하라” 또는 “특정 도구만 호출하라” 같은 옵션(예: tool_choice)을 제공합니다. 여기서 다음 모순이 생기면 400이 납니다.
tool_choice로get_weather를 강제했는데tools에 해당 도구가 없음- 도구 호출을 강제했는데, 동시에 “도구를 쓰지 말라”는 시스템 프롬프트를 넣음(논리 모순 자체가 아니라, 구현에 따라 검증에서 걸릴 수 있음)
- 도구 호출 강제 옵션의 필드명이 SDK 버전과 맞지 않음
해결 전략
- 강제 호출은 디버깅/테스트 단계에서만 사용하고, 프로덕션에서는 모델이 판단하게 두는 편이 안정적
- 강제 호출을 쓰면
tools목록을 최소화해서 불확실성을 줄이기 - SDK 버전 업 시,
tool_choice관련 breaking change를 릴리즈 노트로 확인
실전 체크리스트: 400이 뜨면 여기부터 본다
1) 요청 JSON을 “그대로” 저장했는가
프록시/미들웨어에서 payload가 변형되면 원인 추적이 불가능해집니다. 400은 재현이 중요하니, 실패 요청을 샘플링해서 저장하세요.
2) tools[].name 과 tool_use.name 매칭
오타가 가장 많습니다. 특히 스네이크 케이스와 카멜 케이스 혼용을 금지하세요.
3) tool_use.input 검증
로컬에서 JSON Schema validator로 검증하면 즉시 잡힙니다.
4) tool_use.id 와 tool_result.tool_use_id 매칭
동시성 환경에서 가장 많이 꼬입니다. id를 키로 상태를 묶으세요.
5) content 블록의 type 누락 여부
블록 배열을 쓰는 순간부터는 모든 블록에 type이 필요하다고 가정하고 점검하세요.
Node.js 예제: tool_use 실행 루프(개념 코드)
아래 예제는 “모델이 도구 호출을 요청하면 실행하고, 결과를 tool_result로 다시 넣어 최종 답을 받는” 최소 루프입니다. 실제 필드명은 사용하는 SDK에 맞춰 조정하세요.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const tools = [
{
name: "get_weather",
description: "도시의 현재 날씨를 조회합니다.",
input_schema: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
},
];
async function getWeather({ city }) {
// 실제로는 외부 API 호출
return { city, summary: "맑음", tempC: 3 };
}
async function run() {
const messages = [
{
role: "user",
content: "서울 날씨 알려줘",
},
];
const res1 = await client.messages.create({
model: "claude-3-...",
max_tokens: 500,
tools,
messages,
});
// SDK에 따라 res1.content가 문자열/배열로 다릅니다.
const blocks = Array.isArray(res1.content) ? res1.content : [{ type: "text", text: res1.content }];
const toolUse = blocks.find((b) => b.type === "tool_use");
if (!toolUse) {
return res1;
}
if (toolUse.name !== "get_weather") {
throw new Error("Unknown tool: " + toolUse.name);
}
// 입력 검증은 여기서 수행하는 것이 안전합니다.
const toolOutput = await getWeather(toolUse.input);
messages.push({ role: "assistant", content: blocks });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(toolOutput),
},
],
});
const res2 = await client.messages.create({
model: "claude-3-...",
max_tokens: 500,
tools,
messages,
});
return res2;
}
run().then(console.log).catch(console.error);
위 루프에서 400이 난다면, 거의 항상 다음 중 하나입니다.
tools스키마 불일치toolUse.input이 스키마를 위반tool_use_id매칭 실패- 블록의
type누락 또는 role/content 구조 위반
마무리: 400은 “재시도”가 아니라 “스키마” 문제다
tool_use는 모델의 능력을 확장하지만, 동시에 API 계약(contract)이 엄격해집니다. 400이 뜨면 우선 “서버가 나쁘다”가 아니라 내 요청이 스펙을 만족하는가를 의심해야 합니다.
정리하면, Claude 3 API tool_use 400의 대표 원인 7가지는 다음과 같습니다.
tools배열 스키마 오류tool_use.name불일치tool_use.input스키마 위반- messages/content 블록 구조 오류
tool_use.id와tool_result.tool_use_id불일치- JSON 비표준 값/직렬화 문제로 스키마가 깨짐
tool_choice등 강제 설정의 모순/버전 불일치
이 7가지만 체계적으로 점검해도, tool_use 도입 초기에 겪는 400의 대부분은 빠르게 해결됩니다.