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

编辑图状态与注入人工反馈
update_state · as_node · Human Feedback

图执行中途暂停后,你不仅能"批准或拒绝",还能直接修改图的内部状态。
本课演示如何精准编辑 State,以及如何将人工输入无缝注入到 Agent 流程中。

1 回顾:人机协作的三种模式

在 Module 3 的前几课中,我们讨论了为什么需要在 AI Agent 流程中引入人的参与。LangGraph 的 human-in-the-loop 支持三种核心场景:

场景 英文术语 具体含义 本课涉及
批准 Approval 中断 Agent,让用户决定是否继续执行某个动作 已在 3-2 学习
调试 Debugging 回溯图的历史检查点,重现或规避问题 将在 3-5 学习
编辑 Editing 在断点处直接修改图的 State,然后恢复执行 本课重点

前一课学习的断点(Breakpoint)让我们能够暂停图的执行,但暂停之后只能选择"继续"或"终止"。而本课要解决的问题是:暂停之后,如何主动修改 State,然后带着修改后的状态继续执行?

核心问题

想象一个场景:Agent 收到了用户输入"帮我乘以 2 和 3",但在它真正运行之前,你发现用户其实想问"乘以 3 和 3"。断点让你能暂停,而 update_state 让你能在不重新触发整个对话的情况下,直接把那条消息改掉。

本课涉及的关键 API 是 graph.update_state(config, values),它是 LangGraph 提供的核心状态编辑接口,配合断点使用可以实现强大的人机协作交互。

2 graph.update_state() — 直接编辑图状态

搭建测试用的 Agent 图

Notebook 首先构建了一个带有断点的计算助手 Agent,使用 interrupt_before=["assistant"] 在助手节点之前暂停:

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)

触发断点:图在 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()
# 输出显示图已收到消息,正在等待 assistant 节点前的断点
================================ Human Message ================================
Multiply 2 and 3

此时图已暂停。我们可以检查当前状态:

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

核心 API:graph.update_state()

现在来看本课最关键的调用——update_state。它的完整签名如下:

graph.update_state(
    config,          # thread 配置,指向哪个 checkpoint(哪条线程)
    values,          # 要更新的 State 字段的字典
    as_node=None    # 可选:伪装成哪个节点执行了此更新(后面详述)
)

在 Notebook 中,我们向消息列表追加了一条新的人工消息,修正了计算需求:

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_state 返回一个新的 checkpoint 配置,说明状态已经被保存到一个新的检查点中。

返回值的含义

每次调用 update_state 都会创建一个新的检查点(checkpoint)。返回的配置中包含了这个新检查点的 ID。这意味着状态的修改本身也是可以被时间旅行功能追踪的——你可以看到状态是什么时候、被谁修改的。

3 add_messages Reducer 与消息覆盖机制

update_state 不是简单地"替换"整个 State,它遵循 State 中为每个字段定义的 Reducer 规则。理解这一点至关重要。

MessagesState 中的 Reducer

MessagesState 是 LangGraph 内置的状态类,其 messages 字段使用 add_messages 作为 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]

两种消息更新行为的对比

add_messages Reducer 的工作方式取决于你传入的消息是否携带 id

传入消息是否有 id Reducer 行为 效果 适用场景
无 id(新消息) 追加(Append) 新消息添加到 messages 列表末尾 注入人工反馈、添加上下文
有 id(已存在消息的 id) 覆盖(Overwrite) 找到 id 匹配的消息并替换其内容 修正用户输入、纠正 AI 输出

场景一:追加新消息(无 id)

Notebook 中的第一个 update_state 调用传入了一个没有 id 的新 HumanMessage,所以 add_messages 将它追加到了现有列表的末尾:

messages 更新前
messages: [
  "Multiply 2 and 3"
]
update_state 调用前
messages 更新后
messages: [
  "Multiply 2 and 3",
  "No, actually multiply 3 and 3!"
]
追加了新消息

验证结果:

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!

场景二:覆盖现有消息(带 id)

如果我们想替换而不是追加,只需要在传入的消息对象中保留原消息的 id。这样 add_messages 会找到具有该 id 的消息并覆盖它:

# 假设已有消息的 id 是 "msg-123"
# 传入同 id 的新消息 → 覆盖,而非追加
graph.update_state(
    thread,
    {"messages": [HumanMessage(
        content="No, actually multiply 3 and 3!",
        id="msg-123"   # 与原消息 id 相同 → 覆盖该消息
    )]},
)
关键区别

