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-1 Streaming
📓 Notebook 📖 Explained
LANGGRAPH MODULE 3 · LESSON 1

Streaming Output and Interruption Mechanism
Streaming & Interruption in LangGraph

How to get output in real time during graph execution? How to make the graph "pause waiting for manual confirmation"?
This section is the basis of Human-in-the-loop, a thorough understanding of Streaming and Interrupt.

1Why streaming output is needed: from "waiting for results" to "real-time observation"

By default, callinggraph.invoke()yesblocking: You can’t get the final State until the graph is completely run. This is fine for simple tasks, but for:

LangGraph providesFirst-class streaming support (First-class Streaming), allowing you to obtain output of various granularities in real time during graph execution. The ultimate goal of this section is to understand how Streaming paves the way for the core feature of Module 3: Human-in-the-loop.

core points

The streaming output of LangGraph istwo dimensions: One isNode granularity(Each node spits out data once after execution), and the second isToken granularity(It is passed out immediately when LLM generates each token). Both are important and serve different purposes.

Comparison of streaming output vs batch output

Batch mode invoke()
Node A executes
↓(wait)
Node B execution
↓(wait)
The final result is returned in one go
Streaming mode stream()
Node A executes → output chunk immediately
↓(real time)
Node B executes → output chunk immediately
↓(real time)
Continue pushing until the graph is executed

2Graph structure review: Chatbot example used in this section

This notebook reuses the Chatbot diagram with summary from Module 2. You must first understand the structure of this picture before you can understand the subsequent streaming output.

from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState

# ─── State:继承 MessagesState,额外加 summary 字段 ───
class State(MessagesState):
    summary: str

# ─── Node 1:调用 LLM 进行对话 ───
def call_model(state: State, config: RunnableConfig):
    summary = state.get("summary", "")
    if summary:
        system_message = f"Summary of conversation earlier: {summary}"
        messages = [SystemMessage(content=system_message)] + state["messages"]
    else:
        messages = state["messages"]
    response = model.invoke(messages, config)  # config 支持 token 流式
    return {"messages": response}

# ─── Node 2:对历史对话做摘要(消息过多时触发)───
def summarize_conversation(state: State):
    summary = state.get("summary", "")
    if summary:
        summary_message = f"This is summary of the conversation to date: {summary}\n\n" \
                          "Extend the summary by taking into account the new messages above:"
    else:
        summary_message = "Create a summary of the conversation above:"
    messages = state["messages"] + [HumanMessage(content=summary_message)]
    response = model.invoke(messages)
    # 保留最近 2 条消息,其余删除
    delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
    return {"summary": response.content, "messages": delete_messages}

# ─── 路由函数:消息超过 6 条时触发摘要 ───
def should_continue(state: State):
    if len(state["messages"]) > 6:
        return "summarize_conversation"
    return END

# ─── 构建图 ───
workflow = StateGraph(State)
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)

workflow.add_edge(START, "conversation")
workflow.add_conditional_edges("conversation", should_continue)
workflow.add_edge("summarize_conversation", END)

# 使用 MemorySaver 作为 checkpointer,支持线程级别的对话记忆
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)

Chatbot graph structure used in this section

START
conversation
call_model — call LLM
should_continue() routing judgment
Messages ≤ 6
END
Messages > 6
summarize_conversation
Compress history, delete old messages
END
About RunnableConfig

call_modelThe function accepts a second parameterconfig: RunnableConfig, and pass it intomodel.invoke(messages, config). This is enabled in Python 3.10 and belowToken level streaming outputis required. config carries streaming callback information, allowing LangGraph to capture and emit events immediately when the token is generated.

3stream_mode="updates": only look at the incremental changes of each node

stream_mode="updates"It is the most commonly used streaming mode. Every time a node in the graph is executed, achunk, the chunk only contains the changes made by this node to StateChange part, rather than the full State.

Basic usage

# 创建一个对话线程(thread_id 用于区分不同对话)
config = {"configurable": {"thread_id": "1"}}

# 使用 stream() 方法,指定 stream_mode="updates"
for chunk in graph.stream(
    {"messages": [HumanMessage(content="hi! I'm Lance")]},
    config,
    stream_mode="updates"
):
    print(chunk)
Output (each chunk is an update of a node)
{'conversation': {'messages': [AIMessage(content='Hi Lance! How\'s it going? What can I do for you today?', ...)]}}

