LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 3 · Human in the Loop
1 3-1 Streaming
2 3-2 Breakpoints
3 3-3 Edit State
4 3-4 Dyn Breakpts
5 3-5 Time Travel
SUM Module Summary
HomeLangGraphModule 3 · Human in the Loop3-3 Edit State
📓 Notebook 📖 Explained
LANGGRAPH MODULE 3 · LESSON 3

Edit graph state and inject human feedback
update_state · as_node · Human Feedback

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.

1Review: Three modes of human-machine collaboration

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?

core issues

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.

2graph.update_state() — directly edit graph state

Build an Agent graph for testing

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)

Trigger breakpoint: the graph pauses before the assistant node

# 发送初始输入,图会在断点处停止
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 节点前的断点
================================ Human Message ================================
Multiply 2 and 3

The graph is paused at this time. We can check the current status:

state = graph.get_state(thread)
print(state.next)  # ('assistant',) — 下一步等待执行 assistant

Core API: graph.update_state()

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!")]},
)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6a414-f419-6182-8001-b9e899eca7e5'}}

update_stateReturns a new checkpoint configuration indicating that the state has been saved to a new checkpoint.

The meaning of the return value

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.

3add_messages reducer and message coverage mechanism

update_stateRather than simply "replacing" the entire State, it follows thereducer rules. It is crucial to understand this.

reducer in MessagesState

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]

Comparison of two message update behaviors

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

Scenario 1: Append new message (without id)

First in Notebookupdate_stateThe call passes in aNew HumanMessage without id, soadd_messagesput itAppendReaching the end of the existing list:

messages before update
messages: [
  "Multiply 2 and 3"
]
before update_state is called
messages after update
messages: [
  "Multiply 2 and 3",
  "No, actually multiply 3 and 3!"
]
New message added

Verification result:

new_state = graph.get_state(thread).values
for m in new_state['messages']:
    m.pretty_print()
================================ Human Message ================================
Multiply 2 and 3
================================ Human Message ================================
No, actually multiply 3 and 3!

Scenario 2: Overwriting existing message (with id)

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 相同 → 覆盖该消息
    )]},
)
Key differences

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.

4Resume graph execution after editing state

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()

The meaning of passing None

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.

Complete edit - restore process code

# ① 发送初始输入,图在 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()
================================ Human Message ================================
No, actually multiply 3 and 3!
================================== Ai Message ==================================
Tool Calls:
  multiply (call_Mbu8MfA0krQh8rkZZALYiQMk)
  Args: a: 3, b: 3
================================= Tool Message =================================
Name: multiply

9

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()
================================= Tool Message =================================
Name: multiply

9
================================== Ai Message ==================================
3 multiplied by 3 equals 9.

Complete execution timing with state editing

START
Breakpoint trigger: interrupt_before["assistant"]
The graph is paused, waiting for manual intervention
graph.update_state() injects new messages
graph.stream(None, thread) resume execution
assistant
LLM decides to call multiply(3,3)
tools
Execute tool, return 9
↓ (breakpoint again)
assistant
generate final reply
END

5as_node parameter: disguised as an update of a specific node

update_stateThe third parameter ofas_nodeIt is an advanced feature and the key to realizing "artificial feedback node".

What problem does as_node solve?

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"   # 关键参数
)
Why as_node is important

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."

Illustration of how as_node works

The routing meaning of as_node="human_feedback"

Do not specify as_node
I don’t know the current situation
After which node
Routing may be wrong
Specify as_node="human_feedback"
Figure knows that it is currently in
after human_feedback
Correct routing to assistant

as_node and the next field of the graph

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

6Add human_feedback node to implement manual input

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.

design ideas

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)

Graph structure with human_feedback node

START
interrupt_before
human_feedback
Placeholder node, waiting for manual injection
assistant
LLM decision
tools_condition route
need tools
tools
After completion
Back to human_feedback
direct answer
END

Complete manual feedback workflow code

# ① 启动图,图执行到 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:

================================ Human Message ================================
Multiply 2 and 3
================================ Human Message ================================
no, multiply 3 and 3
================================== Ai Message ==================================
Tool Calls:
  multiply (call_sewrDyCrAJBQQecusUoT6OJ6)
  Args: a: 3, b: 3
================================= Tool Message =================================
Name: multiply

9
Note the type of user_input

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.

7Edit status remotely via LangSmith Studio API

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.

Using the LangGraph SDK client

from langgraph_sdk import get_client

# 连接到本地运行的 LangGraph 开发服务器
client = get_client(url="http://127.0.0.1:2024")
# 生产环境中换成你的 LangGraph Cloud URL

Trigger breakpoints and get status via API

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])
Advantages of dynamic breakpoints

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.

Get and modify status via API

# 获取当前状态
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.

Resume execution via API

# 传入 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)

8Complete workflow summary and best practices

Complete human editing workflow

① Run the graph until the breakpoint

callgraph.stream(initial_input, thread), the picture is setinterrupt_beforepause before the node. It is safe to check the State at this point.

② Check current status

callgraph.get_state(thread), viewstate.values(current data) andstate.next(The node to be executed next).

③ Edit status on demand

callgraph.update_state(thread, new_values, as_node=...). You can append a new message, overwrite an existing message, or modify any State field.

④ Resume execution

callgraph.stream(None, thread), the graph continues from the latest checkpoint, running subsequent nodes using the modified state.

⑤ Repeat until complete

If there are multiple breakpoints, repeat steps ②③④ until the graph execution reaches END or the breakpoint is no longer triggered.

Quick Check on Key Concepts

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.

When to choose which status editing method

Append new message (no id)

  • • Inject additional human instructions
  • • Add background information or context
  • • passhuman_feedbackNode input comments
  • • The original message remains unchanged and new content is added

Overwrite existing message (with id)

  • • Correct incorrect information entered by the user
  • • Fixed errors in AI output
  • • Edit messages directly through the Studio UI
  • • Keep message list length constant
Summary of design patterns

Explicitly 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.

Panorama of knowledge points in this course

Status editing API
  • graph.update_state()
  • graph.get_state()
  • as_nodeparameter
  • stream(None, thread)restore
reducer mechanism
  • add_messagesProcess messages
  • • No id → Append
  • • Has id → Overwrite (Overwrite)
  • • Generate new checkpoint after update
graph architecture pattern
  • human_feedbackplaceholder node
  • tools → human_feedbackloop
  • interrupt_beforetrigger pause
  • • Explicitly declare the location of human-robot collaboration
Remote API correspondence
  • langgraph_sdkclient
  • client.threads.get_state()
  • client.threads.update_state()
  • client.runs.stream(None)