LangChain 챗봇5장 · LLM 외부 도구 연동14 / 21← 이전목차다음 →MetaContents

5장. LLM 외부 도구 연동

실습 1 — @tool 로 상품 찾기·요금 계산 도구 만들기

구름다리여행의 상품 표(trip_products.csv)로 도구 두 개를 만듭니다. 지역과 가격으로 상품을 찾는 find_products, 상품코드와 인원으로 요금을 계산하는 calc_price 입니다. 상품 12개는 모두 연습용으로 지어낸 가상 데이터입니다. 만든 도구를 우리가 직접 불러 보고, 모델에게 갈 설명서를 파일로 남깁니다. 모델은 부르지 않으므로 API 키가 없어도 됩니다.

1단계. 상품 표 읽기

Python
import pandas as pd

# 가상 상품 12개. 가격은 어른 1명 기준(만 원), 남은자리가 0 이면 매진
products = pd.read_csv("trip_products.csv")

print(products.shape)
print(products.to_string(index=False))
출력
(12, 7)
상품코드             상품명 지역  박수  가격  남은자리        출발일
 P01  제주 동쪽 바다 2박 3일 제주   2  39     6 2026-12-05
 P02 제주 한라산 걷기 3박 4일 제주   3  52     0 2026-12-12
 P03 부산 야경과 시장 1박 2일 부산   1  18    12 2026-11-28
 P04  부산 바다 열차 2박 3일 부산   2  29     4 2026-12-19
 P05 강릉 커피와 바다 1박 2일 강릉   1  16     9 2026-11-21
 P06   강릉 온천 쉼 2박 3일 강릉   2  33     2 2026-12-26
 P07  경주 역사 걷기 2박 3일 경주   2  27     7 2026-12-05
 P08    여수 밤바다 1박 2일 여수   1  21     0 2026-11-28
 P09 전주 한옥 하룻밤 1박 2일 전주   1  15    10 2026-12-12
 P10    속초 설악산 2박 3일 속초   2  31     5 2027-01-09
 P11   통영 섬 여행 3박 4일 통영   3  44     3 2027-01-16
 P12  제주 감귤 체험 1박 2일 제주   1  24     8 2027-01-23

코드가 의미하는 것 pd.read_csv 로 읽은 상품 표는 도구가 찾아볼 회사 데이터입니다. 모델은 이 표를 직접 보지 못하고, 뒤에서 만들 도구가 products 를 읽어 결과 글만 모델에게 건넵니다. 표 전체를 찍어 둔 것은 3단계의 도구 결과가 맞는지 눈으로 대조하려는 것입니다.

2단계. 도구 두 개 만들기

Python
from langchain_core.tools import tool


@tool(parse_docstring=True)
def find_products(region: str = "", max_price: int = 0) -> str:
    """구름다리여행 상품을 지역과 가격으로 찾는다.

    Args:
        region: 지역 이름. 예: 제주, 부산. 비우면 모든 지역
        max_price: 어른 1명 가격의 상한(만 원). 0 이면 가격을 따지지 않는다
    """
    df = products
    if region:
        df = df[df["지역"] == region]
    if max_price > 0:
        df = df[df["가격"] <= max_price]
    if df.empty:
        return "조건에 맞는 상품이 없습니다."
    # 모델이 읽기 쉽게 한 상품을 한 줄의 글로 만든다
    return "\n".join(
        f"{r.상품코드} {r.상품명} | {r.가격}만 원 | 남은 자리 {r.남은자리} | 출발 {r.출발일}" for r in df.itertuples()
    )


@tool(parse_docstring=True)
def calc_price(code: str, adults: int, children: int = 0) -> str:
    """상품코드와 인원으로 총 여행 요금(만 원)을 계산한다. 아동 요금은 어른의 80% 이다.

    Args:
        code: 상품코드. 예: P01
        adults: 어른 수
        children: 아동(만 2~12세 미만) 수
    """
    rows = products[products["상품코드"] == code]
    if rows.empty:
        return f"없는 상품코드입니다: {code}"
    row = rows.iloc[0]
    total = round(row["가격"] * adults + row["가격"] * 0.8 * children, 1)
    # :g 는 109.2 는 그대로, 36.0 은 36 으로 짧게 적는 형식이다
    return f"{code} {row['상품명']} · 어른 {adults}명, 아동 {children}명 · {float(total):g}만 원"


