图执行中途暂停后,你不仅能"批准或拒绝",还能直接修改图的内部状态。
本课演示如何精准编辑 State,以及如何将人工输入无缝注入到 Agent 流程中。
在 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 提供的核心状态编辑接口,配合断点使用可以实现强大的人机协作交互。
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)
# 发送初始输入,图会在断点处停止
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 节点前的断点
此时图已暂停。我们可以检查当前状态:
state = graph.get_state(thread)
print(state.next) # ('assistant',) — 下一步等待执行 assistant
现在来看本课最关键的调用——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!")]},
)
update_state 返回一个新的 checkpoint 配置,说明状态已经被保存到一个新的检查点中。
每次调用 update_state 都会创建一个新的检查点(checkpoint)。返回的配置中包含了这个新检查点的 ID。这意味着状态的修改本身也是可以被时间旅行功能追踪的——你可以看到状态是什么时候、被谁修改的。
update_state 不是简单地"替换"整个 State,它遵循 State 中为每个字段定义的 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 输出 |
Notebook 中的第一个 update_state 调用传入了一个没有 id 的新 HumanMessage,所以 add_messages 将它追加到了现有列表的末尾:
验证结果:
new_state = graph.get_state(thread).values
for m in new_state['messages']:
m.pretty_print()
如果我们想替换而不是追加,只需要在传入的消息对象中保留原消息的 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,触发的是追加逻辑。两者都是常见用法,需要根据你的目的选择。
修改状态只是第一步,完整的流程还需要让图从修改后的状态继续执行。恢复执行的方法非常简单:传入 None 作为输入再次调用 graph.stream()。
当你向 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()
由于图仍然设置了 interrupt_before=["assistant"],它在工具结果返回后再次在 assistant 节点前暂停。传入 None 再次恢复:
# ④ 图再次在断点处停下,再次传 None 让它完成最后一步
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
update_state 的第三个参数 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" # 关键参数
)
假设图的边是 human_feedback → assistant。如果我们调用 update_state 时不指定 as_node,图不知道"当前处于 human_feedback 节点完成后的状态",就无法正确路由到 assistant。as_node="human_feedback" 告诉图:"把这次更新视为 human_feedback 节点的执行结果,下一步按照 human_feedback 的出边走。"
调用 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
前面的例子是在外部"悄悄修改"了图的消息。更优雅的设计是:在图结构中显式地加入一个代表人类反馈的节点,让人机协作成为图架构的一部分。
新的图引入了一个 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 前暂停
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" 时,输出如下:
input() 返回的是一个字符串(str),而不是 HumanMessage 对象。add_messages Reducer 足够智能,能够自动将字符串转换为 HumanMessage 对象并追加到消息列表中,因此这里不需要手动封装。
除了在 Python 代码中直接调用 graph.update_state(),LangGraph 还提供了基于 HTTP 的 API(通过 langgraph_sdk 客户端),可以远程编辑运行在服务器上的图的状态。这是 LangSmith Studio 可视化界面背后的底层机制。
from langgraph_sdk import get_client
# 连接到本地运行的 LangGraph 开发服务器
client = get_client(url="http://127.0.0.1:2024")
# 生产环境中换成你的 LangGraph Cloud URL
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 在不同的使用场景下可以有不同的断点策略,而不需要重新部署代码。
# 获取当前状态
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 远程调用。
# 传入 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) |
调用 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 调用时动态传入 |
human_feedback 节点输入意见将 human_feedback 节点显式加入图结构是生产级 Human-in-the-loop 应用的推荐做法。相比直接从外部调用 update_state,它让人机协作的位置在图的架构中一目了然,也更易于测试和维护。当 tools → human_feedback 这条边存在时,每次工具调用之后都会先等待人工确认,再交给 assistant 节点处理——这是构建受控 Agent 系统的核心模式。
graph.update_state()graph.get_state()as_node 参数stream(None, thread) 恢复add_messages 处理 messageshuman_feedback 占位节点tools → human_feedback 循环interrupt_before 触发暂停langgraph_sdk 客户端client.threads.get_state()client.threads.update_state()client.runs.stream(None)