The structure of chunk is a dictionary,The key is the node nameThe value is the updated content returned by the node. For example, the aboveconversationThe node returnedmessagesUpdate of fields to include LLM's response.

Extract and format LLM responses

for chunk in graph.stream(
    {"messages": [HumanMessage(content="hi! I'm Lance")]},
    config,
    stream_mode="updates"
):
    # 从 conversation 节点的更新中取出消息,美化打印
    chunk['conversation']["messages"].pretty_print()
output
================================== Ai Message ==================================

Hi Lance! How's it going? What can I do for you today?
Note: The key of chunk corresponds to the node name

If there are multiple nodes in the graph executing at the same time (parallel situation), you will receive multiple chunks, and the key of each chunk corresponds to the respective node name. When accessing the data in the chunk, you must first find the corresponding update dictionary through the node name, and then get the fields you need.

Data flow diagram of updates mode

stream_mode="updates" — Incremental update mode

The graph executes a node → push the node's state change dict → the graph continues executing the next node → push again...

conversation node
{'conversation': {'messages': [AIMessage(...)]}}
summarize node
{'summarize_conversation': {'summary': '...', 'messages': [...]}}

4stream_mode="values": Output the complete status of the graph at each step

stream_mode="values"with"updates"The difference is: each push is notChanges to a certain node, butThe complete State of the current entire graph

code example

# 使用新的 thread_id 开启新对话
config = {"configurable": {"thread_id": "2"}}

input_message = HumanMessage(content="hi! I'm Lance")
for event in graph.stream(
    {"messages": [input_message]},
    config,
    stream_mode="values"
):
    # event 就是完整的 State 字典
    for m in event['messages']:
        m.pretty_print()
    print("---"*25)  # 分隔符,区分不同节点执行后的状态
Output (two pushes: once on input and once after LLM reply)
================================ Human Message =================================

hi! I'm Lance
-------------------------------------------------------------------------------
================================ Human Message =================================

hi! I'm Lance
================================== Ai Message ==================================

Hello, Lance! How can I assist you today?
-------------------------------------------------------------------------------

Note:first pushOccurs after the graph receives input and before the first node is executed (there is only Human Message in the state);Second pushOccurs after the conversation node is executed (there is Human + AI Message in the state).

Key differences between updates vs values

Dimensions stream_mode="updates" stream_mode="values"
The contents of each chunk Contains only the fields modified by this node (increment) Contains all fields of the entire State (full amount)
chunk structure {node_name: {changed_fields}} {all_state_fields}
Push timing After each node is executed After each node is executed (including initial input)
Typical uses Only care about the output of a certain node, such as LLM reply Need to see the complete history, such as debugging and observing all messages
Data volume Smaller (only changing parts) Larger (complete state, grows with conversation)
How to choose?

In production applications,updates mode is more commonly used: You can accurately specify the output of which node to focus on. The data volume is small and the processing is more efficient. values mode is more suitableDebugging and monitoring, you can completely observe the global state changes of the graph after each step.

5astream_events: Token-level fine-grained streaming output

The first two modes (updates/values) areNode granularityStreaming - you must wait for the node to finish executing before pushing. However, chat applications often require LLM to immediately display each token to the user when it generates it, which requiresastream_events

How astream_events works

graph.astream_events()is aasynchronous method(Requiredasync for), which triggers various events synchronously when executed inside the node. You can filter out the information you care about based on the event type.

Step 1: Observe all event types

