1장. 과정 소개와 LLM 의 이해
실습 5 — 메시지를 쌓아 대화 흉내 내기
LangChain 의 메시지 객체로 대화를 만들고, 가짜 모델(FakeListChatModel)로 세 번 주고받습니다. 요청마다 앞의 대화를 모두 함께 보내는 모습을 보고, 보내는 토큰이 어떻게 늘어나는지 셉니다. 가짜 모델은 질문을 읽지 않고 정해 둔 답을 차례로 돌려주므로 API 키가 없어도 됩니다.
1단계. 메시지 만들기
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
# 시스템 메시지(지시)와 사용자 메시지(질문)를 순서대로 담는다
messages = [
SystemMessage("너는 구름다리여행의 친절한 여행 비서다. 짧게 답한다."),
HumanMessage("다음 달에 제주도에 가요."),
]
for m in messages:
# type 은 역할 이름(system, human, ai), content 는 내용이다
print(m.type, "|", m.content)system | 너는 구름다리여행의 친절한 여행 비서다. 짧게 답한다.
human | 다음 달에 제주도에 가요.코드가 의미하는 것 본문 "메시지와 역할" 절의 사전 목록을 LangChain 의 메시지 객체로 바꾼 것입니다. SystemMessage 와 HumanMessage 는 역할 이름표가 붙은 상자로, m.type 이 그 이름표(system, human)이고 m.content 가 내용입니다. 앞으로 모든 장에서 모델과 주고받는 말은 이 객체들로 다룹니다.
2단계. 가짜 모델에 보내기
from langchain_core.language_models.fake_chat_models import FakeListChatModel
# 가짜 모델이 차례로 돌려줄 답 세 개
RESPONSES = [
"좋습니다. 며칠 일정으로 생각하고 계신가요?",
"2박 3일이면 동쪽 해안 일정을 추천드립니다.",
"다음 달, 제주도, 2박 3일, 동쪽 해안 일정입니다.",
]
model = FakeListChatModel(responses=RESPONSES)
# invoke = "불러서 실행하다". 메시지 목록을 보내고 답을 받는다
reply = model.invoke(messages)
# 답은 AIMessage 객체로 온다. 진짜 모델로 바꿔도 같은 꼴이다
print(type(reply).__name__, "|", reply.content)AIMessage | 좋습니다. 며칠 일정으로 생각하고 계신가요?코드가 의미하는 것 model.invoke(messages) 는 메시지 목록을 모델에 보내고 답을 받는 한 번의 요청입니다. 답이 AIMessage 로 온다는 것이 핵심으로, 2장에서 가짜 모델을 진짜 모델(ChatOpenAI)로 바꿔도 보내는 법과 받는 꼴은 똑같습니다. 가짜 모델은 질문을 읽지 않고 RESPONSES 의 첫 답을 돌려줬습니다.
3단계. 세 번 주고받기
# 답을 처음부터 다시 받도록 가짜 모델을 새로 만든다
model = FakeListChatModel(responses=RESPONSES)
history = [SystemMessage("너는 구름다리여행의 친절한 여행 비서다. 짧게 답한다.")]
user_inputs = ["다음 달에 제주도에 가요.", "2박 3일이요.", "일정을 정리해 주세요."]
for k, text in enumerate(user_inputs, start=1):
history.append(HumanMessage(text)) # 새 질문을 기록에 붙이고
reply = model.invoke(history) # 기록 전체를 보낸다
print(f"{k}번째 요청: 메시지 {len(history)}개 보냄 → {reply.content}")
history.append(reply) # 받은 답도 기록에 붙인다
print("마지막 기록의 메시지 수:", len(history))1번째 요청: 메시지 2개 보냄 → 좋습니다. 며칠 일정으로 생각하고 계신가요?
2번째 요청: 메시지 4개 보냄 → 2박 3일이면 동쪽 해안 일정을 추천드립니다.
3번째 요청: 메시지 6개 보냄 → 다음 달, 제주도, 2박 3일, 동쪽 해안 일정입니다.
마지막 기록의 메시지 수: 7코드가 의미하는 것 history.append(...) 로 질문과 답을 쌓고, 요청마다 history 전체를 보냅니다. 이것이 "기억하지 못하는 LLM"에게 대화를 이어 가게 하는 방법입니다. 출력의 2, 4, 6 은 요청마다 함께 보내는 메시지가 늘어나는 모습이고, 마지막 7 은 셋째 답까지 쌓인 기록의 길이입니다.
4단계. 요청마다 보낸 토큰 세고 저장하기
import json
from pathlib import Path
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
sent = []
for k in range(len(user_inputs)):
# k+1 번째 요청에 보낸 메시지: 시스템 1개 + 앞의 질문·답 k쌍 + 새 질문 1개
upto = history[: 2 + 2 * k]
sent.append(sum(len(enc.encode(m.content)) for m in upto))
print("요청마다 보낸 토큰:", sent)
# 기록을 OpenAI 메시지 꼴의 json 으로 남긴다
log = [{"role": m.type, "content": m.content} for m in history]
Path("out_chat_history.json").write_text(json.dumps(log, ensure_ascii=False, indent=2), encoding="utf-8")
print("저장: out_chat_history.json")요청마다 보낸 토큰: [33, 56, 78]
저장: out_chat_history.json코드가 의미하는 것 history[: 2 + 2 * k] 는 k+1 번째 요청 때 실제로 보냈던 메시지 묶음을 다시 잘라 낸 것이고, 그 토큰을 모두 더하면 그 요청의 입력 토큰이 됩니다. 33, 56, 78 은 대화가 길어질수록 같은 질문 하나에도 보내는 토큰이 늘어난다는 뜻이며, 요금도 그만큼 늘어납니다. 기록을 OpenAI 메시지 꼴의 json 으로 남기는 것은 대화를 저장하는 가장 흔한 모양을 익히려는 것입니다.
이 실습이 뜻하는 것
이 실습은 콜센터에서 상담원이 바뀔 때마다 상담 기록 메모를 통째로 넘기는 일과 같습니다. 메모의 첫 장(지시)과 손님의 말을 적고(1단계), 상담원에게 넘겨 답을 받고(2단계), 세 번 주고받으며 메모를 쌓고(3단계), 넘긴 메모의 분량을 잽니다(4단계). 여기서 메모가 history, 상담원이 모델, 메모 분량이 토큰 수입니다. 비유와 다른 점은, 이 상담원(가짜 모델)은 메모를 읽지도 않고 정해 둔 답을 차례로 말한다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 메시지 | system, human | SystemMessage 의 역할 이름은 system, HumanMessage 는 human 입니다. OpenAI 의 user 를 LangChain 은 human 이라 부릅니다 |
| 2. 가짜 모델 | AIMessage | 답이 글이 아니라 AIMessage 객체로 왔습니다. 글은 .content 에 들어 있습니다 |
| 3. 세 번 주고받기 | 메시지 2개, 4개, 6개 | 요청마다 두 개씩 늘었습니다. 마지막 답까지 붙이면 기록은 7개입니다 |
| 4. 보낸 토큰 | 33, 56, 78 | 질문은 짧아도 앞의 대화가 쌓여 요청마다 22~23 토큰씩 늘었습니다 |
한 줄로 줄이면, LangChain 의 메시지로 대화를 쌓고 요청마다 기록 전체를 보내는 흐름을 직접 만든 실습입니다. 세 번만 주고받아도 보내는 토큰이 33 에서 78 로 두 배를 넘었습니다. 프로젝트에서 열 번 주고받는 대화로 이 늘어남을 더 크게 봅니다. 가짜 모델의 답은 질문과 상관없이 정해 둔 것이고, 2장에서 같은 코드에 진짜 모델을 끼웁니다.
코드 설명
SystemMessage("…"),HumanMessage("…")는 역할과 내용을 함께 담은 메시지 객체입니다.m.type이 역할 이름,m.content가 내용입니다.FakeListChatModel(responses=[…])는 부를 때마다 목록의 답을 하나씩 차례로 돌려줍니다. 3단계에서 새로 만든 것은 2단계에서 첫 답을 이미 썼기 때문입니다.model.invoke(history)는 메시지 목록 전체를 받습니다. 진짜 모델도 같은invoke로 부르므로, 2장에서는model을 만드는 줄만 바꾸면 됩니다.history[: 2 + 2 * k]는 k+1 번째 요청 때 보낸 메시지만 잘라 냅니다. k 가 0 이면 시스템과 첫 질문 2개, 1 이면 4개입니다.