따라하기 — 주문 조회 도구 만들기
한 줄 요약
code/ch03_order_agent.py 를 만듭니다. 이 절에서는 도구 함수 자체만 만들고, LLM 없이 먼저 돌려 봅니다. 도구가 제대로 동작하는지부터 확인하는 것이 순서입니다.
1. 왜 LLM 없이 먼저 돌려 보나
에이전트를 만들다 보면 이런 상황이 옵니다.
고객 : 주문 O001203 상태 알려주세요
상담원: 죄송합니다, 조회에 실패했습니다.
원인이 셋 중 어디일까요?
- 모델이 도구를 안 불렀다
- 모델이 인자를 이상하게 채웠다
- 도구 자체가 고장 났다
3번을 먼저 배제해 두면 남는 후보가 둘로 줄어듭니다. 그래서 도구는 항상 혼자 먼저 돌려 봅니다. 이 과정의 모든 실습 파일이 [1] 도구만 따로 확인 으로 시작하는 이유입니다.
2. 파일 만들기
code 폴더에 ch03_order_agent.py 를 만들고 아래를 전부 붙여 넣습니다. (파일 전체입니다. 다음 절들에서 뒷부분을 하나씩 설명합니다.)
새 파일을 만드세요 —
agentic_ai/code/ch03_order_agent.py
VS Code 왼쪽 탐색기에서code폴더를 오른쪽 클릭 → New File → 파일명ch03_order_agent.py→ 아래를 전부 복사해 붙여 넣고 저장(Ctrl+S).
# -*- coding: utf-8 -*-
"""3장 — 에이전트 ①: 사내 데이터 조회 (Function Calling)
실행: python code/ch03_order_agent.py
1장에서 LLM 이 답하지 못했던 질문을 드디어 해결합니다.
"승승장구몰 주문 O001203은 지금 어떤 상태인가요?"
핵심 개념
LLM 은 우리 함수를 '직접 실행'하지 못한다.
"이 함수를 이런 값으로 불러 줘" 라는 요청서를 만들 뿐이고, 실행은 우리 쪽에서 한다.
"""
import sys
import pathlib
sys.path.append(str(pathlib.Path(__file__).resolve().parent))
import pandas as pd
from common import get_genai_client, GEMINI_MODEL, DATA
from google.genai import types
client = get_genai_client()
# 데이터는 파일을 열 때 한 번만 읽는다(도구가 불릴 때마다 읽으면 느려진다).
orders = pd.read_csv(DATA / "orders.csv", encoding="utf-8-sig")
customers = pd.read_csv(DATA / "customers.csv", encoding="utf-8-sig")
def get_order_status(order_id: str) -> str:
"""주문번호로 주문 상태를 조회한다. 예: 'O001203'
[중요] 이 설명글(독스트링)과 타입힌트가 모델에게 주는 '사용 설명서'다.
모델은 함수 안의 코드를 볼 수 없고, 이름·설명·인자만 보고
"지금 이 도구를 써야겠다"를 판단한다. 그래서 설명을 분명하게 써야 한다.
"""
# 사용자가 소문자나 공백을 섞어 넣어도 찾을 수 있게 다듬는다.
oid = order_id.strip().upper()
hit = orders[orders["order_id"] == oid]
# 없는 주문번호라도 예외를 내지 않고 안내 문장을 돌려준다.
# 예외를 던지면 에이전트가 멈추지만, 문장을 돌려주면 모델이 읽고 상황을 설명해 준다.
if hit.empty:
return f"주문번호 {oid} 를 찾을 수 없습니다. 번호를 다시 확인해 주세요."
r = hit.iloc[0]
name = customers.loc[customers["customer_id"] == r.customer_id, "name"]
customer = name.iloc[0] if len(name) else r.customer_id
return (f"주문 {r.order_id} | 고객 {customer} | {r.product_name} {r.quantity}개 "
f"| {int(r.amount):,}원 | 주문일 {r.order_date} | 상태 '{r.status}'")
SYSTEM = ("너는 승승장구몰의 CS 상담원이다. 주문 관련 질문에는 반드시 get_order_status "
"도구로 조회한 결과만 근거로 답한다. 존댓말로 간결하게 답한다.")
def ask(question: str) -> str:
"""도구를 쥐여 주고 질문한다. 도구 실행까지 SDK 가 알아서 처리한다."""
resp = client.models.generate_content(
model=GEMINI_MODEL,
contents=question,
config=types.GenerateContentConfig(
tools=[get_order_status], # 파이썬 함수를 그대로 도구로 넘긴다
system_instruction=SYSTEM,
temperature=0, # 도구 선택은 매번 같아야 하므로 0
),
)
return (resp.text or "").strip()
def peek(question: str):
"""모델이 '무엇을 부르려 했는지'만 들여다본다(실행은 하지 않는다)."""
resp = client.models.generate_content(
model=GEMINI_MODEL,
contents=question,
config=types.GenerateContentConfig(
tools=[get_order_status],
system_instruction=SYSTEM,
temperature=0,
# 자동 실행을 꺼서, 모델의 '결정'만 확인한다
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
),
)
return resp.function_calls or []
if __name__ == "__main__":
# ── 1. 도구만 따로 확인 ────────────────────────────────────
print("=" * 62)
print("[1] 도구가 제대로 도는지 먼저 확인 (LLM 없이)")
print(" ", get_order_status("O001203"))
print(" ", get_order_status("O999999")) # 없는 주문번호
# ── 2. 모델의 '결정' 들여다보기 ────────────────────────────
print("\n" + "=" * 62)
print("[2] 모델은 무엇을 부르려 하는가 (아직 실행 전)")
for fc in peek("주문 O001203 상태 알려주세요"):
print(f" 부르려는 함수: {fc.name}")
print(f" 채워 넣은 값 : {dict(fc.args)}")
print(" → 모델은 '이 함수를 이 값으로 불러 달라'고 요청만 합니다. 실행은 우리 쪽 몫입니다.")
# ── 3. 실제로 답하게 하기 ──────────────────────────────────
print("\n" + "=" * 62)
print("[3] 1장에서 못 풀던 질문")
for q in ["승승장구몰 주문 O001203은 지금 어떤 상태인가요?",
"주문 O999999 어떻게 됐나요?"]:
print(f"\n 고객 : {q}")
print(f" 상담원: {ask(q)}")
print("\n" + "=" * 62)
print("관찰: 없는 주문번호에도 지어내지 않고 '찾을 수 없다'고 답합니다.")
print(" 도구가 사실을 돌려주기 때문입니다.")
3. 실행
터미널에서 실행 — 프로젝트 폴더
agentic_ai에서,(agentic)표시를 확인한 뒤.
python code/ch03_order_agent.py
이 절에서는 [1] 부분만 봅니다.
==============================================================
[1] 도구가 제대로 도는지 먼저 확인 (LLM 없이)
주문 O001203 | 고객 오지우 | 승승 스마트워치 Fit 5 1개 | 159,000원 | 주문일 2026-05-14 | 상태 '배송완료'
주문번호 O999999 를 찾을 수 없습니다. 번호를 다시 확인해 주세요.
이 두 줄은 LLM을 전혀 거치지 않았습니다. 그냥 파이썬 함수를 두 번 부른 결과입니다. 즉시 나옵니다.
4. 도구 뜯어보기
pandas 문법은 1장 1절·이 장 1절에서 말한 그대로입니다 —
orders[orders["order_id"] == oid]같은 줄은 AI에게 받아 쓰면 됩니다.여기서 배울 것은 그 바깥입니다 — 데이터를 어디서 읽을지, 못 찾으면 무엇을 돌려줄지, 독스트링에 뭐라고 쓸지. 아래 설명은 전부 그 이야기입니다.
데이터는 한 번만 읽는다
아래는 code/ch03_order_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
orders = pd.read_csv(DATA / "orders.csv", encoding="utf-8-sig")
customers = pd.read_csv(DATA / "customers.csv", encoding="utf-8-sig")
함수 바깥에 있습니다. 파일이 처음 실행될 때 딱 한 번만 읽습니다.
만약 함수 안에 넣었다면, 도구가 불릴 때마다 2,400행짜리 CSV를 새로 읽게 됩니다. 8장에서 도구가 수십 번 불리는 걸 생각하면 큰 차이입니다.
입력을 다듬는다
아래는 code/ch03_order_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
oid = order_id.strip().upper()
사용자가 " o001203 " 처럼 입력해도, 모델이 그대로 넘겨도 찾을 수 있게 합니다.
모델이 넘기는 값을 그대로 믿지 마세요. 대개 정확하지만 가끔 공백이나 대소문자가 다릅니다. 도구 안에서 방어하는 것이 훨씬 안전합니다. 시스템 지시로 "대문자로 넘겨라"라고 부탁하는 것보다 확실합니다.
못 찾아도 예외를 던지지 않는다
이 장에서 가장 중요한 설계 판단입니다.
아래는 code/ch03_order_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
if hit.empty:
return f"주문번호 {oid} 를 찾을 수 없습니다. 번호를 다시 확인해 주세요."
예외(raise)를 던지지 않고 문장을 돌려줍니다. 왜일까요?
| 방식 | 무슨 일이 벌어지나 |
|---|---|
raise ValueError(...) |
프로그램이 멈춘다. 고객은 아무 답도 못 받는다. |
return "찾을 수 없습니다..." |
모델이 이 문장을 읽고 고객에게 설명해 준다 |
두 번째가 훨씬 낫습니다. 도구는 모델의 감각기관이므로, 실패도 모델이 읽을 수 있는 형태로 알려 주는 것이 맞습니다.
이 원칙은 앞으로 계속 나옵니다. 4장에서 웹검색이 실패할 때, 5장에서 pandas 식이 틀렸을 때 — 전부 예외 대신 설명 문장을 돌려줍니다. 그러면 모델이 읽고 스스로 고쳐 다시 시도하기도 합니다.
다른 표와 이어 붙인다
아래는 code/ch03_order_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
name = customers.loc[customers["customer_id"] == r.customer_id, "name"]
customer = name.iloc[0] if len(name) else r.customer_id
orders.csv 에는 customer_id (O001203의 경우 C0146)만 있습니다. 사람이 읽기엔 이름이 낫죠. 그래서 customers.csv 에서 이름을 찾아 붙입니다.
if len(name) else r.customer_id — 이름을 못 찾으면 ID라도 보여 줍니다. 간단한 안전장치입니다.
반환 문자열 — 모양까지 코드가 정한다
아래는 code/ch03_order_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
return (f"주문 {r.order_id} | 고객 {customer} | {r.product_name} {r.quantity}개 "
f"| {int(r.amount):,}원 | 주문일 {r.order_date} | 상태 '{r.status}'")
앞에 f 가 붙은 문자열은 중괄호 안의 값을 그 자리에 끼워 넣습니다(2장에서 본 f-문자열입니다). 여기서 눈여겨볼 것은 콜론 뒤의 표기입니다.
| 표기 | 하는 일 |
|---|---|
{int(r.amount):,} |
159000 을 159,000 으로 — :, 가 천 단위마다 쉼표를 넣습니다 |
{r.quantity}개 |
값 뒤에 그냥 글자를 붙인 것 |
2장에서는 같은 일을 시스템 지시로 부탁했습니다 — "숫자에는 천단위 쉼표를 붙여". 부탁은 대개 지켜지지만 가끔 안 지켜집니다. 여기서는 코드가 만들어 내니 매번 반드시 그렇게 나옵니다. 이 차이가 7절 「코드가 보장하는 것 vs 원하는 답이 나올 가능성을 높이는 것」의 핵심입니다.
5. 여기서 눈여겨볼 것
이 도구 함수 어디에도 AI가 없습니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
oid = order_id.strip().upper() # 문자열 처리
hit = orders[orders["order_id"] == oid] # pandas 필터
if hit.empty: ... # 조건 분기
return f"주문 {r.order_id} | ..." # 문자열 포맷
전부 평범한 파이썬입니다. AI는 이 함수를 "언제 부를지"만 정합니다.
스스로 해 보기
get_order_status("o001203")(소문자)를 넣어 보세요. 잘 되나요?.upper()를 지우면 어떻게 되나요?- 반환 문자열에 배송 채널(
r.channel)을 추가해 보세요. 그러면 모델의 답이 달라질까요? 실행 전에 예측하고 확인하세요. orders.csv를 열어 다른 주문번호를 하나 골라 넣어 보세요.
핵심 정리
- 도구는 LLM 없이 먼저 돌려 봅니다. 문제가 생겼을 때 원인 후보를 줄이기 위해서입니다.
- 데이터 읽기는 함수 바깥에서 한 번만 합니다.
- 모델이 넘긴 값은 도구 안에서 다듬습니다 (
.strip().upper()). - 실패할 때 예외를 던지지 말고 설명 문장을 돌려줍니다. 모델이 읽고 고객에게 설명해 줍니다.
- 도구 함수 안에는 AI가 없습니다. 평범한 파이썬입니다.
- 다음 절에서 이 도구를 모델에게 쥐여 줍니다.