📚 Agentic AI - 기업용 자율 에이전트 개발 5장 · 에이전트 ③ CSV 데이터분석 Agentic AI란

따라하기 — pandas 식을 실행하는 도구 만들기

한 줄 요약

code/ch05_csv_agent.py 를 만듭니다. 이 파일의 주인공은 run_pandas 도구 하나입니다. 이 도구 하나로 "카테고리별 매출"부터 "리뷰 평균 평점"까지 무한히 많은 질문에 답할 수 있게 됩니다.


1. 파일 만들기

code 폴더에 ch05_csv_agent.py 를 만들고 아래를 전부 붙여 넣습니다.

새 파일을 만드세요agentic_ai/code/ch05_csv_agent.py
VS Code 왼쪽 탐색기에서 code 폴더를 오른쪽 클릭 → New File → 파일명 ch05_csv_agent.py 입력 → 아래를 전부 복사해 붙여 넣고 저장(Ctrl+S / Cmd+S).

# -*- coding: utf-8 -*-
"""5장 — 에이전트 ③: CSV 데이터분석 (LangChain)

실행:  python code/ch05_csv_agent.py

4장에서는 ReAct 루프를 for 문으로 직접 짰습니다.
여기서는 LangChain 이 그 루프를 대신 돌려 줍니다. create_agent 한 줄이면 됩니다.

핵심 개념 — 숫자는 코드에게
  LLM 은 계산기가 아닙니다. 2,400건을 머릿속으로 더하면 틀립니다.
  그래서 '무엇을 계산할지'만 LLM 이 정하고, 실제 계산은 pandas 가 합니다.
"""
import sys
import pathlib

sys.path.append(str(pathlib.Path(__file__).resolve().parent))

import pandas as pd
from langchain_core.tools import tool          # 함수를 도구로 등록하는 데코레이터
from langchain.agents import create_agent      # ReAct 루프를 대신 돌려 주는 헬퍼
from common import get_chat, DATA

# 데이터는 한 번만 읽는다
orders = pd.read_csv(DATA / "orders.csv", encoding="utf-8-sig")
inventory = pd.read_csv(DATA / "inventory.csv", encoding="utf-8-sig")
reviews = pd.read_csv(DATA / "reviews.csv", encoding="utf-8-sig")
products = pd.read_csv(DATA / "products.csv", encoding="utf-8-sig")

# 월별 집계를 쉽게 하려고 'month' 열을 미리 만들어 둔다
orders["month"] = pd.to_datetime(orders["order_date"]).dt.strftime("%Y-%m")

# 계산에 필요한 함수만 골라 둔 목록. 여기 없는 기능은 실행되지 않는다.
SAFE_BUILTINS = {
    "len": len, "sum": sum, "min": min, "max": max, "round": round,
    "sorted": sorted, "abs": abs, "int": int, "float": float, "str": str,
    "list": list, "dict": dict, "set": set, "range": range, "zip": zip,
}


@tool
def run_pandas(expr: str) -> str:
    """승승장구몰 데이터를 pandas 식 한 줄로 계산한다.

    쓸 수 있는 표:
      orders    : 주문 2,400건. 열 = order_id, order_date, month('YYYY-MM'),
                  customer_id, product_id, product_name, category,
                  quantity, unit_price, amount,
                  status('배송완료'/'배송중'/'결제완료'/'취소'/'환불')
      inventory : 재고. 열 = product_id, product_name, stock, warehouse, reorder_level
      reviews   : 리뷰 650건. 열 = product_id, product_name, rating(1~5), review_text
      products  : 상품 40개. 열 = product_id, product_name, category, price, rating
      pd        : pandas

    규칙 1: 매출을 집계할 때는 status 가 '취소'·'환불'인 행을 반드시 제외한다.
    규칙 2: 평점을 물으면 reviews 의 rating 을 집계한다.
            products 의 rating 은 카탈로그 표시값이라 리뷰 실제 평균과 다르다.
    예: "orders[~orders['status'].isin(['취소','환불'])].groupby('category')['amount'].sum().nlargest(3)"
    """
    try:
        # [안전장치] 계산에 필요한 함수만 열어 둔다.
        # open·__import__ 같은 위험한 기능은 이 목록에 없으므로 아예 쓸 수 없다.
        result = eval(expr, {"__builtins__": SAFE_BUILTINS},
                   {"orders": orders, "inventory": inventory,
                    "reviews": reviews, "products": products, "pd": pd})
    except Exception as e:
        # 식이 틀려도 죽지 않는다. 오류를 알려 주면 모델이 고쳐서 다시 시도한다.
        return f"실행 오류: {type(e).__name__}: {e}"
    return str(result)


