前端转agent-【python】-11 LangGraph 高级特性:时间旅行与人工介入

前端转agent-【python】-11 LangGraph 高级特性:时间旅行与人工介入

之前的教程里我们用 LangGraph 搭出了带路由和工具调用的 Agent,它自己就能跑完整条链路。

但如果工具调用有风险(比如删除订单),你肯定希望 Agent 在执行前停一下 ,等你点个头。

出问题时,你还想回退到之前的步骤 看看哪里出了错------就像 Vue DevTools 里的时间旅行调试。

今天就用 CheckpointerInterrupt 给 Agent 加上这两项超能力。

为什么需要时间旅行和人工介入?

特性 解决什么问题 Vue 3 类比
Checkpointer(时间旅行) 状态回放、断点续跑、调试 Pinia DevTools 里的状态快照 / undo/redo
Interrupt(人工介入) 高风险操作前暂停,等待人类审批 表单提交前的二次确认弹窗

LangGraph 的 Checkpointer 会在每一步自动保存状态快照,你可以像 git log 一样查看历史、回退到任意版本。

Interrupt 则让你在图的某个节点前"踩刹车",等外部信号(比如前端按钮点击)再继续。

环境准备

bash 复制代码
pip install langgraph langgraph-checkpoint ollama

模型:

bash 复制代码
ollama pull qwen3:4b

快速回顾:带工具调用的 Agent

我们先搭一个基础版 Agent,LLM 可以调用计算器和天气工具,沿用 09 篇的逻辑。

python 复制代码
# base_agent.py (无 checkpointer 和 interrupt,仅作回顾)
import ollama, json
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END
import sys

MODEL = "qwen3:4b"

class AgentState(TypedDict):
    messages: Annotated[list, "对话历史"]
    user_input: str
    tool_call: dict | None
    final_answer: str

def get_weather(city: str) -> str:
    return {"北京":"晴 25°C","上海":"小雨 22°C"}.get(city, "未知")

def calculator(expr: str) -> str:
    try: return str(eval(expr, {"__builtins__": {}}))
    except: return "计算错误"

TOOLS = {
    "get_weather": (get_weather, "city"),
    "calculator": (calculator, "expression"),
}

SYS_PROMPT = """你是工具助手。可用工具:
1. get_weather --- 查询天气,参数:{"city": "城市名"}
2. calculator --- 计算表达式,参数:{"expression": "数学表达式"}

需要工具时严格按以下JSON格式输出(tool名必须完全匹配上面列出的名称):
{"tool":"get_weather","args":{"city":"北京"}}
或
{"tool":"calculator","args":{"expression":"1+1"}}
不需要工具时直接回答即可。"""

def llm_node(state: AgentState):
    msgs = [{"role":"system","content":SYS_PROMPT}] + state["messages"] + [{"role":"user","content":state["user_input"]}]
    res = ollama.chat(model=MODEL, messages=msgs)
    content = res["message"]["content"].strip()
    try:
        tc = json.loads(content)
        if "tool" in tc and "args" in tc:
            return {"tool_call": tc}
    except: pass
    return {"final_answer": content}

def tool_node(state: AgentState):
    tc = state["tool_call"]
    tool_name = tc["tool"]
    # 容错匹配:LLM可能返回近似的工具名
    if tool_name not in TOOLS:
        alias_map = {"weather_tool": "get_weather", "weather": "get_weather", "calc": "calculator", "calc_tool": "calculator"}
        tool_name = alias_map.get(tool_name, tool_name)
    if tool_name not in TOOLS:
        return {"messages": state["messages"], "tool_call": None, "final_answer": f"未知工具: {tc['tool']}"}
    func, _ = TOOLS[tool_name]
    if tool_name == "get_weather":
        result = func(tc["args"]["city"])
    else:
        result = func(tc["args"]["expression"])
    new_msgs = state["messages"] + [
        {"role":"user","content":state["user_input"]},
        {"role":"assistant","content":f"调用工具:{tc['tool']}({json.dumps(tc['args'])})"},
        {"role":"tool","content":result}
    ]
    return {"messages": new_msgs, "tool_call": None}

def should_continue(state: AgentState) -> Literal["tools", "end"]:
    return "tools" if state.get("tool_call") else "end"

base_builder = StateGraph(AgentState)
base_builder.add_node("llm", llm_node)
base_builder.add_node("tools", tool_node)
base_builder.set_entry_point("llm")
base_builder.add_conditional_edges("llm", should_continue, {"tools":"tools","end":END})
base_builder.add_edge("tools", "llm")
base_agent = base_builder.compile()

if __name__ == "__main__":
    query = sys.argv[1] if len(sys.argv) > 1 else input("请输入问题: ")
    result = base_agent.invoke({"messages": [], "user_input": query, "tool_call": None})
    print("\n" + "="*40)
    print("回答:", result["final_answer"])
    print("="*40)

特性一:时间旅行 ------ Checkpointer

