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 & Interruption in LangGraph

如何在图执行过程中实时获取输出?如何让图"暂停等待人工确认"?
本节是 Human-in-the-loop 的基础,彻底搞懂 Streaming 与 Interrupt。

1 为什么需要流式输出:从"等待结果"到"实时观察"

默认情况下,调用 graph.invoke()阻塞式的:你等图完整跑完,才能拿到最终 State。对于简单任务这没问题,但对于:

LangGraph 提供了一流的流式支持(First-class Streaming),让你可以在图执行过程中实时获取各种粒度的输出。本节的终极目标是理解 Streaming 是如何为 Module 3 的核心特性——Human-in-the-loop——铺路的。

核心要点

LangGraph 的流式输出有两个维度:一是节点粒度(每个节点执行完后吐出一次数据),二是Token 粒度(LLM 生成每个 token 时立刻传出来)。两者都很重要,用途不同。

流式输出 vs 批量输出的对比

批量模式 invoke()
Node A 执行
↓(等待)
Node B 执行
↓(等待)
最终结果一次性返回
流式模式 stream()
Node A 执行 → 立刻输出 chunk
↓(实时)
Node B 执行 → 立刻输出 chunk
↓(实时)
持续推送,直到图执行完

2 图的结构回顾:本节使用的 Chatbot 示例

本节 notebook 复用了 Module 2 的带摘要的 Chatbot 图。先理解这张图的结构,才能看懂后面的流式输出。

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 图结构

START
conversation
call_model — 调用 LLM
should_continue() 路由判断
消息 ≤ 6 条
END
消息 > 6 条
summarize_conversation
压缩历史,删除旧消息
END
关于 RunnableConfig

call_model 函数接受第二个参数 config: RunnableConfig,并将其传入 model.invoke(messages, config)。这对于在 Python 3.10 及以下版本中启用 Token 级流式输出是必需的。config 携带了流式回调信息,使 LangGraph 能够在 token 生成时立刻捕获并发出事件。

3 stream_mode="updates":只看每个节点的增量变化

stream_mode="updates" 是最常用的流式模式。图每执行完一个节点,就会推送一个 chunk,chunk 里只包含这个节点对 State 做出的变化部分,而不是完整 State。

基本用法

# 创建一个对话线程(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)
输出(每个 chunk 就是一个节点的更新)
{'conversation': {'messages': [AIMessage(content='Hi Lance! How\'s it going? What can I do for you today?', ...)]}}

chunk 的结构是一个字典,键是节点名称值是该节点返回的更新内容。比如上面的 conversation 节点返回了 messages 字段的更新,里面包含 LLM 的回复。

提取并格式化 LLM 回复

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

Hi Lance! How's it going? What can I do for you today?
注意:chunk 的键对应节点名

如果图中有多个节点同时执行(并行情况),你会收到多个 chunk,每个 chunk 的键对应各自的节点名。在访问 chunk 里的数据时,要先通过节点名找到对应的更新字典,再取你需要的字段。

updates 模式的数据流图示

stream_mode="updates" — 增量更新模式

图执行一个节点 → 推送该节点的状态变更 dict → 图继续执行下一个节点 → 再次推送...

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

4 stream_mode="values":每步输出图的完整状态

stream_mode="values""updates" 的区别在于:每次推送的不是某个节点的变更,而是当前整张图的完整 State

代码示例

# 使用新的 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)  # 分隔符,区分不同节点执行后的状态
输出(两次推送:输入时一次,LLM 回复后一次)
================================ Human Message =================================

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

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

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

注意:第一次推送发生在图接收到输入之后、第一个节点执行之前(状态里只有 Human Message);第二次推送发生在 conversation 节点执行完之后(状态里有 Human + AI Message)。

updates vs values 的关键区别

维度 stream_mode="updates" stream_mode="values"
每个 chunk 的内容 只包含该节点修改的字段(增量) 包含整个 State 的所有字段(全量)
chunk 结构 {node_name: {changed_fields}} {all_state_fields}
推送时机 每个节点执行完毕后 每个节点执行完毕后(也包括初始输入时)
典型用途 只关心某节点的输出,比如 LLM 回复 需要看完整历史,比如调试、观察所有消息
数据量 较小(只有变化部分) 较大(完整状态,随对话增长)
如何选择?

生产应用中,updates 模式更常用:你可以精确指定关注哪个节点的输出,数据量小,处理更高效。values 模式更适合调试和监控,可以完整观察每一步后图的全局状态变化。

