타임트리

[LangGraph] 과거 대화 이력의 요약 본문

LLM/LangGraph

[LangGraph] 과거 대화 이력의 요약

sean_j 2025. 1. 1. 04:13

대화 기록 요약 추가

멀티턴을 위해 대화 기록을 저장해두고, 모델 호출 시 프롬프트에 삽입하는 것은 채팅 관련 서비스에서 필수적인 기능 중 하나이다. 하지만 대화가 길어질수록 여러 제약이 따르는데, 예를 들면 Context length를 초과하게 되거나, Input token의 증가로 호출 비용이 증가하는 등의 문제가 있다. 그래서 일반적으로 대화 기록을 효과적으로 관리하는 방법을 사용한다.

대표적으로 아래와 같은 방법들들이 있다.

  1. Vector DB Integration: 대화 이력을 임베딩 벡터로 저장하고, 유사도 검색으로 관련 대화 가져오는 방법. 맥락 검색 정확도가 높으나, 임베딩/검색 과정에서 추가 리소스 필요
  2. Sliding Window: 최근 K개의 대화를 저장해 최신 대화에 집중 가능하나, 과거 맥락 손실
  3. Summary-based history: 이전 대화 내용을 요약하고 요약본만 사용. 대화가 길어저도 전체 맥락 유지 가능하나, 요약 과정에서 중요한 세부 정보가 손실될 가능성

이번에는 2번과 3번 방법을 절충한 대화 기록 요약 노드 summarize_chat_history을 만들어보고 그래프에 통합해보자. 만들고자 하는 대화 이력 관리 로직은 다음과 같다.

 

 

  • summarize_chat_history
    • 메세지 개수나 메세지 길이를 사용해서 대화가 너무 긴지 확인
    • 대화가 너무 길다면 요약 만들기
    • 마지막 K개의 메세지를 제외한 나머지 메세지 제거

위 방법을 통해, 과거 맥락을 잃어버리는 부분을 요약을 통해 어느정도 유지하고, 최신 대화는 정확한 이력을 가져가도록 함으로써 Sliding Window의 단점을 어느 정도 극복할 수 있다.

이때 마지막 K개를 제외한 나머지 메세지를 제거하는 부분은 앞서 RemoveMessage를 사용했던 [LangGraph] Delete Messages, 그리고 [LangGraph] ToolNode의 ReAct 에이전트를 구축했던 부분과 크게 다를 게 없다!

 

추가로, chat_model 노드에서는 과거 대화 이력의 요약을 담고있는 summary를 State로부터 가져와 프롬프트에 추가해준다.

챗봇 구축

1. 상태 정의

먼저 State를 정의하자. State에는 메세지의 이력을 담고 있는 messages와 대화의 이력을 요약본으로 갖고 있는 summary를 키로 지정한다.

from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]  # 메세지 리스트
    summary: str  # 요약본

2. 노드 정의

먼저 chatbot 노드를 정의하자. 기존에는 단순히 State의 messages를 가져와 모델을 호출했다면, 이번에는 {sumamry}플레이스 홀더에 대화 이력을 넣어줄 수 있도록 간단한 프롬프트를 작성하자.

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage


llm = ChatOpenAI(model="gpt-4o-mini")

def chatbot(state: State):
    summary = state.get("summary", "")
    # summary가 없다면 (대화 이력이 K개 미만이라면), 메세지만 사용
    if not summary:
        prompt = state["messages"]
    # summary가 있다면, llm 호출 시 prompt에 포함
    else:
        system_template = f"기존 대화 내용의 요약: {summary}"
        prompt = [SystemMessage(content=system_template)] + state["messages"]
    response = llm.invoke(prompt)
    return {"messages": [response]}

 

다음으로 summarize_chat_history 노드를 정의하자.

 

해당 노드에서는 state의 messages의 담긴 내용을 LLM을 호출해 요약한다. summary에 값이 존재하는지 유무에 따라 프롬프트를 달리하여 분기처리를 하자.

  • summary가 있는 경우: summary와 messages를 함께 넘겨 요약 수행
  • summary가 없는 경우: messages를 넘겨 요약 수행
    그 후 K=2를 사용해서 가장 최근 2개의 대화 이력만 남기는 로직을 만들어보자.
