LangChain 챗봇3장 · LangChain 기본 구조와 LCEL19 / 21← 이전목차다음 →MetaContents

3장. LangChain 기본 구조와 LCEL

프로젝트 1 — 여행지 소개 체인

구름다리여행은 손님의 조건(누구와, 어느 계절에, 예산)을 받아 알맞은 여행지를 골라 세 문장짜리 소개글을 보여 주려 합니다. 가상의 여행지 표 travel_spots.csv 에서 조건에 맞는 곳을 pandas 로 고른 뒤, 프롬프트 템플릿·모델·파서를 이은 체인으로 소개글을 만들어 보세요. 형식 지시를 지켰는지 확인하고, 온도 0 과 1 에서 소개글이 어떻게 다른지도 견줘 보세요. 여행지 이름은 모두 지어낸 것이고 실제 관광지와 관계가 없습니다. 지역은 "제주 동쪽 해안"처럼 넓게만 적었습니다. 모델을 부르므로 API 키가 있어야 합니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.

열뜻예
여행지지어낸 여행지 이름물빛 해안 산책길
지역넓은 지역 묘사제주 동쪽 해안
특징그곳에서 할 수 있는 것평평한 바닷가 길과 얕은 모래사장
어울리는 손님가족, 부모님, 연인, 친구가족
추천 계절봄, 여름, 가을, 겨울여름
예산한 사람 1박 예산(만 원)10

표는 8행 6열의 연습용 가상 데이터입니다.

요구사항은 다섯 가지입니다.

  1. travel_spots.csv 를 읽고, 손님 조건 "가족, 여름, 예산 15만 원 이하"에 맞는 여행지를 예산이 낮은 차례로 고릅니다.
  2. 여행지 한 곳의 열 값을 빈칸에 넣는 ChatPromptTemplate 을 만듭니다. 시스템 메시지에 "세 문장, 첫 문장은 여행지 이름으로 시작, 마지막 문장에 추천 계절과 예산, 표에 없는 사실은 지어내지 않기"를 적습니다.
  3. prompt | model | StrOutputParser() 체인을 만들고, 고른 여행지 모두의 소개글을 batch 로 한 번에 만듭니다.
  4. 소개글마다 문장 수를 세어 형식 지시(세 문장)를 지켰는지 확인합니다.
  5. 첫째 여행지의 소개글을 온도 0 과 온도 1 에서 두 번씩 만들어 견주고, 결과를 out_ch03_spot_intro.csv 로 저장합니다.

단계별 힌트입니다.

  1. 조건 고르기는 spots[(spots["어울리는 손님"] == "가족") & (spots["추천 계절"] == "여름") & (spots["예산"] <= 15)] 처럼 & 로 잇고, sort_values("예산") 으로 늘어놓습니다.
  2. 빈칸 이름은 영어로 짓는 편이 안전합니다({name}, {region} 등). 표의 행을 사전으로 바꿀 때 열 이름을 빈칸 이름으로 바꿔 줍니다.
  3. picked.iterrows() 로 행을 하나씩 꺼내 입력 사전 목록을 만들고 chain.batch(목록) 에 넣습니다.
  4. 문장 수는 마침표(.)로 나눈 조각 가운데 빈 것을 뺀 수로 어림합니다.
  5. 온도가 다른 모델은 ChatOpenAI(model=MODEL, temperature=1, …) 로 하나 더 만들고, 프롬프트와 파서는 그대로 둔 채 가운데만 바꿔 끼웁니다.

확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.

채점 기준배점
조건으로 여행지를 바르게 골랐다20
프롬프트에 형식과 제약 지시가 있다20
| 로 이은 체인과 batch 를 썼다20
문장 수로 형식을 확인했다15
온도 비교를 결과에 기대어 설명했다25

코드의 흐름

예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.

조건 고르기, 틀 만들기, 체인 부르기, 온도 견주기의 네 단계를 이은 코드 흐름도

상자 하나가 예시 풀이의 단계 하나이고, 회청 화살표를 따라가면 실행 차례입니다. 상자 아래의 글은 그 단계에서 쓰는 핵심 코드입니다. 1단계의 고르기는 모델 없이 pandas 가 하고, 2단계부터 체인이 일합니다. 4단계는 3단계의 체인에서 모델 부품만 바꿔 끼웁니다.

실제 데이터로 해 보려면

실제 여행지 정보로 같은 체인을 만들어 볼 수 있습니다. 자료마다 이용 약관을 확인하고, 사진이나 설명 글을 그대로 옮겨 싣지 않습니다.

