실무 Multi-Agent 오케스트레이션 7장 · 다중 도구 라우팅 7 / 7 ← 이전목차다음 → TechLead Cro

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에 추가한다고 합시다. 각 질문의 기대 도구를 정하고, 그렇게 정한 이유를 한 문장씩 적으세요. 지금 도구 네 개로는 답할 수 없는 질문이 하나 섞여 있습니다. 어느 것일까요. (코드를 실행하지 않고 글로 답합니다.)

  1. "운동화 얼마예요?"
  2. "주문 HR20260620024 결제 금액이 얼마였죠?"
  3. "제가 지난주에 주문한 텀블러 언제 와요?"
해답 보기
  1. search_products — 가격은 상품 자체의 정보이고, 고객은 상품ID가 아니라 이름을 말했습니다.
  2. get_order_status — 주문번호가 있고, 묻는 것은 배송 위치가 아니라 주문 내용(결제 금액)입니다. track_shipping은 택배사와 송장번호를 돌려줄 뿐 금액은 돌려주지 않습니다.
  3. 지금 도구로는 답할 수 없습니다. 주문 도구 두 개는 모두 주문번호를 받는데 고객은 주문번호를 말하지 않았습니다. "텀블러"라는 말 때문에 search_products가 불릴 수 있지만, 그것은 상품 카탈로그를 볼 뿐 이 고객의 주문을 찾지 못합니다.

3번의 올바른 기대는 "도구를 부르지 않고 주문번호를 되묻는다"입니다. 이런 질문도 기대 도구 표에 넣을 수 있습니다. 기대를 None으로 두고 called가 비어 있는지 보면 됩니다.

그리고 3번은 지금 도구의 빈자리를 보여 줍니다. "이 고객의 주문 목록"을 조회하는 도구가 없습니다. 그 도구를 만들려면 먼저 "이 고객이 누구인가"를 알아야 합니다. 지금 도구는 주문번호만 알면 누구의 주문이든 보여 줍니다. 이 문제는 8장에서 다룹니다.

← 이전 절정리와 체크리스트8장 →왜 안전한 도구가 필요한가 — 주문번호만 알면 누구 주문이든 보인다
오명운 · macro@prag-ai.com