LangChain 챗봇4장 · 대화형 챗봇과 메모리 전략12 / 22← 이전목차다음 →MetaContents

4장. 대화형 챗봇과 메모리 전략

메모리 전략 고르기

상담원에게 손님 사정을 넘기는 방법은 세 가지입니다. 기록철을 통째로 넘기기, 최근 몇 장만 넘기기, 요약 메모와 최근 몇 장을 함께 넘기기. 통째로 넘기면 빠짐없지만 읽는 데 오래 걸리고, 최근 몇 장은 빠르지만 앞의 일을 모르고, 요약은 둘 사이를 메우지만 요약을 쓰는 품이 듭니다. 챗봇의 쓰임에 따라 골라야 합니다.

모두 보내기, Window, Summary 의 세 메모리 전략을 보내는 양과 기억하는 것과 드는 품으로 나란히 견준 비교 그림

세 상자를 위에서 아래로 읽으면 보내는 것, 기억하는 것, 드는 품입니다. 왼쪽 파랑 상자(모두 보내기)는 기억은 가장 좋지만 보내는 양이 대화만큼 늘고, 가운데 보라 상자(Window)는 양이 일정하지만 앞의 것을 잊고, 오른쪽 하늘 상자(Summary)는 요약으로 앞의 것을 지키는 대신 요약 호출이 더 듭니다. 여기서 기록철 통째가 모두 보내기, 최근 몇 장이 Window, 요약 메모와 최근 몇 장이 Summary 입니다. 비유와 다른 점은, 사람의 요약 메모는 한 번 쓰면 그만이지만 챗봇의 요약은 대화가 이어질 때마다 고쳐 써야 한다는 것입니다.

세 전략 견주기

견줄 것모두 보내기WindowSummary
보내는 것기록 전부최근 기록(토큰 상한까지)요약 + 최근 기록
보내는 토큰대화만큼 계속 는다상한 안에서 일정요약만큼 더해 거의 일정
앞의 일을 기억빠짐없이잊는다요약에 남긴 만큼
더 드는 것없음없음요약할 때마다 모델 호출
어울리는 챗봇몇 번 주고받고 끝나는 상담앞의 일이 중요하지 않은 FAQ오래 이어지며 조건을 쌓는 여행 비서

옛 이름과 지금 방식

인터넷의 LangChain 글에는 ConversationBufferMemory 같은 옛 메모리 클래스가 자주 나옵니다. 이 클래스들은 langchain 1.x 본체에는 없고 langchain-classic 이라는 따로 된 패키지로 옮겨졌습니다. 이 과정은 그 패키지를 설치하지 않고, 지금 판의 부품으로 같은 일을 합니다.

옛 이름하던 일이 장에서 쓰는 방식
ConversationBufferMemory기록을 모두 담아 보낸다(buffer, 쌓아 두는 곳)InMemoryChatMessageHistory + RunnableWithMessageHistory
ConversationBufferWindowMemory최근 k 쌍만 보낸다trim_messages
ConversationSummaryMemory대화를 요약해 보낸다요약 체인(prompt | model | StrOutputParser())을 직접 만든다

짧은 예시

1장의 chat_log.json 으로, 마지막 질문을 보낼 때 세 전략이 보내는 메시지 수와 토큰을 셉니다. 요약 글은 사람이 쓴 것으로 대신합니다.

Python
import json
from pathlib import Path

import tiktoken
from langchain_core.messages import SystemMessage, convert_to_messages, trim_messages

enc = tiktoken.get_encoding("o200k_base")


def count_tokens(messages):
    """메시지 목록의 글을 tiktoken 으로 세어 더한다(역할 표시 토큰은 빼고 센 어림값)."""
    return sum(len(enc.encode(m.content)) for m in messages)


