4장. 대화형 챗봇과 메모리 전략
최근 대화만 남기는 Window 메모리
상담 기록철이 두꺼워지면 상담원은 앞에서부터 다 읽지 않고 맨 뒤의 몇 장만 펼쳐 봅니다. 책상이 좁아서 펼칠 수 있는 장 수도 정해져 있습니다. Window 메모리가 이 방법입니다. 기록을 뒤에서부터 세어 정한 양(토큰 상한)만큼만 남기고 앞쪽은 보내지 않습니다. LangChain 에서는 trim_messages 함수가 이 일을 합니다.
세 상자는 trim_messages 가 하는 차례입니다. 기록 전체를 받아 맨 뒤 메시지부터 토큰을 더해 가다가, 합이 상한을 넘기 직전에 멈추고 그 앞쪽을 잘라 냅니다. 시스템 메시지는 따로 챙겨 늘 맨 앞에 남깁니다. 여기서 책상의 크기가 토큰 상한(max_tokens), 맨 뒤의 몇 장이 남는 메시지입니다. 비유와 다른 점은, 상담원은 앞쪽에 중요한 장이 있으면 찾아 펴 보지만 trim_messages 는 중요한지와 상관없이 오래된 것부터 자른다는 것입니다.
trim_messages 의 인자
| 인자 | 뜻 | 이 장의 값 |
|---|---|---|
max_tokens | 남길 토큰의 상한 | 60 ~ 300 |
token_counter | 메시지 목록을 받아 토큰 수를 돌려주는 함수. len 을 주면 메시지 개수를 센다 | tiktoken 으로 센 함수 |
strategy | "last" 는 뒤에서부터 남기고, "first" 는 앞에서부터 남긴다. strategy 는 "전략"이라는 뜻 | "last" |
include_system | 맨 앞의 시스템 메시지를 늘 남길지 | True |
start_on | 남긴 기록이 이 역할의 메시지로 시작하게 한다 | "human" |
start_on="human" 은 남긴 기록이 답(ai)으로 시작하지 않게 합니다. 질문 없이 답부터 보이면 모델이 무엇에 대한 답인지 헷갈리기 때문입니다.
짧은 예시
token_counter=len 으로 주면 "토큰" 대신 "메시지 개수"를 셉니다. 메시지 7개의 기록을 상한 4개로 줄여 봅니다.
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, trim_messages
history = [
SystemMessage("너는 여행 비서다."),
HumanMessage("제주도 가요."), AIMessage("며칠인가요?"),
HumanMessage("2박 3일이요."), AIMessage("좋습니다."),
HumanMessage("숙소는 바닷가로요."), AIMessage("함덕을 추천합니다."),
]
kept = trim_messages(
history,
max_tokens=4, # 상한: 여기서는 메시지 4개
token_counter=len, # len: 메시지 개수를 센다
strategy="last", # 뒤에서부터 남긴다
include_system=True, # 시스템 메시지는 늘 남긴다
start_on="human", # 남긴 기록이 사람의 질문으로 시작하게 한다
)
for m in kept:
print(m.type, "|", m.content)system | 너는 여행 비서다.
human | 숙소는 바닷가로요.
ai | 함덕을 추천합니다.시스템 메시지를 포함해 상한 4개 안에서 뒤쪽이 남았습니다. 시스템 1개를 빼면 3개가 남는 자리인데, 뒤에서 셋째가 답("좋습니다.")이라 start_on="human" 때문에 그것까지 떼어 냈습니다. "제주도"와 "2박 3일"은 모델에게 가지 않습니다.
코드가 의미하는 것
이 코드는 책상에 펼칠 수 있는 장 수를 정하고 기록철의 뒤쪽부터 펼치는 일을 흉내 냅니다. 토큰 대신 메시지 개수로 세어 결과를 눈으로 따라가기 쉽게 했습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
max_tokens=4 | 남길 양의 상한을 정한다 | 책상의 크기입니다. 실제 챗봇에서는 토큰으로 정합니다 |
token_counter=len | 메시지 개수로 센다 | 세는 방법을 바꿔 끼울 수 있다는 뜻입니다. 실습 5에서는 tiktoken 으로 센 함수를 넘깁니다 |
strategy="last" | 뒤에서부터 남긴다 | 최근 대화가 지금 질문과 가장 가깝다고 보는 선택입니다 |
include_system=True | 시스템 메시지를 늘 남긴다 | 비서의 말투와 규칙은 잘라 내지 않는다는 뜻입니다 |
start_on="human" | 남긴 기록이 질문으로 시작하게 한다 | 답만 덩그러니 남아 모델이 헷갈리지 않게 합니다 |
결과에서 읽을 것은 두 가지입니다.
- 상한이 4 인데 3개만 남았다. 뒤에서 셋째인 "좋습니다."가 답이라
start_on="human"이 떼어 냈습니다. 상한은 "이만큼까지"이지 "꼭 이만큼"이 아닙니다. - 조건이 사라졌다. "제주도"와 "2박 3일"이 든 앞쪽 메시지가 모두 빠졌습니다. 이 기록만 받은 모델은 목적지를 모릅니다.
주의할 점
- 1장 프로젝트의 "최근 4개만"이 Window 메모리입니다. 그때 열 번의 입력 토큰 합은 모두 보내기 1,786 토큰에서 906 토큰으로 줄었지만, 마지막 정리 질문에서 "2박 3일"과 "아이 둘"을 잃었습니다. 실습 5에서 상한을 바꿔 가며 다시 봅니다.
- 실제 API 가 세는 토큰은 메시지마다 역할 표시 같은 몇 개가 더 붙습니다. tiktoken 으로 세는
token_counter는 어림값이므로 상한에 여유를 둡니다.