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두 도구입니다.