[LangGraph教程]LangGraph04——支持人机协作的聊天机器人

在上一节中,我们构建了带记忆功能的聊天机器人,详见[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 指定为 configconfigurable 部分

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_idcheckpoint_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()
相关推荐
普if加的帕2 小时前
java Springboot使用扣子Coze实现实时音频对话智能客服
java·开发语言·人工智能·spring boot·实时音视频·智能客服
KoiC2 小时前
Dify接入RAGFlow无返回结果
人工智能·ai应用
lilye662 小时前
精益数据分析(20/126):解析经典数据分析框架,助力创业增长
大数据·人工智能·数据分析
盈达科技2 小时前
盈达科技:登顶GEO优化全球制高点,以AICC定义AI时代内容智能优化新标杆
大数据·人工智能
安冬的码畜日常2 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
FIT2CLOUD飞致云3 小时前
问答页面支持拖拽和复制粘贴文件,MaxKB企业级AI助手v1.10.6 LTS版本发布
人工智能·开源
起个破名想半天了3 小时前
计算机视觉cv入门之答题卡自动批阅
人工智能·opencv·计算机视觉
早睡早起吧3 小时前
目标检测篇---Fast R-CNN
人工智能·目标检测·计算机视觉·cnn
爱喝奶茶的企鹅3 小时前
Ethan独立开发产品日报 | 2025-04-24
人工智能·程序员·开源