# convert_to_messages: {"role", "content"} 사전 목록을 LangChain 메시지로 바꾼다
msgs = convert_to_messages(json.loads(Path("chat_log.json").read_text(encoding="utf-8")))
sent = msgs[:-1]        # 마지막 정리 질문까지(마지막 답은 뺀다)

full = sent
window = trim_messages(sent, max_tokens=100, token_counter=count_tokens,
                       strategy="last", include_system=True, start_on="human")
summary = [msgs[0], SystemMessage("앞의 대화 요약: 제주도 2박 3일, 아이 둘. 오전 출발, 동쪽 해안 숙소.")] + sent[-5:]

for name, m in [("모두 보내기", full), ("Window", window), ("Summary", summary)]:
    keeps = any("2박 3일" in x.content for x in m)
    print(f"{name}: 메시지 {len(m)}개, {count_tokens(m)} 토큰, 2박 3일 있음 {keeps}")
출력
모두 보내기: 메시지 20개, 317 토큰, 2박 3일 있음 True
Window: 메시지 6개, 96 토큰, 2박 3일 있음 False
Summary: 메시지 7개, 126 토큰, 2박 3일 있음 True

모두 보내기는 1장 프로젝트의 열째 요청(317 토큰)과 같은 양을 보냅니다. Window 는 토큰이 3분의 1 아래로 줄었지만 "2박 3일"이 없고, Summary 는 Window 보다 조금 더 보내면서 "2박 3일"을 지킵니다. 이 요약 글은 예시로 사람이 쓴 것이라, 모델이 쓴 요약은 실습 6과 프로젝트에서 봅니다.

코드가 의미하는 것

이 코드는 같은 기록철을 세 방법으로 넘길 때 넘기는 장 수와 분량, 빠진 조건을 한 번에 재는 일입니다. 요약 글은 사람이 쓴 것으로 대신해 모델을 부르지 않습니다.

코드하는 일챗봇에서 뜻하는 것
count_tokens(messages)메시지 글의 토큰을 더한다넘기는 분량, 곧 요금을 재는 자입니다. 역할 표시 토큰은 빼고 세므로 어림값입니다
convert_to_messages(json.loads(…))1장의 대화 파일을 메시지로 바꾼다파일에 남긴 기록을 LangChain 부품이 다루는 모양으로 되살립니다
trim_messages(sent, max_tokens=100, …)뒤에서부터 100 토큰까지 남긴다Window 전략입니다
[msgs[0], SystemMessage(…)] + sent[-5:]시스템, 요약, 최근 5개를 잇는다Summary 전략입니다. sent[-5:] 에는 최근 4개와 정리 질문이 들어 있습니다
any("2박 3일" in x.content for x in m)보낼 메시지에 조건이 있는지 본다토큰 수로는 보이지 않는 "잊은 것"을 글자로 확인합니다

결과에서 읽을 것은 세 가지입니다.

  • 모두 보내기가 가장 많다. 메시지 20개, 317 토큰입니다. 요약 호출이 없는 대신 대화만큼 늘어납니다.
  • Window 는 가장 적게 보내고 조건을 잃었다. 96 토큰으로 줄었지만 "2박 3일 있음"이 False 입니다.
  • Summary 는 30 토큰 더 보내고 조건을 지켰다. 126 토큰으로 Window 보다 30 토큰 많고, True 입니다.

주의할 점

  • 세 전략은 섞어 쓸 수 있습니다. 이 장의 Summary 도 "요약 + 최근 몇 개"라서 Window 를 품고 있습니다.
  • 아무 전략을 써도 모델은 받은 것만 압니다. 챗봇이 엉뚱하게 잊었다면 먼저 "이번 요청에 무엇을 보냈는지"를 찍어 봅니다.
  • 손님의 이름, 연락처 같은 개인정보는 요약에 남기지 않도록 요약 프롬프트에 적어 둡니다. 요약은 오래 남는 기록이기 때문입니다.
← 이전 절4장 · 앞부분을 요약하는 Summary 메모리다음 절 →4장 · 실습 지도