langgraph / / 2024. 10. 15. 21:45

[번역][langgraph tutorial] Quick Start - Part 3. 챗봇에 메모리 기능 추가하기

langgraph의 공식문서를 번역해 놓은 자료입니다. 이해하기 쉽게 예제는 변경하였습니다. 또한 필요한 경우 부연 설명을 추가하였습니다. 문제가 되면 삭제하겠습니다.

https://langchain-ai.github.io/langgraph/tutorials/introduction/#part-3-adding-memory-to-the-chatbot

Part 3: 챗봇에 메모리 추가하기

챗봇은 이제 도구를 사용하여 사용자 질문에 대답할 수 있지만, 이전 컨텍스트(Context)를 기억하지 못하기 때문에 일관된 멀티 턴 대화를 진행하는 데 한계가 있다.

LangGraph지속적인 체크포인트(checkpointing)를 통해 이 문제를 해결한다. 그래프를 컴파일할 때 checkpointer를 제공하고, 그래프를 호출할 때 thread_id를 제공하면, LangGraph는 각 단계 후 상태를 자동으로 저장한다. 동일한 thread_id로 다시 그래프를 호출하면, 저장된 상태가 로드되어 챗봇이 이전 대화에서 멈춘 지점부터 대화를 이어갈 수 있다.

이후에 체크포인트 기능이 단순한 채팅 메모리보다 훨씬 강력하다는 것을 보게 될 것이다. 이를 통해 복잡한 상태를 언제든지 저장하고 복구할 수 있으며, 오류 복구, 사람이 개입하는 워크플로우, 시간 여행(Time Travel) 인터랙션 등의 기능을 구현할 수 있다. 하지만 그 전에, 우선 체크포인트를 추가하여 멀티 턴 대화를 가능하게 해보자.

시작하려면 MemorySaver 체크포인터를 생성하자.

from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()

여기서는 메모리 기반 체크포인터를 사용하고 있다. 튜토리얼에서는 모든 상태를 메모리에 저장하지만, 실제 운영 애플리케이션에서는 SqliteSaverPostgresSaver를 사용하여 데이터베이스에 연결하는 방식으로 변경하는 것이 일반적이다.

다음으로 그래프를 정의한다. 이제 BasicToolNode를 직접 만들었으니, 이를 LangGraph의 미리 만들어진 ToolNodetools_condition으로 교체한다. 이렇게 미리 만들어진 기능들은 API를 병렬로 실행하는 등의 유용한 작업을 수행한다. 그 외의 부분은 2부에서 작성한 내용을 그대로 사용한다.

from typing import Annotated

from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatOpenAI(model="gpt-3.5-turbo")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
)
# 도구가 호출될 때마다 다음 단계를 결정하기 위해 챗봇으로 돌아온다.
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")

마지막으로, 제공된 checkpointer를 사용하여 그래프를 컴파일하자.

graph = graph_builder.compile(checkpointer=memory)

그래프의 연결 구조는 이전에 만든 방법과 동일하다. 우리가 하고 있는 것은 각 노드를 처리하는 동안 State를 체크포인트로 저장하는 것이 추가된 것 뿐이다. 이것으로 그래프의 동작을 변경하지 않으면서도 상태를 유지하고 복원할 수 있게 해준다.

try:
    display(
        Image(
            graph.get_graph().draw_mermaid_png(
                output_file_path="./add_memory_to_chatbot.png"
            )
        )
    )
except Exception:
    pass

이제 챗봇과 상호작용할 수 있다. 먼저, 이 대화를 위한 키로 사용할 thread를 선택하자.

config = {"configurable": {"thread_id": "1"}}

다음으로 챗봇을 호출하자.

user_input = "안녕, 내 이름은 홍길동이야."

events = graph.stream(
    {"messages": [("user", user_input)]}, config, stream_mode="values"
)
for event in events:
    event["messages"][-1].pretty_print()

실행결과

================================ Human Message =================================

내 이름 기억하니?
================================== Ai Message ==================================

안녕하세요, 홍길동님! 무엇을 도와드릴까요?

참고: config는 그래프를 호출할 때 두 번째 인수로 제공되었다. 중요한 점은 이 구성이 그래프 입력 내부에 중첩되지 않았다는 것이다({'messages': []} 구조 내에 포함되지 않음).

이제 후속 질문을 해보자. "내 이름을 기억하는지 확인"을 시도해보자.

user_input = "내 이름 기억하니?"

