在上一节中,我们构建了带记忆功能的聊天机器人,详见[LangGraph教程]LangGraph03------为聊天机器人添加记忆。但是之前的程序仍然有可以优化的地方。
Agent虽能高效执行任务,但有时可能因信息不足或复杂情况而表现得不可靠。此时,人工输入就显得至关重要,它能为代理提供必要的信息或指导,助力任务的成功完成。类似地,对于一些关键或高风险的操作,提前设定人工批准环节是非常有必要的。这不仅能确保操作符合预期,还能有效防止潜在的错误或不当操作带来不良后果。
LangGraph的持久性层
LangGraph的持久性层为实现人机协作工作流程提供了有力支持,通过检查点程序实现。
当使用检查点程序编译图时,检查点程序会在每个超步保存图状态的检查点checkpoint
。
超步可以理解为图执行过程中的一个逻辑步骤或阶段。它代表了一组相关的操作或计算,这些操作在图的状态更新过程中被视为一个整体。
在 LangGraph 中,每当一个超步完成时,检查点程序可以保存当前的图状态,以便在需要时恢复或继续执行。
检查点

检查点是在每个超步保存的图状态的快照,由 StateSnapshot
对象表示,具有以下关键属性
- values:此时刻状态通道的值。
- next:图中接下来要执行的节点名称的元组。
- config:与此检查点关联的配置。
- metadata:与此检查点关联的元数据。
- tasks:PregelTask 对象的元组,其中包含有关要执行的下一个任务的信息。如果之前尝试过该步骤,则将包含错误信息。如果图是从节点内部动态中断的,则任务将包含与中断相关的其他数据。
这里举一个简单图的例子:
python
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from typing import Annotated
from typing_extensions import TypedDict
from operator import add
class State(TypedDict):
foo: str
bar: Annotated[list[str], add]
def node_a(state: State):
return {"foo": "a", "bar": ["a"]}
def node_b(state: State):
return {"foo": "b", "bar": ["b"]}
workflow = StateGraph(State)
workflow.add_node(node_a)
workflow.add_node(node_b)
workflow.add_edge(START, "node_a")
workflow.add_edge("node_a", "node_b")
workflow.add_edge("node_b", END)
checkpointer = MemorySaver()
graph = workflow.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": ""}, config)
这个thread_id
最新的检查点如下所示:
python
StateSnapshot(
values={'foo': 'b', 'bar': ['a', 'b']},
next=(),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}},
metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2},
created_at='2024-08-29T19:19:38.821749+00:00',
parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, tasks=()
)
获取状态
这些检查点保存在一个thread中,可以在图执行后访问。thread线程是分配给检查点程序保存的每个检查点的唯一 ID 或标识符。当使用检查点程序调用图时,必须将 thread_id
指定为 config
的 configurable
部分
python
{"configurable": {"thread_id": "1"}}
可以通过调用 graph.get_state(config)
来查看图的最新状态。这将返回一个 StateSnapshot
对象,该对象中存储的是和config
中提供的thread_id
关联的检查点。如果不指定checkpoint_id
,则返回和当前thread_id
的最新的检查点,否则返回指定的检查点。
python
# get the latest state snapshot
config = {"configurable": {"thread_id": "1"}}
graph.get_state(config)
# get a state snapshot for a specific checkpoint_id
config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}}
graph.get_state(config)
也可以通过调用 graph.get_state_history(config)
来获取给定线程的图执行的完整历史记录。这将返回一个与 config
中提供的thread_id
关联的 StateSnapshot
对象列表。检查点按时间顺序排列,最新的 StateSnapshot
在列表的最前面。
python
config = {"configurable": {"thread_id": "1"}}
list(graph.get_state_history(config))
由于threads(线程)允许在执行后访问图的状态,因此包括人机协作、内存、时间旅行和容错等多种强大功能都成为可能。
回退状态
如果使用 thread_id
和 checkpoint_id
调用图,那么我们将回到 checkpoint_id
对应的检查点执行最后一个步骤结束时的状态,并且仅执行检查点之后的步骤。
LangGraph 知道 checkpoint_id
之前的步骤是否之前已执行过:
- 如果已执行过,LangGraph 只需重放图中的特定步骤,而不会重新执行该步骤。
- 但
checkpoint_id
之后的所有步骤无论之前是否执行过,都将被重新执行。
python
config = {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}}
graph.invoke(None, config=config)
更新状态
除了从特定的 checkpoints
重放图之外,我们还可以编辑图状态。我们使用 graph.update_state()
来执行此操作。
注意,此更新的处理方式与来自节点的任何更新的处理方式完全相同。这意味着 update_state
不会自动覆盖每个通道的通道值,而仅覆盖没有 reducer
(归约器)的通道。
python
# 假设图的当前状态是 {"foo": 1, "bar": ["a"]}
graph.update_state(config, {"foo": 2, "bar": ["b"]})
那么图的新状态将是{"foo": 2, "bar": ["a", "b"]}
,因为没有为foo
键指定 reducer
(归约器),因此 update_state
会覆盖它。但是为 bar
键指定了 reducer
(归约器),因此它会将 "b" 附加到 bar
的状态。
人机环路
人机环路工作流程将人工输入集成到自动化流程中,从而允许在关键阶段进行决策、验证或更正。它允许在执行流程中根据用户反馈进行暂停和恢复,而实现这一功能的关键接口就是interrupt
函数。
当在节点内部调用interrupt
时,执行流程会暂停,等待用户输入,并使用他们的输入恢复图的执行,从而实现人机环路工作流程。interrupt
函数 与 Command
对象结合使用,以使用人工提供的值恢复图的执行。
在接下来的内容中,我们将基于现有的代码基础,介绍如何添加并使用这样的human_assistance
工具,以实现更高效的人机协作。
python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph
class State(TypedDict):
messages:Annotated[list, add_messages]
graph_builder = StateGraph(State)
# -------------------新增代码------------------------
from langgraph.types import Command, interrupt
from langchain.tools import tool
@tool
def human_assistance(query: str) -> str:
"""请求人类协助回答问题或提供信息。"""
human_response = interrupt({"query": query})
return human_response["data"]
from langchain_community.tools.tavily_search import TavilySearchResults
tool = TavilySearchResults(max_results=2)
tools = [tool,human_assistance]
#--------------------------------------------------
from langchain_openai import ChatOpenAI
local_llm = ["deepseek-r1:8b","qwen2.5:latest"]
llm = ChatOpenAI(model=local_llm[1], temperature=0.0, api_key="ollama", base_url="http://localhost:11434/v1")
llm_with_tools = llm.bind_tools(tools)
def chatbot(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
from langgraph.prebuilt import ToolNode
graph_builder.add_node("chatbot", chatbot)
tool_node = ToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)
from langgraph.prebuilt import tools_condition
graph_builder.add_conditional_edges(
"chatbot",
tools_condition,
)
graph_builder.add_edge("tools", "chatbot")
graph_builder.set_entry_point("chatbot")
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
现在让我们用一个会调用新的 human_assistance 工具的问题来提示聊天机器人
python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
聊天机器人生成了一个工具调用,但随后执行被中断!请注意,如果我们检查图状态,我们会看到它停在了 tools 节点处
要恢复执行,我们传递一个 Command 对象,其中包含工具期望的数据。此数据的格式可以根据我们的需要进行自定义。在这里,我们只需要一个带有键 "data" 的 dict
python
human_response = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
" It's much more reliable and extensible than simple autonomous agents."
)
human_command = Command(resume={"data": human_response})
events = graph.stream(human_command, config, stream_mode="values")
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()