Message Passing Pattern

정의

Message Passing Pattern은 LangChain에서 멀티턴 대화를 구현하는 가장 기본적인 방식. 이전 대화 내용을 리스트에 수동으로 저장하고, 프롬프트에 직접 전달하는 패턴이다.

핵심 개념

대화 리스트 = [메시지1, 메시지2, 메시지3, ...]
                    ↓
             프롬프트에 직접 삽입
                    ↓
         "이전 대화: [메시지들...]"
         "새 질문: [현재 입력]"

기본 구현

from langchain.schema import HumanMessage, AIMessage
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
 
# 1. 대화 리스트 초기화
chat_history = []
 
# 2. 프롬프트 구성 (messages 플레이스홀더)
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 도움을 주는 어시스턴트입니다."),
    ("placeholder", "{messages}"),
    ("human", "{input}")
])
 
# 3. 모델과 체인
model = ChatOpenAI(model="gpt-4o")
chain = prompt | model
 
# 4. 첫 번째 대화
user_input = "안녕하세요, 저는 말차를 좋아해요"
chat_history.append(HumanMessage(content=user_input))
 
response = chain.invoke({
    "messages": chat_history,
    "input": user_input
})
 
chat_history.append(AIMessage(content=response.content))
 
# 5. 두 번째 대화 (이전 메시지 포함)
user_input = "디저트 추천해줄래?"
chat_history.append(HumanMessage(content=user_input))
 
response = chain.invoke({
    "messages": chat_history,    # ← 모든 이전 메시지 포함
    "input": user_input
})
 
chat_history.append(AIMessage(content=response.content))

메시지 타입

HumanMessage vs. AIMessage

from langchain.schema import HumanMessage, AIMessage
 
# 사용자 메시지
human_msg = HumanMessage(content="일본어로 '잘 먹겠습니다' 말해줄래?")
 
# AI 메시지
ai_msg = AIMessage(content="いただきます (이타다키마스)")
 
# 메시지 리스트
chat_history = [human_msg, ai_msg]
print(chat_history)
# Output:
# [
#   HumanMessage(content="일본어로 '잘 먹겠습니다' 말해줄래?"),
#   AIMessage(content="いただきます (이타다키마스)")
# ]

실제 예제: 기본 대화

from langchain.schema import HumanMessage, AIMessage
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
 
# 설정
chat_history = []
model = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 친절한 어시스턴트입니다."),
    ("placeholder", "{messages}"),
    ("human", "{input}")
])
chain = prompt | model
 
# 대화 1
print("=== 대화 1 ===")
user_input = "일본어로 '잘 먹겠습니다' 말해줄래?"
chat_history.append(HumanMessage(content=user_input))
 
response = chain.invoke({
    "messages": chat_history,
    "input": user_input
})
print(f"사용자: {user_input}")
print(f"AI: {response.content}")
chat_history.append(AIMessage(content=response.content))
 
# 대화 2 (이전 컨텍스트 활용)
print("\n=== 대화 2 ===")
user_input = "내가 방금 뭐라고 했지?"
chat_history.append(HumanMessage(content=user_input))
 
response = chain.invoke({
    "messages": chat_history,  # ← 이전 메시지 포함!
    "input": user_input
})
print(f"사용자: {user_input}")
print(f"AI: {response.content}")
chat_history.append(AIMessage(content=response.content))
# AI: "당신은 일본어로 '잘 먹겠습니다'를 어떻게 말하는지 물어봤습니다."

수동 루프 예제

from langchain.schema import HumanMessage, AIMessage
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
 
chat_history = []
model = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 친절한 어시스턴트입니다."),
    ("placeholder", "{messages}"),
    ("human", "{input}")
])
chain = prompt | model
 
# 대화 루프
while True:
    user_input = input("사용자: ")
    if user_input.lower() == "quit":
        break
    
    # 1. 사용자 입력 저장
    chat_history.append(HumanMessage(content=user_input))
    
    # 2. 모델에 invoke (전체 히스토리 포함)
    response = chain.invoke({
        "messages": chat_history,
        "input": user_input
    })
    
    # 3. AI 응답 출력 및 저장
    print(f"AI: {response.content}")
    chat_history.append(AIMessage(content=response.content))

프롬프트 보이기 (Debug용)

# 실제 프롬프트에 무엇이 전달되는지 보기
from langchain.schema import HumanMessage, AIMessage
 
chat_history = [
    HumanMessage(content="일본어로 '잘 먹겠습니다' 말해줄래?"),
    AIMessage(content="いただきます (이타다키마스)"),
    HumanMessage(content="내가 방금 뭐라고 했지?")
]
 
# 프롬프트에 전달될 메시지들
print("=== 프롬프트에 전달될 메시지 ===")
for msg in chat_history:
    print(f"{msg.__class__.__name__}: {msg.content}")
 
# 출력:
# HumanMessage: 일본어로 '잘 먹겠습니다' 말해줄래?
# AIMessage: いただきます (이타다키마스)
# HumanMessage: 내가 방금 뭐라고 했지?

장점과 단점

✅ 장점

  • 가장 기본적 — LangChain의 핵심 개념만 사용
  • 완전한 제어 — 메시지 추가/삭제/조작 자유도 높음
  • 단순한 구조 — 이해하기 쉬움
  • 플렉시블 — 도메인별 커스터마이제이션 용이

❌ 단점

  • 수동 관리 — 매번 append 필요
  • 토큰 낭비 — 모든 이전 메시지를 프롬프트에 포함
  • 스케일 문제 — 대화가 길수록 비용 ↑
  • 세션 관리 없음 — 다중 사용자 지원 복잡
  • 메모리 저장 불편 — 파일/DB 저장 시 별도 로직 필요

vs. ChatMessageHistory

항목Message PassingChatMessageHistory
저장 방식수동 appendadd_user_message() 메서드
메시지 타입HumanMessage, AIMessage동일
코드 길이짧음중간
구조화낮음높음
invoke 시 저장수동수동 (Method 2)

vs. RunnableWithMessageHistory

항목Message PassingRunnableWithMessageHistory
자동화
세션 관리
invoke 저장수동자동
다중 사용자복잡간단

토큰 효율성 문제

대화 1: 100 tokens
대화 2: 100 + 전체 히스토리 = 300 tokens ⬆️
대화 3: 100 + 전체 히스토리 = 500 tokens ⬆️⬆️
대화 10: 100 + 전체 히스토리 = 1500+ tokens 🔥

해결책: RunnableWithMessageHistory + Message Summarization

심화 예제: 음악 추천 대화

from langchain.schema import HumanMessage, AIMessage
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
 
chat_history = []
model = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 음악 추천 전문가입니다."),
    ("placeholder", "{messages}"),
    ("human", "{input}")
])
chain = prompt | model
 
# 대화 흐름
messages_and_inputs = [
    "안녕",
    "나는 말차를 좋아해",
    "디저트 추천해줄래?",  # ← AI가 "말차"를 기억해야 함
]
 
for user_input in messages_and_inputs:
    chat_history.append(HumanMessage(content=user_input))
    
    response = chain.invoke({
        "messages": chat_history,
        "input": user_input
    })
    
    print(f"사용자: {user_input}")
    print(f"AI: {response.content}\n")
    
    chat_history.append(AIMessage(content=response.content))

학습 경로

  1. Message Passing Pattern (본 문서) — 기초 이해
  2. ChatMessageHistory — 구조화
  3. RunnableWithMessageHistory — 자동화
  4. Message Summarization — 토큰 최적화

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