자료구하는 곳내용
한국관광공사의 관광 정보공공데이터포털에서 관광 정보 API 신청지역별 관광지 이름, 주소, 개요. 인증키 발급이 필요하다
지방자치단체의 관광지 목록공공데이터포털의 파일 데이터시군마다 관광지 목록을 csv 로 내려받을 수 있는 곳이 있다

내려받은 표의 열을 이 프로젝트의 열(여행지, 지역, 특징, 어울리는 손님, 추천 계절, 예산)에 맞게 고르고 이름을 바꾸면 아래 풀이를 그대로 쓸 수 있습니다. "어울리는 손님"처럼 원래 자료에 없는 열은 사람이 직접 채웁니다. 인증키는 코드에 적지 않고 .env 에 발급받은_인증키 자리를 만들어 넣습니다.

결과 보기 (예시 풀이)

1단계. 조건에 맞는 여행지 고르기

Python
import pandas as pd

# 가상의 여행지 8곳
spots = pd.read_csv("travel_spots.csv")
print(spots[["여행지", "어울리는 손님", "추천 계절", "예산"]].to_string(index=False))

# 손님 조건: 가족, 여름, 한 사람 1박 15만 원 이하
GUEST = {"손님": "가족", "계절": "여름", "예산": 15}
picked = spots[
    (spots["어울리는 손님"] == GUEST["손님"])
    & (spots["추천 계절"] == GUEST["계절"])
    & (spots["예산"] <= GUEST["예산"])
].sort_values("예산")            # 예산이 낮은 차례로
print()
print("조건에 맞는 곳:", len(picked), "곳")
print(picked[["여행지", "지역", "예산"]].to_string(index=False))
출력
      여행지 어울리는 손님 추천 계절  예산
물빛 해안 산책길      가족    여름  10
  구름숲 휴양림      가족    여름  15
 돌담 골목 마을     부모님    가을  10
 노을빛 작은 섬      연인    가을  10
   별바람 고원      연인    여름  15
 단풍 계곡 온천     부모님    가을  15
 하얀 눈꽃 마을      가족    겨울  15
  유채꽃 들판길      친구     봄  25

조건에 맞는 곳: 2 곳
      여행지       지역  예산
물빛 해안 산책길 제주 동쪽 해안  10
  구름숲 휴양림    강원 산간  15

코드가 의미하는 것 조건 셋을 & 로 이어 표에서 거르고 sort_values("예산") 으로 늘어놓는 일은 모델 없이 pandas 가 합니다. 숫자 조건은 코드가 틀림없이 거르므로, 고르기는 코드에, 글쓰기는 모델에 나눠 맡깁니다. 이 단계는 실제로 돌린 출력입니다.

2단계. 형식 지시가 든 프롬프트 틀 만들기

Python
from langchain_core.prompts import ChatPromptTemplate

SYSTEM = """너는 구름다리여행의 여행지 소개 작가다.
아래 여행지 정보로 소개글을 쓴다.
- 꼭 세 문장으로 쓴다.
- 첫 문장은 여행지 이름으로 시작한다.
- 둘째 문장은 특징을 손님에게 맞춰 풀어 쓴다.
- 마지막 문장에 추천 계절과 한 사람 1박 예산을 적는다.
- 정보에 없는 사실(거리, 시설, 가격)은 지어내지 않는다."""

HUMAN = """여행지: {name}
지역: {region}
특징: {feature}
어울리는 손님: {guest}
추천 계절: {season}
예산: 한 사람 1박 {budget}만 원"""

prompt = ChatPromptTemplate.from_messages([("system", SYSTEM), ("human", HUMAN)])

# 표의 행 하나를 빈칸 이름의 사전으로 바꾼다
def to_input(row):
    return {"name": row["여행지"], "region": row["지역"], "feature": row["특징"],
            "guest": row["어울리는 손님"], "season": row["추천 계절"], "budget": row["예산"]}

# iterrows(): 표의 행을 (행 번호, 행) 짝으로 하나씩 꺼낸다
inputs = [to_input(row) for _, row in picked.iterrows()]
print("빈칸:", prompt.input_variables)
print(prompt.invoke(inputs[0]).to_messages()[1].content)
출력
빈칸: ['budget', 'feature', 'guest', 'name', 'region', 'season']
여행지: 물빛 해안 산책길
지역: 제주 동쪽 해안
특징: 평평한 바닷가 길과 얕은 모래사장
어울리는 손님: 가족
추천 계절: 여름
예산: 한 사람 1박 10만 원

