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 LoopModule Summary
LANGGRAPH MODULE 3 · SUMMARY

Human in the Loop
Module Summary

From streaming output to time travel, Module 3 has built a complete human-machine collaboration and controllable AI execution system.

5sub-course
3core mechanism
2Breakpoint type
Retrospective checkpoints
Module learning path
Streaming output
L1 · stream()
updates / values / events
static breakpoint
L2 · interrupt_before
get_state · invoke(None)
Status edit
L3 · update_state
as_node · Manual injection
dynamic interrupt
L4 · interrupt()
Conditional triggering · Runtime judgment
time travel
L5 · get_state_history
Replay · Fork
Three major knowledge areas
Streaming and Observability
stream_mode="updates": Output increment after each node is executed
stream_mode="values": Output a complete State snapshot at each step
astream_events: Token level fine-grained output
messages mode: Streaming format designed specifically for chat UI
invoke and stream: Two calling methods: blocking vs real-time
Human-machine collaborative control
interrupt_before / after: Static breakpoint fixed at compile time
graph.get_state(): View the current State at the breakpoint
graph.invoke(None):Resume execution from breakpoint
update_state() + as_node: Directly modify the State field
interrupt(): Runtime conditional dynamic interrupt
Time travel debugging
Checkpointer: Automatically save State snapshot at each step
get_state_history(): Traverse all checkpoints in history
checkpoint_id: Uniquely identifies each historical moment
Replay: Re-execute the same path from a historical checkpoint
Fork: Open a new execution branch after modifying the historical status
Detailed explanation of 5 sub-courses
1

Streaming & Interruption

stream() · astream_events · interrupt_before/after

defaultinvoke()It's blocking - it doesn't return until the graph is finished. Streaming output lets you execute on graphsevery stepAll can obtain data in real time, which is the basis of human-in-the-loop perception.

  • stream_mode="updates": After each node is executed, immediately spit out the node's response to State.incremental update, suitable for monitoring node progress
  • stream_mode="values": The output graph of each stepComplete State, suitable for debugging scenarios that require a full picture
  • astream_events:Token granular streaming, every time LLM generates a token, it will be pushed immediately to achieve the "typewriter" effect.
  • interrupt_before/after: Declared at compile time to allow the graph to automatically pause before/after the specified node and wait for external instructions.
Core insights:Streaming output is not just "good looking", it is a prerequisite for realizing Human-in-the-loop - you must be able toReal time observationDo what you are doing to intervene at the right time.
In-depth explanation →
2

Breakpoints

compile(interrupt_before) · get_state · invoke(None)

Breakpoint (Breakpoint) is atcompile timeFixed pause position, every time execution reaches this nodeunconditionallyInterrupt and wait for external decisions before continuing - this is the most direct way of human-machine collaboration.

  • compile(interrupt_before=["node"]): Execute on nodebeforePause, suitable for "approval before execution" scenario
  • compile(interrupt_after=["node"]): Execute on nodeafterPause, suitable for "check the results before deciding" scenario
  • graph.get_state(config): After the breakpoint is triggered, query the snapshot of the current State to understand where the graph "stopped"
  • graph.invoke(None, config): Pass inNoneAs input, from breakpointResume execution
  • MemorySaver Requirements: Breakpoints must be used with checkpointer, otherwise the pause state cannot be saved.
Core insights:Breakpoints turn the graph into a "state machine waiting for instructions".invoke(None)inNoneIt means "without adding new input, just continue with the saved state".
In-depth explanation →
3

Edit State & Human Feedback

update_state · as_node · human_feedback node

Breakpoints can only "approve or deny", whereasupdate_state()allows you toDirectly modify the internal state of the graph— Correct erroneous data, inject human input, and then continue execution with the new state.

  • graph.update_state(config, values): Write new values to the current State, similar to the returned dictionary of nodes
  • as_node parameter: Specify which node's output this update is "disguised as", affecting how reducer is applied (especially foradd_messagescrucial)
  • Message coverage mechanism:Use for message fieldsupdate_stateWhen, the incoming message with the same ID willcoverOriginal message, not appended
  • human_feedback node: Specialized node function, which "takes place" in the graph process to wait for manual input, and injects manual data during recovery
Core insights:as_nodeSolved a subtle problem: LangGraph needs to know "which node performed this State update" in order to correctly calculate which edge should be taken next.
In-depth explanation →
4

Dynamic Breakpoints

interrupt() · NodeInterrupt · Conditional interrupt

