실무 Multi-Agent 오케스트레이션 6장 · 데이터 조회 도구 구현 5 / 7 ← 이전목차다음 → TechLead Cro

6장. 데이터 조회 도구 구현

따라하기 — 주문 조회 도구 만들기

목표

data/orders.csv를 조회하는 도구 두 개를 만들고, 배송 중인 주문, 출고 전인 주문, 없는 주문번호 세 가지를 물어봅니다. 모델이 질문에 따라 어떤 도구를 불렀는지, 답변의 내용이 실제 데이터와 맞는지 확인합니다.


0. 실습 준비

VS Code에서 haru-market 폴더를 열고 터미널에서 환경을 켭니다.

$ conda activate myenv

이번 장은 새로 설치할 것이 없습니다. data/orders.csv가 있는지만 확인합니다.

$ python config.py

데이터 폴더 줄 끝에 (있음)이 보이면 됩니다.


1. 파일 만들기

haru-market 폴더 맨 위에 새 파일을 만듭니다.

lesson06_order_tools.py

아래 코드 전체를 복사해 붙여 넣고 저장합니다.

# -*- coding: utf-8 -*-
"""[6장] 주문·배송 조회 도구 — data/orders.csv 실제 연동

5장의 가짜 구현을 실제 데이터 조회로 교체한다.
또한 google-genai의 '자동 함수 호출'을 사용한다:
  - 파이썬 함수를 tools=[...] 에 그대로 넘기면
  - SDK가 타입힌트·독스트링으로 도구 선언을 만들고
  - function call → 실행 → 결과 반환 왕복까지 대신 해 준다.
  (5장에서 손으로 했던 그 왕복이다. 원리를 알고 쓰는 것과 모르고 쓰는 것은 다르다.)

실행:  python lesson06_order_tools.py
"""
import pandas as pd
from google.genai import types

from config import DATA_DIR, MODEL, get_client

client = get_client()
ORDERS = pd.read_csv(DATA_DIR / "orders.csv", dtype=str)


# ── 도구 1: 주문 상태 조회 ────────────────────────────────────────────
# 독스트링이 곧 모델이 보는 도구 설명서다. 첫 줄 요약 + Args 를 정확히 쓴다.
def get_order_status(order_id: str) -> dict:
    """하루마켓 주문번호로 주문 상태를 조회한다.

    Args:
        order_id: 하루마켓 주문번호. 'HR'로 시작한다. 예: HR20260701023
    """
    row = ORDERS[ORDERS["order_id"] == order_id]
    if row.empty:
        return {"found": False, "message": f"주문번호 {order_id} 를 찾을 수 없습니다."}
    r = row.iloc[0]
    return {
        "found": True,
        "order_id": r["order_id"],
        "product_name": r["product_name"],
        "option": r["option"],
        "quantity": r["quantity"],
        "order_amount": r["order_amount"],
        "ordered_at": r["ordered_at"],
        "status": r["order_status"],
    }


# ── 도구 2: 배송 추적 ────────────────────────────────────────────────
def track_shipping(order_id: str) -> dict:
    """주문번호로 택배사·송장번호·출고일·배송완료일을 조회한다. 배송 위치 문의에 사용한다.

    Args:
        order_id: 하루마켓 주문번호. 'HR'로 시작한다.
    """
    row = ORDERS[ORDERS["order_id"] == order_id]
    if row.empty:
        return {"found": False, "message": f"주문번호 {order_id} 를 찾을 수 없습니다."}
    r = row.iloc[0]
    if not isinstance(r["tracking_no"], str) or r["tracking_no"] == "":
        return {
            "found": True, "shipped": False,
            "status": r["order_status"],
            "message": "아직 출고 전이라 송장이 등록되지 않았습니다. "
                       "출고된 뒤 다시 조회하면 확인할 수 있습니다.",
        }
    return {
        "found": True, "shipped": True,
        "courier": r["courier"], "tracking_no": r["tracking_no"],
        "shipped_at": r["shipped_at"],
        "delivered_at": r["delivered_at"] if isinstance(r["delivered_at"], str) else "배송중",
        "status": r["order_status"],
    }


SYSTEM = """당신은 하루마켓 고객지원 상담원 '하루'입니다.
주문 관련 문의는 반드시 도구로 조회한 실제 데이터로만 답합니다.
조회 결과에 없는 내용(도착 예정 시간 등)은 추측하지 않습니다.
존댓말로 3~5문장 이내로 답하고, 마지막에 다음 행동을 안내합니다."""