코드가 의미하는 것 SYSTEM 에는 형식(세 문장, 첫 문장과 마지막 문장에 들어갈 것)과 제약(지어내지 않기)을 적고, HUMAN 에는 여행지 정보를 빈칸 여섯 개로 둡니다. to_input 은 한글 열 이름을 영어 빈칸 이름에 짝지어, 표의 행을 체인에 넣을 입력 사전으로 바꾸는 다리입니다.

3단계. 체인으로 소개글 만들고 문장 수 세기

Python
import os

from dotenv import find_dotenv, load_dotenv
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

load_dotenv(find_dotenv(usecwd=True))
MODEL = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
# 소개글은 조금 길 수 있어 최대 300 토큰
model_0 = ChatOpenAI(model=MODEL, temperature=0, max_tokens=300, timeout=30)

chain_0 = prompt | model_0 | StrOutputParser()
# 고른 여행지 두 곳의 소개글을 한 번에 만든다
intros = chain_0.batch(inputs)


def count_sentences(text):
    """마침표로 나눈 조각 가운데 빈 것을 뺀 수로 문장 수를 어림한다."""
    return len([s for s in text.split(".") if s.strip()])


for item, text in zip(inputs, intros):
    print(f"[{item['name']}] 문장 {count_sentences(text)}개")
    print(text)
출력
[물빛 해안 산책길] 문장 3개
물빛 해안 산책길은 제주 동쪽 해안에 있는 바닷가 산책길입니다. 길이 평평하고 모래사장이 얕아 아이와 함께 걷고 물놀이하기 좋습니다. 여름에 가기 좋고, 예산은 한 사람 1박 10만 원입니다.
[구름숲 휴양림] 문장 3개
구름숲 휴양림은 강원 산간의 편백 숲에 자리한 휴양림입니다. 숲길을 걷고 계곡 물놀이터에서 놀 수 있어 가족 모두 즐기기 좋습니다. 여름에 추천하며, 예산은 한 사람 1박 15만 원입니다.

코드가 의미하는 것 chain_0.batch(inputs) 로 고른 여행지의 소개글을 한 번에 만들고, count_sentences 로 형식 지시를 지켰는지 코드로 점검합니다. 모델은 지시를 가끔 어기므로, 화면에 내보내기 전에 이렇게 꼴을 재는 검사를 둡니다. 예상 출력에서는 두 곳 모두 세 문장이지만, 실제로 돌린 뒤 다시 확인합니다.

4단계. 온도 0 과 1 견주고 저장하기

Python
# 온도만 다른 모델을 하나 더 만든다. 프롬프트와 파서는 그대로 둔다
model_1 = ChatOpenAI(model=MODEL, temperature=1, max_tokens=300, timeout=30)
chain_1 = prompt | model_1 | StrOutputParser()

rows = []
for temp, chain in [(0, chain_0), (1, chain_1)]:
    for trial in [1, 2]:
        text = chain.invoke(inputs[0])          # 첫째 여행지로 두 번씩
        rows.append({"온도": temp, "시도": trial, "문장 수": count_sentences(text), "소개글": text})
        print(f"온도 {temp}, {trial}번째: {text}")

result = pd.DataFrame(rows)
# 같은 온도의 두 소개글이 글자 하나까지 같은지 견준다
for temp in [0, 1]:
    pair = result[result["온도"] == temp]["소개글"].tolist()
    print(f"온도 {temp}: 두 번이 같은가?", pair[0] == pair[1])

result.to_csv("out_ch03_spot_intro.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch03_spot_intro.csv")
출력
온도 0, 1번째: 물빛 해안 산책길은 제주 동쪽 해안에 있는 바닷가 산책길입니다. 길이 평평하고 모래사장이 얕아 아이와 함께 걷고 물놀이하기 좋습니다. 여름에 가기 좋고, 예산은 한 사람 1박 10만 원입니다.
온도 0, 2번째: 물빛 해안 산책길은 제주 동쪽 해안에 있는 바닷가 산책길입니다. 길이 평평하고 모래사장이 얕아 아이와 함께 걷고 물놀이하기 좋습니다. 여름에 가기 좋고, 예산은 한 사람 1박 10만 원입니다.
온도 1, 1번째: 물빛 해안 산책길에서 제주 동쪽 바다를 곁에 두고 천천히 걸어 보세요. 평평한 길과 얕은 모래사장 덕분에 아이들도 마음 놓고 뛰놀 수 있습니다. 여름 가족 여행으로 알맞고, 예산은 한 사람 1박 10만 원입니다.
온도 1, 2번째: 물빛 해안 산책길은 아이와 함께 걷기 좋은 제주 동쪽 해안의 바닷길입니다. 길이 평평하고 모래사장이 얕아 온 가족이 물가에서 편하게 쉴 수 있습니다. 여름에 찾기 좋으며 예산은 한 사람 1박 10만 원입니다.
온도 0: 두 번이 같은가? True
온도 1: 두 번이 같은가? False
저장: out_ch03_spot_intro.csv