LangGraph 内置了 MemorySaver 作为检查点存储器。只需在 compile() 时传入,并在 invoke 时提供 config(包含 thread_id)即可自动记录每一步的状态。

python 复制代码
# checkpoint_demo.py
from langgraph.checkpoint.memory import MemorySaver

# 编译时传入 checkpointer
checkpointer = MemorySaver()
agent_with_checkpoint = base_builder.compile(checkpointer=checkpointer)

# 调用时指定 thread_id
config = {"configurable": {"thread_id": "user-123"}}
state = {"messages": [], "user_input": "北京天气", "tool_call": None, "final_answer": ""}
result = agent_with_checkpoint.invoke(state, config)
print(result["final_answer"])

查看历史状态:

python 复制代码
# checkpoint_demo.py
# 获取某个 thread 的所有状态快照
history = list(checkpointer.list(config))
for checkpoint in history:
    print(checkpoint.metadata)  # 包含 step 等信息

回退到某一步(时间旅行):

使用 get_state 获取某个 checkpoint 的状态,然后以其为基础重新 invoke

python 复制代码
# checkpoint_demo.py
# 获取最新的状态
current_state = agent_with_checkpoint.get_state(config)
print(current_state.values["messages"])

# 如果想回到上一步,可以用 checkpoint_id
previous_state = agent_with_checkpoint.get_state(config, checkpoint_id=...)

Vue 3 类比:这就像 Pinia DevTools 中的"状态快照"功能,你可以点击任一历史记录,把 store 恢复到那个时刻。在 LangGraph 中,checkpoint 就是那些快照。

特性二:人工介入 ------ Interrupt

在构建图时,我们可以指定 interrupt_before 参数,让图在运行到某个节点前暂停。

此时 invoke 会返回一个 Interrupt 异常(或特定状态),等外部确认后再调用 invoke 继续。

最适合的场景:工具执行前,等待人类审批。

python 复制代码
# interrupt_demo.py
# 编译时指定在 "tools" 节点之前中断
agent_with_interrupt = base_builder.compile(
    checkpointer=checkpointer,          # 必须配合 checkpointer,否则状态丢失
    interrupt_before=["tools"]          # 在 tools 节点前中断
)

交互流程:

  1. 用户发送消息 → Agent 运行到 LLM 输出工具调用 → 暂停在 tools 前。
  2. 人类查看工具调用详情,决定批准或拒绝。
  3. 如果批准,用 None 作为输入再次 invoke,继续执行;如果拒绝,可以修改状态后继续。

完整 Demo:带审批的工具调用

我们将上面的代码整合,演示一个需要人工审批的"删除订单"工具。因 Qwen3:4b 能力有限,我们用模拟的"删除订单"工具展示流程。

python 复制代码
# interrupt_demo.py
import ollama, json
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

MODEL = "qwen3:4b"

class AgentState(TypedDict):
    messages: Annotated[list, "对话历史"]
    user_input: str
    tool_call: dict | None
    final_answer: str
    approved: bool   # 是否批准工具执行

# 工具:模拟删除订单(高风险)
def delete_order(order_id: str) -> str:
    return f"订单 {order_id} 已删除"

def get_weather(city: str) -> str:
    return {"北京":"晴","上海":"雨"}.get(city, "未知")

TOOLS = {
    "get_weather": (get_weather, ["city"]),
    "delete_order": (delete_order, ["order_id"]),
}

SYS_PROMPT = """你是工具助手。可用工具:
- get_weather(city)
- delete_order(order_id)
需要工具时输出JSON:{"tool":"工具名","args":{...}};否则直接回答。"""

# 节点
def llm_node(state: AgentState):
    msgs = [{"role":"system","content":SYS_PROMPT}] + state["messages"] + [{"role":"user","content":state["user_input"]}]
    res = ollama.chat(model=MODEL, messages=msgs)
    content = res["message"]["content"].strip()
    try:
        tc = json.loads(content)
        if "tool" in tc and "args" in tc:
            return {"tool_call": tc}
    except: pass
    return {"final_answer": content}

def tool_node(state: AgentState):
    tc = state["tool_call"]
    func, _ = TOOLS[tc["tool"]]
    if tc["tool"] == "get_weather":
        result = func(tc["args"]["city"])
    else:
        result = func(tc["args"]["order_id"])
    new_msgs = state["messages"] + [
        {"role":"user","content":state["user_input"]},
        {"role":"assistant","content":f"调用工具:{tc['tool']}({json.dumps(tc['args'])})"},
        {"role":"tool","content":result}
    ]
    return {"messages": new_msgs, "tool_call": None, "approved": False}

def should_continue(state: AgentState) -> Literal["tools", "end"]:
    if state.get("tool_call"):
        return "tools"
    return "end"