SYSTEM = (
    "너는 승승장구몰의 데이터 분석가다. "
    "숫자는 반드시 run_pandas 도구로 계산하고, 머릿속으로 계산하지 않는다. "
    "매출 집계에서는 취소·환불 건을 제외한다. "
    "결과는 한국어로, 숫자에는 천단위 쉼표를 붙여 간결하게 설명한다."
)

llm = get_chat(temperature=0)

# 4장에서 for 문으로 짜던 루프를 이 한 줄이 대신한다
agent = create_agent(llm, tools=[run_pandas], system_prompt=SYSTEM)


def ask(question: str) -> str:
    """에이전트에게 한국어로 묻고 답을 받는다."""
    out = agent.invoke(
        {"messages": [{"role": "user", "content": question}]},
        # 4장의 MAX_STEPS 에 해당한다. 단위가 달라서(노드 1회 = 1) 두 배쯤 잡는다.
        # 안 넣으면 기본값이 10007 이라 사실상 무제한이다.
        {"recursion_limit": 10},
    )
    return out["messages"][-1].text.strip()      # 마지막 메시지가 최종 답변


if __name__ == "__main__":
    print("=" * 62)
    print("[1] 도구만 따로 확인 (LLM 없이)")
    print("  ", run_pandas.invoke({"expr": "len(orders)"}))
    print("  ", run_pandas.invoke({
        "expr": "inventory[inventory['product_id']=='P0003'][['stock','reorder_level']].to_dict('records')"}))

    print("\n" + "=" * 62)
    print("[2] 한국어로 묻고 집계 결과 받기")
    questions = [
        "카테고리별 매출 상위 3개를 알려주세요.",
        "P0003 스마트워치의 현재 재고와 재주문 기준을 알려주세요.",
        "P0003 스마트워치 리뷰의 평균 평점과 3점 이하 리뷰가 몇 건인지 알려주세요.",
    ]
    for q in questions:
        print(f"\n  Q: {q}")
        print(f"  A: {ask(q)}")

    print("\n" + "=" * 62)
    print("관찰: 4장에서 30줄 남짓 짜던 루프가 create_agent 한 줄로 줄었습니다.")
    print("      하는 일은 같습니다 — 생각하고, 도구를 부르고, 결과를 보고 다시 판단합니다.")

2. 실행 — [1] 부분

터미널에서 실행 — 프로젝트 폴더 agentic_ai 에서, (agentic) 표시를 확인한 뒤.

python code/ch05_csv_agent.py
==============================================================
[1] 도구만 따로 확인 (LLM 없이)
   2400
   [{'stock': 3, 'reorder_level': 30}]

습관대로 LLM 없이 먼저 확인합니다. 3·4장과 같습니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

run_pandas.invoke({"expr": "len(orders)"})

@tool 이 붙었으므로 invoke 에 딕셔너리로 넘기는 것을 기억하세요.

두 번째 줄의 stock: 3, reorder_level: 30 — 이 과정의 주인공 P0003 스마트워치입니다. 재고가 3개인데 재주문 기준이 30개입니다. 기억해 두세요. 8장에서 이 숫자가 결정적인 근거가 됩니다.


3. run_pandas 뜯어보기

여기서도 pandas를 외울 필요는 없습니다.
오히려 이 장은 "pandas 식을 사람이 안 짜도 된다" 는 것을 보여 주는 장입니다. 질문을 던지면 모델이 식을 만들고 run_pandas 가 실행합니다.