1·2단계는 모델을 부르지 않으므로 따로 돌려 본 실제 출력을 실었고, 3·4단계가 예상 출력입니다. 조건에 맞는 곳은 여덟 곳 가운데 물빛 해안 산책길(10만 원)과 구름숲 휴양림(15만 원) 두 곳입니다. 예상대로라면 두 소개글 모두 세 문장이고, 첫 문장이 여행지 이름으로 시작하며, 마지막 문장에 계절과 예산이 들어 있어 형식 지시를 지킵니다. 온도 0 의 두 소개글은 글자 하나까지 같고, 온도 1 의 두 소개글은 첫 문장부터 표현이 달라집니다. 온도 1 의 첫째 소개글은 "물빛 해안 산책길에서 … 걸어 보세요"처럼 권하는 말투로 바뀌어, 이름으로 시작하라는 지시는 지켰지만 꼴이 조금 흐트러졌습니다. 모델이 쓰는 답의 글은 실행할 때마다, 모델마다 조금씩 달라지므로 온도 0 에서도 아주 드물게 두 답이 다를 수 있습니다. 규정 안내처럼 늘 같아야 하는 글은 온도 0, 광고 문구처럼 여러 안을 받아 고르고 싶은 글은 온도를 올립니다.

코드가 의미하는 것 model_1 을 새로 만들어 prompt | model_1 | StrOutputParser() 로 가운데 부품만 바꿔 끼우고, 같은 입력을 두 번씩 넣어 pair[0] == pair[1] 로 견줍니다. 온도는 모델을 만들 때 정해지므로, 같은 체인에서 온도를 달리하려면 이처럼 모델 부품을 하나 더 만듭니다. 예상 출력의 True, False 는 키를 받아 실제로 돌린 뒤 다시 확인할 값입니다.

이 프로젝트가 뜻하는 것

이 프로젝트는 여행사 직원이 손님 조건에 맞는 상품 카드를 골라, 정해 둔 양식의 소개 문구를 붙여 주는 일과 같습니다. 상품 목록에서 조건으로 거르고(1단계), 소개 문구의 양식을 만들고(2단계), 양식대로 문구를 쓰게 하고(3단계), 문구를 한결같이 쓸지 다양하게 쓸지 견줍니다(4단계). 여기서 상품 목록이 travel_spots.csv, 양식이 시스템 메시지의 형식 지시, 문구를 쓰는 직원이 모델입니다. 비유와 다른 점은, 직원은 표에 없는 정보를 손님에게 되묻지만 모델은 지어내기 쉬워서 "정보에 없는 사실은 지어내지 않는다"는 제약을 꼭 적어야 한다는 것입니다.

단계출력뜻
1. 고르기8곳 가운데 2곳가족·여름·15만 원 이하를 모두 만족하는 곳은 둘이고, 예산이 낮은 물빛 해안 산책길이 앞에 옵니다
2. 틀빈칸 여섯 개표의 열 여섯 개가 빈칸 여섯 개에 짝지어 들어갔습니다. 빈칸 이름은 가나다·알파벳 차례로 찍힙니다
3. 소개글두 곳 모두 문장 3개형식 지시(세 문장, 이름으로 시작, 끝에 계절과 예산)를 지켰습니다
4. 온도온도 0 같음 True, 온도 1 같음 False온도 0 은 다시 불러도 같은 글, 온도 1 은 부를 때마다 다른 글입니다

한 줄로 줄이면, 표에서 고르는 일은 pandas 에게, 글을 쓰는 일은 체인에게 나눠 맡긴 프로젝트입니다. 고르기를 모델에게 맡기지 않은 까닭은, "15만 원 이하" 같은 조건은 코드가 틀림없이 거르지만 모델은 가끔 틀리기 때문입니다. 4단계에서는 3단계의 체인에서 가운데 모델만 바꿔 끼웠습니다. 부품을 바꿔 끼워도 나머지는 그대로라는 LCEL 의 좋은 점이 여기서 드러납니다. 4장에서는 소개글을 글이 아니라 정해 둔 꼴의 사전(구조화된 출력)으로 받는 법을 배웁니다.

← 이전 절3장 · 실습 5 — RunnableParallel 로 요약과 키워드 한 번에 뽑기다음 절 →3장 · 정리