5 astream_events:Token 级别的细粒度流式输出

前面两种模式(updates / values)都是节点粒度的流式——必须等节点执行完才推送。但聊天应用往往需要 LLM 生成每个 token 时就立刻显示给用户,这就需要 astream_events

astream_events 的工作原理

graph.astream_events() 是一个异步方法(需要 async for),它在节点内部执行时同步触发各类事件,你可以根据事件类型过滤出你关心的信息。

第一步:观察所有事件类型

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']}")

每个 event 字典包含以下关键字段:

字段 说明 示例值
event 事件类型 "on_chat_model_stream", "on_chain_start"
name 事件名称 "ChatDeepSeek", "conversation"
data 事件携带的数据 包含 chunk 字段(AIMessageChunk)
metadata.langgraph_node 触发该事件的 LangGraph 节点名 "conversation"

第二步:过滤出特定节点的 Token 流

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
输出(每个 | 之间就是一个 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| ...||||
关键理解:on_chat_model_stream

事件类型 "on_chat_model_stream" 是 Token 级流式的核心。每当聊天模型生成一个 token,就会触发一次这个事件,event["data"]["chunk"] 就是对应的 AIMessageChunk。通过 metadata["langgraph_node"] 可以精确知道这个 token 来自哪个节点,从而过滤出你关心的输出。

完整的 Token 流式打印代码

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="|")

三种流式方法汇总对比

方法 粒度 是否异步 典型用途
.stream(stream_mode="updates") 节点级(增量) 同步 观察每个节点的输出变化
.stream(stream_mode="values") 节点级(全量) 同步 调试、观察完整 State
.astream_events() Token 级(最细) 异步 实时打字机效果、精细监控

6 LangGraph API 的 messages 模式:专为聊天设计

当你通过 LangGraph API(LangSmith Studio 或部署后的服务) 远程调用图时,除了 valuesupdates 之外,还有一种仅在 API 层面支持的特殊模式:stream_mode="messages"

前提条件

使用 LangGraph API 需要先启动本地开发服务器(在 /studio 目录运行 langgraph dev),然后通过 langgraph_sdk 连接。本节的 API 示例假设服务已在 http://127.0.0.1:2024 运行。

通过 API 使用 values 模式

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)

API 返回的 event 对象结构:event.event(类型字符串)和 event.data(数据内容)。

messages 模式:细粒度事件流

stream_mode="messages" 专门为消息型 LangGraph(state 里有 messages 字段)设计,会推送四类事件:

事件类型 说明
metadata 本次运行的元信息,包含 run_id
messages/partial 消息的部分内容(token 级流式),逐步积累直到完整
messages/complete 完整的已形成消息(非流式内容,如 tool call 结果)
messages/metadata 消息级别的元信息
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)  # 查看事件类型
输出(每行一个事件类型)
metadata
messages/metadata
messages/partial
messages/partial
... (多个 partial,对应工具调用参数逐渐积累)
messages/metadata
messages/complete
messages/partial
messages/partial
... (最终回复的 token 流)

完整的 messages 模式处理代码

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))
messages 模式的独特价值

messages 模式的最大优势是对工具调用(Tool Call)的流式支持:你可以看到工具的参数是如何被 LLM 逐步"写出来"的,例如 {'a': 2}{'a': 2, 'b': 3},这对于调试 Agent 的工具调用行为非常有帮助。

7 中断机制:interrupt_before 与 interrupt_after

流式输出让你观察图的执行;而中断机制(Interruption)则让你控制图的执行——在特定节点前后暂停图,等待外部(通常是人)做出决策,再恢复执行。这正是 Human-in-the-loop 的核心机制。

前提:必须使用 Checkpointer

中断机制依赖 Checkpointer(检查点机制)。图每执行一个节点,就把当前状态存档到 Checkpointer(如 MemorySaver 或数据库)。图被中断后,状态被冻结存档;当你恢复时,图从存档点继续,不需要从头运行。没有 Checkpointer 就无法使用中断功能。

在 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_beforeinterrupt_after 都接受一个节点名列表,可以同时声明多个中断点:

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

interrupt_before vs interrupt_after 的区别

参数 暂停时机 此时 State 的状态 典型用途
interrupt_before=["X"] X 节点执行之前 X 节点尚未修改 State 在危险操作前让人审批(如发邮件、执行代码)
interrupt_after=["X"] X 节点执行之后 X 节点已经更新 State 让人审查 LLM 的输出,决定是否继续

