지난 글까지 호출을 어떻게 실행하고 묶을지를 봤다. 그런데 실행 전에 결정되는 것이 하나 있다. 모델이 그 도구를 고를지, 인자를 어떻게 채울지는 전적으로 스키마에 적힌 텍스트만 보고 정해진다. 함수 본문이 얼마나 정확하든 모델은 그것을 볼 수 없다. 이 사실을 받아들이면 도구 스키마는 API 문서가 아니라 프롬프트의 일부라는 게 분명해진다.
이름이 가장 강한 신호다
모델은 도구 목록을 훑을 때 이름을 먼저 본다. 설명문은 후보를 좁힌 뒤에 읽는다. 그래서 이름 하나만 고쳐도 선택 정확도가 눈에 띄게 바뀐다.
좋은 이름은 동사 + 대상의 꼴이고, 대상이 구체적이다.
| 나쁜 이름 | 왜 | 나은 이름 |
|---|---|---|
search |
무엇을 검색하는지 없다 | search_orders |
get_data |
대상도 동작도 모호하다 | get_customer_profile |
handler |
동사가 아니다 | cancel_subscription |
query_v2 |
버전은 모델에게 의미가 없다 | query_inventory |
do_refund_and_notify |
두 가지를 한다 | issue_refund + send_notice |
마지막 줄은 이름 문제로 보이지만 실은 설계 문제다. 한 도구가 두 가지를 하면 모델은 둘 중 하나만 필요한 상황에서도 그 도구를 부르거나, 부르기를 망설인다. 부작용이 다르면 도구를 나눈다.
설명문에는 '언제 쓰지 않는가'를 쓴다
설명문에서 자주 빠지는 것은 기능이 아니라 경계다. 무엇을 하는지는 대개 이름에서 짐작되지만, 무엇을 못 하는지는 적어 두지 않으면 알 방법이 없다.
{
"name": "search_orders",
"description": "주문 이력을 키워드로 검색한다. 최근 2년치만 들어 있고, 배송 추적 상태는 포함되지 않는다(그쪽은 track_shipment를 쓸 것). 주문번호를 이미 알고 있다면 이 도구 대신 lookup_order를 쓴다.",
"input_schema": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "상품명 또는 주문자명. 주문번호를 여기에 넣지 않는다."
},
"since": {
"type": "string",
"format": "date",
"description": "이 날짜 이후 주문만. YYYY-MM-DD. 사용자가 기간을 말하지 않았으면 생략한다."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 20
}
},
"required": ["keyword"]
}
}
설명문에 들어간 정보가 넷이다. 범위(최근 2년), 제외 항목(배송 상태), 대안 도구 둘. 이 중 대안 도구를 가리키는 문장이 특히 잘 듣는다. 비슷한 도구가 여럿일 때 모델이 헤매는 이유는 각 도구가 자기 얘기만 하기 때문이다. 서로를 가리켜 주면 경계가 생긴다.
인자 쪽도 마찬가지다. since의 "사용자가 기간을 말하지 않았으면 생략한다"는 한 줄이 없으면 모델은 오늘 날짜나 임의의 과거 날짜를 채워 넣는다. 선택 인자는 비워도 된다는 사실을 명시해야 실제로 비운다.
타입으로 말할 수 있는 것은 설명문에 쓰지 않는다
설명문은 토큰을 쓰고, 모델이 반드시 지킨다는 보장도 없다. 반면 스키마의 제약은 구조화 출력 경로에서 강제되는 경우가 많다. 같은 내용이라면 제약 쪽에 넣는 편이 싸고 확실하다.
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"]
}
이렇게 쓰면 "description": "상태는 pending, shipped, delivered, cancelled 중 하나입니다"가 통째로 필요 없어진다. 같은 요령으로 옮길 수 있는 것들이 있다.
| 설명문에 쓰던 말 | 옮길 곳 |
|---|---|
| "~ 중 하나" | enum |
| "1에서 50 사이" | minimum / maximum |
| "YYYY-MM-DD 형식" | format: "date" |
| "ORD-로 시작하는 8자리" | pattern |
| "비워도 됨" | required에서 빼기 |
| "기본값은 20" | default |
설명문에는 제약으로 표현할 수 없는 것만 남긴다 — 값의 출처, 쓰지 말아야 할 상황, 다른 도구와의 관계. 이렇게 정리하면 스키마가 짧아지면서 오히려 정확해진다.
도구가 몇 개까지 괜찮은가
도구 목록은 매 요청마다 통째로 컨텍스트에 들어간다. 개수가 늘면 두 가지가 동시에 나빠진다. 토큰이 늘고, 비슷한 도구 사이의 혼동이 는다. 후자가 먼저 온다.
경험적으로 갈리는 지점은 이렇다.
- 10개 이하 — 대체로 문제없다. 이름과 설명만 정리하면 된다.
- 10~30개 — 혼동이 보이기 시작한다. 이름의 접두사로 묶고(
order_*,ship_*), 겹치는 도구를 합치거나 경계 문장을 넣는다. - 30개 이상 — 목록을 그대로 다 주면 안 된다. 상황에 따라 부분집합만 노출한다.
마지막 경우의 흔한 처리가 단계별 노출이다. 대화 초반에는 도구를 몇 개만 주고, 사용자의 의도가 정해지면 그 영역의 도구를 추가로 붙인다. 다른 방법은 도구를 계층으로 두는 것이다 — 상위 도구 하나가 영역을 고르고, 그 결과로 하위 도구 목록이 들어온다. 어느 쪽이든 공통점은 하나다. 한 번에 모델이 판단해야 할 선택지 수를 줄인다.
여기서 주의할 것은 캐시다. 도구 목록은 프롬프트 앞쪽의 고정 부분이라 캐시가 가장 잘 듣는 자리인데, 대화 중간에 목록을 바꾸면 그 지점부터 캐시가 깨진다. 도구를 동적으로 붙일 거라면 바뀌는 지점을 최대한 뒤로 두거나, 대화 시작 시점에 한 번만 정하는 편이 낫다.
반환값도 스키마 설계의 일부다
입력 스키마만 다듬고 반환값을 방치하는 경우가 많은데, 모델이 다음 행동을 정하는 재료는 반환값이다. 두 가지만 지켜도 크게 나아진다.
필요한 필드만 돌려준다. 내부 API 응답을 그대로 넘기면 모델이 안 쓰는 필드 수십 개가 매 호출마다 컨텍스트에 쌓인다. 도구 결과는 대화 이력에 남아 다음 턴에도 계속 실려 다니므로, 여기서 절약한 토큰은 반복해서 절약된다.
빈 결과에 말을 붙인다. []만 돌려주면 모델은 이것이 "없음"인지 "조회 실패"인지 구별하지 못한다. {"results": [], "note": "조건에 맞는 주문이 없다. 기간을 넓히거나 키워드를 바꿔 볼 것"} 정도면 충분하다.
스키마를 고쳤는지 확인하는 법
스키마 수정은 프롬프트 수정과 똑같이 취급해야 한다. 즉, 고치기 전후를 같은 입력으로 비교해야 한다. 최소한의 형태는 이렇다 — 도구를 써야 하는 질문 20개와 쓰면 안 되는 질문 20개를 모아 두고, 스키마를 고칠 때마다 선택률(써야 할 때 골랐는가)과 오선택률(안 써도 될 때 골랐는가)을 함께 본다.
둘을 같이 봐야 하는 이유가 있다. 설명문에 "적극적으로 사용하세요"를 넣으면 선택률은 오르지만 오선택률이 같이 오른다. 한쪽만 보면 개선으로 착각하기 쉽다.
도구 스키마는 코드베이스에서 가장 자주 방치되면서 가장 자주 읽히는 텍스트다. 호출이 이상할 때 프롬프트를 열기 전에 그 도구의 설명문부터 다시 읽어 볼 값어치가 있다.
읽어주셔서 감사합니다. 😊