Static breakpoints pause unconditionally, while dynamic breakpoints allowNode internal codeIt decides whether to trigger an interrupt based on the runtime data, and can carry customized interrupt cause information to the outside.

  • from langgraph.types import interrupt: The core function of dynamic breakpoint, called inside the node
  • interrupt(value): Trigger interrupt and pass it outvalue(String or Dictionary) that can be used to describe the reason for the interruption
  • Conditional trigger: Called only when certain conditions are met (such as risk score exceeding the threshold)interrupt(), otherwise the node executes normally
  • Pitfalls during recovery: directinvoke(None)meetingRetriggersame interrupt; the correct approach is to firstupdate_stateWrite processing results and then resume
Core insights:Dynamic breakpoints leave the judgment of "whether manual intervention is needed" to the data. Not all tool calls require approval—only those that are truly high risk. This is the design idea of ​​production-level Agent.
In-depth explanation →
5

Time Travel

get_state_history · checkpoint_id · Replay · Fork

LangGraph's checkpointer automatically saves a snapshot after each node is executed. Time travel allows you to go back to any historical moment——replayoriginal path orbifurcationTowards a new branch.

  • graph.get_state_history(config): Returns a generator of all historical checkpoints, each containing the complete State andcheckpoint_id
  • Replay: Move past a certain checkpointconfigpass toinvoke(), follow the same path again
  • Fork: firstupdate_state(past_config, new_values)Modify the historical status, and theninvoke()Open a new execution branch
  • Analogy Git: Checkpoint ≈ commit, replay ≈ checkout to view history, fork ≈ create a new branch from historical commit
Core insights:Time travel changes "debugging" from passive to active - you can no longer just "look at the error log", but you can go back directly to the moment of the error, change the data, and then observe whether the results meet expectations.
In-depth explanation →
Quick review of core concepts
conceptCore APIProblem solvedTypical scenario
stream updatesgraph.stream(..., stream_mode="updates")Node granular real-time output incrementMonitor the execution progress of multi-step Agent
stream valuesgraph.stream(..., stream_mode="values")Output a complete State snapshot at each stepDebugging status change process
astream_eventsgraph.astream_events(..., version="v2")Token level fine-grained streamingChat interface "typewriter" effect
static breakpointcompile(interrupt_before=["node"])Unconditionally pause before nodeUnified approval for all tool calls
get_stategraph.get_state(config)Query the current State at the breakpointAfter the breakpoint is triggered, check the data before making a decision
Resume executiongraph.invoke(None, config)Resume execution from breakpointRelease after manual approval
update_stategraph.update_state(config, values)Directly modify the internal state of the graphCorrect AI intermediate results before continuing
as_nodeupdate_state(..., as_node="node_name")Declare update source nodeWhen the message field uses add_messages
dynamic breakpointfrom langgraph.types import interruptConditionally triggered interrupts at runtimeApproval is required only if the amount exceeds the threshold
historical checkpointgraph.get_state_history(config)Traverse all historical execution snapshotsBacktracking Agent decision-making links
Replay Replayinvoke(None, past_checkpoint_config)Taking the same path again from a certain point in historyVerify behavioral reproducibility
Forkupdate_state(past_cfg) + invokeModify the historical status and open a new execution branchA/B testing the results of different inputs
Module core harvest
📡

Streaming is the eyes of HiL

If you can't observe what the graph is doing in real time, you won't be able to intervene at the right time.stream_mode="updates"It is the lightest way,astream_eventsIt is the most fine-grained method and can be used as needed.

🔒

Static breakpoints are safety valves

Set up all high-risk operations (sending emails, writing databases, calling paid APIs)interrupt_before, is the simplest security guarantee. cooperateMemorySaverto work properly.

🔧

update_state is a surgical scalpel

Modify a single field accurately without affecting other states.as_nodeThe parameter allows LangGraph to correctly calculate the next route after the update, so don’t forget it.

Dynamic breakpoints are more accurate

Let the data decide whether to pause, not the code.interrupt(value)It can also carry the reason for the interruption to let the external system know "why manual intervention is needed".

Checkpoints are time machines

As long as checkpointer is mounted, each node is an automatic save point. Time travel makes "go back and retry" from impossible to a line of code. This is one of the core advantages of LangGraph that distinguishes it from ordinary Agent frameworks.

🌿

Fork is a parallel universe

Forked execution creates an independent branch of execution history, and the original thread is not affected. This is a powerful tool for experimenting with different strategies, conducting A/B testing, and recovering from errors.

Return to Module 3 Home Page
Comprehensive practical projects
Project

Smart Contract Approval Assistant
Smart Contract Approval Agent