本课 Notebook 中 LangSmith Studio API 的例子里,last_message 是从状态中取出来的,它已经有 id,所以修改 content 后再传回去,触发的是覆盖逻辑;而第一个本地 Python 例子中,HumanMessage(...) 是新建的,没有 id,触发的是追加逻辑。两者都是常见用法,需要根据你的目的选择。

4 编辑状态后恢复图执行

修改状态只是第一步,完整的流程还需要让图从修改后的状态继续执行。恢复执行的方法非常简单:传入 None 作为输入再次调用 graph.stream()

传入 None 的含义

当你向 graph.stream(None, thread) 传入 None 时,LangGraph 不会创建新的输入,而是从当前线程的最新检查点(checkpoint)加载 State,并从下一个待执行的节点继续运行。这就是断点 + 状态编辑 + 恢复执行这个三段式工作流的核心机制。

完整的编辑-恢复流程代码

# ① 发送初始输入,图在 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

由于图仍然设置了 interrupt_before=["assistant"],它在工具结果返回后再次在 assistant 节点前暂停。传入 None 再次恢复:

# ④ 图再次在断点处停下,再次传 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.

带状态编辑的完整执行时序

START
断点触发:interrupt_before["assistant"]
图暂停,等待人工干预
graph.update_state() 注入新消息
graph.stream(None, thread) 恢复执行
assistant
LLM 决定调用 multiply(3,3)
tools
执行工具,返回 9
↓ (再次断点)
assistant
生成最终回复
END

5 as_node 参数:伪装成特定节点的更新

update_state 的第三个参数 as_node 是一个高级特性,也是实现"人工反馈节点"的关键。

as_node 解决了什么问题?

图在执行过程中会记录"上一个执行的节点是谁",因为边(Edge)的跳转逻辑依赖于此信息。当你直接调用 update_state 时,LangGraph 需要知道:这次更新算作"哪个节点"完成了执行?

通过 as_node 参数,你可以指定这次状态更新"假装"是某个节点完成的,从而让图按照正确的边的逻辑来决定下一步去哪里。

# 指定 as_node="human_feedback"
# 意思是:假装 human_feedback 节点刚刚执行完,并产生了这些状态更新
graph.update_state(
    thread,
    {"messages": user_input},
    as_node="human_feedback"   # 关键参数
)
为什么 as_node 很重要

假设图的边是 human_feedback → assistant。如果我们调用 update_state 时不指定 as_node,图不知道"当前处于 human_feedback 节点完成后的状态",就无法正确路由到 assistant。as_node="human_feedback" 告诉图:"把这次更新视为 human_feedback 节点的执行结果,下一步按照 human_feedback 的出边走。"

as_node 的工作原理图示

as_node="human_feedback" 的路由含义

不指定 as_node
图不知道当前
处于哪个节点之后
路由可能出错
指定 as_node="human_feedback"
图知道当前处于
human_feedback 之后
正确路由到 assistant

as_node 与图的 next 字段

调用 update_state(as_node="human_feedback") 后,图的 state.next 会变成 ('assistant',),因为从 human_feedback 节点之后,边直接连接到了 assistant

# 调用 update_state(as_node="human_feedback") 之后
state = graph.get_state(thread)
print(state.next)   # ('assistant',) — 下一步执行 assistant

6 添加 human_feedback 节点实现人工输入

前面的例子是在外部"悄悄修改"了图的消息。更优雅的设计是:在图结构中显式地加入一个代表人类反馈的节点,让人机协作成为图架构的一部分。

设计思路

新的图引入了一个 human_feedback 节点,它本身是一个空函数(no-op),不做任何实际处理。它的作用只是作为图中一个"占位符"——当图执行到这里时,通过 interrupt_before 暂停,等待人工调用 update_state(as_node="human_feedback") 来注入反馈内容,然后继续执行。

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

带 human_feedback 节点的图结构

START
interrupt_before
human_feedback
占位节点,等待人工注入
assistant
LLM 决策
tools_condition 路由
需要工具
tools
完成后
回到 human_feedback
直接回答
END

完整的人工反馈工作流代码

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

当用户输入 "no, multiply 3 and 3" 时,输出如下:

================================ 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
注意 user_input 的类型

