Open in Colab Open in LangChain Academy

그래프 상태 편집

복습

우리는 Human-in-the-Loop의 동기에 대해 논의했습니다:

(1) 승인 - 에이전트를 중단하고, 사용자에게 상태를 표시하며, 사용자가 작업을 승인할 수 있도록 할 수 있습니다

(2) 디버깅 - 그래프를 되감아 문제를 재현하거나 회피할 수 있습니다

(3) 편집 - 상태를 수정할 수 있습니다

중단점이 사용자 승인을 어떻게 지원하는지 보여주었지만, 그래프가 중단되었을 때 그래프 상태를 수정하는 방법은 아직 알아보지 않았습니다!

목표

이제 그래프 상태를 직접 편집하고 사람의 피드백을 삽입하는 방법을 보여드리겠습니다.

%%capture --no-stderr
%pip install --quiet -U langgraph langchain_openai langgraph_sdk langgraph-prebuilt
from dotenv import load_dotenv
 
load_dotenv("../.env", override=True)
True
import os
import getpass
 
 
def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")
 
 
_set_env("OPENAI_API_KEY")

상태 편집

이전에 중단점을 소개했습니다.

중단점을 사용하여 그래프를 중단하고 다음 노드를 실행하기 전에 사용자 승인을 기다렸습니다.

하지만 중단점은 그래프 상태를 수정할 수 있는 기회이기도 합니다.

assistant 노드 전에 중단점이 있는 에이전트를 설정해보겠습니다.

from langchain_openai import ChatOpenAI
 
 
def multiply(a: int, b: int) -> int:
    """Multiply a and b.
 
    Args:
        a: first int
        b: second int
    """
    return a * b
 
 
# This will be a tool
def add(a: int, b: int) -> int:
    """Adds a and b.
 
    Args:
        a: first int
        b: second int
    """
    return a + b
 
 
def divide(a: int, b: int) -> float:
    """Divide a by b.
 
    Args:
        a: first int
        b: second int
    """
    return a / b
 
 
tools = [add, multiply, divide]
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools)
from IPython.display import Image, display
 
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState
from langgraph.graph import START, StateGraph
from langgraph.prebuilt import tools_condition, ToolNode
 
from langchain_core.messages import HumanMessage, SystemMessage
 
# System message
sys_msg = SystemMessage(
    content="당신은 입력값에 대해 산술 연산을 수행하는 임무를 맡은 유용한 보조자입니다."
)
 
 
# Node
def assistant(state: MessagesState):
    response = llm_with_tools.invoke([sys_msg] + state["messages"])
    return {"messages": [response]}
 
 
# Graph
builder = StateGraph(MessagesState)
 
# Define nodes: these do the work
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
 
# Define edges: these determine the control flow
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
    "assistant",
    # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
    # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
    tools_condition,
)
builder.add_edge("tools", "assistant")
 
memory = MemorySaver()
graph = builder.compile(interrupt_before=["assistant"], checkpointer=memory)
 
# Show
display(Image(graph.get_graph(xray=True).draw_mermaid_png()))

실행해봅시다!

챗 모델이 응답하기 전에 그래프가 중단된 것을 볼 수 있습니다.

# Input
initial_input = {"messages": "2와 3을 곱하세요"}
 
# Thread
thread = {"configurable": {"thread_id": "1"}}
 