# 构建图
builder = StateGraph(AgentState)
builder.add_node("llm", llm_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("llm")
builder.add_conditional_edges("llm", should_continue, {"tools":"tools","end":END})
builder.add_edge("tools", "llm")

# 编译,加入 checkpointer 和中断
memory = MemorySaver()
agent = builder.compile(
    checkpointer=memory,
    interrupt_before=["tools"]   # 工具执行前中断
)

# 交互主循环
if __name__ == "__main__":
    state = {
        "messages": [],
        "user_input": "",
        "tool_call": None,
        "final_answer": "",
        "approved": False
    }
    config = {"configurable": {"thread_id": "user-1"}}
    print("🤖 带审批的 Agent (高风险操作需人工确认)")
    while True:
        user = input("\n🧑 你: ")
        if user == "/bye": break
        if user == "/history":
            # 展示时间旅行
            history = list(memory.list(config))
            for cp in history:
                print(f"Checkpoint {cp.metadata.get('step','?')}: {cp.values.get('tool_call')}")
            continue

        state["user_input"] = user
        state["tool_call"] = None
        state["final_answer"] = ""
        state["approved"] = False

        # 第一次 invoke,可能中断在 tools 前
        result = agent.invoke(state, config)
        # 检查是否中断(如果 tool_call 不为空且还未执行)
        if result.get("tool_call") and not result.get("final_answer"):
            tc = result["tool_call"]
            print(f"⚠️  工具调用待审批:{tc['tool']}({json.dumps(tc['args'])})")
            approve = input("批准执行?(y/n): ").strip().lower()
            if approve == 'y':
                # 批准后继续执行,输入 None 表示不更新状态
                result = agent.invoke(None, config)
                print(f"🤖 Agent: {result.get('final_answer', '工具执行完毕')}")
            else:
                # 拒绝:我们可以手动修改状态,例如替换 tool_call 为 None,然后继续
                agent.update_state(config, {"tool_call": None, "final_answer": "操作已被用户拒绝"})
                result = agent.invoke(None, config)
                print(f"🤖 Agent: {result.get('final_answer')}")
        else:
            print(f"🤖 Agent: {result.get('final_answer', '无响应')}")
        
        # 更新持久状态
        state["messages"] = result["messages"]

运行效果:

css 复制代码
🧑 你: 删除订单 1001
⚠️  工具调用待审批:delete_order({"order_id":"1001"})
批准执行?(y/n): y
🤖 Agent: 订单 1001 已删除

🧑 你: 北京天气
⚠️  工具调用待审批:get_weather({"city":"北京"})
批准执行?(y/n): n
🤖 Agent: 操作已被用户拒绝

如果想回退,可以使用 memory.list(config) 查看历史,然后用 get_state 恢复到某个 checkpoint。

Vue 3 横向对比

LangGraph 高级特性 Vue 3 对应方案
MemorySaver 自动保存状态 Pinia DevTools 记录每次 mutation 的快照
get_state(config, checkpoint_id) 点击 DevTools 中的历史状态进行 time-travel
interrupt_before=["tools"] 在 Pinia action 中调用 confirm() 弹窗,等待用户确认后再继续
update_state() 后继续 用户点击"拒绝"后手动修改 store 中的状态,并重新触发后续流程

你在 Vue 3 中实现一个高风险操作,可能会这样写:

typescript 复制代码
async function deleteOrder(orderId: string) {
  const confirmed = await showConfirmDialog('确定删除?');
  if (confirmed) {
    await api.deleteOrder(orderId);
    store.commit('removeOrder', orderId);
  }
}

LangGraph 的 interrupt 就是这个 showConfirmDialog 在服务端的实现,只不过它暂停了整张图,等待外部信号。

总结

  • Checkpointer 让 Agent 有了"记忆"和"回放",可以断点续跑、调试历史。
  • Interrupt 让 Agent 在危险动作前停下来,等人类点一下"批准"------这就是人机协同的基础。
  • 两个特性配合使用,才能构建安全、可观测、可控的生产级 Agent。
  • 对 Vue 3 开发者来说,这些概念就是 Pinia 的 devtools + 确认弹窗,只不过搬到了后端。

现在你的 Agent 不再是无人驾驶,而是有了刹车和后视镜,安全上路吧 🚗💨。

bash 复制代码
python interrupt_demo.py
相关推荐
Token炼金师1 小时前
从safetensors到像素:ComfyUI Checkpoint加载机制的底层拆解
人工智能
AI闲人1 小时前
AI 写代码越来越快,为什么 Code Review 反而更慢了?
人工智能·code review·ai 编程
武子康1 小时前
调查研究-202 SGLang 深度解析:为什么大模型推理框架不只是“把模型跑起来“
人工智能·openai·agent
我是大卫1 小时前
Trae 读取 agents.md 并驱动 AI 完整底层原理
人工智能
石小石Orz1 小时前
AI具身交互:实现一个会说话的3D虚拟伴侣
前端·人工智能·后端
恋猫de小郭2 小时前
如何让 AI 快速搭建一套生产 Agent ?全面理解 Agent 架构。
前端·人工智能·ai编程
aneasystone本尊3 小时前
学习 turbovec 的量化算法
人工智能
九酒13 小时前
AI Agent 开发踩坑记:口播功能非得用 APP 原生实现吗?
前端·人工智能·agent