7장. 다중 도구 라우팅
실습문제와 해답
7장에서 배운 내용을 직접 측정하고 확장하며 익힙니다. 먼저 스스로 해 본 뒤 해답을 펼쳐 보세요. 해답은 새 파일로 만들어 실행합니다. 두 파일 모두 lesson06_order_tools.py와 lesson07_multi_tools.py가 폴더에 있어야 합니다.
문제 1 — 도구 선언은 몇 토큰인가
"안녕하세요" 한마디를 도구 없이, 주문 도구 2개와 함께, 도구 4개와 함께 세 번 보내고 입력 토큰을 비교하는 파일 ex07_token_cost.py를 만드세요.
- 입력 토큰은
response.usage_metadata.prompt_token_count에 들어 있습니다. - 도구는
lesson06_order_tools.py와lesson07_multi_tools.py에서 불러옵니다. - 실행하기 전에 짐작해 보세요. 도구 4개를 붙이면 입력이 몇 토큰쯤 될까요?
해답 보기
from google.genai import types
from config import MODEL, get_client
from lesson06_order_tools import get_order_status, track_shipping
from lesson07_multi_tools import check_stock, search_products
client = get_client()
tool_sets = {
"도구 0개": [],
"도구 2개": [get_order_status, track_shipping],
"도구 4개": [get_order_status, track_shipping, search_products, check_stock],
}
base = None
for label, tools in tool_sets.items():
r = client.models.generate_content(
model=MODEL,
contents="안녕하세요",
config=types.GenerateContentConfig(tools=tools) if tools else None,
)
n = r.usage_metadata.prompt_token_count
if base is None:
base = n
print(f"{label}: 입력 {n}토큰 (도구 선언 몫 {n - base}토큰)")
$ python ex07_token_cost.py
실행 결과
도구 0개: 입력 2토큰 (도구 선언 몫 0토큰)
도구 2개: 입력 188토큰 (도구 선언 몫 186토큰)
도구 4개: 입력 402토큰 (도구 선언 몫 400토큰)
해설 — 고객이 보낸 글은 세 번 모두 같습니다. 달라진 것은 tools=뿐인데 입력이 2토큰에서 402토큰이 되었습니다. 도구 하나가 대략 100토큰입니다.
모델은 이 인사에 도구를 부르지 않습니다. 그래도 선언은 읽어야 하므로 토큰이 듭니다. 도구를 붙인다는 것은 그 도구를 쓰지 않는 호출에도 비용을 더한다는 뜻입니다.
독스트링을 길게 쓰면 이 숫자도 커집니다. 설명은 경계를 긋는 데 필요한 만큼만 씁니다. 숫자는 SDK와 모델의 버전에 따라 조금 다를 수 있습니다.
문제 2 — 도구를 하나 더 추가하고 라우팅 다시 재기
하루마켓에 "어떤 종류의 상품을 파나요?"라는 문의가 들어옵니다. 지금 도구 네 개 중에는 카테고리 목록을 돌려주는 것이 없습니다. 다섯 번째 도구 list_categories를 만들고, 도구 다섯 개로 라우팅을 다시 재는 파일 ex07_fifth_tool.py를 만드세요.
list_categories()는 인자가 없고,products.csv의 카테고리 이름과 카테고리별 상품 수를 돌려줍니다.- 독스트링에 무엇을 하는가, 언제 쓰는가, 언제 쓰지 않는가를 적습니다.
lesson07_multi_tools.py의ask()에tools=로 도구 다섯 개를 넘깁니다.- 질문 다섯 개와 기대 도구를 먼저 적고 정확도를 셉니다. 새 도구를 부를 질문 하나와, 원래 도구 네 개가 각각 불릴 질문을 넣습니다.
실행하기 전에 짐작해 보세요. 도구가 하나 늘어도 원래 질문들은 여전히 같은 도구로 갈까요?
해답 보기
import lesson07_multi_tools as m
def list_categories() -> list[dict]:
"""하루마켓이 판매하는 상품 카테고리 목록과 카테고리별 상품 수를 조회한다.
'어떤 종류의 상품을 파는지' 묻는 문의에 사용한다.
특정 상품의 가격·옵션·재고 조회에는 사용하지 않는다.
"""
counts = m.PRODUCTS["category"].value_counts()
return [{"category": name, "product_count": int(n)} for name, n in counts.items()]
FIVE_TOOLS = m.ALL_TOOLS + [list_categories]
test_set = [
("주문 HR20260620069 배송 어디쯤이에요?", "track_shipping"),
("텀블러 보온 몇 시간 가나요?", "search_products"),
("P008 텀블러 재고 몇 개 남았어요?", "check_stock"),
("HR20260620069 주문한 거 뭐였죠?", "get_order_status"),
("하루마켓에서는 어떤 종류의 상품을 파나요?", "list_categories"),
]
correct = 0
for q, expected in test_set:
print(f"\n고객: {q} (기대 도구: {expected})")
called = m.ask(q, tools=FIVE_TOOLS)
ok = expected in called
correct += ok
print(f" → {'기대한 도구 호출됨' if ok else '라우팅 어긋남: ' + str(called)}")
print(f"\n라우팅 정확도: {correct}/{len(test_set)}")
$ python ex07_fifth_tool.py
실행 결과 (문장은 실행마다 다릅니다)
고객: 주문 HR20260620069 배송 어디쯤이에요? (기대 도구: track_shipping)
[도구] track_shipping({'order_id': 'HR20260620069'})
하루: 고객님의 주문 상품은 우체국택배(송장번호: 83455521031)로 지난 6월 21일에 출고되어 현재 배송 중입니다. 정확한 배송 위치나 도착 예정일은 도구로 조회된 데이터가 없어 확인이 어려운 점 양해 부탁드립니다. 추가로 궁금하신 사항이 있으시면 언제든지 말씀해 주세요.
→ 기대한 도구 호출됨
고객: 텀블러 보온 몇 시간 가나요? (기대 도구: search_products)
[도구] search_products({'keyword': '텀블러'})
하루: 스테인리스 텀블러 500ml 상품은 12시간 보온 및 보냉 기능을 제공합니다. 와이드 입구로 제작되어 세척이 편리하며, 가격은 18,900원입니다. 실버, 블랙, 민트 세 가지 옵션으로 준비되어 있습니다.
→ 기대한 도구 호출됨
고객: P008 텀블러 재고 몇 개 남았어요? (기대 도구: check_stock)
[도구] check_stock({'product_id': 'P008'})
하루: '스테인리스 텀블러 500ml'(상품ID: P008)의 현재 재고는 150개 남아 있습니다. 옵션별 상세 재고는 확인되지 않지만, 전체 재고는 여유 있게 준비되어 있습니다. 추가로 궁금한 점이 있으시면 언제든 말씀해 주세요.
→ 기대한 도구 호출됨
고객: HR20260620069 주문한 거 뭐였죠? (기대 도구: get_order_status)
[도구] get_order_status({'order_id': 'HR20260620069'})
하루: 고객님께서 주문하신 상품은 '콜드브루 원액 500ml 2병'이며, 옵션은 단일 상품입니다. 2026년 6월 20일에 1개 주문하셨고, 현재 배송 중 상태입니다. 추가로 배송 조회나 도움이 필요하시면 언제든 말씀해 주세요.
→ 기대한 도구 호출됨
고객: 하루마켓에서는 어떤 종류의 상품을 파나요? (기대 도구: list_categories)
[도구] list_categories({})
하루: 하루마켓에서는 디지털, 패션의류, 식품·음료, 패션잡화, 신발 등 다양한 카테고리의 상품을 판매하고 있습니다. 이 외에도 주방용품, 주방가전, 생활가전, 홈·침구, 홈·인테리어 등 일상생활에 필요한 여러 품목을 만나보실 수 있습니다. 궁금하신 특정 카테고리가 있으시면 편하게 문의해 주세요.
→ 기대한 도구 호출됨
라우팅 정확도: 5/5
해설 — 도구가 다섯 개가 되어도 5/5입니다. 이 교재를 준비하며 세 번 실행했을 때 세 번 모두 5/5였습니다.
- 새 질문은 새 도구로 갔습니다. "어떤 종류의 상품을 파나요?"에
list_categories가 불렸습니다.search_products도 상품을 다루는 도구이지만, 새 도구의 독스트링에 "어떤 종류의 상품을 파는지 묻는 문의에 사용한다", "특정 상품의 가격·옵션·재고 조회에는 사용하지 않는다"고 경계를 적어 두었습니다. - 원래 질문들은 원래 도구로 갔습니다. 도구를 하나 붙인 뒤에 확인할 것은 새 도구만이 아닙니다. 새 설명이 기존 도구의 영역과 겹치면 멀쩡하던 질문이 다른 도구로 갈 수 있으므로, 표 전체를 다시 돌려 확인합니다.
list_categories({})처럼 인자가 빈 딕셔너리로 찍혔습니다. 인자가 없는 도구는 이렇게 불립니다.
도구를 추가하는 순서를 정리하면 이렇습니다. 함수를 만든다 → 독스트링에 경계를 적는다 → 기대 도구 표에 질문을 더한다 → 표 전체를 다시 돌린다. 문제 1에서 본 것처럼 도구 하나는 매 호출의 입력 토큰을 늘리므로, 꼭 필요한 도구만 붙입니다.
문제 3 — 기대 도구를 정해 보기
아래 세 질문을 test_set에 추가한다고 합시다. 각 질문의 기대 도구를 정하고, 그렇게 정한 이유를 한 문장씩 적으세요. 지금 도구 네 개로는 답할 수 없는 질문이 하나 섞여 있습니다. 어느 것일까요. (코드를 실행하지 않고 글로 답합니다.)
- "운동화 얼마예요?"
- "주문 HR20260620024 결제 금액이 얼마였죠?"
- "제가 지난주에 주문한 텀블러 언제 와요?"
해답 보기
search_products— 가격은 상품 자체의 정보이고, 고객은 상품ID가 아니라 이름을 말했습니다.get_order_status— 주문번호가 있고, 묻는 것은 배송 위치가 아니라 주문 내용(결제 금액)입니다.track_shipping은 택배사와 송장번호를 돌려줄 뿐 금액은 돌려주지 않습니다.- 지금 도구로는 답할 수 없습니다. 주문 도구 두 개는 모두 주문번호를 받는데 고객은 주문번호를 말하지 않았습니다. "텀블러"라는 말 때문에
search_products가 불릴 수 있지만, 그것은 상품 카탈로그를 볼 뿐 이 고객의 주문을 찾지 못합니다.
3번의 올바른 기대는 "도구를 부르지 않고 주문번호를 되묻는다"입니다. 이런 질문도 기대 도구 표에 넣을 수 있습니다. 기대를 None으로 두고 called가 비어 있는지 보면 됩니다.
그리고 3번은 지금 도구의 빈자리를 보여 줍니다. "이 고객의 주문 목록"을 조회하는 도구가 없습니다. 그 도구를 만들려면 먼저 "이 고객이 누구인가"를 알아야 합니다. 지금 도구는 주문번호만 알면 누구의 주문이든 보여 줍니다. 이 문제는 8장에서 다룹니다.