After the graph execution is paused midway, you can not only "approve or reject", but also directly modify the internal state of the graph.
This lesson demonstrates how to accurately edit State and how to seamlessly inject manual input into the Agent process.
In the previous lessons of Module 3, we discussed why we need to include humans in the AI Agent process. LangGraph's human-in-the-loop supports three core scenarios:
| scene | English terms | specific meaning | This lesson covers |
|---|---|---|---|
| approve | Approval | Interrupt the Agent and let the user decide whether to continue performing an action | Already studied in 3-2 |
| debug | Debugging | Retrace historical checkpoints of the graph to reproduce or avoid problems | Will study in 3-5 |
| Edit | Editing | Modify the State of the graph directly at the breakpoint, and then resume execution | Key points of this lesson |
The breakpoints learned in the previous lesson allow us toPause graph execution, but after pausing, you can only choose "continue" or "terminate". The problem to be solved in this lesson is:After pausing, how to actively modify the State and then continue execution with the modified state?
Imagine a scenario where the Agent receives the user input "Help me multiply 2 and 3", but before it actually runs, you find out that the user actually wanted to ask "Multiply 3 and 3". Breakpoints allow you to pause, andupdate_stateAllows you to directly change that message without re-triggering the entire conversation.
The key APIs covered in this lesson aregraph.update_state(config, values), which is the core state editing interface provided by LangGraph. Used with breakpoints, it can achieve powerful human-computer collaborative interaction.
Notebook first built a computing assistant Agent with breakpoints, usinginterrupt_before=["assistant"]Pause before helper node:
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState, START, StateGraph
from langgraph.prebuilt import tools_condition, ToolNode
from langchain_core.messages import HumanMessage, SystemMessage
# 定义三个算术工具
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
def divide(a: int, b: float) -> float:
"""Divide a by b."""
return a / b
tools = [add, multiply, divide]
llm = ChatDeepSeek(model="deepseek-v4-pro")
llm_with_tools = llm.bind_tools(tools)
# 系统消息
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic on a set of inputs.")
# assistant 节点:调用绑定了工具的 LLM
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# 构建图
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
memory = MemorySaver()
# 关键:在 assistant 节点之前设置断点
graph = builder.compile(interrupt_before=["assistant"], checkpointer=memory)
# 发送初始输入,图会在断点处停止
initial_input = {"messages": "Multiply 2 and 3"}
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
# 输出显示图已收到消息,正在等待 assistant 节点前的断点
The graph is paused at this time. We can check the current status:
state = graph.get_state(thread)
print(state.next) # ('assistant',) — 下一步等待执行 assistant
Now let’s look at the most critical call in this lesson——update_state. Its full signature is as follows:
graph.update_state(
config, # thread 配置,指向哪个 checkpoint(哪条线程)
values, # 要更新的 State 字段的字典
as_node=None # 可选:伪装成哪个节点执行了此更新(后面详述)
)
In the Notebook, we appended a new manual message to the message list, correcting the computational requirements:
graph.update_state(
thread,
{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]},
)
update_stateReturns a new checkpoint configuration indicating that the state has been saved to a new checkpoint.
every callupdate_statewill create aNew checkpoint. The returned configuration contains the ID of this new checkpoint. This means that state modifications themselves can also be tracked by time travel functionality - you can see when the state was modified and by whom.
update_stateRather than simply "replacing" the entire State, it follows thereducer rules. It is crucial to understand this.
MessagesStateis LangGraph's built-in state class, whichmessagesField usageadd_messagesAs a reducer:
# MessagesState 的内部定义(LangGraph 源码简化版)
from typing import Annotated
from langgraph.graph.message import add_messages
class MessagesState(TypedDict):
# Annotated 声明 reducer:add_messages 函数处理合并逻辑
messages: Annotated[list, add_messages]
add_messagesThe way the reducer works depends on whether your incoming message carriesid:
| Whether the incoming message has an id | reducer behavior | Effect | Applicable scenarios |
|---|---|---|---|
| no id (new message) | Append | New messages are added to the end of the messages list | Inject human feedback, add context |
| has id (the id of the existing message) | Overwrite | Find the message matching id and replace its content | Correct user input, correct AI output |
First in Notebookupdate_stateThe call passes in aNew HumanMessage without id, soadd_messagesput itAppendReaching the end of the existing list:
Verification result:
new_state = graph.get_state(thread).values
for m in new_state['messages']:
m.pretty_print()
if we wantreplaceInstead of appending, just keep the original message in the incoming message object.id. This wayadd_messageswill find the message with that id and overwrite it:
# 假设已有消息的 id 是 "msg-123"
# 传入同 id 的新消息 → 覆盖,而非追加
graph.update_state(
thread,
{"messages": [HumanMessage(
content="No, actually multiply 3 and 3!",
id="msg-123" # 与原消息 id 相同 → 覆盖该消息
)]},
)
In the example of LangSmith Studio API in the Notebook of this lesson,last_messageis taken out of the state, itAlready has id, so after modifying content and then sending it back, what is triggered iscoverlogic; whereas in the first native Python example,HumanMessage(...)It's newly built,no id, triggered byAppendlogic. Both are common uses and need to be chosen based on your purpose.
Modifying the state is only the first step. The complete process also requires the diagram to continue execution from the modified state. The method to resume execution is very simple:incomingNoneas inputcall againgraph.stream()。
when yougraph.stream(None, thread)incomingNoneWhen , LangGraph will not create new input, but from the current thread'sLatest checkpoint (checkpoint)Load the State and continue running from the next node to be executed. This is the core mechanism of the three-stage workflow of breakpoint + status editing + resume execution.
# ① 发送初始输入,图在 assistant 节点前暂停
initial_input = {"messages": "Multiply 2 and 3"}
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
# 此时图暂停,等待人工干预
# ② 编辑状态:注入一条新的用户消息
graph.update_state(
thread,
{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]},
)
# ③ 传入 None,从当前状态继续执行
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
Since the graph is still setinterrupt_before=["assistant"], which pauses again in front of the assistant node after the tool results return. incomingNoneRestore again:
# ④ 图再次在断点处停下,再次传 None 让它完成最后一步
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
update_stateThe third parameter ofas_nodeIt is an advanced feature and the key to realizing "artificial feedback node".
During the execution of the graph, "who was the last executed node" is recorded, because the jump logic of the edge depends on this information. When you call directlyupdate_stateAt this time, LangGraph needs to know: "Which node" counts as completing the execution of this update?
passas_nodeParameters, you can specify that this status update "pretends" to be completed by a certain node, so that the graph determines where to go next according to the correct edge logic.
# 指定 as_node="human_feedback"
# 意思是:假装 human_feedback 节点刚刚执行完,并产生了这些状态更新
graph.update_state(
thread,
{"messages": user_input},
as_node="human_feedback" # 关键参数
)
Suppose the edge of the graph ishuman_feedback → assistant. If we callupdate_statenot specifiedas_node, the graph does not know "the current state after the human_feedback node is completed" and cannot be routed to the assistant correctly.as_node="human_feedback"Tell the graph: "Consider this update as the execution result of the human_feedback node, and follow the outgoing edge of human_feedback in the next step."
callupdate_state(as_node="human_feedback")After, figure ofstate.nextwill become('assistant',), because fromhuman_feedbackAfter the node, the edge is directly connected toassistant:
# 调用 update_state(as_node="human_feedback") 之后
state = graph.get_state(thread)
print(state.next) # ('assistant',) — 下一步执行 assistant
The previous example was a message that the graph was "quietly modified" externally. A more elegant design is: in the graph structureExplicitly add a node representing human feedback, making human-machine collaboration part of the graph architecture.
The new diagram introduces ahuman_feedbackNode, itself is an empty function (no-op) and does not do any actual processing. Its role is just as a "placeholder" in the diagram - when the diagram is executed here, passinterrupt_beforePause, waiting for manual callupdate_state(as_node="human_feedback")to inject feedback content and then continue execution.
# no-op 节点:本身不做任何事,只作为占位符
def human_feedback(state: MessagesState):
pass # 什么都不做,真正的内容由 update_state 注入
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# 构建新图
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_node("human_feedback", human_feedback)
# 关键:图从 human_feedback 开始,工具完成后也回到 human_feedback
builder.add_edge(START, "human_feedback")
builder.add_edge("human_feedback", "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "human_feedback") # 工具结束后回到人工反馈节点
memory = MemorySaver()
# 在 human_feedback 节点之前设置断点
graph = builder.compile(interrupt_before=["human_feedback"], checkpointer=memory)
# ① 启动图,图执行到 human_feedback 前暂停
initial_input = {"messages": "Multiply 2 and 3"}
thread = {"configurable": {"thread_id": "5"}}
for event in graph.stream(initial_input, thread, stream_mode="values"):
event["messages"][-1].pretty_print()
# ② 获取真实的人工输入(这里用 input() 模拟)
user_input = input("Tell me how you want to update the state: ")
# 实际应用中,这个 user_input 来自 Web UI、API 等
# ③ 注入人工反馈,伪装成 human_feedback 节点的输出
graph.update_state(thread, {"messages": user_input}, as_node="human_feedback")
# ④ 继续执行图(从 assistant 节点开始)
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()
When the user enters "no, multiply 3 and 3", the output is as follows:
input()What is returned is a string (str), instead ofHumanMessageobject.add_messagesThe reducer is smart enough to automatically convert the string intoHumanMessageObject and appended to the message list, so manual encapsulation is not required here.
In addition to calling it directly in Python codegraph.update_state(), LangGraph also provides an HTTP-based API (vialanggraph_sdkclient), you can remotely edit the state of the graph running on the server. This is the underlying mechanism behind LangSmith Studio's visual interface.
from langgraph_sdk import get_client
# 连接到本地运行的 LangGraph 开发服务器
client = get_client(url="http://127.0.0.1:2024")
# 生产环境中换成你的 LangGraph Cloud URL
initial_input = {"messages": "Multiply 2 and 3"}
# 创建一个新线程
thread = await client.threads.create()
# 流式运行,并传入 interrupt_before(无需在代码里预定义断点)
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=initial_input,
stream_mode="values",
interrupt_before=["assistant"], # API 调用时动态传入断点
):
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
When calling via the API, you canDo not modify agent codepassed ininterrupt_before, implement dynamic breakpoints. This is very useful for Agents in production environments: the same Agent can have different breakpoint strategies in different usage scenarios without the need to redeploy the code.
# 获取当前状态
current_state = await client.threads.get_state(thread['thread_id'])
# 取出最后一条消息
last_message = current_state['values']['messages'][-1]
print(last_message)
# {'content': 'Multiply 2 and 3', 'id': '882dabe4-...', 'type': 'human', ...}
# 修改消息内容(保留 id → 触发覆盖)
last_message['content'] = "No, actually multiply 3 and 3!"
# 将修改后的消息写回(同 id → 覆盖原消息)
await client.threads.update_state(
thread['thread_id'],
{"messages": last_message}
)
Note that the API in the SDK isclient.threads.update_state(), corresponding to the localgraph.update_state(), the function is exactly the same, but it is called remotely through HTTP.
# 传入 input=None,从当前最新状态继续执行
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=None, # None = 从当前 checkpoint 继续
stream_mode="values",
interrupt_before=["assistant"],
):
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
| Operation | Native Python calls | SDK remote call |
|---|---|---|
| Get status | graph.get_state(thread) |
await client.threads.get_state(thread_id) |
| update status | graph.update_state(thread, values) |
await client.threads.update_state(thread_id, values) |
| Resume execution | graph.stream(None, thread) |
await client.runs.stream(thread_id, agent, input=None) |
callgraph.stream(initial_input, thread), the picture is setinterrupt_beforepause before the node. It is safe to check the State at this point.
callgraph.get_state(thread), viewstate.values(current data) andstate.next(The node to be executed next).
callgraph.update_state(thread, new_values, as_node=...). You can append a new message, overwrite an existing message, or modify any State field.
callgraph.stream(None, thread), the graph continues from the latest checkpoint, running subsequent nodes using the modified state.
If there are multiple breakpoints, repeat steps ②③④ until the graph execution reaches END or the breakpoint is no longer triggered.
| concept | illustrate | Things to note |
|---|---|---|
update_state(thread, values) |
Directly modify the graph state of the specified thread and create a new checkpoint | Follow the reducer rules defined in State |
add_messages Reducer |
Process the merge logic of messages field | No id → append; same id → overwrite |
as_nodeparameter |
Disguise status updates as execution results of specific nodes | Routing decisions affecting the graph (state.next) |
human_feedbacknode |
Placeholder nodes in the graph serve as "slots" for manual input | The function body is pass, and the content is injected by update_state |
stream(None, thread) |
Resume execution from the latest checkpoint | Pass None instead of new input to avoid resetting state |
interrupt_before=[...] |
Trigger a breakpoint before the specified node to pause graph execution. | It can be set in compile() or dynamically passed in when calling the API. |
human_feedbackNode input commentsExplicitly add the human_feedback node to the graph structureThis is the recommended practice for production-grade Human-in-the-loop applications. Compared to calling directly from the outsideupdate_state, it makes the location of human-machine collaboration clear at a glance in the diagram's architecture, and is easier to test and maintain. Whentools → human_feedbackWhen this edge exists, each tool call will wait for manual confirmation before being handed over to the assistant node for processing - this is the core pattern for building a controlled Agent system.
graph.update_state()graph.get_state()as_nodeparameterstream(None, thread)restoreadd_messagesProcess messageshuman_feedbackplaceholder nodetools → human_feedbackloopinterrupt_beforetrigger pauselanggraph_sdkclientclient.threads.get_state()client.threads.update_state()client.runs.stream(None)