From streaming output to time travel, Module 3 has built a complete human-machine collaboration and controllable AI execution system.
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.
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.
NoneAs input, from breakpointResume executioninvoke(None)inNoneIt means "without adding new input, just continue with the saved state".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.
add_messagescrucial)update_stateWhen, the incoming message with the same ID willcoverOriginal message, not appendedas_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.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.
value(String or Dictionary) that can be used to describe the reason for the interruptioninterrupt(), otherwise the node executes normallyinvoke(None)meetingRetriggersame interrupt; the correct approach is to firstupdate_stateWrite processing results and then resumeLangGraph'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.
checkpoint_idconfigpass toinvoke(), follow the same path againupdate_state(past_config, new_values)Modify the historical status, and theninvoke()Open a new execution branchIf 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.
Set up all high-risk operations (sending emails, writing databases, calling paid APIs)interrupt_before, is the simplest security guarantee. cooperateMemorySaverto work properly.
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.
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".
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.
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.
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.
# ── 依赖导入 ──────────────────────────────────────────────
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]}
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.
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.
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 打印
"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 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.
# ════════════════════════════════════════════════
# 【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)
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.
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.
# ════════════════════════════════════════════════
# 【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) # 现在可以正常继续
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.
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.
# ════════════════════════════════════════════════
# 【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
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.
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.