text "카테고리별 매출 상위 3개를 알려주세요" ↓ 모델이 만든 식 orders[~orders['status'].isin(['취소','환불'])].groupby('category')['amount'].sum().nlargest(3)

저 긴 식을 여러분이 쓸 일은 없습니다. 우리가 쓰는 것은 그 식을 안전하게 실행해 주는 함수 하나뿐입니다.

8절에 정답 대조용 pandas 식이 한 번 나옵니다. 그것도 읽지 않고 붙여 넣어도 됩니다 — 답이 맞는지 확인하라고 제공하는 것이지, 여러분이 쓸 줄 알아야 하는 것이 아닙니다.

데이터는 미리 읽어 둔다

아래는 code/ch05_csv_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

orders = pd.read_csv(DATA / "orders.csv", encoding="utf-8-sig")
inventory = pd.read_csv(DATA / "inventory.csv", encoding="utf-8-sig")
reviews = pd.read_csv(DATA / "reviews.csv", encoding="utf-8-sig")
products = pd.read_csv(DATA / "products.csv", encoding="utf-8-sig")

함수 바깥에서 한 번만. 3장에서 배운 그대로입니다. 도구가 불릴 때마다 CSV를 다시 읽으면 느려집니다.

미리 만들어 두는 열

아래는 code/ch05_csv_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

orders["month"] = pd.to_datetime(orders["order_date"]).dt.strftime("%Y-%m")

order_date2026-05-14 형태입니다. 월별로 묶으려면 2026-05 가 필요하죠.

이걸 미리 만들어 두는 이유는 모델이 만들 식을 단순하게 하기 위해서입니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

# month 열이 없으면 모델이 이런 걸 만들어야 한다
orders.groupby(pd.to_datetime(orders['order_date']).dt.strftime('%Y-%m'))['amount'].sum()

# month 열이 있으면
orders.groupby('month')['amount'].sum()

식이 복잡할수록 틀릴 확률이 올라갑니다. 자주 쓸 것은 미리 만들어 두는 것이 실제로 품질을 높이는 방법입니다.

이것도 사람의 판단입니다. 어떤 열을 미리 만들어 둘지는 데이터와 예상 질문을 아는 사람만 정할 수 있습니다.

eval — 문자열을 파이썬 식으로 실행

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

result = eval(expr, {"__builtins__": SAFE_BUILTINS}, {...})

eval 은 문자열을 파이썬 식으로 실행하는 내장 함수입니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

>>> eval("1 + 2")
3
>>> eval("len([1,2,3])")
3

모델이 만든 "orders.groupby('category')['amount'].sum()" 이라는 문자열을 실제 pandas 연산으로 바꿔 주는 것이 이 함수입니다.

두 번째·세 번째 인자가 중요합니다. 다음 절 전체가 이 이야기입니다.

인자
{"__builtins__": SAFE_BUILTINS} 쓸 수 있는 내장 함수 목록 (안전장치)
{"orders": orders, ...} 식에서 쓸 수 있는 이름들

세 번째 인자에 넣은 것만 식에서 쓸 수 있습니다. 그래서 독스트링에 적은 orders·inventory·reviews·products·pd 가 여기 다 들어 있어야 합니다.

오류를 문장으로 돌려준다

아래는 code/ch05_csv_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

except Exception as e:
    return f"실행 오류: {type(e).__name__}: {e}"

4장 9절의 원칙 그대로입니다. 다만 여기서는 한 가지가 더 있습니다.

모델이 만든 식orders.groupby('상품카테고리')…↳ 없는 열 이름도구 실행 결과"실행 오류: KeyError: '상품카테고리'"모델의 다음 행동"아, 열 이름이 틀렸구나"→ 'category'로 고쳐서 재시도예외로 실행을 멈추지 않고 오류 메시지를 반환하는 이유입니다. 모델은 오류 메시지를 '관찰' 결과로 활용합니다.
오류 문장이 모델에게는 '관찰'이 되어 다음 시도에서 식을 고치게 한다