中断后的图状态

图被中断后,你可以查看当前 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 字典
输出
('tools',)
{'messages': [HumanMessage(content='search for the latest AI news'), AIMessage(content='', tool_calls=[{'name': 'tavily_search', 'args': {'query': 'latest AI news 2024'}, ...}])]}

snapshot.next 返回一个元组,表示图暂停时,下一步待执行的节点名称。如果图已经执行完毕(正常结束),snapshot.next 为空元组 ()

interrupt_before=["tools"] 的执行流程

START
agent (LLM)
正常执行,LLM 决定调用工具
INTERRUPTED — 等待人工确认
人工审查工具调用,决定是否继续
↓ .invoke(None, config) 恢复
tools
执行搜索/计算工具
END

8 Human-in-the-loop 完整模式:暂停、审批、恢复

掌握了中断机制后,我们来看完整的 Human-in-the-loop 工作流:图暂停 → 人工检查/修改 State → 恢复执行

第一步:恢复执行(不修改 State)

如果人工审查后认为 LLM 的决策正确,直接让图继续:

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

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

传入 None 告诉 LangGraph:不要重新输入新的初始状态,而是从 Checkpointer 中加载上次暂停时存档的状态,从中断点继续执行。thread_id 是找到正确存档点的唯一标识。

第二步:修改 State 后再恢复(人工干预)

这是 Human-in-the-loop 的精髓:人工修改图的 State,然后恢复执行:

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

完整的 Human-in-the-loop 生命周期

1. 触发图执行

用户输入问题,图开始运行。LLM 分析问题,决定需要调用工具(如搜索、执行代码)。

2. 到达中断点,图暂停

在 tools 节点执行前(interrupt_before=["tools"]),图自动暂停,将当前 State 存档到 Checkpointer。snapshot.next = ('tools',)

3. 人工审查

人工通过 graph.get_state(config) 查看当前状态:LLM 想做什么(工具调用参数)、当前对话历史等。

4a. 拒绝 / 修改

如果人工不满意,使用 graph.update_state() 修改 State,例如更正工具调用参数、注入额外上下文信息,然后恢复执行。

4b. 批准并继续

如果人工满意,调用 graph.invoke(None, config),图从中断处继续执行,完成工具调用并返回最终结果。

典型应用场景

代码执行 Agent

LLM 生成代码后,在执行前暂停,让开发者审查代码内容,确认没有危险操作后才允许运行。

邮件发送 Agent

Agent 起草完邮件后暂停,人工检查收件人、正文是否正确,确认无误后才真正发送。

数据库写操作

在执行 INSERT/UPDATE/DELETE 前中断,展示即将执行的 SQL,人工确认后才执行,防止误操作。

内容审核 Agent

AI 生成内容后暂停,编辑人员审核是否符合规范,修改或批准后再发布到平台。

流式输出与中断机制的联系

流式输出和中断机制共同构成了 Human-in-the-loop 的基础设施:流式输出让人实时看到 Agent 的"思考过程",中断机制让人在关键节点干预 Agent 的"行动"。两者结合,就是一个既透明又可控的 AI Agent 系统。

本节知识点总结

流式输出(Streaming)
  • stream_mode="updates" — 节点增量
  • stream_mode="values" — 完整 State
  • astream_events() — Token 级
  • • API 专属 "messages" 模式
中断机制(Interruption)
  • • 需要 Checkpointer 才能使用
  • interrupt_before — 节点执行前暂停
  • interrupt_after — 节点执行后暂停
  • get_state() — 查看暂停时的 State
  • invoke(None, config) — 恢复执行
关键方法
  • .stream(input, config, stream_mode=)
  • .astream_events(input, config, version=)
  • .get_state(config)
  • .update_state(config, values, as_node=)
Human-in-the-loop 模式
  • • 实时观察 Agent 的决策过程
  • • 在危险操作前强制人工确认
  • • 允许人工修正 Agent 的计划
  • • 同一 thread_id 保证状态连续性
Module 3 学习路线
Lesson 1(本节) Streaming 流式输出 + 中断机制基础
Lesson 2 Breakpoints 断点:静态中断点的深入使用
Lesson 3 Edit State + Human Feedback:人工编辑状态
Lesson 4 Dynamic Breakpoints:动态中断点
Lesson 5 Time Travel:时间旅行与状态回放