좋은 도구 설계 원칙과 안티패턴
도구 정의가 문법적으로 올바른 것과 잘 설계된 것은 다르다. 잘 설계된 도구는 모델이 정확한 시점에, 정확한 인자로, 꼭 필요한 만큼만 호출하게 만든다. 잘못 설계된 도구는 과다 호출·과소 호출·잘못된 인자·캐시 파괴를 유발한다. 이 절은 CCA-F가 "이 도구 정의의 무엇이 문제인가"를 묻는 전형적 패턴을 원칙과 안티패턴으로 정리한다.
원칙 1 — 설명은 "무엇"이 아니라 "언제"를 명시한다
모델은 description으로 호출 시점을 판단한다. 단순히 도구가 무엇을 하는지만 적으면 호출 정확도가 낮다. 트리거 조건을 명세적으로 적으면 측정 가능한 호출 정확도 향상이 나타난다. 특히 최신 Opus 모델은 도구를 더 보수적으로 호출하는 경향이 있어, 설명에 "이럴 때 부르라"는 조건을 넣는 것이 더 중요해졌다.
# tools/search_db.py — 나쁜 예와 좋은 예bad = { "name": "search", "description": "Search the database.", # 무엇만 말하고 언제를 안 말함 "input_schema": {"type": "object", "properties": {"q": {"type": "string"}}},}good = { "name": "search_orders", "description": ( "Search the order database by customer or product. " "Call this when the user asks about past orders, order status, " "or whether a specific item was purchased — do not answer from prior knowledge." ), "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "Customer name or product name to search"} }, "required": ["query"], },}
함정: 시험은 종종 CRITICAL: You MUST always use this tool처럼 과격한 강제 문구가 들어간 설명을 보여주고 "무엇이 문제인가"를 묻는다. 최신 Opus 모델(4.6 이후)은 시스템 프롬프트와 도구 설명을 훨씬 충실히 따르기 때문에, 과거 모델의 소극성을 극복하려고 쓰던 강제 문구가 이제는 과다 호출(overtriggering)을 일으킨다. 정답은 "강제 문구를 빼고 트리거 조건을 차분히 서술하라"이지, "가드레일을 더 추가하라"가 아니다.
원칙 2 — 도구 수를 통제하고, 많으면 동적 발견을 쓴다
도구가 너무 많으면 모델이 혼란스러워져 잘못된 도구를 고른다. 도구 집합은 작고 초점이 분명해야 한다. 그러나 정말로 많은 도구가 필요한 경우(수십 개의 도구 라이브러리)에는 모든 스키마를 컨텍스트에 미리 올리지 말고 도구 검색(tool search)을 쓴다. 도구 검색은 관련 도구의 스키마만 골라 로드하며, 스키마를 교체(swap)하지 않고 추가(append)하기 때문에 프롬프트 캐시를 보존한다. 이것이 캐시 관점에서 중요한 차이다. 도구를 추가·제거·재정렬하면 도구는 프롬프트의 위치 0에 렌더링되므로 전체 캐시가 무효화된다.
원칙 3 — bash로 시작하되, 필요할 때 전용 도구로 승격한다
도구 표면(tool surface) 설계의 핵심 의사결정이다. bash 도구는 모델에게 폭넓은 프로그래밍 레버리지를 준다. 거의 모든 작업을 할 수 있다. 그러나 하네스에는 불투명한 명령 문자열 하나만 전달되며, 모든 작업이 같은 형태로 들어온다. 어떤 작업을 전용 도구로 승격하면, 하네스는 타입이 있는 인자를 받는 작업별 후크를 얻게 되어 그 작업을 가로채고(intercept)·게이팅하고(gate)·렌더링하고(render)·감사하고(audit)·병렬화할(parallelize) 수 있다.
전용 도구로 승격할 기준은 네 가지다.
- 보안 경계. 게이팅이 필요한 작업, 특히 되돌리기 어려운 작업(외부 API 호출, 메시지 전송, 데이터 삭제)은 사용자 확인 뒤로 게이팅할 수 있다.
send_email도구는 게이팅하기 쉽지만bash -c "curl -X POST ..."는 어렵다. - 신선도 검사. 전용
edit도구는 모델이 마지막으로 읽은 뒤 파일이 바뀌었으면 쓰기를 거부할 수 있다. bash는 이 불변식을 강제하지 못한다. - 렌더링. 어떤 작업은 커스텀 UI가 필요하다. Claude Code는 "질문하기"를 도구로 승격해 모달로 렌더링하고 선택지를 제시하며 에이전트 루프를 차단한다.
- 스케줄링.
glob·grep같은 읽기 전용 도구는 병렬 안전으로 표시할 수 있다. 같은 작업이 bash를 통해 들어오면 하네스는 병렬 안전한 grep과 병렬 위험한git push를 구분하지 못해 직렬화할 수밖에 없다.
요지: 폭(breadth)이 필요하면 bash로 시작하고, 게이팅·렌더링·감사·병렬화가 필요하면 전용 도구로 승격하라. 시험은 "이 작업을 bash로 둘지 전용 도구로 만들지"를 묻고, 정답의 근거로 위 네 기준 중 하나(특히 보안 경계와 되돌릴 수 없음)를 댈 수 있어야 한다.
원칙 4 — 스키마와 도구 집합을 결정적으로 유지해 캐시를 지킨다
프롬프트 캐싱은 접두사 일치(prefix match)다. 렌더링 순서는 tools → system → messages이므로, 도구 정의는 가장 앞에 온다. 도구를 추가·제거·재정렬하면 모든 캐시가 무효화된다. 따라서 도구 목록은 결정적으로 직렬화해야 한다(예: 이름순 정렬). 또한 도구 설명이나 스키마에 타임스탬프·UUID 같은 휘발성 값을 끼워 넣으면 매 요청마다 접두사가 달라져 캐시가 절대 적중하지 않는다. 사용자별로 도구 집합을 다르게 만드는 tools=build_tools(user) 패턴도 위치 0의 도구가 사용자마다 달라지므로 교차 사용자 캐시 공유를 막는다. "모드"가 필요하면 도구 집합을 교체하지 말고, 모드 전환을 기록하는 도구를 주거나 모드를 메시지 내용으로 전달하라.
원칙 5 — 부작용 도구는 검증하고 사람의 승인을 고려한다
도구 러너(tool runner)는 모델이 요청하면 네 함수를 자동 실행한다. 이메일 전송·DB 수정·금융 거래처럼 부작용이 있는 도구는 함수 안에서 입력을 검증하고, 파괴적 작업에는 확인을 요구하라. 각 실행 전 사람의 승인이 필요하면 도구 러너 대신 수동 에이전트 루프를 써서 사람-개입(human-in-the-loop)을 구현한다. 또한 도구 실행이 실패하면 결과에 is_error: true와 정보가 담긴 오류 메시지를 넣어 모델이 다른 접근을 시도하거나 명확화를 요청하도록 한다.
정리
- 설명은 "무엇"이 아니라 "언제 부를지"를 명세적으로 적는다.
CRITICAL/MUST같은 강제 문구는 최신 모델에서 과다 호출을 일으키므로 빼고 트리거 조건을 차분히 서술한다. - 도구 수는 작고 초점 있게 유지하고, 정말 많으면 도구 검색으로 동적 발견한다. 도구 검색은 스키마를 추가하므로 캐시를 보존한다.
- bash는 폭이 필요할 때 쓰고, 보안 경계·신선도 검사·렌더링·병렬화가 필요한 작업은 전용 도구로 승격한다. 되돌릴 수 없는 작업은 전용 도구로 게이팅한다.
- 도구 집합과 스키마를 결정적으로 유지하라. 도구 추가·제거·재정렬, 설명 속 타임스탬프·UUID, 사용자별 도구 집합은 위치 0의 접두사를 바꿔 캐시를 파괴한다.
- 부작용 도구는 입력을 검증하고, 승인이 필요하면 도구 러너 대신 수동 루프를 쓴다. 실패 시
is_error: true로 모델이 회복하게 한다.