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

3장. LangChain 기본 구조와 LCEL

실습 2 — 가짜 모델로 체인 조립하고 시험하기

프롬프트, 가짜 모델(FakeListChatModel), 파서를 | 로 이어 체인을 조립하고, 부품 사이에 엿보는 부품을 끼워 단계마다 무엇이 흐르는지 찍어 봅니다. 마지막으로 batch 로 손님 질문 세 개를 한꺼번에 넣습니다. 가짜 모델은 질문을 읽지 않고 정해 둔 답을 차례로 돌려주므로 API 키가 없어도 됩니다.

1단계. 부품 셋 만들기

Python
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단계. 부품을 하나씩 불러 흐르는 것 보기

Python
# 체인을 쓰지 않고 손으로 한 걸음씩 넘긴다
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단계. | 로 잇고 엿보는 부품 끼우기

Python
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 로 여러 질문 한꺼번에

Python
# 엿보기 없는 체인. 가짜 모델도 다시 만들어 첫 답부터 나오게 한다
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 를 빼고 동시에 돌려도 짝이 섞이지 않습니다. 다만 요청 한도 안에서 숫자를 정합니다.
← 이전 절3장 · 실습 1 — 프롬프트 템플릿 만들고 채워 보기다음 절 →3장 · 실습 3 — 구름다리여행 Q&A 챗봇 체인