3장. LangChain 기본 구조와 LCEL
실습 2 — 가짜 모델로 체인 조립하고 시험하기
프롬프트, 가짜 모델(FakeListChatModel), 파서를 | 로 이어 체인을 조립하고, 부품 사이에 엿보는 부품을 끼워 단계마다 무엇이 흐르는지 찍어 봅니다. 마지막으로 batch 로 손님 질문 세 개를 한꺼번에 넣습니다. 가짜 모델은 질문을 읽지 않고 정해 둔 답을 차례로 돌려주므로 API 키가 없어도 됩니다.
1단계. 부품 셋 만들기
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}"),
])
# 가짜 모델이 차례로 돌려줄 답 네 개(질문과 상관없이 이 차례로 나온다)
RESPONSES = [
"출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.",
"국내선 상품은 15kg 짐 한 개가 포함되어 있습니다.",
"5만 원 이상 결제하면 최대 6개월까지 할부가 됩니다.",
"반려동물 동반 상품은 '함께 여행' 메뉴에서만 예약할 수 있습니다.",
]
model = FakeListChatModel(responses=RESPONSES)
parser = StrOutputParser()
for part in [prompt, model, parser]:
print(type(part).__name__)ChatPromptTemplate
FakeListChatModel
StrOutputParser코드가 의미하는 것 FakeListChatModel(responses=RESPONSES) 는 질문을 읽지 않고 목록의 답을 차례로 내놓는 대역이지만, 진짜 모델과 같은 손잡이를 가진 부품이라 같은 자리에 끼울 수 있습니다. 부품 셋을 따로 만들어 두는 것은 키와 요금 없이 챗봇의 짜임부터 맞춰 보려는 것입니다.
2단계. 부품을 하나씩 불러 흐르는 것 보기
# 체인을 쓰지 않고 손으로 한 걸음씩 넘긴다
value = prompt.invoke({"question": "예약 날짜를 바꿀 수 있나요?"})
print("1) 프롬프트 뒤:", type(value).__name__, "| 메시지", len(value.to_messages()), "개")
reply = model.invoke(value)
print("2) 모델 뒤:", type(reply).__name__, "|", reply.content)
text = parser.invoke(reply)
print("3) 파서 뒤: 글자열인가?", isinstance(text, str), "|", text)1) 프롬프트 뒤: ChatPromptValue | 메시지 2 개
2) 모델 뒤: AIMessage | 출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.
3) 파서 뒤: 글자열인가? True | 출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.코드가 의미하는 것 prompt.invoke → model.invoke → parser.invoke 를 손으로 이어 부른 것은 체인이 안에서 하는 일을 풀어 본 것입니다. 앞 부품의 결과를 손대지 않고 다음 부품에 넣었다는 점이 | 의 뜻 그대로입니다.
3단계. | 로 잇고 엿보는 부품 끼우기
from langchain_core.runnables import RunnableLambda
def peek(value):
"""흐르는 값의 꼴을 찍고, 값은 바꾸지 않고 그대로 넘긴다."""
print(" [엿보기]", type(value).__name__)
return value
# 답을 처음부터 다시 받도록 가짜 모델을 새로 만든다
model = FakeListChatModel(responses=RESPONSES)
# 부품 사이마다 엿보는 부품을 끼운다. 엿보기를 빼면 prompt | model | parser 와 같다
spy_chain = prompt | RunnableLambda(peek) | model | RunnableLambda(peek) | parser
answer = spy_chain.invoke({"question": "예약 날짜를 바꿀 수 있나요?"})
print("체인의 결과:", answer) [엿보기] ChatPromptValue
[엿보기] AIMessage
체인의 결과: 출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.코드가 의미하는 것 RunnableLambda(peek) 는 값을 바꾸지 않고 들여다보기만 하는 부품으로, 벨트 중간에 세운 검문소입니다. 챗봇이 엉뚱한 답을 낼 때 어느 부품 사이에서 꼴이 틀어졌는지 찾는 방법이고, 다 고친 뒤 빼면 원래 체인과 같습니다.
4단계. batch 로 여러 질문 한꺼번에
# 엿보기 없는 체인. 가짜 모델도 다시 만들어 첫 답부터 나오게 한다
model = FakeListChatModel(responses=RESPONSES)
chain = prompt | model | parser
questions = ["예약 날짜를 바꿀 수 있나요?", "짐은 몇 kg 까지 되나요?", "카드 할부 돼요?"]
# batch: 사전 목록을 넣으면 같은 차례의 답 목록이 돌아온다
# max_concurrency=1: 한 번에 하나씩 차례로 돌린다(가짜 모델의 답 차례가 섞이지 않게)
answers = chain.batch([{"question": q} for q in questions], config={"max_concurrency": 1})
for q, a in zip(questions, answers):
print(f"Q. {q}\nA. {a}")
print("돌아온 답의 수:", len(answers))Q. 예약 날짜를 바꿀 수 있나요?
A. 출발 7일 전까지 한 번 무료로 바꿀 수 있습니다.
Q. 짐은 몇 kg 까지 되나요?
A. 국내선 상품은 15kg 짐 한 개가 포함되어 있습니다.
Q. 카드 할부 돼요?
A. 5만 원 이상 결제하면 최대 6개월까지 할부가 됩니다.
돌아온 답의 수: 3코드가 의미하는 것 chain.batch([...]) 는 손님 질문 묶음을 체인에 한꺼번에 넣는 일로, 챗봇을 내놓기 전에 시험 질문 여러 개를 한 번에 돌려 볼 때 씁니다. 결과 목록의 차례가 넣은 목록의 차례와 같으므로 zip 으로 질문과 답을 다시 짝지을 수 있습니다.
이 실습이 뜻하는 것
이 실습은 김밥 줄을 세우고 사람마다 무엇을 넘겨받는지 지켜보는 일과 같습니다. 줄에 설 세 사람을 정하고(1단계), 손으로 하나씩 넘겨 보고(2단계), 사람 사이에 지켜보는 사람을 세우고(3단계), 주문 세 건을 한꺼번에 넣어 봅니다(4단계). 여기서 줄에 선 사람이 부품, 지켜보는 사람이 peek, 주문 세 건이 batch 입니다. 비유와 다른 점은, 이 줄의 요리사(가짜 모델)는 주문서를 읽지 않고 정해 둔 요리를 차례로 낸다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 부품 | ChatPromptTemplate, FakeListChatModel, StrOutputParser | 체인에 들어갈 세 부품입니다. 만들기만 했고 아직 아무것도 실행하지 않았습니다 |
| 2. 하나씩 | ChatPromptValue(메시지 2개) → AIMessage → 글자열 True | 본문 "파이프로 잇기와 데이터가 바뀌는 모습" 절의 표대로 꼴이 바뀌었습니다 |
| 3. 엿보기 | [엿보기] ChatPromptValue, [엿보기] AIMessage | 체인 안에서도 2단계와 같은 꼴이 흐릅니다. 체인의 결과도 2단계와 같은 글입니다 |
| 4. batch | 질문 3개, 답 3개 | 넣은 차례 그대로 답이 돌아왔습니다. 가짜 모델이라 답은 RESPONSES 의 앞 세 개입니다 |
한 줄로 줄이면, 손으로 하나씩 넘기는 것과 | 로 이은 체인이 같은 일을 한다는 것을 눈으로 확인한 실습입니다. 2단계와 3단계의 꼴과 결과가 똑같습니다. 체인은 손으로 넘기는 일을 대신 해 줄 뿐입니다. 4단계의 답이 질문과 맞아 보이는 것은 RESPONSES 를 질문 차례에 맞춰 적어 두었기 때문이고, 질문 차례를 바꾸면 답이 엉뚱해집니다. 실습 3에서 가운데 부품을 진짜 모델로 바꿉니다.
코드 설명
RunnableLambda(peek)는 값을 찍기만 하고 그대로 돌려주는 함수를 부품으로 만듭니다. 체인 사이에 끼워도 결과는 바뀌지 않으므로, 체인이 이상할 때 어디서 꼴이 바뀌는지 찾는 데 씁니다.FakeListChatModel은 부를 때마다 목록의 다음 답을 내놓습니다. 3단계와 4단계에서 새로 만든 것은 앞 단계에서 답을 이미 썼기 때문입니다.chain.batch([...], config={"max_concurrency": 1})는 여러 입력을 차례로 하나씩 돌립니다.config는 실행 방식을 정하는 설정 사전이고,max_concurrency는 동시에 돌릴 최대 개수입니다. 가짜 모델은 답의 차례가 정해져 있어서 하나씩 돌려야 질문과 답의 짝이 늘 같습니다.- 진짜 모델은 질문을 읽고 답하므로
max_concurrency를 빼고 동시에 돌려도 짝이 섞이지 않습니다. 다만 요청 한도 안에서 숫자를 정합니다.