LangGraph 공식문서를 번역한 내용입니다. 필요한 경우 부연 설명을 추가하였고 이해하기 쉽게 예제를 일부 변경하였습니다. 문제가 되면 삭제하겠습니다.
https://langchain-ai.github.io/langgraph/how-tos/memory/delete-messages/
그래프에서 일반적인 상태 중 하나는 메시지 목록이다. 일반적으로 상태에 메시지를 추가만 한다. 그러나 때때로 메시지를 제거할 때도 있다(상태를 직접 수정하거나 그래프의 일환으로). 이를 위해 RemoveMessage
수정자를 사용할 수 있다. 이 가이드에서는 이를 수행하는 방법을 다룬다.
핵심 아이디어는 각 상태 키가 리듀서 키를 가진다는 것다. 이 키는 업데이트를 상태에 적용하는 법을 지정한다. 기본 MessagesState
에는 messages
키가 있으며, 이 키에 대한 리듀서는 RemoveMessage
수정자를 받는다. 그 후 리듀서는 이 RemoveMessage
를 사용하여 메시지를 삭제한다.
그래프 상태에 메시지 목록이라는 키가 있다고 해서 RemoveMessage
수정자가 작동하는 것은 아니다. 이 수정자가 작동하려면 이를 처리할 수 있는 리듀서가 정의되어 있어야 한다.
참고: 많은 모델들이 메시지 목록에 대해 특정 규칙을 예상한다. 예를 들어, 일부는 사용자 메시지로 시작하는 것을 예상하고, 다른 일부는 도구 호출 메시지가 도구 메시지로 이어지기를 기대한다. 메시지를 삭제할 때 이러한 규칙을 위반하지 않도록 주의해야 한다.
준비
먼저, 메시지를 사용하는 간단한 그래프를 구축해 보자. 이 그래프는 필요한 리듀서를 가진 MessagesState
를 사용한다는 점에 유의하자.
pip install langgraph langchain_openai
에이전트 생성
간단한 ReAct 스타일 에이전트를 만들자.
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode
load_dotenv()
memory = MemorySaver()
@tool
def search(query: str):
"""Call to surf the web."""
return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈."
tools = [search]
tool_node = ToolNode(tools)
model = ChatOpenAI(model_name="gpt-4o-mini")
bound_model = model.bind_tools(tools)
def should_continue(state: MessagesState):
"""Return the next node to execute."""
last_message = state["messages"][-1]
# 만일 함수 호출이 없다면, 끝낸다.
if not last_message.tool_calls:
return END
# 만일 함수 호출이 있다면, 계속한다.
return "action"
# 모델을 호출하는 함수를 정의한다.
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
# 리스트를 반환한다. 기존 리스트에 추가될 것이기 때문이다.
return {"messages": response}
workflow = StateGraph(MessagesState)
# 두 개의 노드가 서로를 순환하도록 정의한다.
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# 진입점을 'agent'로 설정한다.
# 이것은 이 노드가 처음으로 호출되는 것을 의미한다.
workflow.add_edge(START, "agent")
# 조건부 엣지를 추가한다.
workflow.add_conditional_edges(
# 우선, 시작 노드를 정의한다. 'agent'를 사용한다.
# 이것은 'agent' 노드가 호출된 후에 호출되는 엣지를 의미한다.
"agent",
# 다음으로, 다음에 호출될 노드를 결정할 함수를 전달한다.
should_continue,
# 다음으로, 경로 맵을 전달한다. 이 엣지가 갈 수 있는 모든 노드들이다.
["action", END],
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
app = workflow.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "2"}}
input_message = HumanMessage(content="안녕! 내 이름은 홍길동이야")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
event["messages"][-1].pretty_print()
input_message = HumanMessage(content="내 이름이 뭐야?")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
event["messages"][-1].pretty_print()
================================ Human Message =================================
안녕! 내 이름은 홍길동이야
================================== Ai Message ==================================
안녕하세요, 홍길동님! 어떻게 도와드릴까요?
================================ Human Message =================================
내 이름이 뭐야?
================================== Ai Message ==================================
홍길동님이라고 하셨어요!
수동으로 메시지 삭제
먼저, 메시지를 수동으로 삭제하는 방법을 알아보자. 현재 스레드 상태를 살펴보자.
messages = app.get_state(config).values["messages"]
print(messages)
[HumanMessage(content='안녕! 내 이름은 홍길동이야', additional_kwargs={}, response_metadata={}, id='30450c92-3ce7-4e66-9356-783ee051cc64'), AIMessage(content='안녕하세요, 홍길동님! 어떻게 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 53, 'total_tokens': 69, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-e0482692-c724-4ad3-b98b-6832b9ef0144-0', usage_metadata={'input_tokens': 53, 'output_tokens': 16, 'total_tokens': 69, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), HumanMessage(content='내 이름이 뭐야?', additional_kwargs={}, response_metadata={}, id='ed3a028e-575a-4c20-b892-8521fd751545'), AIMessage(content='홍길동님이세요!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 82, 'total_tokens': 90, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-1be8aebf-6795-4134-9cb0-f15c80c002ec-0', usage_metadata={'input_tokens': 82, 'output_tokens': 8, 'total_tokens': 90, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]
update_state
를 호출하고 첫 번째 메시지의 ID를 전달할 수 있다. 이렇게 하면 해당 메시지가 삭제된다.
from langchain_core.messages import RemoveMessage
app.update_state(config, {"messages": RemoveMessage(id=messages[0].id)})
{'configurable': {'thread_id': '2',
'checkpoint_ns': '',
'checkpoint_id': '1ef75157-f251-6a2a-8005-82a86a6593a0'}}
이제 메시지를 확인하면 첫 번째 메시지가 삭제된 것을 확인할 수 있다.
messages = app.get_state(config).values["messages"]
print(messages)
[AIMessage(content='안녕하세요, 홍길동님! 어떻게 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 53, 'total_tokens': 69, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-f1980973-e22a-4451-a090-8e0a10680499-0', usage_metadata={'input_tokens': 53, 'output_tokens': 16, 'total_tokens': 69, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), HumanMessage(content='내 이름이 뭐야?', additional_kwargs={}, response_metadata={}, id='3ecad44f-a540-4309-9336-ba0927c149c7'), AIMessage(content='홍길동님이십니다! 다른 질문이나 요청이 있으신가요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 82, 'total_tokens': 101, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-091756b2-b837-48d0-b1f8-61b0423044e1-0', usage_metadata={'input_tokens': 82, 'output_tokens': 19, 'total_tokens': 101, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]
프로그래밍으로 메시지 삭제
그래프 내부에서 프로그래밍적으로 메시지를 삭제할 수도 있다. 여기서는 그래프 실행이 끝날 때 3개 이상 이전의 오래된 메시지를 삭제하도록 그래프를 수정할 것이다.
from langchain_core.messages import RemoveMessage
from langgraph.graph import END
def delete_messages(state):
messages = state["messages"]
if len(messages) > 3:
return {"messages": [RemoveMessage(id=m.id) for m in messages[:-3]]}
# 모델을 호출하는 함수를 정의한다.
def call_model(state: MessagesState):
response = bound_model.invoke(state["messages"])
# 리스트를 반환한다. 기존 리스트에 추가될 것이기 때문이다.
return {"messages": response}
# 바로 끝내는 대신에 delete_messages를 호출하는 로직을 수정할 필요가 있다.
def should_continue(state: MessagesState) -> Literal["action", "delete_messages"]:
"""Return the next node to execute."""
last_message = state["messages"][-1]
# 만일 함수 호출이 없다면, 끝낸다.
if not last_message.tool_calls:
return "delete_messages"
# 만일 함수 호출이 있다면, 계속한다.
return "action"
workflow = StateGraph(MessagesState)
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# 이것이 우리가 정의하는 새로운 노드이다.
workflow.add_node(delete_messages)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
)
workflow.add_edge("action", "agent")
# 추가하고 있는 새로운 엣지이다. 메시지를 삭제한 후에 끝낸다.
workflow.add_edge("delete_messages", END)
app = workflow.compile(checkpointer=memory)
이제 실행해보자. 그래프를 두 번 호출한 후 상태를 확인해 보자.
from langchain_core.messages import HumanMessage
config = {"configurable": {"thread_id": "3"}}
input_message = HumanMessage(content="안녕! 내 이름은 홍길동이야")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
print([(message.type, message.content) for message in event["messages"]])
input_message = HumanMessage(content="내 이름이 뭐야?")
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
print([(message.type, message.content) for message in event["messages"]])
[('human', '안녕! 내 이름은 홍길동이야')]
[('human', '안녕! 내 이름은 홍길동이야'), ('ai', '안녕하세요, 홍길동님! 어떻게 도와드릴까요?')]
[('human', '안녕! 내 이름은 홍길동이야'), ('ai', '안녕하세요, 홍길동님! 어떻게 도와드릴까요?'), ('human', '내 이름이 뭐야?')]
[('human', '안녕! 내 이름은 홍길동이야'), ('ai', '안녕하세요, 홍길동님! 어떻게 도와드릴까요?'), ('human', '내 이름이 뭐야?'), ('ai', '홍길동님이라고 하셨습니다! 다른 질문이 있으신가요?')]
[('ai', '안녕하세요, 홍길동님! 어떻게 도와드릴까요?'), ('human', '내 이름이 뭐야?'), ('ai', '홍길동님이라고 하셨습니다! 다른 질문이 있으신가요?')]
이제 상태를 확인하면 메시지가 세 개만 남아 있는 것을 볼 수 있다. 왜냐하면 이전 메시지를 삭제했기 때문이다. 그렇지 않으면 네 개가 될 것이다.
messages = app.get_state(config).values["messages"]
print(messages)
[AIMessage(content='안녕하세요, 홍길동님! 어떻게 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 53, 'total_tokens': 69, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-6d5a41a7-bd10-40e0-a95d-1d1dc55ba86f-0', usage_metadata={'input_tokens': 53, 'output_tokens': 16, 'total_tokens': 69, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), HumanMessage(content='내 이름이 뭐야?', additional_kwargs={}, response_metadata={}, id='ea150b31-548b-4378-8017-66905c06b053'), AIMessage(content='당신의 이름은 홍길동입니다!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 11, 'prompt_tokens': 82, 'total_tokens': 93, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-161cbcaf-502d-468c-bf1e-b107858b9d28-0', usage_metadata={'input_tokens': 82, 'output_tokens': 11, 'total_tokens': 93, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]
메시지를 삭제할 때는 남은 메시지 목록이 여전히 유효한지 확인해야 한다. 현재 메시지 목록은 실제로 유효하지 않을 수 있다. 이는 현재 AI 메시지로 시작하고 있는데, 일부 모델에서는 이를 허용하지 않기 때문이다.