from langchain_core.messages import HumanMessage
from langgraph.graph.message import RemoveMessage


def summarize_chat_history(state: State):
    summary = state.get("summary", "")
    if not summary:
        summary_template = "아래 대화 내용을 요약해주세요.\n"
    else:
        summary_template = f"""지금까지의 대화 내용의 요약입니다.: {summary}
        아래 주어진 추가된 대화 이력을 고려해서 지금까지의 대화 내용의 요약과 결합해서 새로운 요약을 만들어주세요.
        [추가된 대화 이력]\n"""
    prompt = [HumanMessage(content=summary_template)] + state["messages"]
    summary = llm.invoke(prompt).content

    # 가장 최근 2개의 대화 이력만 남기고 나머지 메세지 삭제
    delete_messages = [RemoveMessage(id=message.id) for message in state["messages"][:-2]]
    return {
        "messages": delete_messages,
        "summary": summary
    }

3. 조건부 분기 처리

다음으로, chatbot 노드에서 "대화가 너무 길다"의 기준을 정해야 한다. 대화가 길지 않은 경우 해당 대화 이력을 그대로 LLM을 호출할 때 사용하고, 대화가 너무 길어지는 경우 최근 K=2개의 메세지를 남기고 나머지는 요약한다. 여기서는 5개 이상의 메세지 이력이 쌓인 경우 메세지를 요약해 State의 summary에 저장해보자.

 

이 로직을 따르면, 처음 4개의 메세지(user -> ai -> user -> ai)까지는 그대로 저장되지만, 3번째 턴이 발생하는 순간 2번째 턴까지의 메세지는 (처음 4개) 요약되고, 해당 턴의 대화 이력 (user -> ai)만 메세지 리스트 messages에 갖고 있게 된다. 그 다음 4번째 턴에서는 메세지 리스트 messages에 대화 이력이 2개만 존재하므로 요약은 그대로 두고, 4번째 턴의 대화는 메세지 리스트 messages에 담긴다.

 

즉, 3번째 턴 이후부터 홀수 번째 대화마다 요약이 생성된다.

from typing import Literal
from langgraph.graph import END


def should_summarize(state: State):
    messages = state["messages"]
    # messages에 5개 이상의 메세지가 쌓인 경우, 요약 노드로 이동
    if len(messages) > 5:
        return "summarize_chat_history"
    return END

4. 그래프 구축

이제 모든 노드와 조건부 분기 함수가 정의되었으니, 그래프를 만들어보자. 또, 대화 이력 관리를 위한 인메모리 객체인 MemorySaver를 정의하자.

 

 

from langgraph.graph import START
from langgraph.checkpoint.memory import MemorySaver


flow = StateGraph(State)

flow.add_node("chatbot", chatbot)
flow.add_node("summarize_chat_history", summarize_chat_history)

flow.add_edge(START, "chatbot")
flow.add_edge("summarize_chat_history", END)
flow.add_conditional_edges(
    "chatbot",
    should_summarize
)

memory = MemorySaver()
graph = flow.compile(checkpointer=memory)

5. 그래프 사용

이제 위에서 만든 그래프를 사용해보자. 의도한대로 잘 작동하는지 확인하기 위해 3번 호출해서 6개의 메세지를 생성하고 결과를 확인하자.

def print_update(update):
    "그래프가 실행되며 노드별 업데이트된 상태에서 메세지 출력, 단, summary가 업데이트된다면 summary도 출력"
    for k, v in update.items():
    print(f"====== node: {k} ======")
        for message in v["messages"]:
            message.pretty_print()
        if "summary" in v:
            print(v["summary"])

def call_graph(content: str, thread_id: int):
    input = {"messages": HumanMessage(content=content)}
    config = {"configurable": {"thread_id": str(thread_id)}}
    events = graph.stream(input, config, stream_mode="updates")
    for event in events:
        print_update(event)
# 3개의 턴 실행 (6개의 메세지)
THREAD_ID = 12
call_graph("안녕하세요, 제 이름은 Sean입니다.", thread_id=THREAD_ID)
call_graph("제 전공은 통계학과에요.", thread_id=THREAD_ID)
call_graph("저는 최근 AI 멀티 에이전트에 관심이 있습니다.", thread_id=THREAD_ID)
====== node: chatbot ======
================================== Ai Message ==================================