events = graph.stream(
    {"messages": [("user", user_input)]}, config, stream_mode="values"
)
for event in events:
    event["messages"][-1].pretty_print()

실행결과

================================ Human Message =================================

내 이름 기억하니?
================================== Ai Message ==================================

네, 홍길동님의 이름을 기억하고 있습니다. 무엇을 도와드릴까요?

우리가 메모리를 위해 외부 리스트를 사용하지 않고 있다는 점에 주목하자. 이 모든 것은 체크포인터에 의해 처리된다. 이 LangSmith 트레이스를 통해 전체 실행 과정을 확인할 수 있다.

# 유일한 차이점은 thread_id를 "1" 대신 "2"로 변경하는 것이다.
events = graph.stream(
    {"messages": [("user", user_input)]},
    {"configurable": {"thread_id": "2"}},
    stream_mode="values",
)
for event in events:
    event["messages"][-1].pretty_print()

실행결과

================================ Human Message =================================

내 이름 기억하니?
================================== Ai Message ==================================

죄송합니다. 이름을 기억하지 못합니다. 무엇을 도와드릴까요?

여기서 유일한 변경 사항은 config에서 thread_id를 수정한 것 뿐이다. 비교를 위해 이번 호출의 LangSmith 트레이스를 확인해보자.

이제 우리는 두 개의 서로 다른 스레드에서 몇 개의 체크포인트를 만들었다. 그렇다면 체크포인트에는 어떤 정보가 들어갈까? 주어진 구성의 그래프 상태를 언제든지 확인하려면 get_state(config)를 호출하자.

snapshot = graph.get_state(config)
print(snapshot)
StateSnapshot(values={'messages': [HumanMessage(content='안녕, 내 이름은 홍길동이야.', additional_kwargs={}, response_metadata={}, id='cc43f8ab-9fb2-4cc1-aa45-6639016e994d'), AIMessage(content='안녕하세요 홍길동님! 무엇을 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 97, 'total_tokens': 126, 'completion_tokens_details': {'audio_tokens': 0, 'reasoning_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-24e88ce6-1022-4f3f-8675-93181a43c97c-0', usage_metadata={'input_tokens': 97, 'output_tokens': 29, 'total_tokens': 126, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), HumanMessage(content='내 이름 기억하니?', additional_kwargs={}, response_metadata={}, id='682c427c-83bc-4119-ae41-4cd0b4371c19'), AIMessage(content='네, 홍길동님의 이름을 기억하고 있습니다. 무엇을 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 37, 'prompt_tokens': 141, 'total_tokens': 178, 'completion_tokens_details': {'audio_tokens': 0, 'reasoning_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-4e3de271-f408-414e-b7c9-100291420eff-0', usage_metadata={'input_tokens': 141, 'output_tokens': 37, 'total_tokens': 178, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efa1ad8-630f-6234-8004-33794bcbe615'}}, metadata={'source': 'loop', 'writes': {'chatbot': {'messages': [AIMessage(content='네, 홍길동님의 이름을 기억하고 있습니다. 무엇을 도와드릴까요?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 37, 'prompt_tokens': 141, 'total_tokens': 178, 'completion_tokens_details': {'audio_tokens': 0, 'reasoning_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-4e3de271-f408-414e-b7c9-100291420eff-0', usage_metadata={'input_tokens': 141, 'output_tokens': 37, 'total_tokens': 178, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}, 'step': 4, 'parents': {}}, created_at='2024-11-13T10:53:32.454313+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efa1ad8-570d-622e-8003-d1d81b13cef3'}}, tasks=())
print(snapshot.next)  # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
()

위의 스냅샷에는 현재 상태 값, 해당 구성, 그리고 처리할 다음 노드가 포함되어 있다. 우리의 경우, 그래프가 END 상태에 도달했기 때문에 next는 비어 있다.

이제 챗봇은 LangGraph의 체크포인트 시스템 덕분에 세션 간 대화 상태를 유지할 수 있게 되었다. 이 기능은 더 자연스럽고 맥락적인 상호작용을 가능하게 하며, LangGraph의 체크포인트 시스템은 단순한 채팅 메모리보다 훨씬 더 복잡한 그래프 상태도 처리할 수 있어 매우 표현력 있고 강력하다.

다음 단계에서는 챗봇이 진행하기 전에 가이드나 검증이 필요한 상황을 처리할 수 있도록 사람의 관찰(human oversight)을 도입할 예정이다.

관련자료

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유