따라하기 — 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_date 는 2026-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절의 원칙 그대로입니다. 다만 여기서는 한 가지가 더 있습니다.
모델이 오류를 읽고 스스로 고칩니다. 이게 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()로 변환합니다. 모델은 문자열만 읽습니다. - 도구 하나로 무한한 질문에 답하게 된 대신, 모델이 만든 코드를 실행하는 위험이 생겼습니다. 다음 절에서 다룹니다.