3장. LangChain 기본 구조와 LCEL
체인과 LCEL
김밥집 주방에는 김을 까는 사람, 밥과 속을 올리는 사람, 마는 사람, 써는 사람이 한 줄로 서 있습니다. 앞사람이 넘긴 것을 받아 자기 일만 하고 옆으로 넘깁니다. 한 사람이 처음부터 끝까지 다 하는 것보다 일이 깔끔하게 나뉘고, 써는 사람만 바꾸고 싶으면 그 자리만 바꾸면 됩니다. LangChain 의 체인이 이 김밥 줄입니다.
상자 셋이 체인을 이루는 부품이고, 회청 화살표가 | 입니다. 프롬프트 템플릿이 손님의 질문을 메시지로 채우고, 모델이 그 메시지를 읽고 답을 쓰고, 출력 파서가 답에서 글만 꺼냅니다. 여기서 한 줄로 선 사람들이 부품이고, 옆으로 넘기는 손이 | 입니다. 비유와 다른 점은, 김밥 줄은 사람이 쉬지 않고 계속 일하지만 체인은 invoke 로 불렀을 때만 한 번 움직인다는 것입니다.
부품을 잇는 두 가지 방법
| 견줄 것 | 함수를 차례로 부르기 | LCEL 로 잇기 |
|---|---|---|
| 생김새 | parser.invoke(model.invoke(prompt.invoke(x))) | chain = prompt | model | parser |
| 읽는 차례 | 안쪽에서 바깥쪽으로 | 왼쪽에서 오른쪽으로 |
| 부품 바꾸기 | 괄호 안을 찾아 고친다 | 그 자리만 바꿔 끼운다 |
| 여러 개 한꺼번에 | 반복문을 직접 쓴다 | chain.batch([...]) 한 줄 |
| 조각으로 받기 | 직접 짜기 어렵다 | chain.stream(x) 한 줄 |
LCEL 은 새로운 언어를 따로 배우는 것이 아닙니다. 파이썬의 | 기호에 "앞의 결과를 뒤로 넘긴다"는 뜻을 붙여 둔 것입니다. 1장의 "LangChain 생태계" 절에서 가짜 모델로 맛본 prompt | model | StrOutputParser() 가 바로 이것입니다.
짧은 예시
가짜 모델로 체인을 만들고, 체인이 무엇으로 이루어졌는지 들여다봅니다.
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=["출발 7일 전까지 한 번 무료로 바꿀 수 있습니다."])
parser = StrOutputParser()
# | 로 이으면 RunnableSequence(차례대로 실행하는 부품 묶음) 하나가 된다
chain = prompt | model | parser
print(type(chain).__name__)
# steps: 체인 안의 부품 목록
print([type(step).__name__ for step in chain.steps])
print(chain.invoke({"question": "예약 날짜를 바꿀 수 있나요?"}))RunnableSequence
['ChatPromptTemplate', 'FakeListChatModel', 'StrOutputParser']
출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.| 로 이은 결과는 RunnableSequence 라는 부품 하나입니다. sequence 는 "차례, 순서"라는 뜻입니다. 체인도 부품이므로, 체인 뒤에 다시 | 로 다른 부품을 이을 수 있습니다.
코드가 의미하는 것
이 코드는 김밥 줄에 세 사람을 세우고 주문 하나를 넣어 보는 일을 흉내 낸 것입니다. 부품 셋을 만들고, | 로 줄을 세우고, 줄에 누가 섰는지 확인한 뒤, 한 번 돌립니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
ChatPromptTemplate.from_messages([...]) | 시스템 지시와 {question} 빈칸이 있는 틀을 만든다 | 줄의 첫 자리입니다. 손님 질문이 들어오면 모델에게 보낼 메시지로 채웁니다 |
FakeListChatModel(responses=[...]) | 정해 둔 답을 돌려주는 가짜 모델을 만든다 | 요리사 자리를 잠시 채운 대역입니다. 키와 요금 없이 체인의 짜임부터 맞춰 봅니다 |
chain = prompt | model | parser | 세 부품을 차례로 잇는다 | 줄을 세우기만 하고 아무것도 실행하지 않습니다. 이 한 줄이 챗봇 로직의 뼈대가 됩니다 |
chain.steps | 체인 안의 부품을 차례대로 보여 준다 | 줄에 선 사람을 앞에서부터 확인하는 일입니다. 체인이 이상할 때 짜임을 먼저 봅니다 |
chain.invoke({"question": …}) | 사전 하나를 넣어 체인을 한 번 돌린다 | 손님 주문 하나에 줄 전체가 한 번 움직입니다. 사전의 열쇠는 프롬프트의 빈칸 이름입니다 |
결과에서 읽을 것은 세 가지입니다.
- 체인은 부품 하나입니다. 첫 줄의
RunnableSequence는 세 부품을 이은 것이 다시 러너블 하나가 됐다는 뜻입니다. 그래서 체인 뒤에 또 다른 부품을 이을 수 있습니다. - 부품의 차례가 그대로 남습니다. 둘째 줄의 목록은
|로 이은 차례(프롬프트, 모델, 파서) 그대로입니다. 데이터도 이 차례로 흐릅니다. - 끝에 파서가 있어 결과가 글입니다. 셋째 줄은
AIMessage가 아니라 답의 글만 찍혔습니다. 화면에 바로 보여 줄 수 있는 꼴입니다.
주의할 점
|의 왼쪽 결과와 오른쪽 입력의 꼴이 맞아야 합니다. 파서를 프롬프트 앞에 두는 것처럼 차례를 바꾸면 오류가 납니다.- 체인을 만드는 줄(
chain = …)은 아무것도 실행하지 않습니다. 김밥 줄에 사람을 세워 두기만 한 것입니다.invoke를 불러야 처음으로 움직입니다.