3장. LangChain 기본 구조와 LCEL
프로젝트 1 — 여행지 소개 체인
구름다리여행은 손님의 조건(누구와, 어느 계절에, 예산)을 받아 알맞은 여행지를 골라 세 문장짜리 소개글을 보여 주려 합니다. 가상의 여행지 표 travel_spots.csv 에서 조건에 맞는 곳을 pandas 로 고른 뒤, 프롬프트 템플릿·모델·파서를 이은 체인으로 소개글을 만들어 보세요. 형식 지시를 지켰는지 확인하고, 온도 0 과 1 에서 소개글이 어떻게 다른지도 견줘 보세요. 여행지 이름은 모두 지어낸 것이고 실제 관광지와 관계가 없습니다. 지역은 "제주 동쪽 해안"처럼 넓게만 적었습니다. 모델을 부르므로 API 키가 있어야 합니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.
| 열 | 뜻 | 예 |
|---|---|---|
| 여행지 | 지어낸 여행지 이름 | 물빛 해안 산책길 |
| 지역 | 넓은 지역 묘사 | 제주 동쪽 해안 |
| 특징 | 그곳에서 할 수 있는 것 | 평평한 바닷가 길과 얕은 모래사장 |
| 어울리는 손님 | 가족, 부모님, 연인, 친구 | 가족 |
| 추천 계절 | 봄, 여름, 가을, 겨울 | 여름 |
| 예산 | 한 사람 1박 예산(만 원) | 10 |
표는 8행 6열의 연습용 가상 데이터입니다.
요구사항은 다섯 가지입니다.
travel_spots.csv를 읽고, 손님 조건 "가족, 여름, 예산 15만 원 이하"에 맞는 여행지를 예산이 낮은 차례로 고릅니다.- 여행지 한 곳의 열 값을 빈칸에 넣는
ChatPromptTemplate을 만듭니다. 시스템 메시지에 "세 문장, 첫 문장은 여행지 이름으로 시작, 마지막 문장에 추천 계절과 예산, 표에 없는 사실은 지어내지 않기"를 적습니다. prompt | model | StrOutputParser()체인을 만들고, 고른 여행지 모두의 소개글을batch로 한 번에 만듭니다.- 소개글마다 문장 수를 세어 형식 지시(세 문장)를 지켰는지 확인합니다.
- 첫째 여행지의 소개글을 온도 0 과 온도 1 에서 두 번씩 만들어 견주고, 결과를
out_ch03_spot_intro.csv로 저장합니다.
단계별 힌트입니다.
- 조건 고르기는
spots[(spots["어울리는 손님"] == "가족") & (spots["추천 계절"] == "여름") & (spots["예산"] <= 15)]처럼&로 잇고,sort_values("예산")으로 늘어놓습니다. - 빈칸 이름은 영어로 짓는 편이 안전합니다(
{name},{region}등). 표의 행을 사전으로 바꿀 때 열 이름을 빈칸 이름으로 바꿔 줍니다. picked.iterrows()로 행을 하나씩 꺼내 입력 사전 목록을 만들고chain.batch(목록)에 넣습니다.- 문장 수는 마침표(
.)로 나눈 조각 가운데 빈 것을 뺀 수로 어림합니다. - 온도가 다른 모델은
ChatOpenAI(model=MODEL, temperature=1, …)로 하나 더 만들고, 프롬프트와 파서는 그대로 둔 채 가운데만 바꿔 끼웁니다.
확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.
| 채점 기준 | 배점 |
|---|---|
| 조건으로 여행지를 바르게 골랐다 | 20 |
| 프롬프트에 형식과 제약 지시가 있다 | 20 |
| 로 이은 체인과 batch 를 썼다 | 20 |
| 문장 수로 형식을 확인했다 | 15 |
| 온도 비교를 결과에 기대어 설명했다 | 25 |
코드의 흐름
예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.
상자 하나가 예시 풀이의 단계 하나이고, 회청 화살표를 따라가면 실행 차례입니다. 상자 아래의 글은 그 단계에서 쓰는 핵심 코드입니다. 1단계의 고르기는 모델 없이 pandas 가 하고, 2단계부터 체인이 일합니다. 4단계는 3단계의 체인에서 모델 부품만 바꿔 끼웁니다.
실제 데이터로 해 보려면
실제 여행지 정보로 같은 체인을 만들어 볼 수 있습니다. 자료마다 이용 약관을 확인하고, 사진이나 설명 글을 그대로 옮겨 싣지 않습니다.
| 자료 | 구하는 곳 | 내용 |
|---|---|---|
| 한국관광공사의 관광 정보 | 공공데이터포털에서 관광 정보 API 신청 | 지역별 관광지 이름, 주소, 개요. 인증키 발급이 필요하다 |
| 지방자치단체의 관광지 목록 | 공공데이터포털의 파일 데이터 | 시군마다 관광지 목록을 csv 로 내려받을 수 있는 곳이 있다 |
내려받은 표의 열을 이 프로젝트의 열(여행지, 지역, 특징, 어울리는 손님, 추천 계절, 예산)에 맞게 고르고 이름을 바꾸면 아래 풀이를 그대로 쓸 수 있습니다. "어울리는 손님"처럼 원래 자료에 없는 열은 사람이 직접 채웁니다. 인증키는 코드에 적지 않고 .env 에 발급받은_인증키 자리를 만들어 넣습니다.
결과 보기 (예시 풀이)
1단계. 조건에 맞는 여행지 고르기
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단계. 형식 지시가 든 프롬프트 틀 만들기
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단계. 체인으로 소개글 만들고 문장 수 세기
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 견주고 저장하기
# 온도만 다른 모델을 하나 더 만든다. 프롬프트와 파서는 그대로 둔다
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.csv1·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장에서는 소개글을 글이 아니라 정해 둔 꼴의 사전(구조화된 출력)으로 받는 법을 배웁니다.