본문 바로가기
LLM/LangGraph

[LangGraph]메모리 아키텍처—RemoveMessage와 Summarizef로 만드는 메시지 요약, 삭제 노드 구축

by coding_whale 2026. 6. 16.
반응형

1. 도입부 (Introduction)

사용자와 원활하게 대화하며 정교한 작업을 수행하는 AI 에이전트를 만들 때 마주하는 가장 까다로운 장벽은 '기억력의 한계'와 '비용의 압박'이다.

에이전트가 다양한 도구(Web Search, 이메일 전송, RAG DB)를 구동하다 보면, 한두 번의 대화만으로도 시스템 메시지 로그가 걷잡을 수 없이 포화 상태에 이른다. 이는 모델의 주의력 분산(Context Window Saturation)을 야기해 엉뚱한 답변을 내뱉게 만들 뿐만 아니라, 엄청난 API 호출 비용 인플레이션으로 돌아온다.

LangGraph는 이 문제를 단순히 프롬프트 엔지니어링으로 땜질하지 않는다. 그래프 상태를 완벽하게 보존하는 체크포인트(MemorySaver) 시스템과 대화를 동적으로 관리하는 메시지 소거(RemoveMessage) 및 실시간 요약(Summarize) 파이프라인을 아키텍처 수준에서 깔끔하게 통합하여 해결한다.

 

[LangGraph] 에이전트와 LangGraph 도구(Tool) 통합 정리

1. 도입부 (Introduction)단순히 LLM에게 질문을 던지고 텍스트 응답을 받는 시대는 지났다. 이제는 LLM이 스스로 필요한 정보를 찾고, 계산기를 두드리며, 이메일을 보내고, 데이터베이스를 조회하는

myblog01150.tistory.com

 

이번 포스팅에서는 외부 도구를 안전하게 제어하는 방법부터 시작해, 비용 효율적으로 대화 히스토리를 정제하는 실전 에이전트 아키텍처를 완벽하게 해부해 보겠다.

 

 

2. LangGraph 강력한 도구(Tool) 통합 및 분기 처리

LangGraph 에이전트의 핵심 강점은 복잡한 API나 검색 엔진을 단일 함수 단위의 '도구'로 만들어 LLM의 손발로 쥐여줄 수 있다는 점이다.

대표적인 내장 도구 스펙과 실무 활용처

  • DuckDuckGo Search: API 키 발급조차 필요 없는 강력한 오픈소스 웹 검색 도구다. LLM이 학습하지 못한 실시간 최신 정보나 간단한 지식 수집을 해결할 때 쓴다.
  • Gmail Toolkit: Google 개발자 콘솔을 통해 앱 비밀번호 및 토큰을 바인딩하면 에이전트가 직접 메일 초안을 작성하고, 발송하며, 수신함을 조회하는 등 실질적인 오피스 워크플로우를 대행할 수 있다.
  • arXiv API: 최신 학술 논문을 실시간으로 수색하고 문서 메타데이터를 긁어오며, 대규모 LLM 요약 파이프라인과 결합하기에 최적화된 도구다.
  • Retriever Tool: 복잡하고 거대한 소득세법, 규정 문서, 가이드북 등을 벡터 데이터베이스(Chroma, Pinecone 등)에 이식해 두고, 필요할 때 관련된 문서 조각만 동적으로 추출하여 LLM에게 근거 자료로 제공한다.

 

Conditional Edge의 진화: tools_condition

에이전트가 다음 노드로 넘어갈 때, 예전에는 아래와 같이 복잡한 분기 조건 함수를 매번 수동으로 작성했다.

# 기존의 수동적인 should_continue 조건 분기 로직
def should_continue(state: MessagesState):
    messages = state['messages']
    last_ai_message = messages[-1]
    if last_ai_message.tool_calls:
        return 'tools' # 도구 노드로 분기
    return END # 그래프 종료

하지만, 이러한 구조는 에이전트 노드가 늘어날 때마다 보일러플레이트 코드가 반복된다. LangGraph는 이를 간소화하기 위해 프리빌트 헬퍼 함수인 tools_condition을 제공한다. tools_condition을 조건부 엣지에 부착하면, 마지막 AI 메시지에 도구 호출 스펙(tool_calls)이 존재하는지에 따라 'tools' 노드로 흘러가거나 자동으로 종료 노드로 빠지게 설계할 수 있어 아키텍처 가독성이 비약적으로 상승한다.

 

 

3. 에이전트의 채팅 히스토리 관리 아키텍처

사용자와 에이전트가 대화를 지속할 때, LangGraph는 그래프 외부 저장소나 세션 보관 처리를 위해 체크포인터(Checkpointer) 기능을 필수로 사용한다.

 

configurable과 thread_id 기반 세션 격리

다중 사용자가 접속하는 서버 환경에서 각 대화방의 대화 기록은 완벽히 격리되어야 한다. LangGraph는 실행 시점에 configurable 객체 내부의 thread_id 속성을 전달받아 어떤 스토리지에 데이터를 격재할지 식별한다.

