4장. 대화형 챗봇과 메모리 전략
대화 기억의 원리
1장에서 콜센터 상담원 비유를 들었습니다. 전화할 때마다 상담원이 바뀌므로 상담 기록 메모를 넘겨줘야 이어서 이야기할 수 있었습니다. 그 메모를 누가 챙기느냐가 이 절의 질문입니다. 답은 "우리 코드"입니다. 모델은 기억하지 않으므로, 우리 코드가 상담 기록철을 들고 있다가 요청마다 꺼내 보여 주고, 답을 받으면 새 장을 끼워 넣습니다.
질문 하나마다 네 상자를 한 바퀴 돕니다. 기록을 꺼내고, 새 질문을 뒤에 붙여 모델에 보내고, 받은 답을 질문과 함께 기록에 넣습니다. 다음 질문은 한 쌍이 늘어난 기록으로 다시 첫 상자부터 합니다. 여기서 상담 기록철이 대화 기록(메시지 목록), 철을 들고 다니는 사람이 우리 코드, 메모를 받아 읽는 상담원이 모델입니다. 비유와 다른 점은, 상담원은 메모를 훑어보고 필요한 곳만 읽지만 모델은 받은 기록을 모두 토큰으로 세어 읽고 그만큼 요금을 받는다는 것입니다.
누가 무엇을 하나
| 하는 일 | 맡는 쪽 | 이 장에서 쓰는 것 |
|---|---|---|
| 기록 담아 두기 | 우리 코드 | 목록, InMemoryChatMessageHistory |
| 기록을 프롬프트에 넣기 | 우리 코드 | MessagesPlaceholder |
| 기록 꺼내고 덧붙이기를 저절로 | 우리 코드(LangChain) | RunnableWithMessageHistory |
| 너무 긴 기록 줄이기 | 우리 코드 | trim_messages, 요약 체인 |
| 기록을 읽고 답하기 | 모델 | ChatOpenAI |
짧은 예시
기록을 목록에 담고 네 걸음을 함수 하나로 묶습니다. 1장 실습 5와 같은 일을 함수로 정리한 것입니다. 가짜 모델이라 키가 필요 없습니다.
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.messages import HumanMessage, SystemMessage
model = FakeListChatModel(responses=["며칠 일정인가요?", "2박 3일이면 동쪽 해안을 추천합니다."])
history = [SystemMessage("너는 구름다리여행의 여행 비서다.")]
def chat(question):
messages = history + [HumanMessage(question)] # 1·2걸음: 기록을 꺼내 새 질문을 붙인다
reply = model.invoke(messages) # 3걸음: 모두 모델에 보낸다
history.extend([HumanMessage(question), reply]) # 4걸음: 질문과 답을 기록에 넣는다
return len(messages), reply.content
print(chat("제주도에 가요."))
print(chat("2박 3일이요."))
print("기록의 메시지 수:", len(history))(2, '며칠 일정인가요?')
(4, '2박 3일이면 동쪽 해안을 추천합니다.')
기록의 메시지 수: 5첫 요청에는 메시지 2개(시스템, 질문)를, 둘째 요청에는 4개를 보냈습니다. 기록에는 시스템 1개와 질문·답 두 쌍이 남아 5개입니다. 이 함수를 손님마다 따로 만들고, 기록이 길어지면 줄이는 일을 LangChain 의 부품으로 하나씩 바꿔 가는 것이 이 장의 나머지입니다.
코드가 의미하는 것
이 코드는 상담 기록철을 들고 다니는 사람(우리 코드)의 네 걸음을 함수 하나로 적은 것입니다. 기록을 꺼내고, 새 질문을 붙이고, 모델에 보내고, 질문과 답을 기록에 넣습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
history = [SystemMessage(...)] | 시스템 지시 하나로 기록을 시작한다 | 상담 기록철입니다. 모델 안이 아니라 우리 코드의 목록에 들어 있습니다 |
history + [HumanMessage(question)] | 기록 뒤에 새 질문을 붙인 목록을 만든다 | 이번 요청에 모델이 받는 전부입니다. 모델은 이 목록 밖의 것을 모릅니다 |
model.invoke(messages) | 목록을 통째로 모델에 보낸다 | 요청마다 기록 전체를 다시 보낸다는 뜻이고, 그만큼 토큰이 듭니다 |
history.extend([HumanMessage(question), reply]) | 질문과 답을 기록에 넣는다 | 다음 요청을 위한 준비입니다. 이 줄이 없으면 챗봇은 매번 처음 만난 것처럼 답합니다 |
결과에서 읽을 것은 두 가지입니다.
- 보내는 메시지가 2개에서 4개로 늘었다. 둘째 요청에는 첫 질문과 답이 함께 갔습니다. 대화가 길어질수록 보내는 양이 계속 늡니다.
- 기록에는 5개가 남았다. 시스템 1개와 질문·답 두 쌍입니다. 셋째 질문을 하면 이 5개에 새 질문을 더한 6개를 보내게 됩니다.
주의할 점
- 기록은 질문과 답을 둘 다 넣어야 합니다. 질문만 넣으면 모델은 자기가 앞에서 무엇을 추천했는지 모릅니다.
history를 함수 밖의 목록 하나로 두면 모든 손님의 대화가 한 철에 섞입니다. 손님마다 기록을 나누는 법은 "손님마다 기록 상자 나누기" 절에서 봅니다.