안녕하세요, Sean님! 만나서 반갑습니다. 어떻게 도와드릴까요?
====== node: chatbot ======
================================== Ai Message ==================================

통계학 전공은 매우 흥미로운 분야입니다! 데이터 분석, 확률 이론, 실험 설계 등 다양한 주제를 다루죠. 통계학에 대해 어떤 부분에 관심이 있으신가요? 또는 궁금한 점이 있으신가요?
====== node: chatbot ======
================================== Ai Message ==================================

AI 멀티 에이전트 시스템은 매우 흥미로운 분야입니다! 여러 개의 에이전트가 상호작용하고 협력하여 문제를 해결하거나 목표를 달성하는 방식으로 작동하죠. 이 분야에서는 협상, 분산 문제 해결, 그리고 에이전트 간의 통신 등이 중요한 요소로 작용합니다. 

특히 통계학적인 접근은 데이터 분석, 의사결정, 에이전트의 행동 모델링 등에 큰 도움이 될 수 있습니다. AI 멀티 에이전트 시스템에 대해 구체적으로 알고 싶은 부분이나 질문이 있으신가요?
====== node: summarize_chat_history ======
================================ Remove Message ================================


================================ Remove Message ================================


================================ Remove Message ================================


================================ Remove Message ================================


Sean님은 통계학을 전공하며 최근 AI 멀티 에이전트 시스템에 관심을 가지고 있습니다. 이 분야는 여러 에이전트들이 상호작용하며 문제를 해결하거나 목표를 달성하는 시스템으로, 통계학적 접근이 데이터 분석과 의사결정에 도움이 될 수 있습니다. Sean님이 궁금한 점이나 구체적으로 알고 싶은 부분이 있는지 물어봤습니다.

 

결과를 살펴보면, 2개의 턴까지는 4개의 메세지가 messages에 들어가므로 summarize_chat_history 노드에 방문하지 않고 그래프가 종료되었다. 따라서 State에도 summary의 값이 들어가지 않느다. 그런데 3번째 턴이 시작되고 chatbot 노드를 방문한 이후에는 메세지가 6개가 되므로, summarize_chat_history에 들려 summary를 만들게 되고 2개를 제외한 앞선 4개의 메세지를 삭제한다.

 

.get_state 메서드로 해당 thread의 스냅샷을 찍어 메세지 리스트를 출력하면 기대한대로 2개의 메세지가 존재한다.

config = {"configurable": {"thread_id": str(THREAD_ID)}}
snapshot = graph.get_state(config)

print(snapshot.values["messages"])
print("=== summary ===")
print(snapshot.values["summary"])
[HumanMessage(content='저는 최근 AI 멀티 에이전트에 관심이 있습니다.',...), 
AIMessage(content='AI 멀티 에이전트 시스템은 매우 흥미로운 분야입니다! 여러 개의 에이전트가 상호작용하고 협력하여 문제를 해결하거나 목표를 달성하는 방식으로 작동하죠. 이 분야에서는 협상, 분산 문제 해결, 그리고 에이전트 간의 통신 등이 중요한 요소로 작용합니다.\n\n특히 통계학적인 접근은 데이터 분석, 의사결정, 에이전트의 행동 모델링 등에 큰 도움이 될 수 있습니다. AI 멀티 에이전트 시스템에 대해 구체적으로 알고 싶은 부분이나 질문이 있으신가요?',...)]
=== summary === 
Sean님은 통계학을 전공하며 최근 AI 멀티 에이전트 시스템에 관심을 가지고 있습니다. 이 분야는 여러 에이전트들이 상호작용하며 문제를 해결하거나 목표를 달성하는 시스템으로, 통계학적 접근이 데이터 분석과 의사결정에 도움이 될 수 있습니다. Sean님이 궁금한 점이나 구체적으로 알고 싶은 부분이 있는지 물어봤습니다.

출처

LangGraph. "How to add summary of the conversation history". https://langchain-ai.github.io/langgraph/how-tos/memory/add-summary-conversation-history/?h=conversation