- Published on
Claude Tool Use JSON 오류 6가지 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Claude Tool Use를 붙이면, 모델이 tool_use 형태로 구조화된 인자를 잘 내려주기만 해도 생산성이 크게 오릅니다. 문제는 “잘 내려오면”이라는 전제가 생각보다 자주 깨진다는 점입니다. 특히 JSON은 한 글자만 어긋나도 파서가 죽고, 스키마 제약이 조금만 빡세도 모델 출력이 탈선하면서 연쇄 오류로 이어집니다.
이 글에서는 현장에서 가장 많이 만나는 Claude Tool Use JSON 오류 6가지를 유형별로 정리하고, 재현 가능한 코드와 함께 해결 전략을 제공합니다. 핵심은 “모델을 믿지 말고, 계약을 강제하라”입니다.
또한 Tool Use는 결국 서버 사이드 요청-응답 파이프라인의 일부이므로, 장애 전파를 막는 관점도 중요합니다. 타임아웃과 재시도 설계는 gRPC MSA에서 DEADLINE_EXCEEDED 연쇄 장애 차단 글의 패턴을 그대로 응용할 수 있습니다. 프론트가 Next.js라면 요청 캐시/동기화 문제도 함께 보게 되는데, 이때는 Next.js 14 RSC 캐시 무효화로 데이터 꼬임 해결도 참고할 만합니다.
Tool Use JSON 오류가 생기는 구조
Claude Tool Use는 대개 다음 흐름입니다.
- 개발자가
tools에 함수 스펙(이름, 설명, 입력 JSON Schema)을 제공 - 모델이 응답 중
tool_use이벤트로inputJSON을 생성 - 서버가 JSON을 파싱하고 스키마 검증 후 실제 함수를 실행
- 결과를
tool_result로 다시 모델에 전달하거나 사용자에게 반환
오류는 주로 2번과 3번 경계에서 발생합니다. 즉, 모델 출력은 “대체로 JSON 같지만” 엄밀한 JSON이 아니거나, 스키마와 미세하게 다르거나, 타입이 틀리거나, 필드가 누락되거나, 너무 커지거나, 인코딩/이스케이프에서 깨집니다.
아래부터는 현장에서 빈도가 높은 6가지 유형과 해결책입니다.
1) 문법적으로 깨진 JSON: 따옴표, 트레일링 콤마, 주석
증상
JSON.parse실패- 에러 메시지:
Unexpected token또는Expected property name등
원인
모델이 다음을 섞어 출력하는 경우가 있습니다.
- 싱글 쿼트 사용
- 마지막 필드 뒤에 콤마
// comment같은 주석- 문자열 내부에 개행/따옴표를 잘못 넣음
해결
- 가능한 한 Tool Use의
input은 “모델이 직접 JSON만 출력하도록” 유도합니다. 즉, 자연어로 JSON을 만들게 하지 말고,tools스키마에 강하게 의존하게 합니다. - 그래도 깨질 수 있으니, 서버에서 “관대한 파서”를 두고, 실패 시 자동 복구 루틴을 둡니다.
아래는 Node.js에서 jsonrepair로 1차 복구 후 스키마 검증을 거는 패턴입니다.
import { z } from "zod";
import { jsonrepair } from "jsonrepair";
const ToolInputSchema = z.object({
query: z.string().min(1),
limit: z.number().int().min(1).max(50).default(10),
});
export function parseToolInput(raw: string) {
const repaired = jsonrepair(raw);
const parsed = JSON.parse(repaired);
return ToolInputSchema.parse(parsed);
}
운영 팁: 복구가 성공하더라도 원본과 복구본을 로깅해두면, 프롬프트/스키마 개선 포인트를 빠르게 찾을 수 있습니다.
2) 스키마 불일치: required 누락, additionalProperties, enum 탈선
증상
- 파싱은 되지만 스키마 검증에서 실패
- 예:
"limit" is required,additionalProperties not allowed,invalid enum value
원인
스키마가 너무 엄격하거나, 설명이 모호하거나, 모델이 “유용해 보이는” 필드를 임의로 추가합니다.
해결
- 입력 스키마에서 정말로 필요한 필드만
required로 둡니다. - 모델이 추가 필드를 넣어도 무시할 수 있으면
additionalProperties를 완화하거나, 서버에서strip전략을 씁니다. - enum은 설명을 강화하고, 가능한 한 “예시”를 제공하는 것이 중요합니다.
Zod 기준으로는 passthrough 또는 strip을 선택할 수 있습니다.
import { z } from "zod";
const Input = z
.object({
sort: z.enum(["relevance", "recent"]).default("relevance"),
query: z.string(),
})
.strip(); // 알 수 없는 키는 제거
type InputType = z.infer<typeof Input>;
운영 팁: additionalProperties: false를 고집하면, 모델이 "reason" 같은 설명 필드를 덧붙이는 순간 바로 실패합니다. “엄격함”이 목표가 아니라 “성공적으로 도구를 호출해 원하는 결과를 얻는 것”이 목표라면, 서버에서 제거하는 편이 더 안정적입니다.
3) 타입 오류: 숫자가 문자열로, 불리언이 "true"로
증상
limit이"10"처럼 문자열로 들어옴includeArchived가"false"문자열로 들어옴
원인
모델이 JSON 타입을 정확히 맞추지 못하거나, 프롬프트에서 값이 문자열로 예시되어 학습된 패턴을 따라갑니다.
해결
- 스키마에 “coerce” 계층을 둡니다.
- 단, 무조건 coerce하면 의미가 바뀔 수 있으니 변환 규칙을 명확히 제한합니다.
Zod의 coerce를 사용한 예시입니다.
import { z } from "zod";
const Input = z.object({
limit: z.coerce.number().int().min(1).max(100).default(10),
includeArchived: z.coerce.boolean().default(false),
query: z.string().min(1),
});
export function normalize(input: unknown) {
return Input.parse(input);
}
운영 팁: coerce를 쓰면 “문자열 "001"” 같은 값도 숫자로 변환됩니다. 감사 로그가 필요한 도메인(결제, 권한)에서는 원본 값을 별도 저장하세요.
4) 중첩 JSON/이중 인코딩: JSON 문자열 안에 JSON
증상
input이 객체가 아니라 문자열이며, 그 문자열이 다시 JSON 형태- 예:
"{\"query\":\"abc\"}"
원인
모델이 “JSON을 문자열로 감싸서” 전달하거나, 중간 레이어에서 직렬화를 한 번 더 합니다.
해결
input타입을 먼저 검사하고, 문자열이면 한 번 더 파싱합니다.- 최대 2회까지만 파싱하고, 그 이상은 실패 처리로 안전장치를 둡니다.
export function parsePossiblyDoubleEncoded(input: unknown) {
let v: unknown = input;
for (let i = 0; i < 2; i++) {
if (typeof v === "string") {
v = JSON.parse(v);
continue;
}
break;
}
if (typeof v !== "object" || v === null) {
throw new Error("tool input must be an object");
}
return v;
}
운영 팁: 이 문제는 “모델 탓”이 아니라 “SDK/중계 서버 탓”인 경우도 많습니다. 로그에 Content-Type과 직렬화 레이어(예: API Gateway mapping)를 같이 남기면 추적이 빨라집니다.
5) 길이/토큰 초과로 JSON이 잘림: 닫는 괄호 누락
증상
- JSON 마지막이 갑자기 끊김
- 닫는
}또는]가 없음
원인
- 응답 토큰 제한
- 스트리밍 중 연결 끊김
- 프롬프트가 길어져 모델이 JSON을 끝까지 완성하지 못함
해결
- Tool 입력은 “작게” 설계합니다. 큰 텍스트를 도구 입력에 넣지 말고, 별도 저장소 키나 요약본만 전달합니다.
- 스트리밍이라면
tool_use이벤트가 완결된 뒤에만 파싱합니다. - 파싱 실패 시 즉시 재시도하지 말고, “부분 JSON”을 재구성하려는 시도를 제한적으로만 수행합니다(과도한 복구는 잘못된 호출로 이어짐).
실무에서 가장 안전한 방식은 “재요청”입니다. 재요청 프롬프트에는 다음을 명시하세요.
- 이전 출력이 JSON으로 완결되지 않았음
- 오직 JSON만 출력
- 스키마를 다시 첨부
서버 측 재시도는 타임아웃과 결합해야 합니다. 연쇄 장애를 막는 패턴은 gRPC MSA에서 DEADLINE_EXCEEDED 연쇄 장애 차단의 “상위 타임아웃이 하위 타임아웃을 감싼다” 원칙을 그대로 적용하면 됩니다.
6) Tool schema 자체가 JSON Schema 규격과 어긋남
증상
- 모델이 tool을 호출하지 않음
- SDK에서
invalid schema또는tools validation error류 oneOf나anyOf같은 구성이 기대대로 동작하지 않음
원인
Claude Tool Use에서 지원하는 JSON Schema 범위는 구현/SDK마다 차이가 있고, 복잡한 스키마는 모델이 안정적으로 따르기 어렵습니다. 또한 default, nullable, format 같은 키워드가 런타임에서 무시되거나 다르게 해석될 수 있습니다.
해결
- 스키마는 단순하게: 깊은 중첩, 복잡한 분기(
oneOf)를 줄입니다. - “필드 설명”을 스키마의
description에 충분히 넣고, 허용/금지 예시를 함께 제공합니다. - 스키마를 코드로 생성한다면, 배포 전에 JSON Schema 린트와 샘플 검증을 CI에 넣습니다.
AJV로 스키마 자체를 검증하고, 샘플 입력을 통과시키는 예시입니다.
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true, strict: false });
const toolSchema = {
type: "object",
properties: {
query: { type: "string", description: "검색어" },
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
},
required: ["query"],
additionalProperties: false,
} as const;
ajv.compile(toolSchema);
const validateInput = ajv.compile(toolSchema);
const ok = validateInput({ query: "hello", limit: 10 });
if (!ok) {
console.error(validateInput.errors);
}
운영 팁: additionalProperties: false는 모델 출력 안정성을 떨어뜨릴 수 있습니다. “스키마는 단순하게, 런타임에서 strip”이 더 실전적입니다.
운영에서 바로 쓰는 방어적 파이프라인
위 6가지를 한 번에 흡수하려면, 도구 호출 처리기를 다음 순서로 고정하는 것이 좋습니다.
- 입력 수신
- 타입/이중 인코딩 정리
- 관대한 JSON 복구(선택)
- 스키마 검증 및 정규화(coerce, strip)
- 도구 실행
- 결과를 모델에 전달
- 실패 시 분류된 에러 코드와 함께 재요청 또는 폴백
간단한 예시(개념 코드)입니다.
import { z } from "zod";
import { jsonrepair } from "jsonrepair";
const Schema = z
.object({
query: z.string().min(1),
limit: z.coerce.number().int().min(1).max(50).default(10),
})
.strip();
function safeJsonParse(raw: string) {
try {
return JSON.parse(raw);
} catch {
return JSON.parse(jsonrepair(raw));
}
}
export async function handleToolUseInput(input: unknown) {
let obj: unknown = input;
// 이중 인코딩 처리
if (typeof obj === "string") obj = safeJsonParse(obj);
// 최종 스키마 정규화
const normalized = Schema.parse(obj);
// 도구 실행
return await search(normalized.query, normalized.limit);
}
async function search(query: string, limit: number) {
return { items: [], query, limit };
}
디버깅 체크리스트
- 모델이 실제로
tool_use를 호출했는가, 아니면 자연어로만 답했는가 input이 객체인가 문자열인가- 파싱 실패라면 “원본 문자열”의 마지막 몇 글자가 잘렸는지
- 스키마 실패라면 어떤 필드가 누락/초과/타입 불일치인지
- 동일 대화에서 재시도 시 프롬프트가 점점 길어지며 토큰을 잡아먹고 있지 않은지
- 장애가 반복될 때 재시도 폭주가 생기지 않도록 상한, 지수 백오프, 서킷 브레이커가 있는지
프론트가 Next.js이고 Tool 결과를 RSC나 서버 액션에서 합성한다면, 캐시 때문에 “이전 실패 결과가 재사용”되는 문제가 섞여 보일 수 있습니다. 이런 경우 캐시 무효화 설계를 다시 점검하세요: Next.js 14 RSC 캐시 무효화로 데이터 꼬임 해결
마무리
Claude Tool Use의 JSON 오류는 “모델이 가끔 실수한다” 수준이 아니라, 시스템 설계가 방어적으로 되어 있지 않으면 언제든 재현되는 종류의 문제입니다. 해결의 핵심은 다음 3가지입니다.
- 스키마는 단순하게 설계하고, 서버에서 정규화(coerce, strip)한다
- 파싱 실패를 복구할지, 재요청할지 정책을 명확히 한다
- 타임아웃/재시도/서킷 브레이커로 연쇄 장애를 막는다
위 6가지 패턴과 파이프라인을 적용하면, Tool Use를 “데모에서는 잘 되는데 운영에서 자주 터지는 기능”이 아니라 “운영 가능한 계약 기반 인터페이스”로 만들 수 있습니다.