A contract processing workflow that integrates streaming progress display, manual approval, status editing, dynamic risk interruption and historical review. All five core knowledge points of Module 3 are organically integrated to simulate real enterprise-level approval scenarios.

L1 Streaming L2 Breakpoints L3 update_state L4 interrupt() L5 Time Travel
Project characteristics
Streaming display of progress of each approval step
Forced to wait for manual confirmation before submission
You can manually modify the contract terms before continuing.
High-risk clauses automatically trigger breaks
Supports backtracking to any historical approval version
Workflow architecture diagram
START → analyze_document node
LLM parses contract structure + extracts key terms
risk_assessment node· L4
Calculate risk score → called when score > 0.7interrupt()dynamic interrupt
⏸ Static breakpoint interrupt_before=["submit"]· L2
Pause before each submission → wait for manual confirmation orupdate_state()Modify terms· L3
submit_contract node → END· L5 traceable
Automatically save Checkpoint at each step → Support Replay / Fork time travel
The whole process stream_mode="updates" displays the progress in real time · L1
Code block 1 / 5 State definition and graph structure - L1 + L2
# ── 依赖导入 ──────────────────────────────────────────────
from typing import Literal, Optional
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt   # 【L4】动态断点函数

# ════════════════════════════════════════════════
# State:继承 MessagesState,扩展合同专用字段
# ════════════════════════════════════════════════
class ContractState(MessagesState):
    contract_text: str       # 合同全文
    risk_score:    float     # 风险分数 0.0-1.0
    key_clauses:   list[str] # 提取的关键条款
    decision:      str       # 审批决定:pending/approved/rejected

llm    = ChatOpenAI(model="gpt-4o-mini")
memory = MemorySaver()   # 【L2/L5】Checkpointer:断点+时间旅行的必要前提

# ── 节点函数 ──────────────────────────────────────────────
def analyze_document(state: ContractState):
    """解析合同,提取结构与关键条款"""
    response = llm.invoke([
        SystemMessage(content="你是一个专业的合同分析师。提取合同中的关键条款和风险点。"),
        HumanMessage(content=f"请分析以下合同:\n{state['contract_text']}"),
    ])
    clauses = response.content.split("\n")[:5]
    return {"key_clauses": clauses, "messages": [response]}
L1 · The basis of streaming: State field

Each chunk of streaming output contains the node's incremental update to State.Changes to each field in ContractStatewill passstream_mode="updates"Real-time push allows the outside world to see when the contract text is written and when the risk score is calculated.

L2 · MemorySaver is the prerequisite for breakpoints

After the breakpoint is triggered, the execution status of the graph must be saved., waiting for recovery before continuing from the breakpoint.MemorySaverIt's this state store - without it,interrupt_beforeAn error will be reported. It is also the data source for L5 time travel.

Code block 2 / 5 Streaming progress monitoring - L1
import asyncio

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

# ── 【L1】stream_mode="updates":节点粒度实时输出 ────────
# 每个节点执行完后,立即推送该节点对 State 的增量变化
# 不需要等整个图跑完,用户能实时看到每一步的进展
for chunk in graph.stream(
    {"contract_text": contract_text},
    config=config,
    stream_mode="updates",
):
    node_name = list(chunk.keys())[0]
    print(f"[{node_name}] 执行完成 → {list(chunk[node_name].keys())}")

# 输出示例:
# [analyze_document] 执行完成 → ['key_clauses', 'messages']
# ⏸ 图暂停(interrupt_before="submit_contract")

# ── 【L1】astream_events:Token 级别流式输出 ─────────────
# 捕获 LLM 每生成一个 token 就立刻推送的事件
# 适合聊天界面需要"打字机"效果的场景
async def stream_tokens():
    async for event in graph.astream_events(
        {"contract_text": contract_text},
        config=config,
        version="v2",
    ):
        if event["event"] == "on_chat_model_stream":
            token = event["data"]["chunk"].content
            print(token, end="", flush=True)  # 逐 token 打印
Comparison of three values of stream_mode

"updates": Incremental dictionary, minimizing the amount of data,Production first choice"values": Complete State, convenient for viewing the full picture during debugging."messages": Only push the increment of the message list, optimized for chat scenarios. The performance overhead of the three increases in sequence.

Event type of astream_events

Event names followon_<component>_<lifecycle>format.on_chat_model_streamIs LLM streaming token;on_tool_start/endIt is a tool call;on_chain_start/endis the node life cycle. useevent["name"]Filter events for a specific node.

