3장. LangChain 기본 구조와 LCEL
파이프로 잇기와 데이터가 바뀌는 모습
공장의 컨베이어 벨트를 따라가 보면, 같은 벨트 위의 물건이 자리마다 모습을 바꿉니다. 처음에는 밀가루였다가, 반죽이 되고, 빵이 되고, 봉지에 담긴 상품이 됩니다. 체인의 | 도 같습니다. 같은 질문이 부품을 하나 지날 때마다 다른 꼴의 값으로 바뀝니다.
왼쪽에서 오른쪽으로, 체인에 넣은 사전이 프롬프트를 지나 ChatPromptValue 로, 모델을 지나 AIMessage 로, 파서를 지나 글(str)로 바뀝니다. 상자 사이의 화살표 하나가 | 하나이고, 화살표마다 부품 하나가 일합니다. 여기서 밀가루가 사전, 반죽이 프롬프트 값, 빵이 AI 메시지, 봉지에 담긴 상품이 글입니다. 비유와 다른 점은, 공장의 빵은 되돌릴 수 없지만 체인은 중간 단계를 따로 불러 언제든 다시 들여다볼 수 있다는 것입니다.
자리마다 흐르는 것
| 자리 | 꼴 | 안에 든 것 | 확인하는 법 |
|---|---|---|---|
| 체인에 넣기 전 | dict | {"question": "취소하면 환불되나요?"} | 그냥 찍는다 |
| 프롬프트 뒤 | ChatPromptValue | 시스템 메시지와 사용자 메시지 | .to_messages() |
| 모델 뒤 | AIMessage | 답의 글, 토큰 수 같은 정보 | .content, .usage_metadata |
| 파서 뒤 | str | 답의 글만 | 그냥 찍는다 |
이 표를 외워 두면 오류 글을 읽기 쉬워집니다. "dict 를 받아야 하는데 str 이 왔다" 같은 오류는 두 부품 사이의 꼴이 맞지 않는다는 뜻입니다.
짧은 예시
체인을 쓰지 않고 부품의 invoke 를 하나씩 불러, 자리마다 흐르는 꼴을 찍어 봅니다.
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([("system", "너는 구름다리여행의 여행 비서다."), ("human", "{question}")])
model = FakeListChatModel(responses=["출발 10일 전까지는 전액 환불됩니다."])
parser = StrOutputParser()
# prompt | model | parser 가 안에서 하는 일을 한 걸음씩 손으로 한다
step0 = {"question": "취소하면 환불되나요?"}
step1 = prompt.invoke(step0)
step2 = model.invoke(step1)
step3 = parser.invoke(step2)
for name, value in [("넣은 것", step0), ("프롬프트 뒤", step1), ("모델 뒤", step2)]:
print(f"{name}: {type(value).__name__}")
print("파서 뒤: 글자열인가?", isinstance(step3, str))
# 모델의 답 글과 파서가 꺼낸 글이 같은지 견준다
print(step2.content == step3)넣은 것: dict
프롬프트 뒤: ChatPromptValue
모델 뒤: AIMessage
파서 뒤: 글자열인가? True
True네 자리에서 꼴이 dict → ChatPromptValue → AIMessage → 글자열로 바뀌었습니다. 마지막 줄의 True 는 파서가 AIMessage 의 .content 를 그대로 꺼냈다는 뜻입니다. 실습 2에서 체인 사이에 엿보는 부품을 끼워 같은 것을 체인 안에서 봅니다.
코드가 의미하는 것
이 코드는 컨베이어 벨트를 멈추고 자리마다 물건을 집어 들어 모습을 확인하는 일을 흉내 낸 것입니다. prompt | model | parser 가 안에서 하는 세 번의 넘기기를 손으로 하고, 자리마다 꼴을 찍습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
step1 = prompt.invoke(step0) | 사전을 프롬프트 값으로 바꾼다 | 손님 질문이 모델에게 보낼 메시지가 되는 자리입니다 |
step2 = model.invoke(step1) | 프롬프트 값을 받아 AIMessage 를 돌려준다 | 모델이 답을 쓰는 자리입니다. 토큰 수 같은 정보도 여기에 붙어 옵니다 |
step3 = parser.invoke(step2) | AIMessage 에서 글만 꺼낸다 | 화면에 보여 줄 글이 되는 자리입니다 |
step2.content == step3 | 모델의 답 글과 파서가 꺼낸 글을 견준다 | 파서가 답을 바꾸지 않고 꺼내기만 했는지 확인합니다 |
결과에서 읽을 것은 두 가지입니다.
- 자리마다 꼴이 바뀝니다. 앞의 네 줄이 dict, ChatPromptValue, AIMessage, 글자열 차례입니다. 앞의 "자리마다 흐르는 것" 표와 같습니다.
- 파서는 글을 고치지 않습니다. 마지막 줄의 True 는 파서가
.content를 그대로 꺼냈다는 뜻입니다. 체인이 하는 일은 이 세 번의 넘기기를 대신 해 주는 것뿐입니다.
주의할 점
- 파서를 빼고
prompt | model까지만 이으면 체인의 결과는AIMessage입니다. 답의 글이 필요한데AIMessage(content='…')가 찍히면 파서를 빠뜨린 것입니다. - 토큰 수(
usage_metadata)는 모델 뒤의AIMessage에만 있고, 파서를 지나면 사라집니다. 토큰을 세야 하면 파서 앞에서 꺼냅니다.