for t in [find_products, calc_price]:
    print("이름:", t.name)
    print("설명:", t.description)
    print("인자:", {k: v["type"] for k, v in t.args.items()})
출력
이름: find_products
설명: 구름다리여행 상품을 지역과 가격으로 찾는다.
인자: {'region': 'string', 'max_price': 'integer'}
이름: calc_price
설명: 상품코드와 인원으로 총 여행 요금(만 원)을 계산한다. 아동 요금은 어른의 80% 이다.
인자: {'code': 'string', 'adults': 'integer', 'children': 'integer'}

코드가 의미하는 것 두 함수 위의 @tool(parse_docstring=True) 가 보통 함수를 모델이 고를 수 있는 도구로 바꿉니다. 도구가 표의 행을 한 줄 글로 바꿔 돌려주는 것은 결과가 결국 모델이 읽는 글이 되기 때문이고, 없는 상품코드에 오류 대신 안내 글을 돌려주는 것은 모델이 그 글을 읽고 다시 고를 수 있게 하려는 것입니다. 찍어 본 인자의 자료형은 모델이 인자를 채울 때 따르는 규칙입니다.

3단계. 도구를 직접 불러 보기

Python
# invoke 에 인자 사전을 넘기면 함수를 실행한 결과 글이 돌아온다
print(find_products.invoke({"region": "제주", "max_price": 40}))
print("---")
print(calc_price.invoke({"code": "P01", "adults": 2, "children": 1}))
print(calc_price.invoke({"code": "P03", "adults": 2}))
print(calc_price.invoke({"code": "P99", "adults": 1}))
출력
P01 제주 동쪽 바다 2박 3일 | 39만 원 | 남은 자리 6 | 출발 2026-12-05
P12 제주 감귤 체험 1박 2일 | 24만 원 | 남은 자리 8 | 출발 2027-01-23
---
P01 제주 동쪽 바다 2박 3일 · 어른 2명, 아동 1명 · 109.2만 원
P03 부산 야경과 시장 1박 2일 · 어른 2명, 아동 0명 · 36만 원
없는 상품코드입니다: P99

코드가 의미하는 것 도구.invoke({인자 사전}) 은 모델이 부를 자리에 우리가 먼저 서서 도구를 불러 보는 시험입니다. 모델을 붙이기 전에 도구의 계산이 맞는지 확인해 두면, 나중에 답이 틀렸을 때 도구 탓인지 모델 탓인지 가르기 쉽습니다. P99 처럼 없는 값을 일부러 넣어 보는 것은 모델이 틀리게 부르는 경우를 미리 겪어 보는 것입니다.

4단계. 모델에게 갈 설명서 저장하기

Python
import json
from pathlib import Path

from langchain_core.utils.function_calling import convert_to_openai_tool

# bind_tools 가 모델에게 보낼 설명서를 미리 만들어 본다
specs = [convert_to_openai_tool(t) for t in [find_products, calc_price]]
for spec in specs:
    fn = spec["function"]
    print(fn["name"], "| 꼭 넣을 인자:", fn["parameters"].get("required", []))
    for arg, info in fn["parameters"]["properties"].items():
        print(f"  {arg:10} {info['type']:8} {info['description']}")

Path("out_ch05_tool_schema.json").write_text(json.dumps(specs, ensure_ascii=False, indent=2), encoding="utf-8")
print("저장: out_ch05_tool_schema.json")
출력
find_products | 꼭 넣을 인자: []
  region     string   지역 이름. 예: 제주, 부산. 비우면 모든 지역
  max_price  integer  어른 1명 가격의 상한(만 원). 0 이면 가격을 따지지 않는다
calc_price | 꼭 넣을 인자: ['code', 'adults']
  code       string   상품코드. 예: P01
  adults     integer  어른 수
  children   integer  아동(만 2~12세 미만) 수
저장: out_ch05_tool_schema.json