# thread_id를 통해 대화 줄기를 식별하고 스레드를 분리 관리함
config = {
    'configurable': {
        'thread_id': 'unique_user_session_01'
    }
}

개발자는 이 thread_id만 세션 키나 채팅방 ID로 지정해주면 대화 흐름 보존과 사용자 격리를 동시에 안전하게 구현할 수 있다.

 

 

4. 토큰 최소화를 위한 메시지 압축 프로토콜 (삭제 & 요약)

우리가 구성할 고도화된 하이브리드 압축 메모리 에이전트의 핵심 동작 메커니즘을 상세히 분석해 보자. 단순히 메시지를 소거해버리면 앞뒤 맥락이 완전히 잘려 나가므로, 요약(Summarization)과 선별 삭제(Selective Deletion)를 결합하여 단점을 완벽히 극복한다.

이 하이브리드 패턴을 활용하면, 전체 메시지 리스트의 길이가 늘어나도 실제 LLM에 전송되는 토큰 분량은 항상 일정한 규격으로 콤팩트하게 억제된다.

 

 

5. 실전 코드 구현: 하이브리드 압축 메모리 에이전트

이제 DuckDuckGo 웹 검색, 로컬 RAG 세금 계산 검색기, 그리고 Gmail 툴킷까지 한데 묶은 하이브리드 에이전트에 대화 세션 유지 + 자동 요약 + 자동 과거 로그 청소 노드를 장착한 전체 컴파일 코드를 직접 구현해 보자.

from typing import Sequence
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage, RemoveMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver

# ==========================================
# 1. 상태(State) 구조 확장 정의
# ==========================================
class AgentState(MessagesState):
    # 전역 상태 스키마에 압축된 대화 맥락을 저장할 summary 스트링을 선언한다.
    summary: str

# LLM 모델 인스턴스 구축
llm = ChatOpenAI(model='gpt-4o')
small_llm = ChatOpenAI(model='gpt-4o-mini') # 요약 연산을 처리할 경량/저비용 모델

# ==========================================
# 2. 연산 및 도구 정의 (연산, 검색, RAG, Gmail)
# ==========================================
@tool
def add(a: int, b: int) -> int:
    """숫자 a와 b를 더합니다."""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """숫자 a와 b를 곱합니다."""
    return a * b

# (참고) search_tool, retriever_tool, gmail_tool_list는 사전에 바인딩된 변수라 가정한다.
all_tool_list = [add, multiply, search_tool, retriever_tool] + gmail_tool_list
llm_with_tools = llm.bind_tools(all_tool_list)

# 도구들을 대행해 줄 프리빌트 도구 노드 구축
tool_node = ToolNode(all_tool_list, name="tool_node")

# ==========================================
# 3. 그래프 핵심 노드(Node) 비즈니스 로직
# ==========================================

def agent(state: AgentState):
    messages = state['messages']
    summary = state['summary']
    
    # 💡 핵심 팁: 상태창에 저장되어 있던 과거 요약본이 발견되면, 이번 LLM 대화의 가장 최전선(0번 인덱스)에
    # 시스템 가이드 메시지 헤더 형태로 자연스럽게 이식하여 대화 맥락 유실을 방지한다.
    if summary != '':
        messages = [SystemMessage(content=f'Here is the summary of the earlier conversation: {summary}')] + messages
        
    response = llm_with_tools.invoke(messages)
    return {'messages': [response]}


def summarize_messages(state: AgentState):
    messages = state['messages']
    summary = state['summary']
    
    # 누적된 메시지 내용 전체를 프롬프트에 담아 경량 LLM으로 압축시킨다.
    summary_prompt = f'summarize this chat history below:\n\nchat_history: {messages}'
    
    # 이전 턴까지 저장되어 오던 요약본이 이미 있다면, 과거 요약 맥락까지 녹여내어 '점진적 요약'을 단행한다.
    if summary != '':
        summary_prompt = f'''summarize this chat history below while looking at the summary of earlier conversations.
        chat_history: {messages}
        summary: {summary}'''
        
    summary_response = small_llm.invoke(summary_prompt)
    return {'summary': summary_response.content}


def delete_messages(state: AgentState):
    messages = state['messages']
    
    # 💡 핵심 팁: LangGraph에서 메모리 사용량을 줄이기 위한 메시지 소거 방법.
    # RemoveMessage(id=...)를 반환 리스트에 담아 던지면, 랭그래프 엔진이 전역 messages 장부에서 해당 ID 데이터를 완전히 지워버린다.
    # 여기서는 대화의 최신 흐름 추적을 위해 '마지막 3개의 메시지'만 남겨두고 이전 원시 메시지는 싹 다 소거한다.
    delete_actions = [RemoveMessage(id=message.id) for message in messages[:-3]]
    return {'messages': delete_actions}


# ==========================================
# 4. 조건부 분기 라우터(Conditional Edge) 설계
# ==========================================
def should_continue(state: AgentState):
    messages = state['messages']
    last_ai_message = messages[-1]
    
    # 만약 LLM이 특정 툴을 실행하라고 요청했다면, 도구 노드로 이탈시킨다.
    if last_ai_message.tool_calls:
        return 'tools'
    
    # 도구 연산이 전혀 없거나 완료된 대화 마무리 단계라면, 컨텍스트 압축 노드로 전이시킨다.
    return 'summarize_messages'

