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 ""

토큰 비용 비교

대화 수전체 히스토리요약본절감율
101000 tokens300 tokens70%
303000 tokens500 tokens83%
10010000 tokens800 tokens92%

요약 품질 트레이드오프

토큰 절감 ↑           정보 보존 ↓
│                          │
├─ 최대 압축 (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 호출 추가 비용

통합 개념


관련 소스: word-chain-chatbot (Method 4 예제)