코드가 의미하는 것 convert_to_openai_tool 로 만든 사전은 실습 3에서 bind_tools 가 모델에게 보낼 설명서와 같은 꼴입니다. 이것을 out_ch05_tool_schema.json 으로 남기면 모델이 도구에 대해 아는 것의 전부를 파일 하나로 읽어 볼 수 있습니다. 모델은 이 설명서도 토큰으로 읽으므로, 이 글이 길수록 모델을 부를 때마다 입력이 늘어납니다.

이 실습이 뜻하는 것

이 실습은 회사 내선 전화번호부에 새 부서 두 곳(상품안내팀, 요금계산팀)을 올리고, 올리기 전에 내가 직접 전화를 걸어 보는 일과 같습니다. 부서가 쓸 자료를 펼치고(1단계), 부서 이름과 하는 일과 준비물을 적고(2단계), 직접 전화해 답을 들어 보고(3단계), 전화번호부에 실릴 칸을 파일로 남깁니다(4단계). 여기서 부서가 도구, 전화번호부의 칸이 도구 설명서입니다. 비유와 다른 점은, 사람은 전화번호부에 없는 부서도 수소문해 찾지만 모델은 설명서에 적힌 도구만 부를 수 있다는 것입니다.

단계출력뜻
1. 표 읽기(12, 7)상품 12개, 열 일곱 개의 표입니다. P02 와 P08 은 남은 자리가 0 인 매진 상품입니다
2. 도구 만들기이름 find_products, calc_price함수 이름이 도구 이름, docstring 첫 단락이 설명, 타입 힌트가 string·integer 가 됐습니다
3. 직접 부르기P01·P12, 109.2만 원, 36만 원도구가 표에서 찾고 계산한 값입니다. P01 은 39 x 2 + 31.2 = 109.2 입니다
3. 없는 코드"없는 상품코드입니다: P99"오류로 멈추지 않고 안내 글을 돌려줍니다
4. 설명서꼭 넣을 인자 [], ['code', 'adults']기본값을 준 인자는 꼭 넣을 것에서 빠지고, Args: 의 글이 인자마다의 설명이 됐습니다

한 줄로 줄이면, @tool 로 함수를 도구로 바꾸면 모델에게 보낼 설명서(이름, 설명, 인자)가 저절로 만들어지고, 도구는 모델 없이도 보통 함수처럼 불러 시험할 수 있다는 것을 본 실습입니다. "제주, 40만 원 이하"로 찾자 P01(39만 원)과 P12(24만 원)가 나왔고, P02(52만 원)는 가격 상한에 걸려 빠졌습니다. find_products 는 두 인자에 모두 기본값을 주어 꼭 넣을 인자가 하나도 없습니다. 모델이 보는 것은 4단계의 설명서뿐이므로, 설명을 꼼꼼히 쓸수록 모델이 도구와 인자를 덜 틀리게 고릅니다. 실습 2에서는 모델의 요청대로 이 도구를 실행하는 쪽을 짭니다.

코드 설명

  • @tool(parse_docstring=True) 는 함수를 도구로 바꾸면서 docstring 의 Args: 아래 줄을 인자마다의 설명으로 읽습니다. 첫 줄은 도구의 설명이 됩니다.
  • find_products 의 두 인자에는 기본값("", 0)을 주었습니다. 모델이 지역만, 또는 가격만 넣어 불러도 되게 하려는 것입니다.
  • df.itertuples() 는 표의 행을 하나씩 꺼내 r.상품코드 처럼 점으로 열을 읽게 합니다. 열 이름에 기호를 쓰지 않아서 이렇게 읽을 수 있습니다.
  • 도구.invoke({...}) 는 우리가 인자를 넣어 직접 부르는 방법입니다. 모델이 부를 때도 결국 이 함수가 실행됩니다.
  • convert_to_openai_tool 은 도구를 OpenAI 에 보내는 설명서(사전)로 바꿉니다. out_ch05_tool_schema.json 을 열면 모델이 보는 것이 이 글뿐이라는 것을 확인할 수 있습니다.
← 이전 절5장 · 실습 지도다음 절 →5장 · 실습 2 — 도구 실행기 만들기