# ==========================================
# 5. 그래프 아키텍처 오케스트레이션 조립 및 컴파일
# ==========================================
graph_builder = StateGraph(AgentState)

# 노드 등록
graph_builder.add_node('agent', agent)
graph_builder.add_node('tools', tool_node)
graph_builder.add_node('summarize_messages', summarize_messages)
graph_builder.add_node('delete_messages', delete_messages)

# 엣지 결합 및 제어 흐름 구성
graph_builder.add_edge(START, 'agent')
graph_builder.add_conditional_edges(
    'agent', 
    should_continue,
    ['tools', 'summarize_messages'] # 판단 분기에서 갈 수 있는 유효 경로 후보군 목록
)
graph_builder.add_edge('tools', 'agent') # 도구 사용 후 에이전트 노드로 회귀하여 추가 판단 실행
graph_builder.add_edge('summarize_messages', 'delete_messages') # 요약 업데이트 완료 후 청소기 가동
graph_builder.add_edge('delete_messages', END) # 깔끔하게 메모리가 최적화된 상태로 종료

# 영속성 보장 체크포인터(MemorySaver) 부착 후 에이전트 기동 준비
checkpointer = MemorySaver()
graph = graph_builder.compile(
    checkpointer=checkpointer
)

 

 

6. 실무 적용 시나리오 추적 및 검증 (Runtime Trace)

실제 서비스에서 이 에이전트가 어떤 순서로 대화를 격리하고 메모리를 콤팩트하게 유지하는지 2단계 시나리오를 통해 추적해 보자.

[시나리오 1: 대화의 시작 및 연산 이메일 초안 작성]

  • 유저의 최초 발화: "qkrwlsdud18@gmail.com으로 집이 15억일 때 종합부동산세를 계산하고 요약해서 이메일 초안을 작성해주세요" (세션 스레드 ID: tax_summary_session)
  • 에이전트 내부 오디팅 트레이스:
    1. agent 가동 ──> 15억 자산 계산을 위해 로컬 real_estate_tax_retriever RAG 도구와 사칙연산 도구를 동적으로 선택한다.
    2. 도구 연산이 완료되자, 계산 결과에 맞춰 멋지게 이메일 초안을 작성한다.
    3. LLM 호출이 완료되었으므로 summarize_messages 노드가 실행된다.
    4. gpt-4o-mini 모델이 메시지를 낚아채어 전역 summary 상태창에 *"15억 원 주택의 종합부동산세 산출 근거를 담은 이메일 송신용 초안"*이라는 텍스트 요약본을 작성한다.
    5. delete_messages 노드가 기동해 그동안 주렁주렁 매달려 있던 RAG 탐색 본문 메시지들과 덧셈, 곱셈 툴의 세부 결과 데이터(ToolMessage들)를 RemoveMessage로 소거하고 오직 최신 상태 메시지 3개만 남겨둔 뒤 깔끔하게 세션을 저장한다.

 

[시나리오 2: 유저의 점진적 수정 피드백 수렴]

  • 유저의 점진적 수정 요청: "결과의 출처를 첨부해주세요"
  • 에이전트 내부 오디팅 트레이스:
    1. 에이전트는 DB에서 tax_summary_session을 복원한다. 이때 메시지 리스트는 방금 날아온 HumanMessage를 포함해 단 4개뿐이며, 토큰 소모량이 극히 미미하다.
    2. 에이전트 노드는 summary 변수 값을 참조하여 이 대화가 종부세 이메일 초안에 관한 맥락임을 정확히 인지한다.
    3. 불필요하게 이전 메시지를 처음부터 훑을 필요 없이, 상태창의 요약정보를 지표 삼아 최신 메일 초안에 출처 정보를 깔끔하게 덧붙여 전송을 완료한다.

 

7. 결론: 똑똑하고 확장성 있는 에이전트 인프라의 완성

LangGraph 에이전트에서 히스토리를 정교하게 가다듬는 작업은 단순한 코딩 스타일의 문제가 아니다. 이는 API 비용 청구서를 실질적으로 낮추고, 대화 중 발생하는 컨텍스트 희석(Context Dilution) 현상을 미연에 방지하여 시스템의 신뢰도와 사용자 경험을 영리하게 끌어올리는 백엔드 설계의 필수 전술이다.

기억해야 할 핵심 원칙은 단 두 가지다. 스냅샷 데이터베이스는 MemorySaver로 격리 구축하고, 비대해지는 메시지는 그래프 내부 상태창인 RemoveMessage와 요약 노드를 통해 능동적으로 압축하자. 이 기법들을 서비스 아키텍처에 도입한다면, 비정형 데이터 수집부터 복잡한 피드백 루프까지 사고 치지 않고 완벽하게 수행하는 스마트한 프로덕션급 AI 동료를 마주하게 될 것이다.

반응형