이 글은 누구를 위한 것인가
- LLM이 툴을 잘못 호출하거나 파라미터를 채우지 못하는 팀
- tool description을 어떻게 써야 할지 모르는 개발자
- OpenAPI 스펙을 LLM 툴로 자동 변환하고 싶은 엔지니어
들어가며
함수 호출 실패의 80%는 스키마 설계 문제다. LLM은 description을 읽고 언제, 어떻게 함수를 호출할지 결정한다. description이 모호하면 LLM도 모호하게 행동한다.
이 글은 bluefoxdev.kr의 LLM 함수 호출 최적화 를 참고하여 작성했습니다.
1. 좋은 스키마 설계 원칙
[나쁜 vs 좋은 description]
나쁜 예:
name: "search"
description: "검색합니다"
→ LLM이 언제 써야 하는지 모름
좋은 예:
name: "search_products"
description: "상품 카탈로그에서 사용자의 검색어로 관련 상품을 찾습니다.
구매 의도가 있거나 특정 상품을 찾을 때 호출합니다.
일반 대화나 추천 요청 시에는 호출하지 마세요."
→ 명확한 사용 시점과 제외 조건
[파라미터 설계 원칙]
필수 vs 선택 명시: required 배열 활용
열거형 사용: enum으로 가능한 값 제한
기본값 설명: description에 default 명시
단위 명시: "가격 (원화, 정수)" 처럼
예시 포함: "예: '나이키 운동화'"
[에러 복구 설계]
툴 실패 시 LLM이 다른 방법 시도 가능하게
에러 메시지를 구체적으로: "상품 ID가 없음" not "에러"
부분 성공 표현: 10개 중 7개 처리 완료
2. 스키마 설계 및 검증
import anthropic
import json
from typing import Any
client = anthropic.Anthropic()
# 잘 설계된 툴 스키마 예시
WELL_DESIGNED_TOOLS = [
{
"name": "search_products",
"description": """상품 카탈로그에서 사용자의 검색어로 관련 상품을 조회합니다.
사용자가 특정 상품을 찾거나 구매 의도를 보일 때 호출하세요.
카테고리 필터와 가격 범위로 결과를 좁힐 수 있습니다.
최대 20개 결과를 반환합니다.""",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색어. 예: '방수 등산화 남성용'",
},
"category": {
"type": "string",
"description": "상품 카테고리 필터 (선택사항)",
"enum": ["전자제품", "의류", "식품", "스포츠", "뷰티", "도서"],
},
"min_price": {
"type": "integer",
"description": "최소 가격 (원화, 정수). 예: 10000",
},
"max_price": {
"type": "integer",
"description": "최대 가격 (원화, 정수). 지정 안 하면 제한 없음",
},
"sort_by": {
"type": "string",
"enum": ["relevance", "price_asc", "price_desc", "rating", "newest"],
"description": "정렬 기준. 기본값: relevance",
},
"limit": {
"type": "integer",
"description": "반환할 최대 결과 수 (1-20). 기본값: 10",
"minimum": 1,
"maximum": 20,
},
},
"required": ["query"],
},
},
{
"name": "get_product_details",
"description": "상품 ID로 상품 상세 정보를 조회합니다. search_products로 찾은 상품의 ID를 사용하세요.",
"input_schema": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"description": "상품 고유 ID. 형식: 'PROD-XXXXXX'",
"pattern": "^PROD-[A-Z0-9]{6}$",
},
"include_reviews": {
"type": "boolean",
"description": "리뷰 포함 여부. 기본값: false",
},
},
"required": ["product_id"],
},
},
]
def validate_tool_input(tool_name: str, tool_input: dict, schema: dict) -> tuple[bool, str]:
"""툴 입력 검증"""
from jsonschema import validate, ValidationError
try:
validate(instance=tool_input, schema=schema["input_schema"])
return True, ""
except ValidationError as e:
return False, f"입력 검증 실패: {e.message}"
def openapi_to_claude_tool(openapi_spec: dict, operation_id: str) -> dict:
"""OpenAPI 스펙을 Claude 툴 스키마로 변환"""
for path, path_item in openapi_spec.get("paths", {}).items():
for method, operation in path_item.items():
if operation.get("operationId") == operation_id:
parameters = operation.get("parameters", [])
request_body = operation.get("requestBody", {})
properties = {}
required = []
for param in parameters:
name = param["name"]
schema = param.get("schema", {})
properties[name] = {
"type": schema.get("type", "string"),
"description": param.get("description", ""),
}
if "enum" in schema:
properties[name]["enum"] = schema["enum"]
if param.get("required"):
required.append(name)
return {
"name": operation_id,
"description": operation.get("summary", ""),
"input_schema": {
"type": "object",
"properties": properties,
"required": required,
},
}
raise ValueError(f"Operation {operation_id} not found")
async def test_tool_schema(tool: dict, test_queries: list[str]) -> dict:
"""툴 스키마 품질 테스트"""
results = {"successful_calls": 0, "failed_calls": 0, "errors": []}
for query in test_queries:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=500,
tools=[tool],
messages=[{"role": "user", "content": query}],
)
tool_uses = [b for b in response.content if b.type == "tool_use"]
if tool_uses:
is_valid, error = validate_tool_input(
tool["name"], tool_uses[0].input, tool
)
if is_valid:
results["successful_calls"] += 1
else:
results["failed_calls"] += 1
results["errors"].append({"query": query, "error": error})
else:
results["failed_calls"] += 1
results["errors"].append({"query": query, "error": "툴 미호출"})
results["success_rate"] = results["successful_calls"] / len(test_queries)
return results
마무리
좋은 툴 description은 세 가지를 담아야 한다: 언제 호출하는가, 무엇을 하는가, 언제 호출하지 않는가. 파라미터에 enum을 적극 활용하면 LLM의 자유도를 줄여 에러를 방지한다. 스키마 변경 시 20개 테스트 쿼리로 success rate를 측정하면 퇴행을 조기에 발견할 수 있다.