그래프 상태 편집
복습
우리는 Human-in-the-Loop의 동기에 대해 논의했습니다:
(1) 승인 - 에이전트를 중단하고, 사용자에게 상태를 표시하며, 사용자가 작업을 승인할 수 있도록 할 수 있습니다
(2) 디버깅 - 그래프를 되감아 문제를 재현하거나 회피할 수 있습니다
(3) 편집 - 상태를 수정할 수 있습니다
중단점이 사용자 승인을 어떻게 지원하는지 보여주었지만, 그래프가 중단되었을 때 그래프 상태를 수정하는 방법은 아직 알아보지 않았습니다!
목표
이제 그래프 상태를 직접 편집하고 사람의 피드백을 삽입하는 방법을 보여드리겠습니다.
%%capture --no-stderr
%pip install --quiet -U langgraph langchain_openai langgraph_sdk langgraph-prebuiltfrom 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()================================[1m Human Message [0m=================================
2와 3을 곱하세요
state = graph.get_state(thread)
stateStateSnapshot(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()================================[1m Human Message [0m=================================
2와 3을 곱하세요
================================[1m Human Message [0m=================================
아니, 사실 3과 3을 곱해 봐!
이제 단순히 None을 전달하고 현재 상태부터 진행하도록 하여 에이전트를 계속 진행해봅시다.
현재 상태를 출력한 다음 남은 노드들을 실행합니다.
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()================================[1m Human Message [0m=================================
아니, 사실 3과 3을 곱해 봐!
==================================[1m Ai Message [0m==================================
Tool Calls:
multiply (call_E5DVuKkpUvvWHlU1aRn3yrBs)
Call ID: call_E5DVuKkpUvvWHlU1aRn3yrBs
Args:
a: 3
b: 3
=================================[1m Tool Message [0m=================================
Name: multiply
9
이제 breakpoint가 있는 assistant로 돌아왔습니다.
다시 None을 전달하여 진행할 수 있습니다.
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()=================================[1m Tool Message [0m=================================
Name: multiply
9
==================================[1m Ai Message [0m==================================
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()================================[1m Human Message [0m=================================
2와 3을 곱하세요
================================[1m Human Message [0m=================================
3곱하기 3
==================================[1m Ai Message [0m==================================
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
=================================[1m Tool Message [0m=================================
Name: multiply
9
# Continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()=================================[1m Tool Message [0m=================================
Name: multiply
9
==================================[1m Ai Message [0m==================================
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}})]