# 첫 번째 중단이 발생할 때까지 그래프를 실행하십시오
for event in graph.stream(initial_input, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
================================ Human Message =================================

2와 3을 곱하세요
state = graph.get_state(thread)
state
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='5d929da1-add6-454c-8573-87d3e254fcbe')]}, next=('assistant',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7da-39b4-6568-8000-bdd9c9cc0472'}}, metadata={'source': 'loop', 'step': 0, 'parents': {}}, created_at='2025-10-01T04:18:10.144799+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7da-39b3-6032-bfff-d1b6b9d0edca'}}, tasks=(PregelTask(id='677cecc9-9809-0833-99ec-ac2cda6d4eff', name='assistant', path=('__pregel_pull', 'assistant'), error=None, interrupts=(), state=None, result=None),), interrupts=())

이제 상태 업데이트를 직접 적용할 수 있습니다.

기억하세요, messages 키에 대한 업데이트는 add_messages 리듀서를 사용합니다:

  • 기존 메시지를 덮어쓰려면 메시지 id를 제공할 수 있습니다.
  • 단순히 메시지 목록에 추가하려면 아래와 같이 id를 지정하지 않고 메시지를 전달할 수 있습니다.
graph.update_state(
    thread,
    {"messages": [HumanMessage(content="아니, 사실 3과 3을 곱해 봐!")]},
)
{'configurable': {'thread_id': '1',
  'checkpoint_ns': '',
  'checkpoint_id': '1f09e7da-39cb-692a-8001-389b3f40550f'}}

확인해봅시다.

새로운 메시지와 함께 update_state를 호출했습니다.

add_messages 리듀서가 이를 우리의 상태 키인 messages에 추가합니다.

new_state = graph.get_state(thread).values
for m in new_state["messages"]:
    m.pretty_print()
================================ Human Message =================================

2와 3을 곱하세요
================================ Human Message =================================

아니, 사실 3과 3을 곱해 봐!

이제 단순히 None을 전달하고 현재 상태부터 진행하도록 하여 에이전트를 계속 진행해봅시다.

현재 상태를 출력한 다음 남은 노드들을 실행합니다.

for event in graph.stream(None, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
================================ Human Message =================================

아니, 사실 3과 3을 곱해 봐!
================================== Ai Message ==================================
Tool Calls:
  multiply (call_E5DVuKkpUvvWHlU1aRn3yrBs)
 Call ID: call_E5DVuKkpUvvWHlU1aRn3yrBs
  Args:
    a: 3
    b: 3
================================= Tool Message =================================
Name: multiply

9

이제 breakpoint가 있는 assistant로 돌아왔습니다.

다시 None을 전달하여 진행할 수 있습니다.

for event in graph.stream(None, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
================================= Tool Message =================================
Name: multiply

9
================================== Ai Message ==================================

3과 3을 곱하면 9입니다.

Studio에서 그래프 상태 편집

로컬 개발 서버를 시작하려면 이 모듈의 /studio 디렉토리에서 터미널에 다음 명령어를 실행하세요:

langgraph dev

다음과 같은 출력을 볼 수 있습니다:

- 🚀 API: http://127.0.0.1:2024
- 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
- 📚 API Docs: http://127.0.0.1:2024/docs

브라우저를 열고 Studio UI로 이동하세요: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024.

LangGraph API는 그래프 상태 편집을 지원합니다.

from langgraph_sdk import get_client
 
client = get_client(url="http://127.0.0.1:2024")

우리의 에이전트는 studio/agent.py에 정의되어 있습니다.

코드를 보면 중단점이 없는 것을 알 수 있습니다!

물론 agent.py에 추가할 수 있지만, API의 매우 유용한 기능 중 하나는 중단점을 전달할 수 있다는 것입니다!

여기서는 interrupt_before=["assistant"]를 전달합니다.

initial_input = {"messages": "2와 3을 곱하세요"}
thread = await client.threads.create()
async for chunk in client.runs.stream(
    thread["thread_id"],
    "agent",
    input=initial_input,
    stream_mode="values",
    interrupt_before=["assistant"],
):
    print(f"Receiving new event of type: {chunk.event}...")
    messages = chunk.data.get("messages", [])
    if messages:
        print(messages[-1])
    print("-" * 50)
Receiving new event of type: metadata...
--------------------------------------------------
Receiving new event of type: values...
{'content': '2와 3을 곱하세요', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6', 'example': False}
--------------------------------------------------

현재 상태를 확인할 수 있습니다

current_state = await client.threads.get_state(thread["thread_id"])
current_state
{'values': {'messages': [{'content': '2와 3을 곱하세요',
    'additional_kwargs': {},
    'response_metadata': {},
    'type': 'human',
    'name': None,
    'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6',
    'example': False}]},
 'next': ['assistant'],
 'tasks': [{'id': '18097497-3ca8-283c-7cf9-1d5cddc135d9',
   'name': 'assistant',
   'path': ['__pregel_pull', 'assistant'],
   'error': None,
   'interrupts': [],
   'checkpoint': None,
   'state': None,
   'result': None}],
 'metadata': {'langgraph_auth_user': None,
  'langgraph_auth_user_id': '',
  'langgraph_auth_permissions': [],
  'langgraph_request_id': '742e8bb4-922f-4246-a2b4-1014021c89db',
  'graph_id': 'agent',
  'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',
  'user_id': '',
  'created_by': 'system',
  'run_attempt': 1,
  'langgraph_version': '0.6.8',
  'langgraph_api_version': '0.4.31',
  'langgraph_plan': 'developer',
  'langgraph_host': 'self-hosted',
  'langgraph_api_url': 'http://127.0.0.1:2024',
  'run_id': '01999dfe-c682-7170-8d09-d1dc2519ec5e',
  'thread_id': '1f5c505b-f858-403e-831c-fc7296cae2a6',
  'source': 'loop',
  'step': 0,
  'parents': {}},
 'created_at': '2025-10-01T04:19:04.800272+00:00',
 'checkpoint': {'checkpoint_id': '1f09e7dc-42f0-68fa-8000-a471614c744b',
  'thread_id': '1f5c505b-f858-403e-831c-fc7296cae2a6',
  'checkpoint_ns': ''},
 'parent_checkpoint': {'checkpoint_id': '1f09e7dc-42ed-6a4c-bfff-9c6d466617fe',
  'thread_id': '1f5c505b-f858-403e-831c-fc7296cae2a6',
  'checkpoint_ns': ''},
 'interrupts': [],
 'checkpoint_id': '1f09e7dc-42f0-68fa-8000-a471614c744b',
 'parent_checkpoint_id': '1f09e7dc-42ed-6a4c-bfff-9c6d466617fe'}

상태의 마지막 메시지를 확인할 수 있습니다.

last_message = current_state["values"]["messages"][-1]
last_message
{'content': '2와 3을 곱하세요',
 'additional_kwargs': {},
 'response_metadata': {},
 'type': 'human',
 'name': None,
 'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6',
 'example': False}

메시지를 편집할 수 있습니다!

last_message["content"] = "아니, 사실 3과 3을 곱해 봐!"
last_message
{'content': '아니, 사실 3과 3을 곱해 봐!',
 'additional_kwargs': {},
 'response_metadata': {},
 'type': 'human',
 'name': None,
 'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6',
 'example': False}
last_message
{'content': '아니, 사실 3과 3을 곱해 봐!',
 'additional_kwargs': {},
 'response_metadata': {},
 'type': 'human',
 'name': None,
 'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6',
 'example': False}

기억하세요, 앞서 말했듯이 messages 키에 대한 업데이트는 동일한 add_messages 리듀서를 사용합니다.

기존 메시지를 덮어쓰려면 메시지 id를 제공할 수 있습니다.

여기서 그렇게 했습니다. 위에서 보이듯이 메시지 content만 수정했습니다.

await client.threads.update_state(
    thread["thread_id"],
    {"messages": last_message},
)
{'checkpoint': {'thread_id': '1f5c505b-f858-403e-831c-fc7296cae2a6',
  'checkpoint_ns': '',
  'checkpoint_id': '1f09e7de-fbcb-65da-8001-cbb8abb7d841'},
 'configurable': {'thread_id': '1f5c505b-f858-403e-831c-fc7296cae2a6',
  'checkpoint_ns': '',
  'checkpoint_id': '1f09e7de-fbcb-65da-8001-cbb8abb7d841'},
 'checkpoint_id': '1f09e7de-fbcb-65da-8001-cbb8abb7d841'}

이제 None을 전달하여 재개합니다.

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id="agent",
    input=None,
    stream_mode="values",
    interrupt_before=["assistant"],
):
    print(f"다음 유형의 새 이벤트 수신: {chunk.event}...")
    messages = chunk.data.get("messages", [])
    if messages:
        print(messages[-1])
    print("-" * 50)
다음 유형의 새 이벤트 수신: metadata...
--------------------------------------------------
다음 유형의 새 이벤트 수신: values...
{'content': '아니, 사실 3과 3을 곱해 봐!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '35e1f8ae-cb14-4cd8-8461-2b52512070b6', 'example': False}
--------------------------------------------------
다음 유형의 새 이벤트 수신: values...
{'content': '', 'additional_kwargs': {'tool_calls': [{'id': 'call_goNOut6Kb62fx7l2DT2x2kk1', 'function': {'arguments': '{"a":3,"b":3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 17, 'prompt_tokens': 144, 'total_tokens': 161, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLicZqnLl2ZounaymbSdTYyUxrboI', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, 'type': 'ai', 'name': None, 'id': 'run--8bf65604-55f7-4d64-9cfe-321cb6d718e0-0', 'example': False, 'tool_calls': [{'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_goNOut6Kb62fx7l2DT2x2kk1', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 144, 'output_tokens': 17, 'total_tokens': 161, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}
--------------------------------------------------
다음 유형의 새 이벤트 수신: values...
{'content': '9', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'multiply', 'id': '16969706-9791-4845-a691-2dba55dda3a7', 'tool_call_id': 'call_goNOut6Kb62fx7l2DT2x2kk1', 'artifact': None, 'status': 'success'}
--------------------------------------------------

예상대로 도구 호출의 결과로 9를 얻습니다.

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id="agent",
    input=None,
    stream_mode="values",
    interrupt_before=["assistant"],
):
    print(f"다음 유형의 새 이벤트 수신: {chunk.event}...")
    messages = chunk.data.get("messages", [])
    if messages:
        print(messages[-1])
    print("-" * 50)
다음 유형의 새 이벤트 수신: metadata...
--------------------------------------------------
다음 유형의 새 이벤트 수신: values...
{'content': '3과 3을 곱하면 9입니다!', 'additional_kwargs': {'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 13, 'prompt_tokens': 169, 'total_tokens': 182, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLict3gOAN5TKhR3bduFlpySVD77j', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, 'type': 'ai', 'name': None, 'id': 'run--fa793243-3f12-439e-bf44-d9bc3a2513f0-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 169, 'output_tokens': 13, 'total_tokens': 182, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}
--------------------------------------------------

사용자 입력 대기

따라서 중단점 이후에 에이전트 상태를 편집할 수 있다는 것이 명확합니다.

이제 이 상태 업데이트를 수행하기 위해 사람의 피드백을 허용하려면 어떻게 해야 할까요?

에이전트 내에서 사람의 피드백을 위한 placeholder 역할을 하는 노드를 추가하겠습니다.

human_feedback 노드는 사용자가 상태에 직접 피드백을 추가할 수 있도록 합니다.

human_feedback 노드 전에 interrupt_before를 사용하여 중단점을 지정합니다.

이 노드까지 그래프의 상태를 저장하기 위해 체크포인터를 설정합니다.

# System message
sys_msg = SystemMessage(
    content="당신은 입력값에 대해 산술 연산을 수행하는 임무를 맡은 유용한 보조자입니다."
)
 
 
# no-op node that should be interrupted on
def human_feedback(state: MessagesState):
    pass
 
 
# Assistant node
def assistant(state: MessagesState):
    return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
 
 
# Graph
builder = StateGraph(MessagesState)
 
# Define nodes: these do the work
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_node("human_feedback", human_feedback)
 
# Define edges: these determine the control flow
builder.add_edge(START, "human_feedback")
builder.add_edge("human_feedback", "assistant")
builder.add_conditional_edges(
    "assistant",
    # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
    # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
    tools_condition,
)
builder.add_edge("tools", "human_feedback")
 
memory = MemorySaver()
graph = builder.compile(interrupt_before=["human_feedback"], checkpointer=memory)
# display(Image(graph.get_graph().draw_mermaid_png()))

사용자로부터 피드백을 받을 것입니다.

이전처럼 .update_state를 사용하여 받은 사람의 응답으로 그래프 상태를 업데이트합니다.

as_node="human_feedback" 매개변수를 사용하여 이 상태 업데이트를 지정된 노드인 human_feedback으로 적용합니다.

# Input
initial_input = {"messages": "2와 3을 곱하세요"}
 
# Thread
thread = {"configurable": {"thread_id": "5"}}
 
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
 
# Get user input
user_input = input("상태를 어떻게 업데이트하고 싶은지 알려주세요:")
 
# We now update the state as if we are the human_feedback node
graph.update_state(thread, {"messages": user_input}, as_node="human_feedback")
 
# Continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
================================ Human Message =================================

2와 3을 곱하세요
================================ Human Message =================================

3곱하기 3
================================== Ai Message ==================================
Tool Calls:
  multiply (call_80xihmMD2ycfwhLaqgVHG5Zu)
 Call ID: call_80xihmMD2ycfwhLaqgVHG5Zu
  Args:
    a: 2
    b: 3
  multiply (call_CthUsT1QR2pMWG2ngLTVrRkJ)
 Call ID: call_CthUsT1QR2pMWG2ngLTVrRkJ
  Args:
    a: 3
    b: 3
================================= Tool Message =================================
Name: multiply

9
# Continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
    event["messages"][-1].pretty_print()
================================= Tool Message =================================
Name: multiply

9
================================== Ai Message ==================================

2와 3을 곱한 결과는 6이고, 3 곱하기 3의 결과는 9입니다.
history = graph.get_state_history(thread)
for h in history:
    print(h)
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'), HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='6', name='multiply', id='278ef198-e795-4db4-8387-ac6ea84e0a8d', tool_call_id='call_80xihmMD2ycfwhLaqgVHG5Zu'), ToolMessage(content='9', name='multiply', id='4759e6b7-0689-4bdd-a1a6-2352c4bb2402', tool_call_id='call_CthUsT1QR2pMWG2ngLTVrRkJ'), AIMessage(content='2와 3을 곱한 결과는 6이고, 3 곱하기 3의 결과는 9입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 221, 'total_tokens': 250, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiemkVrN5uUHXXEXe9xs83cNysrd', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--60bf9fa0-a1bc-4104-9391-ea2d0abf8afe-0', usage_metadata={'input_tokens': 221, 'output_tokens': 29, 'total_tokens': 250, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}, next=(), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e5-3cf3-6048-8005-50c9b744acc2'}}, metadata={'source': 'loop', 'step': 5, 'parents': {}}, created_at='2025-10-01T04:23:05.764025+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e5-2b5f-6b88-8004-f42d4c4ddfb6'}}, tasks=(), interrupts=())
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'), HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='6', name='multiply', id='278ef198-e795-4db4-8387-ac6ea84e0a8d', tool_call_id='call_80xihmMD2ycfwhLaqgVHG5Zu'), ToolMessage(content='9', name='multiply', id='4759e6b7-0689-4bdd-a1a6-2352c4bb2402', tool_call_id='call_CthUsT1QR2pMWG2ngLTVrRkJ')]}, next=('assistant',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e5-2b5f-6b88-8004-f42d4c4ddfb6'}}, metadata={'source': 'loop', 'step': 4, 'parents': {}}, created_at='2025-10-01T04:23:03.921128+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-ea48-6032-8003-9ed5091860f6'}}, tasks=(PregelTask(id='63eb7d42-c0bb-d465-2f21-f429cb1dd4d3', name='assistant', path=('__pregel_pull', 'assistant'), error=None, interrupts=(), state=None, result={'messages': [AIMessage(content='2와 3을 곱한 결과는 6이고, 3 곱하기 3의 결과는 9입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 221, 'total_tokens': 250, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiemkVrN5uUHXXEXe9xs83cNysrd', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--60bf9fa0-a1bc-4104-9391-ea2d0abf8afe-0', usage_metadata={'input_tokens': 221, 'output_tokens': 29, 'total_tokens': 250, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}),), interrupts=())
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'), HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='6', name='multiply', id='278ef198-e795-4db4-8387-ac6ea84e0a8d', tool_call_id='call_80xihmMD2ycfwhLaqgVHG5Zu'), ToolMessage(content='9', name='multiply', id='4759e6b7-0689-4bdd-a1a6-2352c4bb2402', tool_call_id='call_CthUsT1QR2pMWG2ngLTVrRkJ')]}, next=('human_feedback',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-ea48-6032-8003-9ed5091860f6'}}, metadata={'source': 'loop', 'step': 3, 'parents': {}}, created_at='2025-10-01T04:22:57.095677+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-ea43-6f1e-8002-4c628e64fe9b'}}, tasks=(PregelTask(id='b7ff1211-3f04-5e77-9038-92b02c1f5560', name='human_feedback', path=('__pregel_pull', 'human_feedback'), error=None, interrupts=(), state=None, result={}),), interrupts=())
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'), HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}, next=('tools',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-ea43-6f1e-8002-4c628e64fe9b'}}, metadata={'source': 'loop', 'step': 2, 'parents': {}}, created_at='2025-10-01T04:22:57.094005+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-e32a-6732-8001-830aad259eb1'}}, tasks=(PregelTask(id='31c216e0-bbbc-509a-1f8b-a87787074436', name='tools', path=('__pregel_pull', 'tools'), error=None, interrupts=(), state=None, result={'messages': [ToolMessage(content='6', name='multiply', id='278ef198-e795-4db4-8387-ac6ea84e0a8d', tool_call_id='call_80xihmMD2ycfwhLaqgVHG5Zu'), ToolMessage(content='9', name='multiply', id='4759e6b7-0689-4bdd-a1a6-2352c4bb2402', tool_call_id='call_CthUsT1QR2pMWG2ngLTVrRkJ')]}),), interrupts=())
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'), HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4')]}, next=('assistant',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-e32a-6732-8001-830aad259eb1'}}, metadata={'source': 'update', 'step': 1, 'parents': {}}, created_at='2025-10-01T04:22:56.349555+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-9b01-6ee2-8000-7daab0592e1d'}}, tasks=(PregelTask(id='fd155c44-902a-7991-93e8-ae2d2c3d4aa8', name='assistant', path=('__pregel_pull', 'assistant'), error=None, interrupts=(), state=None, result={'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}),), interrupts=())
StateSnapshot(values={'messages': [HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b')]}, next=('human_feedback',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-9b01-6ee2-8000-7daab0592e1d'}}, metadata={'source': 'loop', 'step': 0, 'parents': {}}, created_at='2025-10-01T04:22:48.783219+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-9aff-6b38-bfff-402b02474e2a'}}, tasks=(PregelTask(id='eedab009-9525-2f8a-77c4-a066d9d86c74', name='human_feedback', path=('__pregel_pull', 'human_feedback'), error=None, interrupts=(), state=None, result=None),), interrupts=())
StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f09e7e4-9aff-6b38-bfff-402b02474e2a'}}, metadata={'source': 'input', 'step': -1, 'parents': {}}, created_at='2025-10-01T04:22:48.782309+00:00', parent_config=None, tasks=(PregelTask(id='62d51b1a-5347-e4db-4af4-83ab6c33d20f', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': '2와 3을 곱하세요'}),), interrupts=())
snapshot = graph.get_state(thread)
snapshot.values["messages"]
[HumanMessage(content='2와 3을 곱하세요', additional_kwargs={}, response_metadata={}, id='8417703e-93e0-4bdd-8787-61986c5ac75b'),
 HumanMessage(content='3곱하기 3', additional_kwargs={}, response_metadata={}, id='e688ba2e-061d-41b6-b6c8-55d8d5d68dc4'),
 AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'function': {'arguments': '{"a": 2, "b": 3}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'function': {'arguments': '{"a": 3, "b": 3}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 155, 'total_tokens': 205, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiee94djjpqvq3Td8utLsNvqwXar', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--10c54338-dada-4e02-9b9c-7f8d382e987e-0', tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_80xihmMD2ycfwhLaqgVHG5Zu', 'type': 'tool_call'}, {'name': 'multiply', 'args': {'a': 3, 'b': 3}, 'id': 'call_CthUsT1QR2pMWG2ngLTVrRkJ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 155, 'output_tokens': 50, 'total_tokens': 205, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
 ToolMessage(content='6', name='multiply', id='278ef198-e795-4db4-8387-ac6ea84e0a8d', tool_call_id='call_80xihmMD2ycfwhLaqgVHG5Zu'),
 ToolMessage(content='9', name='multiply', id='4759e6b7-0689-4bdd-a1a6-2352c4bb2402', tool_call_id='call_CthUsT1QR2pMWG2ngLTVrRkJ'),
 AIMessage(content='2와 3을 곱한 결과는 6이고, 3 곱하기 3의 결과는 9입니다.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 221, 'total_tokens': 250, '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-2024-08-06', 'system_fingerprint': 'fp_f33640a400', 'id': 'chatcmpl-CLiemkVrN5uUHXXEXe9xs83cNysrd', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--60bf9fa0-a1bc-4104-9391-ea2d0abf8afe-0', usage_metadata={'input_tokens': 221, 'output_tokens': 29, 'total_tokens': 250, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]