Code Block 3 / 5 Static breakpoints and status editing - L2 + L3
# ════════════════════════════════════════════════
# 【L2】静态断点:编译期固定,每次提交前无条件暂停
#   · interrupt_before=["submit_contract"]
#   · 必须挂载 checkpointer,否则断点无法保存状态
# ════════════════════════════════════════════════
graph = builder.compile(
    checkpointer=memory,
    interrupt_before=["submit_contract"],  # 在 submit 节点执行前暂停
)

# ── 第一次运行:触发断点后图自动暂停 ──
graph.invoke(
    {"contract_text": contract_text},
    config=config,
)

# ── 【L2】断点触发后:检查当前 State ──
snapshot = graph.get_state(config)
print("当前 State:", snapshot.values)
print("下一步节点:", snapshot.next)   # ('submit_contract',)

# ── 【L3】用 update_state 修改合同条款(人工审核后纠错)──
# 相当于人类审核员直接在 State 上"划改"了一条条款
# as_node 告诉 LangGraph 这次更新来自哪个节点(影响路由)
graph.update_state(
    config,
    {"contract_text": "[已修正] 合同总金额:100万元(原:150万元)..."},
    as_node="analyze_document",   # 声明此更新来源于 analyze_document 节点
)

# ── 【L2】恢复执行:传入 None 作为新输入 ──
# None 意味着"不增加新输入,使用已保存的 State 继续"
# 图会从断点处(submit_contract 节点)继续往下执行
final_state = graph.invoke(None, config)
L2 · The semantics of invoke(None)

graph.invoke(None, config)inNonemeans "no new user input". When LangGraph sees this signal, it will load the latest snapshot in checkpointer and continue execution from the next node at the breakpoint.This is the standard posture for recovery execution.

L3 · as_node Why is it important?

LangGraph uses node names to determine which edge to take after execution. when youupdate_stateAt this time, the graph must be told "which node this update is equivalent to has just been executed", otherwise the graph does not know where to go next. Use for message fieldsadd_messagesreducer,as_nodeAlso affects the append vs overwrite behavior of messages.

Code block 4 / 5 Dynamic interrupt node - L4
# ════════════════════════════════════════════════
# 【L4】动态断点:运行时根据条件决定是否触发中断
#   · 使用 interrupt() 函数,在节点内部条件性调用
#   · 可携带任意信息作为中断原因传给外部审核人员
# ════════════════════════════════════════════════
def risk_assessment(state: ContractState):
    """计算风险分数,高风险时动态触发中断"""
    # 简化的风险评分(生产中用专门的风险模型)
    text  = state["contract_text"]
    score = 0.9 if "担保" in text or "不可抗力" in text else 0.3

    # ── 【L4】只有高风险(score > 0.7)才触发中断 ──
    # interrupt() 调用后,节点执行立即挂起
    # 传入的字典会被存入检查点,外部可通过 get_state() 读取
    if score > 0.7:
        human_decision = interrupt({
            "reason":           "检测到高风险条款,需要法律顾问审核",
            "risk_score":        score,
            "flagged_keywords":  ["担保", "不可抗力"],
            "contract_preview":  text[:300],
        })
        # interrupt() 返回值是外部通过 update_state 注入的人工决定
        return {"risk_score": score, "decision": human_decision}

    # 低风险:直接通过,不打扰审批人员
    return {"risk_score": score, "decision": "auto_approved"}


# ── 外部代码处理动态中断 ──────────────────────────────────
graph.invoke({"contract_text": risky_contract}, config=config)

# 读取中断原因
snapshot = graph.get_state(config)
interrupt_info = snapshot.tasks[0].interrupts[0].value
print(f"中断原因:{interrupt_info['reason']}")
print(f"风险分数:{interrupt_info['risk_score']}")

# ── 【L4】正确的恢复方式:先写入审批结果,再 invoke ──
# 直接 invoke(None) 会重新执行 risk_assessment 节点,再次触发 interrupt
# 正确做法:先用 update_state 向 State 写入人工决定,再恢复
graph.update_state(config, {"decision": "approved_with_amendments"})
graph.invoke(None, config)  # 现在可以正常继续
L4 · The core difference between dynamic vs static breakpoints

Static breakpoint (L2):every timeEverything passing through this node is paused, regardless of data content. Dynamic breakpoint (L4): only ifruntime conditionsIt was suspended and low-risk contracts were approved directly without disturbing the approving personnel at all. This makes the Agent system smarter and approval efficiency higher.

L4 · Traps of restoring dynamic breakpoints