def ask(question: str):
    print(f"\n고객: {question}")
    response = client.models.generate_content(
        model=MODEL,
        contents=question,
        config=types.GenerateContentConfig(
            system_instruction=SYSTEM,
            temperature=1.0,
            tools=[get_order_status, track_shipping],   # 파이썬 함수를 그대로 전달
            # 자동 함수 호출: SDK가 왕복을 대신한다 (기본 활성화, 최대 호출 수만 제한)
            automatic_function_calling=types.AutomaticFunctionCallingConfig(
                maximum_remote_calls=4
            ),
        ),
    )
    # 어떤 도구가 실제로 불렸는지 이력 확인 (디버깅 습관)
    if response.automatic_function_calling_history:
        for content in response.automatic_function_calling_history:
            for part in content.parts or []:
                if part.function_call:
                    print(f"  [도구 호출] {part.function_call.name}"
                          f"({dict(part.function_call.args)})")
    print(f"하루: {response.text}")


if __name__ == "__main__":
    # 실제 존재하는 주문번호를 데이터에서 하나 뽑아 시연
    shipped = ORDERS[ORDERS["order_status"] == "배송중"].iloc[0]["order_id"]
    preparing = ORDERS[ORDERS["order_status"] == "배송준비"].iloc[0]["order_id"]

    ask(f"주문번호 {shipped} 로 주문한 상품이 뭐였죠? 결제 금액도 알려주세요.")
    ask(f"{preparing} 주문했는데 송장번호 알려주세요.")
    ask("주문번호 HR99999999999 배송 조회해 주세요.")   # 존재하지 않는 주문

    print("""
──────────────────────────────────────────────────────────
확인할 것
  - 모델이 질문에 따라 get_order_status / track_shipping 을 골라 쓴다
  - 출고 전 주문은 "송장이 없다"는 사실을 그대로 전달한다 (추측 금지)
  - 없는 주문번호는 found=False 를 받아 정중히 재확인을 요청한다
남은 문제: 도구가 늘어나면? → 7장 다중 도구 라우팅
──────────────────────────────────────────────────────────""")

2. 코드에서 볼 곳

덩어리 무엇인가 5장과 달라진 점
ORDERS = pd.read_csv(...) 주문 80건을 읽어 둔 표 새로 생겼습니다
get_order_status() 주문 상태 조회 도구 가짜 값 대신 CSV에서 찾습니다
track_shipping() 배송 추적 도구 새로 생겼습니다
SYSTEM 상담원 규칙 "조회 결과에 없는 내용은 추측하지 않습니다"
ask() 질문 하나를 처리하는 함수 호출이 한 번이고, 도구 호출 이력을 출력합니다

FunctionDeclaration이 파일 어디에도 없다는 것을 확인해 보세요. 5장에서 손으로 쓴 선언은 사라지고, 함수의 독스트링이 그 자리를 맡았습니다.

track_shipping()에서 세 갈래로 나뉘는 부분을 다시 봅니다.

row = ORDERS[ORDERS["order_id"] == order_id]
if row.empty:
    return {"found": False, "message": f"주문번호 {order_id} 를 찾을 수 없습니다."}
r = row.iloc[0]
if not isinstance(r["tracking_no"], str) or r["tracking_no"] == "":
    return {
        "found": True, "shipped": False,
        "status": r["order_status"],
        "message": "아직 출고 전이라 송장이 등록되지 않았습니다. "
                   "출고된 뒤 다시 조회하면 확인할 수 있습니다.",
    }
갈래 조건 돌려주는 것
주문이 없다 row.empty found: False
출고 전이다 송장번호 칸이 비었다 found: True, shipped: False
출고됐다 그 밖 found: True, shipped: True와 배송 정보

파일 맨 아래에서는 시험할 주문번호를 데이터에서 직접 뽑습니다.

shipped = ORDERS[ORDERS["order_status"] == "배송중"].iloc[0]["order_id"]
preparing = ORDERS[ORDERS["order_status"] == "배송준비"].iloc[0]["order_id"]

"배송중인 주문 중 첫 번째", "배송준비인 주문 중 첫 번째"의 주문번호입니다. 주문번호를 코드에 적어 두지 않았으므로 데이터가 바뀌어도 그대로 동작합니다.


3. 실행하기

실행하기 전에 짐작해 보세요.

  • 세 질문에서 모델은 get_order_status와 track_shipping 중 무엇을 부를까요?
  • 첫 질문의 답에 나오는 상품 이름과 결제 금액은 어디에서 올까요?
$ python lesson06_order_tools.py

실행 결과 (문장은 실행할 때마다 달라집니다)

고객: 주문번호 HR20260620069 로 주문한 상품이 뭐였죠? 결제 금액도 알려주세요.
  [도구 호출] get_order_status({'order_id': 'HR20260620069'})
하루: 고객님, 주문번호 HR20260620069로 확인해 드립니다. 주문하신 상품은 '콜드브루 원액 500ml 2병'이며, 총 결제 금액은 15,900원입니다. 현재 해당 상품은 배송 중 상태로 안전하게 이동하고 있습니다. 추가로 배송 조회나 다른 궁금한 점이 있으시면 언제든지 말씀해 주세요!

