Message Summarization
정의
Message Summarization은 대화가 길어질 때 LLM이 과거 메시지를 요약하여 저장하는 메모리 최적화 기법. 전체 히스토리 대신 압축된 요약본을 사용하여 토큰 비용을 절감하고, 장시간 대화를 지원한다.
핵심 문제: 토큰 폭증
대화 1: "말차" → 100 tokens
대화 2: ["말차"] + 입력 → 200 tokens
대화 3: ["말차"] + 모든 메시지 → 400 tokens
대화 10: 모든 메시지... → 2000+ tokens 💸
해결: 대화를 요약하여 핵심만 보존 → 토큰 50~70% 감소
기본 구현
from langchain.memory import ChatMessageHistory
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
# 요약 프롬프트
summarization_prompt = ChatPromptTemplate.from_messages([
("system", "다음 대화를 한 문장으로 요약하세요. 최대한 많은 정보를 유지하세요."),
("placeholder", "{chat_history}")
])
model = ChatOpenAI(model="gpt-4o")
summarize_chain = summarization_prompt | model
def summarize_messages(chat_history: ChatMessageHistory) -> str:
"""히스토리를 요약하여 반환"""
if len(chat_history.messages) == 0:
return ""
# 요약 생성
summary = summarize_chain.invoke({
"chat_history": chat_history.messages
})
# 원본 히스토리 삭제
chat_history.clear()
# 요약으로 교체
chat_history.add_ai_message(summary.content)
return ""
# 사용
history = ChatMessageHistory()
history.add_user_message("말차 좋아해")
history.add_ai_message("좋아요, 말차는...")
history.add_user_message("디저트 추천해줘")
history.add_ai_message("말차를 좋아하시니...")
# 요약 실행
summarize_messages(history)
# 원본: 4개 메시지 → 요약: 1개 메시지원칙
Before (원본 히스토리):
HumanMessage: "말차 좋아해"
AIMessage: "좋아요, 말차는..."
HumanMessage: "디저트 추천해줘"
AIMessage: "말차를 좋아하시니..."
After (요약):
AIMessage: "사용자는 말차를 좋아하며, 말차 관련 디저트를 추천해달라고 했습니다."
RunnableWithMessageHistory와 함께 사용
from langchain.runnables import RunnableLambda
def summarize_on_invoke(session_history: dict, session_id: str):
"""invoke 전 요약 실행"""
history = session_history[session_id]
summarize_messages(history)
# 완전 통합 파이프라인
def get_session_history(session_id: str):
if session_id not in session_history_store:
session_history_store[session_id] = ChatMessageHistory()
return session_history_store[session_id]
# 요약 + invoke 체인
summarize_step = RunnableLambda(
lambda x: summarize_messages(get_session_history(x["session_id"]))
)
full_chain = (
summarize_step |
runnable_with_history
)도메인별 커스텀 요약
예제 1: 게임 (끝말잇기)
game_summary_prompt = ChatPromptTemplate.from_messages([
("system", "끝말잇기 게임에서 사용자와 AI가 주고받은 단어만 나열하세요.\n형식: '사용자: 사과 / AI: 과자 / 사용자: 자동차 / AI: 차멀미'"),
("placeholder", "{chat_history}")
])
# 게임에서는 단어만 추출 (게임 로직에 필요)
game_summary = game_summary_prompt | model
def summarize_game(chat_history: ChatMessageHistory) -> str:
if len(chat_history.messages) == 0:
return ""
summary = game_summary.invoke({
"chat_history": chat_history.messages
})
chat_history.clear()
chat_history.add_ai_message(f"게임 기록: {summary.content}")
return ""예제 2: 음악 추천
music_summary_prompt = ChatPromptTemplate.from_messages([
("system", "음악 추천 대화에서 사용자의 선호도, 아티스트, 장르만 추출하세요.\n형식: '선호도: 말차(음료) / 아티스트: (언급된 아티스트) / 장르: (언급된 장르)'"),
("placeholder", "{chat_history}")
])
music_summary = music_summary_prompt | model
def summarize_music_preferences(chat_history: ChatMessageHistory) -> str:
if len(chat_history.messages) == 0:
return ""
summary = music_summary.invoke({
"chat_history": chat_history.messages
})
chat_history.clear()
chat_history.add_ai_message(f"사용자 프로필: {summary.content}")
return ""토큰 비용 비교
| 대화 수 | 전체 히스토리 | 요약본 | 절감율 |
|---|---|---|---|
| 10 | 1000 tokens | 300 tokens | 70% |
| 30 | 3000 tokens | 500 tokens | 83% |
| 100 | 10000 tokens | 800 tokens | 92% |
요약 품질 트레이드오프
토큰 절감 ↑ 정보 보존 ↓
│ │
├─ 최대 압축 (1문장) ├─ 고도 손실
├─ 중간 압축 (3~5문장) ├─ 적정 손실
└─ 최소 압축 (10문장) └─ 최소 손실
권장: 도메인과 대화 길이에 따라 선택
- 게임: 최대 압축 (게임 상태만 필요)
- 음악 추천: 중간 압축 (선호도 유지)
- 상담: 최소 압축 (감정·히스토리 보존)
점진적 요약 (Progressive Summarization)
def progressive_summarize(chat_history: ChatMessageHistory, threshold: int = 10):
"""메시지가 threshold 이상이면 자동 요약"""
if len(chat_history.messages) >= threshold:
summarize_messages(chat_history)
# 사용
for user_input in user_inputs:
chat_history.add_user_message(user_input)
response = chain.invoke({...})
chat_history.add_ai_message(response.content)
# 10개 메시지마다 자동 요약
progressive_summarize(chat_history, threshold=10)실제 예제: 음악 추천 + 요약
from langchain.memory import ChatMessageHistory
from langchain.runnables.history import RunnableWithMessageHistory
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
# 1. 기본 설정
session_history = {}
model = ChatOpenAI(model="gpt-4o")
def get_session_history(session_id: str):
if session_id not in session_history:
session_history[session_id] = ChatMessageHistory()
return session_history[session_id]
# 2. 요약 프롬프트
summarization_prompt = ChatPromptTemplate.from_messages([
("system", "사용자의 음악 취향과 선호도를 요약하세요."),
("placeholder", "{chat_history}")
])
summarize_chain = summarization_prompt | model
def summarize_messages(chat_history: ChatMessageHistory) -> str:
if len(chat_history.messages) == 0:
return ""
summary = summarize_chain.invoke({
"chat_history": chat_history.messages
})
chat_history.clear()
chat_history.add_ai_message(summary.content)
return ""
# 3. 기본 체인
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 음악 추천 전문가입니다."),
("placeholder", "{chat_history}"),
("human", "{input}")
])
chain = prompt | model
runnable_with_history = RunnableWithMessageHistory(
runnable=chain,
get_session_history=get_session_history,
input_messages_key="input",
history_messages_key="chat_history"
)
# 4. 대화 루프 (요약 포함)
messages = ["안녕", "나는 말차를 좋아해", "디저트 추천해줄래?"]
for user_input in messages:
# 요약 (메시지 5개마다)
if len(get_session_history("user1").messages) >= 5:
summarize_messages(get_session_history("user1"))
# 응답
response = runnable_with_history.invoke(
{"input": user_input},
config={"configurable": {"session_id": "user1"}}
)
print(f"사용자: {user_input}")
print(f"AI: {response.content}\n")장점과 단점
✅ 장점
- 토큰 절감 — 50~92% 비용 감소
- 장시간 대화 — 수백 개 메시지도 관리 가능
- 성능 개선 — 프롬프트 길이 감소 → 응답 속도 ↑
- 유연성 — 도메인별 커스터마이제이션 용이
❌ 단점
- 정보 손실 — 요약 과정에서 디테일 누락 가능
- 요약 오류 — LLM 요약 실수 발생 가능
- 복잡도 증가 — 코드 길이 증가
- 비용 — 요약용 API 호출 추가 비용
통합 개념
- RunnableWithMessageHistory — 메모리 관리 프레임워크
- ChatMessageHistory — 메시지 저장소
- Message Passing Pattern — 기본 패턴
- LangChain — 프레임워크
관련 소스: word-chain-chatbot (Method 4 예제)