config = {"configurable": {"thread_id": "3"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")

# version="v2" 是目前推荐的版本,事件格式更稳定
async for event in graph.astream_events(
    {"messages": [input_message]}, config, version="v2"
):
    # 每个 event 有 metadata.langgraph_node(哪个节点触发)
    # event.event(事件类型)
    # event.name(具体名称)
    print(f"Node: {event['metadata'].get('langgraph_node','')}. "
          f"Type: {event['event']}. Name: {event['name']}")

Each event dictionary contains the following key fields:

Field illustrate Example value
event event type "on_chat_model_stream", "on_chain_start"
name event name "ChatDeepSeek", "conversation"
data The data carried by the event containschunkField(AIMessageChunk)
metadata.langgraph_node The name of the LangGraph node that triggered the event "conversation"

Step 2: Filter out the Token stream of specific nodes

node_to_stream = 'conversation'  # 指定只监听哪个节点的输出
config = {"configurable": {"thread_id": "4"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")

async for event in graph.astream_events(
    {"messages": [input_message]}, config, version="v2"
):
    # 只处理聊天模型产生的 token 事件,且来自 conversation 节点
    if (event["event"] == "on_chat_model_stream"
            and event['metadata'].get('langgraph_node', '') == node_to_stream):
        data = event["data"]
        # data["chunk"] 是一个 AIMessageChunk,.content 是这个 token 的文本
        print(data["chunk"].content, end="|")  # 用 | 分隔每个 token
Output (each | is a token)
|The| San| Francisco| |49|ers| are| a| professional| American| football| team|
based| in| the| San| Francisco| Bay| Area|.| They| compete| in| the| National|
Football| League| (|NFL|)| as| a| member| club| ...||||
Key understanding: on_chat_model_stream

event type"on_chat_model_stream"It is the core of Token-level streaming. Whenever the chat model generates a token, this event will be triggered.event["data"]["chunk"]It's correspondingAIMessageChunk. Passmetadata["langgraph_node"]You can accurately know which node this token comes from, thereby filtering out the output you care about.

Complete Token streaming printing code

config = {"configurable": {"thread_id": "5"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")

async for event in graph.astream_events(
    {"messages": [input_message]}, config, version="v2"
):
    if (event["event"] == "on_chat_model_stream"
            and event['metadata'].get('langgraph_node', '') == 'conversation'):
        data = event["data"]
        # end="" 让 token 连续显示,不换行
        print(data["chunk"].content, end="|")

Summary and comparison of three streaming methods

method Granularity Is it asynchronous? Typical uses
.stream(stream_mode="updates") Node level (incremental) sync Observe the output changes of each node
.stream(stream_mode="values") Node level (full amount) sync Debugging and observing complete State
.astream_events() Token level (the finest) asynchronous Real-time typewriter effect, fine monitoring

6LangGraph API’s messages mode: designed for chatting

when you passLangGraph API (LangSmith Studio or deployed service)When calling a graph remotely, exceptvaluesandupdatesIn addition, there is a special mode that is only supported at the API level:stream_mode="messages"

Prerequisites

To use the LangGraph API, you need to start the local development server first (at/studiodirectory runlanggraph dev), then passlanggraph_sdkconnection. The API examples in this section assume that the service is alreadyhttp://127.0.0.1:2024run.

Using the values pattern via the API

from langgraph_sdk import get_client
from langchain_core.messages import convert_to_messages

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

# 创建新线程
thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")

async for event in client.runs.stream(
    thread["thread_id"],
    assistant_id="agent",
    input={"messages": [input_message]},
    stream_mode="values"     # 远程 API 也支持 values 模式
):
    messages = event.data.get('messages', None)
    if messages:
        print(convert_to_messages(messages)[-1])  # 打印最新消息
    print('='*25)

The event object structure returned by the API:event.event(type string) andevent.data(data content).

messages mode: fine-grained event streaming

stream_mode="messages"specially forMessage LangGraph(Contained in statemessagesField) design will push four types of events:

event type illustrate
metadata Meta information of this run, including run_id
messages/partial Partial content of the message (token-level streaming), gradually accumulated until complete
messages/complete The complete formed message (non-streaming content, such as tool call results)
messages/metadata Message level meta-information
thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")

async for event in client.runs.stream(
    thread["thread_id"],
    assistant_id="agent",
    input={"messages": [input_message]},
    stream_mode="messages"
):
    print(event.event)  # 查看事件类型
Output (one event type per line)
metadata
messages/metadata
messages/partial
messages/partial
... (Multiple partials, corresponding to the gradual accumulation of tool call parameters)
messages/metadata
messages/complete
messages/partial
messages/partial
... (final reply token stream)

Complete messages mode processing code

def format_tool_calls(tool_calls):
    """格式化工具调用列表为可读字符串"""
    if tool_calls:
        formatted_calls = []
        for call in tool_calls:
            formatted_calls.append(
                f"Tool Call ID: {call['id']}, Function: {call['name']}, Arguments: {call['args']}"
            )
        return "\n".join(formatted_calls)
    return "No tool calls"

thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")

async for event in client.runs.stream(
    thread["thread_id"],
    assistant_id="agent",
    input={"messages": [input_message]},
    stream_mode="messages"
):
    # 处理运行元信息
    if event.event == "metadata":
        print(f"Metadata: Run ID - {event.data['run_id']}")
        print("-" * 50)

    # 处理流式消息片段
    elif event.event == "messages/partial":
        for data_item in event.data:
            # 用户输入消息
            if "role" in data_item and data_item["role"] == "user":
                print(f"Human: {data_item['content']}")
            else:
                content = data_item.get("content", "")
                tool_calls = data_item.get("tool_calls", [])
                if content:
                    print(f"AI: {content}")
                if tool_calls:
                    print("Tool Calls:")
                    print(format_tool_calls(tool_calls))
The unique value of the messages pattern

The biggest advantage of messages mode is thatTool CallStreaming support: you can see how the tool's parameters are "written out" step by step by LLM, e.g.{'a': 2}{'a': 2, 'b': 3}, which is very helpful for debugging the Agent’s tool calling behavior.

7Interrupt mechanism: interrupt_before and interrupt_after

Streaming output lets youobserveThe execution of the graph; and the interruption mechanism (Interruption) allows you tocontrolGraph execution - before and after a specific nodepause chart, wait for external decisions (usually humans) to be made, and then resume execution. This is exactlyHuman-in-the-loopcore mechanism.

Prerequisite: checkpointer must be used

Interrupt mechanism dependencyCheckpointer(checkpoint mechanism). Each time a node is executed in the graph, the current state is archived to checkpointer (such asMemorySaveror database). After the graph is interrupted, the state is frozen and archived; when you resume, the graph continues from the archive point without having to run it from the beginning.The interrupt functionality cannot be used without checkpointer.

Declare breakpoints during compile()

from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()

# interrupt_before:在执行指定节点"之前"暂停
graph = workflow.compile(
    checkpointer=memory,
    interrupt_before=["tools"]    # 在执行 tools 节点之前暂停
)

# interrupt_after:在执行指定节点"之后"暂停
graph = workflow.compile(
    checkpointer=memory,
    interrupt_after=["conversation"]  # 在 conversation 节点执行完后暂停
)

interrupt_beforeandinterrupt_afterAccept oneNode name list, multiple break points can be declared at the same time:

# 同时声明多个中断点
graph = workflow.compile(
    checkpointer=memory,
    interrupt_before=["tools", "human_review"],  # 在这两个节点前都暂停
    interrupt_after=["planner"]                   # 在 planner 执行后暂停
)

The difference between interrupt_before vs interrupt_after

parameter Pause time The state of State at this time Typical uses
interrupt_before=["X"] X node executionbefore The X node has not modified the State Ask someone to approve dangerous operations (e.g. sending emails, executing code)
interrupt_after=["X"] X node executionafter X node has been updated State Have someone review the output of LLM and decide whether to continue

Graph state after interruption

After the graph is interrupted, you can view the current State:

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

# invoke 会在中断点停下,不会继续执行后续节点
result = graph.invoke(
    {"messages": [HumanMessage(content="search for the latest AI news")]},
    config
)

# 获取当前的图状态快照
snapshot = graph.get_state(config)

# 查看下一步将执行哪个节点(中断在哪里)
print(snapshot.next)      # 例如:('tools',) 表示即将执行 tools 节点

# 查看当前 State
print(snapshot.values)    # 完整的 State 字典
output
('tools',)
{'messages': [HumanMessage(content='search for the latest AI news'), AIMessage(content='', tool_calls=[{'name': 'tavily_search', 'args': {'query': 'latest AI news 2024'}, ...}])]}

snapshot.nextReturns a tuple representingWhen the graph is paused, the name of the node to be executed next. If the graph has been executed (normally ended),snapshot.nextis an empty tuple()

The execution flow of interrupt_before=["tools"]

START
agent (LLM)
Normal execution, LLM decides to call the tool
INTERRUPTED — awaiting manual confirmation
Manual review tool call to decide whether to continue
↓ .invoke(None, config) restore
tools
Execute search/calculation tools
END

8Human-in-the-loop full mode: pause, approve, resume

After mastering the interruption mechanism, let's look at the complete Human-in-the-loop workflow:Graph paused → Manual inspection/modification of State → Resume execution

Step 1: Resume execution (without modifying State)

If LLM’s decision is deemed correct after manual review, let the diagram continue directly:

# 以 None 作为输入调用 invoke,表示"继续从中断处运行,不改变 State"
# 关键:必须使用同一个 config(同一个 thread_id),图才能找到存档点
result = graph.invoke(None, config)

# 图从 tools 节点继续执行,完成后返回最终 State
print(result)
Why None?

incomingNoneTell LangGraph:Do not re-enter new initial state, instead, load the archived state from the checkpointer when it was last paused, and resume execution from the point of interruption.thread_idis the unique identifier to find the correct save point.

Step 2: Modify the State and then restore it (manual intervention)

This is the essence of Human-in-the-loop: manually modify the State of the graph and then resume execution:

# 1. 先查看当前 State
snapshot = graph.get_state(config)
current_messages = snapshot.values["messages"]
print("当前最后一条消息(LLM 的工具调用请求):")
print(current_messages[-1])

# 2. 人工修改 State(例如:修改工具调用的参数)
# 假设 LLM 要搜索 "latest AI news",但人工觉得应该搜索更具体的内容
from langchain_core.messages import AIMessage

# 构造修改后的消息
modified_message = AIMessage(
    content="",
    id=current_messages[-1].id,           # 复用原消息的 id,触发覆盖而非追加
    tool_calls=[{
        "id": current_messages[-1].tool_calls[0]["id"],
        "name": "tavily_search",
        "args": {"query": "GPT-5 release date 2024"}  # 人工修改了搜索词
    }]
)

# 3. 更新图的 State(使用 update_state)
graph.update_state(
    config,
    {"messages": [modified_message]},   # id 相同 → add_messages reducer 原地覆盖该条消息
    as_node="agent"                       # 以 agent 节点的视角更新
)

# 4. 恢复执行(图会使用修改后的 State 继续运行)
result = graph.invoke(None, config)

Complete Human-in-the-loop life cycle

1. Trigger graph execution

The user enters a question and the graph starts running. LLM analyzes the problem and determines the need to invoke tools (e.g. search, code execution).

2. When the interruption point is reached, the graph is paused.

Before the tools node is executed (interrupt_before=["tools"]), the graph automatically pauses and archives the current State to checkpointer.snapshot.next = ('tools',)

3. Manual review

manual passgraph.get_state(config)View the current status: what LLM wants to do (tool call parameters), current conversation history, etc.

4a. Rejection / Modification

If the labor is not satisfactory, usegraph.update_state()Modify the State, such as correcting tool call parameters, injecting additional contextual information, and then resume execution.

4b. Approve and continue

If the human is satisfied, callgraph.invoke(None, config), the graph continues execution where it left off, completing the tool call and returning the final result.

Typical application scenarios

Code Execution Agent

After LLM generates code, it pauses before execution to allow developers to review the code content and confirm that there are no dangerous operations before allowing it to run.

Email sending agent

After the Agent drafts the email, it pauses and manually checks whether the recipient and body are correct, and only sends it after confirmation.

Database write operation

Interrupt before executing INSERT/UPDATE/DELETE, display the SQL to be executed, and execute it only after manual confirmation to prevent misoperation.

Content Moderation Agent

After the AI-generated content is paused, editors review whether it complies with the specifications, modify or approve it, and then publish it to the platform.

The connection between streaming output and interrupt mechanism

Streaming output and the interrupt mechanism together form the infrastructure of Human-in-the-loop:Streaming outputAllowing people to see the Agent's "thinking process" in real time,Interrupt mechanismLet people intervene in the Agent's "actions" at key nodes. The combination of the two forms an AI Agent system that is both transparent and controllable.

Summary of knowledge points in this section

Streaming
  • stream_mode="updates"— node increment
  • stream_mode="values"— Complete State
  • astream_events()— Token level
  • • API exclusive"messages"mode
Interruption
  • • needCheckpointercan be used
  • interrupt_before— Pause before node execution
  • interrupt_after— Pause after node execution
  • get_state()— View the paused State
  • invoke(None, config)— Resume execution
key methods
  • .stream(input, config, stream_mode=)
  • .astream_events(input, config, version=)
  • .get_state(config)
  • .update_state(config, values, as_node=)
Human-in-the-loop mode
  • • Observe the Agent’s decision-making process in real time
  • • Mandatory manual verification before hazardous operations
  • • Allows manual revision of the Agent's plan
  • • The same thread_id ensures state continuity
Module 3 Learning Route
Lesson 1 (this section) Streaming streaming output + interrupt mechanism basics
Lesson 2 Breakpoints Breakpoints: In-depth use of static breakpoints
Lesson 3 Edit State + Human Feedback: Human editing state
Lesson 4 Dynamic Breakpoints: dynamic breakpoints
Lesson 5 Time Travel: Time Travel and State Replay