고객: HR20260620024 주문했는데 송장번호 알려주세요.
  [도구 호출] track_shipping({'order_id': 'HR20260620024'})
하루: 고객님, 주문번호 HR20260620024 건은 현재 배송 준비 중으로 아직 출고되지 않아 송장번호가 등록되지 않았습니다. 상품이 출고된 후 다시 조회하시면 송장번호를 확인하실 수 있습니다. 출고 완료 후 배송 위치 조회를 원하시면 언제든 다시 말씀해 주세요.

고객: 주문번호 HR99999999999 배송 조회해 주세요.
  [도구 호출] track_shipping({'order_id': 'HR99999999999'})
하루: 문의하신 주문번호(HR99999999999)로 배송 정보를 조회하였으나, 일치하는 주문 내역을 찾을 수 없습니다. 입력하신 주문번호를 다시 한번 확인해 주시거나 다른 주문번호를 알려주시면 정확하게 확인해 드리겠습니다. 추가로 도움이 필요하신 주문번호나 문의 사항을 말씀해 주세요.

(이하 생략)

4. 무엇을 관찰했나

질문에 따라 다른 도구가 불렸다

[도구 호출] 줄만 따라 읽습니다.

질문 불린 도구 고른 근거
주문한 상품이 뭐였죠? 결제 금액도 get_order_status 독스트링 "주문 상태를 조회한다"
송장번호 알려주세요 track_shipping 독스트링 "택배사·송장번호·출고일·배송완료일을 조회한다"
배송 조회해 주세요 track_shipping 독스트링 "배송 위치 문의에 사용한다"

코드에는 질문을 도구로 나누는 if 문이 없습니다. 고른 것은 모델이고, 고를 근거(독스트링)를 적어 준 것은 우리입니다. 이 교재를 준비하며 다섯 번 실행했을 때 다섯 번 모두 같은 도구가 불렸습니다.

답변의 사실이 데이터와 맞는다

첫 질문의 답에 나온 값을 data/orders.csv의 해당 줄과 맞춰 봅니다.

답변에 나온 것 orders.csv의 값
콜드브루 원액 500ml 2병 product_name = 콜드브루 원액 500ml 2병
15,900원 order_amount = 15900
배송 중 order_status = 배송중

5장과 달리 이번에는 실제 주문의 실제 값입니다. 그리고 [도구 호출] 줄이 있어서, 이 값이 어느 도구에서 어떤 주문번호로 나왔는지 말할 수 있습니다.

출고 전과 없는 주문을 갈라 전했다

둘째와 셋째는 둘 다 "송장번호를 줄 수 없는" 경우이지만 답이 다릅니다.

질문 도구가 돌려준 것 모델의 답
출고 전 주문 found: True, shipped: False와 안내 문장 "아직 출고되지 않아 송장번호가 등록되지 않았습니다. 출고된 후 다시 조회하시면…"
없는 주문번호 found: False와 안내 문장 "일치하는 주문 내역을 찾을 수 없습니다. 주문번호를 다시 한번 확인해…"

모델이 똑똑해서 가른 것이 아닙니다. 코드가 두 경우를 다른 모양으로 돌려줬기 때문입니다. 답의 "출고된 후 다시 조회하시면"도 도구가 돌려준 message의 문장에서 왔습니다. 그리고 없는 주문번호에도 프로그램은 멈추지 않고 안내가 나갔습니다.

호출은 한 번인데 왕복은 일어났다

ask() 안의 generate_content는 한 번뿐인데 답변이 바로 나왔습니다. [도구 호출] 줄이 그 사이에 SDK가 함수를 실행하고 결과를 돌려보냈다는 증거입니다. 5장에서 손으로 한 ②, ③, ④단계입니다.


5. 지금 폴더의 모습

haru-market/
├── config.py
├── data/
│   └── orders.csv                  ← 이번 장에서 읽는 데이터
├── lesson02_first_call.py
├── lesson03_prompt.py
├── lesson04_intent_classifier.py
├── lesson05_function_calling.py
└── lesson06_order_tools.py         ← 이번 장

핵심 정리

  • 주문 내용을 묻는 질문에는 get_order_status가, 배송을 묻는 질문에는 track_shipping이 불렸습니다.
  • 답변의 상품 이름, 결제 금액, 주문 상태가 orders.csv의 값과 일치했습니다.
  • 출고 전과 없는 주문을 갈라 전한 것은 코드가 두 경우를 다르게 돌려줬기 때문입니다.
  • 어떤 도구가 불렸는지는 [도구 호출] 출력으로 확인합니다.
  • 이번 장의 산출물은 get_order_status, track_shipping 두 도구입니다.
← 이전 절자동 함수 호출 — SDK가 왕복을 대신한다다음 절 →정리와 체크리스트
오명운 · macro@prag-ai.com