모델이 오류를 읽고 스스로 고칩니다. 이게 ReAct 루프의 진짜 위력입니다 — 관찰이 실패여도 다음 판단의 재료가 됩니다.

str(e) 를 그대로 넣은 이유가 여기 있습니다. 4장 검색 도구에서는 예외 이름만 넣었지만(메시지가 길고 쓸모없어서), 여기서는 메시지 자체가 고치는 데 필요한 정보입니다. KeyError: '상품카테고리' 에서 무엇이 틀렸는지 알 수 있으니까요. 무엇을 돌려줄지는 상황마다 다릅니다.

결과를 문자열로

아래는 code/ch05_csv_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

return str(result)

pandas 결과는 Series·DataFrame 같은 객체입니다. 모델은 문자열만 읽으므로 변환합니다.

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

>>> str(orders.groupby('category')['amount'].sum().nlargest(3))
"category\n가전       90229700\n전자기기     53967950\n스포츠레저    53259600\nName: amount, dtype: int64"

읽을 만합니다. 표 형태가 그대로 보존되니까요.


4. 도구 하나로 무한한 질문

3장과 비교해 보세요.

[3장] 도구 = get_order_status(order_id)
        답할 수 있는 질문 = "주문 XXX 상태는?"  ← 한 종류

[5장] 도구 = run_pandas(expr)
        답할 수 있는 질문 = 계산으로 답할 수 있는 모든 것
                            "카테고리별 매출은?"
                            "월별 주문 추이는?"
                            "평점 4점 이상 상품은 몇 개?"
                            "채널별(웹/앱/전화) 매출 비중은?"
                            ...

도구 하나로 질문 종류를 무한히 늘렸습니다. 인자가 "계산식 자체"이기 때문입니다.

대신 위험도 커졌습니다. 모델이 만든 코드를 그대로 실행하니까요. 다음 절이 그 이야기입니다.


스스로 해 보기

[1] 아래에 직접 식을 넣어 실행해 보세요. LLM 없이 도구만 쓰는 연습입니다.

직접 고쳐 보기agentic_ai/code/ch05_csv_agent.py 를 열어 아래처럼 바꾸거나 덧붙인 뒤 다시 실행하세요.

print(run_pandas.invoke({"expr": "orders['category'].unique().tolist()"}))
print(run_pandas.invoke({"expr": "reviews[reviews['product_id']=='P0003']['rating'].mean()"}))
print(run_pandas.invoke({"expr": "orders[orders['product_id']=='P0003'].groupby('month')['quantity'].sum().tail(6)"}))

마지막 것을 실행하면 이런 게 나옵니다.

month
2025-10     3
2026-01     1
2026-02     2
2026-03     3
2026-04     1
2026-05    12

2026년 5월에 12개. 직전 몇 달 평균이 2개 남짓인데 갑자기 6배입니다. 그런데 재고는 3개뿐입니다. 8장에서 이 이야기가 다시 나옵니다.


핵심 정리

  • run_pandas 도구 하나로 계산 질문 전부에 답합니다. 인자가 계산식 자체이기 때문입니다.
  • 데이터는 함수 바깥에서 한 번만 읽습니다.
  • month 같은 열을 미리 만들어 두면 모델이 만들 식이 단순해지고, 그만큼 덜 틀립니다.
  • eval(expr, 전역, 지역) 이 문자열을 파이썬 식으로 실행합니다. 두·세 번째 인자가 무엇을 쓸 수 있는지를 정합니다.
  • 오류를 문장으로 돌려주면 모델이 읽고 스스로 고칩니다. 여기서는 오류 메시지 전체가 유용합니다.
  • 결과는 str() 로 변환합니다. 모델은 문자열만 읽습니다.
  • 도구 하나로 무한한 질문에 답하게 된 대신, 모델이 만든 코드를 실행하는 위험이 생겼습니다. 다음 절에서 다룹니다.
← 이전 절05. LLM은 계산기가 아니다 숫자는 코드에게다음 절 →07. 남의 코드를 실행한다는 것 안전장치 걸기