input() 返回的是一个字符串(str),而不是 HumanMessage 对象。add_messages Reducer 足够智能,能够自动将字符串转换为 HumanMessage 对象并追加到消息列表中,因此这里不需要手动封装。

7 通过 LangSmith Studio API 远程编辑状态

除了在 Python 代码中直接调用 graph.update_state(),LangGraph 还提供了基于 HTTP 的 API(通过 langgraph_sdk 客户端),可以远程编辑运行在服务器上的图的状态。这是 LangSmith Studio 可视化界面背后的底层机制。

使用 LangGraph SDK 客户端

from langgraph_sdk import get_client

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

通过 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])
动态断点的优势

通过 API 调用时,你可以在不修改 agent 代码的情况下传入 interrupt_before,实现动态断点。这对于生产环境的 Agent 非常有用:同一个 Agent 在不同的使用场景下可以有不同的断点策略,而不需要重新部署代码。

通过 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}
)

注意 SDK 中的 API 是 client.threads.update_state(),对应本地的 graph.update_state(),功能完全相同,只是通过 HTTP 远程调用。

通过 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])
操作 本地 Python 调用 SDK 远程调用
获取状态 graph.get_state(thread) await client.threads.get_state(thread_id)
更新状态 graph.update_state(thread, values) await client.threads.update_state(thread_id, values)
恢复执行 graph.stream(None, thread) await client.runs.stream(thread_id, agent, input=None)

8 完整工作流总结与最佳实践

完整的人工编辑工作流

① 运行图直到断点

调用 graph.stream(initial_input, thread),图在设置了 interrupt_before 的节点之前暂停。此时可以安全地检查 State。

② 检查当前状态

调用 graph.get_state(thread),查看 state.values(当前数据)和 state.next(下一步要执行的节点)。

③ 按需编辑状态

调用 graph.update_state(thread, new_values, as_node=...)。可以追加新消息、覆盖现有消息,或修改任意 State 字段。

④ 恢复执行

调用 graph.stream(None, thread),图从最新的 checkpoint 继续,使用修改后的状态运行后续节点。

⑤ 重复直到完成

如果有多个断点,重复步骤 ②③④,直到图执行到 END 或不再触发断点。

关键概念速查

概念 说明 注意事项
update_state(thread, values) 直接修改指定线程的图状态,创建新检查点 遵循 State 中定义的 Reducer 规则
add_messages Reducer 处理 messages 字段的合并逻辑 无 id → 追加;有相同 id → 覆盖
as_node 参数 将状态更新伪装成特定节点的执行结果 影响图的路由决策(state.next)
human_feedback 节点 图中的占位节点,作为人工输入的"插槽" 函数体为 pass,内容由 update_state 注入
stream(None, thread) 从当前最新 checkpoint 恢复执行 传 None 而非新输入,避免重置状态
interrupt_before=[...] 在指定节点之前触发断点,暂停图执行 可在 compile() 中设置,也可在 API 调用时动态传入

何时选择哪种状态编辑方式

追加新消息(无 id)

  • • 注入额外的人工指令
  • • 添加背景信息或上下文
  • • 通过 human_feedback 节点输入意见
  • • 原有消息不变,新内容追加

覆盖现有消息(带 id)

  • • 纠正用户输入的错误信息
  • • 修正 AI 输出中的错误
  • • 通过 Studio UI 直接编辑消息
  • • 保持消息列表长度不变
设计模式总结

将 human_feedback 节点显式加入图结构是生产级 Human-in-the-loop 应用的推荐做法。相比直接从外部调用 update_state,它让人机协作的位置在图的架构中一目了然,也更易于测试和维护。当 tools → human_feedback 这条边存在时,每次工具调用之后都会先等待人工确认,再交给 assistant 节点处理——这是构建受控 Agent 系统的核心模式。

本课知识点全景

状态编辑 API
  • graph.update_state()
  • graph.get_state()
  • as_node 参数
  • stream(None, thread) 恢复
Reducer 机制
  • add_messages 处理 messages
  • • 无 id → 追加(Append)
  • • 有 id → 覆盖(Overwrite)
  • • 更新后生成新 checkpoint
图架构模式
  • human_feedback 占位节点
  • tools → human_feedback 循环
  • interrupt_before 触发暂停
  • • 显式声明人机协作位置
远程 API 对应
  • langgraph_sdk 客户端
  • client.threads.get_state()
  • client.threads.update_state()
  • client.runs.stream(None)