📚 Agentic AI - 기업용 자율 에이전트 개발 3장 · 에이전트 ① 사내 데이터 조회 Agentic AI란

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

한 줄 요약

code/ch03_order_agent.py 를 만듭니다. 이 절에서는 도구 함수 자체만 만들고, LLM 없이 먼저 돌려 봅니다. 도구가 제대로 동작하는지부터 확인하는 것이 순서입니다.


1. 왜 LLM 없이 먼저 돌려 보나

에이전트를 만들다 보면 이런 상황이 옵니다.

고객  : 주문 O001203 상태 알려주세요
상담원: 죄송합니다, 조회에 실패했습니다.

원인이 셋 중 어디일까요?

  1. 모델이 도구를 안 불렀다
  2. 모델이 인자를 이상하게 채웠다
  3. 도구 자체가 고장 났다

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):,} 159000159,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는 이 함수를 "언제 부를지"만 정합니다.


스스로 해 보기

  1. get_order_status("o001203") (소문자)를 넣어 보세요. 잘 되나요? .upper() 를 지우면 어떻게 되나요?
  2. 반환 문자열에 배송 채널(r.channel)을 추가해 보세요. 그러면 모델의 답이 달라질까요? 실행 전에 예측하고 확인하세요.
  3. orders.csv 를 열어 다른 주문번호를 하나 골라 넣어 보세요.

핵심 정리

  • 도구는 LLM 없이 먼저 돌려 봅니다. 문제가 생겼을 때 원인 후보를 줄이기 위해서입니다.
  • 데이터 읽기는 함수 바깥에서 한 번만 합니다.
  • 모델이 넘긴 값은 도구 안에서 다듬습니다 (.strip().upper()).
  • 실패할 때 예외를 던지지 말고 설명 문장을 돌려줍니다. 모델이 읽고 고객에게 설명해 줍니다.
  • 도구 함수 안에는 AI가 없습니다. 평범한 파이썬입니다.
  • 다음 절에서 이 도구를 모델에게 쥐여 줍니다.
← 이전 절03. 독스트링과 타입힌트가 모델에게 주는 설명서다음 절 →05. 따라하기 도구를 붙여 자동으로 호출시키기