After the dynamic breakpoint is triggered, the node is in the "hang during execution" state. If directlyinvoke(None), LangGraph willRe-execute the node, met againinterrupt(), infinite loop. Correct approach: use firstupdate_stateWrite a value to let the node "perceive" that the manual decision has been reached and then recover.

Code Blocks 5 / 5 Time Travel: Replays and Forks - L5
# ════════════════════════════════════════════════
# 【L5】时间旅行:浏览 + 重放 + 分叉
#   · 每个节点执行后 Checkpointer 自动保存快照
#   · 所有快照构成一条时间线,每条有唯一 checkpoint_id
# ════════════════════════════════════════════════

# ── 浏览完整执行历史 ──────────────────────────────────────
history = list(graph.get_state_history(config))  # 最新在前
for i, checkpoint in enumerate(history):
    ckpt_id  = checkpoint.config["configurable"]["checkpoint_id"]
    decision = checkpoint.values.get("decision", "pending")
    risk     = checkpoint.values.get("risk_score", "N/A")
    print(f"[{i}] checkpoint_id={ckpt_id[:12]}... decision={decision} risk={risk}")

# ── Replay(重放):从某个历史检查点重新走相同路径 ─────────
# 取 analyze_document 节点刚执行完的那个快照
analyze_done_ckpt = history[-2]  # 倒数第2个 = 分析完成时的快照

# 传入历史快照的 config(包含 checkpoint_id)来重放
replayed = graph.invoke(None, analyze_done_ckpt.config)
# 结果与历史一致,因为 State 数据相同,图的决策路径也相同

# ── Fork(分叉):修改历史状态后开启新的执行分支 ───────────
# 场景:回到"分析完成"那个时刻,改变合同文本,看结果如何变化

# Step 1:找到目标历史检查点
target = analyze_done_ckpt

# Step 2:在历史快照上执行 update_state,修改合同内容
# 这会创建一个新检查点(分叉点),原来的历史时间线不受影响
forked_config = graph.update_state(
    target.config,
    {"contract_text": "[修改版] 已去除担保条款的低风险合同..."},
    as_node="analyze_document",
)

# Step 3:从分叉点继续执行,走向全新的执行分支
forked_result = graph.invoke(None, forked_config)
print(f"分叉后风险分数:{forked_result['risk_score']}")  # 预计 0.3(低风险)
print(f"分叉后决定:{forked_result['decision']}")         # auto_approved
L5 · The essential difference between Replay vs Fork

Replay: Do not modify the historical State and re-execute directly from a certain checkpoint. The result is the same as history (under deterministic Agent). Used to verify behavior and debug occasional problems.
Fork: Modify the historical State first and then execute. generate aBrand new execution branch, does not affect the original timeline. Used for debugging and A/B testing.

L5 · The role of update_state in Fork

The key to Fork isupdate_state(historical_config, ...)Return anew config(Contains new checkpoint_id). This new config represents the fork point, and subsequent calls to itinvoke(None)Will continue from the fork point, not from the original historical checkpoint. Git analogy: this isgit checkout <old-commit> + git commit, a new branch is created.

Overview of correspondence between knowledge points
Exact mapping of project parts to the 5 sub-courses
Project componentsCorresponding coursesCore APIProblem solved
stream_mode="updates" progress monitoring L1 · Streaming graph.stream(..., stream_mode="updates") See in real time when each node is completed, and approvers understand the progress
astream_events Token stream L1 · Streaming graph.astream_events(..., version="v2") When LLM analyzes the contract, it is displayed token by token to reduce the sense of waiting.
interrupt_before=["submit"] L2 · Breakpoints compile(interrupt_before=[...]) Unconditionally pause before each submission and wait for manual final confirmation
graph.get_state + invoke(None) L2 · Breakpoints get_state(config) / invoke(None, config) Check the data after the breakpoint, and then release it to continue execution after confirming it is correct.
update_state Modify contract terms L3 · Edit State graph.update_state(config, values, as_node=...) If an error is found during the review, change the State directly and continue without resubmitting.
interrupt() in risk_assessment L4 · Dynamic Breakpoints from langgraph.types import interrupt Only when the risk score exceeds the threshold will it be suspended, and low risk will automatically pass
get_state_history historical browsing L5 · Time Travel graph.get_state_history(config) Trace back the complete approval history and view the State snapshot of each node
Replay replay verification L5 · Time Travel invoke(None, historical_config) Re-execute from a certain point in history to verify the reproducibility of approval decisions
Fork test L5 · Time Travel update_state(historical_config) + invoke Modify historical contract content and test the approval results of different versions
Return to Module 3 Home Page