前端转agent-【python】-11 LangGraph 高级特性:时间旅行与人工介入
之前的教程里我们用 LangGraph 搭出了带路由和工具调用的 Agent,它自己就能跑完整条链路。
但如果工具调用有风险(比如删除订单),你肯定希望 Agent 在执行前停一下 ,等你点个头。
出问题时,你还想回退到之前的步骤 看看哪里出了错------就像 Vue DevTools 里的时间旅行调试。
今天就用 Checkpointer 和 Interrupt 给 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 节点前中断
)
交互流程:
- 用户发送消息 → Agent 运行到 LLM 输出工具调用 → 暂停在
tools前。 - 人类查看工具调用详情,决定批准或拒绝。
- 如果批准,用
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