🎯 课程核心知识点梳理
第23-23节课大概率涵盖了LangGraph中以下核心高级功能:
- Checkpointer(持久化):通过检查点保存图状态,支持会话记忆、故障恢复和时间旅行。
- interrupt(人工审核/人机协作):在关键节点暂停执行,等待人类介入后再继续。
- trim_messages(消息裁剪):在调用LLM前裁剪历史消息,控制上下文长度。
- SummarizationMiddleware(摘要中间件):自动将长对话压缩为摘要,保留核心信息。
- Send API(并行执行):实现动态扇出并行,同时处理多个子任务。
- Multi-Agent(多智能体协作):多个专用Agent协同完成复杂任务。
🧪 星系案例:多智能体宇宙探索系统
我们构建一个模拟宇宙探索中心的星系案例,完整涵盖以上6大知识点:
python
# ========== 1. 环境准备 ==========
import os
import sqlite3
from typing import TypedDict, Annotated, List, Literal
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langgraph.graph import StateGraph, END, START, add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command, Send
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
load_dotenv()
llm = ChatOpenAI(
model="qwen-plus",
temperature=0.7,
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
model_kwargs={"extra_body": {"enable_thinking": False}}
)
# ========== 2. 定义状态 ==========
class MissionState(TypedDict):
messages: Annotated[list, add_messages] # 对话历史
mission_id: str # 任务ID
status: str # 任务状态
planets: List[str] # 需要探索的行星列表
planet_results: Annotated[List[str], lambda x, y: (x or []) + (y or [])] # 并行结果归约器
final_report: str # 最终报告
needs_approval: bool # 是否需要审核
# ========== 3. 定义工具 ==========
@tool
def call_for_human_input(question: str) -> str:
"""请求人工协助(实际由 interrupt 实现)"""
return f"问题已提交: {question}"
tools = [call_for_human_input]
tool_node = ToolNode(tools)
llm_with_tools = llm.bind_tools(tools)
# ========== 4. 节点函数 ==========
def mission_control(state: MissionState) -> dict:
"""任务控制中心:分析任务,生成探索计划"""
last_message = state["messages"][-1].content if state["messages"] else ""
# 分析用户请求,判断需要探索哪些行星
planet_keywords = {"火星": "Mars", "木星": "Jupiter", "土星": "Saturn", "金星": "Venus"}
planets_to_explore = []
for cn, en in planet_keywords.items():
if cn in last_message:
planets_to_explore.append(en)
if not planets_to_explore:
planets_to_explore = ["Mars", "Jupiter"] # 默认探索火星和木星
return {
"mission_id": f"EXP-{os.urandom(2).hex().upper()}",
"status": "planning",
"planets": planets_to_explore
}
def human_approval(state: MissionState) -> dict:
"""
人工审核节点 - 使用 interrupt 暂停执行,等待人类决策
课程核心点:人机协作中断
"""
mission_id = state.get("mission_id", "UNKNOWN")
planets = state.get("planets", [])
# 调用 interrupt,暂停执行,向客户端展示信息,等待人类输入
# 这个值会暴露给调用方,调用方需要通过 Command(resume=...) 恢复
decision = interrupt(
f"请审核任务 {mission_id}\n"
f"计划探索的行星: {', '.join(planets)}\n"
f"请选择: approve(批准) / reject(拒绝) / modify(修改)"
)
if decision == "approve":
return {"status": "approved", "needs_approval": False}
elif decision == "reject":
return {"status": "rejected", "needs_approval": False, "final_report": "任务被拒绝执行"}
elif decision == "modify":
# 模拟修改:只探索火星
return {"status": "approved", "planets": ["Mars"], "needs_approval": False}
else:
return {"status": "pending", "needs_approval": True}
def dispatch_planets(state: MissionState):
"""
分发节点:为每个行星创建一个并行任务
课程核心点:Send API 动态扇出并行执行
"""
planets = state.get("planets", [])
if not planets:
return [Send("end", {})]
# 每个行星创建独立的 Send 对象,LangGraph 会并行执行多个 explore_planet 节点
return [Send("explore_planet", {"planet": planet, "mission_id": state["mission_id"]})
for planet in planets]
def explore_planet(state: dict) -> dict:
"""
探索单个行星 - 模拟并行执行
每个并行任务有自己独立的状态,但可以通过 reducer 将结果汇聚到主状态
"""
planet = state["planet"]
mission_id = state["mission_id"]
# 模拟不同行星的探索数据
planet_data = {
"Mars": {"temperature": "-60°C ~ 20°C", "atmosphere": "二氧化碳, 稀薄", "surface": "红色沙漠, 奥林帕斯山"},
"Jupiter": {"temperature": "-110°C", "atmosphere": "氢, 氦", "surface": "气态巨星, 大红斑"},
"Saturn": {"temperature": "-140°C", "atmosphere": "氢, 氦", "surface": "气态, 著名光环"},
"Venus": {"temperature": "462°C", "atmosphere": "二氧化碳, 浓硫酸云", "surface": "火山, 熔岩平原"}
}
data = planet_data.get(planet, {"temperature": "未知", "atmosphere": "未知", "surface": "未知"})
report = f"""
📡 {planet} 探测报告
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌡️ 地表温度: {data['temperature']}
🌍 大气成分: {data['atmosphere']}
🗻 地表特征: {data['surface']}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
return {"planet_results": [report]} # reducer 会自动累积合并
def generate_final_report(state: MissionState) -> dict:
"""
汇聚节点:将所有行星的探索结果合成最终报告
课程核心点:reducer 归约器汇聚并行结果
"""
results = state.get("planet_results", [])
if state.get("status") == "rejected":
return {"final_report": state.get("final_report", "任务已取消")}
full_report = "🚀 宇宙探索任务最终报告\n" + "="*60 + "\n"
for result in results:
full_report += result
full_report += "\n✨ 探索任务圆满完成!✨"
return {"final_report": full_report, "status": "completed"}
def should_continue(state: MissionState) -> Literal["tools", "human_approval", "dispatch", "__end__"]:
"""条件路由"""
last_msg = state["messages"][-1] if state["messages"] else None
# 检查是否需要调用工具
if last_msg and hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
return "tools"
status = state.get("status", "planning")
needs_approval = state.get("needs_approval", True)
if status == "planning":
return "human_approval"
elif status == "approved" and state.get("planets"):
return "dispatch"
return "__end__"
def route_after_tools(state: MissionState) -> Literal["mission_control", "human_approval", "__end__"]:
"""工具执行后的路由"""
last_msg = state["messages"][-1]
if isinstance(last_msg, ToolMessage):
return "mission_control"
return "human_approval"
# ========== 5. 构建图 ==========
builder = StateGraph(MissionState)
# 添加节点
builder.add_node("mission_control", mission_control)
builder.add_node("human_approval", human_approval)
builder.add_node("tools", tool_node)
builder.add_node("dispatch_planets", dispatch_planets)
builder.add_node("explore_planet", explore_planet)
builder.add_node("final_report", generate_final_report)
builder.add_node("end", lambda s: {})
# 定义流程
builder.add_edge(START, "mission_control")
builder.add_conditional_edges("mission_control", should_continue, {
"human_approval": "human_approval",
"dispatch": "dispatch_planets",
"tools": "tools",
"__end__": END
})
builder.add_conditional_edges("human_approval", lambda s: "tools" if s.get("needs_approval") else "mission_control", {
"tools": "tools",
"mission_control": "mission_control"
})
builder.add_conditional_edges("tools", route_after_tools, {
"mission_control": "mission_control",
"human_approval": "human_approval",
"__end__": END
})
builder.add_conditional_edges("dispatch_planets", lambda s: [Send("explore_planet", {"planet": p, "mission_id": s["mission_id"]}) for p in s.get("planets", [])], ["explore_planet"])
builder.add_edge("explore_planet", "final_report")
builder.add_edge("final_report", "end")
builder.add_edge("end", END)
# 使用 SqliteSaver 实现持久化存储
conn = sqlite3.connect("mission_checkpoints.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)
graph = builder.compile(checkpointer=checkpointer)
# ========== 6. 运行测试 ==========
if __name__ == "__main__":
thread_config = {"configurable": {"thread_id": "galaxy_mission_001"}}
print("\n" + "="*70)
print("🚀 星系探索任务 - LangGraph 高级功能演示")
print("="*70)
print("\n📡 正在分析任务需求...")
initial_state = {"messages": [HumanMessage(content="我想探索火星和金星!")]}
# 第一次调用会停在 interrupt,等待人类输入
for event in graph.stream(initial_state, config=thread_config, stream_mode="values"):
if "__interrupt__" in event:
# 检测到中断,输出需要审核的信息
print("\n⚠️ [系统] 任务执行中断,等待人工审批...")
print("="*50)
# 获取当前状态,查看中断点
current_state = graph.get_state(thread_config)
print(f"📌 当前状态: {current_state.values.get('status', 'unknown')}")
print(f"📌 下一步节点: {current_state.next}")
print("="*50)
# 模拟人工输入(实际应来自用户界面)
human_decision = input("\n👨✈️ 请输入审批决策 (approve/reject/modify): ").strip().lower()
print("="*50)
# 使用 Command(resume=...) 恢复执行
final_state = graph.invoke(Command(resume=human_decision), config=thread_config)
print("\n📊 最终报告:")
print(final_state.get("final_report", "无报告"))
break
📝 面试要点速查表
| 知识点 | 核心语法 | 一句话回答 |
|---|---|---|
| Checkpointer | graph.compile(checkpointer=SqliteSaver(conn)) |
检查点持久化图状态,实现会话记忆与时间旅行 |
| interrupt | decision = interrupt("审核信息") |
在节点内抛出可恢复异常,暂停执行等待外部输入 |
| Command resume | graph.invoke(Command(resume="approve"), config) |
恢复中断后的图执行,传入人工输入值 |
| Send API | return [Send("node", {"key": value})] |
在条件边中返回Send对象列表,实现动态扇出并行 |
| Reducer | Annotated[List[str], lambda x, y: (x or []) + (y or [])] |
定义并行任务结果如何合并到主状态 |
| trim_messages | trim_messages(messages, max_tokens=1000) |
在调用LLM前裁剪历史消息 |
| SummarizationMiddleware | SummarizationMiddleware(model=llm, trigger=("messages", 10)) |
自动压缩历史消息为摘要 |
| Multi-Agent | 多个 StateGraph + Send 动态路由 | 多Agent通过分工协作,Router负责任务分发 |
通过这个案例,你可以完整掌握 LangGraph 的高级功能。祝学习顺利!
🔍 代码流程解析
1. 整体目标
实现一个模拟的宇宙探索任务控制系统,包含:
- 任务计划生成
- 人工审批(使用
interrupt中断) - 多行星并行探索(使用
SendAPI) - 结果汇聚与最终报告
- 持久化检查点(
SqliteSaver)
2. 状态定义 (MissionState)
| 字段 | 类型 | 归约器 | 作用 |
|---|---|---|---|
messages |
list |
add_messages |
对话历史,自动追加 |
mission_id |
str |
无(覆盖) | 任务唯一ID |
status |
str |
无(覆盖) | planning / approved / rejected / completed |
planets |
List[str] |
无(覆盖) | 需要探索的行星列表 |
planet_results |
List[str] |
自定义归约器(追加) | 各行星探索报告的累积结果 |
final_report |
str |
无(覆盖) | 最终综合报告 |
needs_approval |
bool |
无(覆盖) | 是否需要人工审批 |
3. 节点功能详解
| 节点名称 | 函数 | 核心功能 | 输出更新 |
|---|---|---|---|
mission_control |
mission_control() |
分析用户输入,提取要探索的行星,生成任务ID | mission_id, planets, status="planning" |
human_approval |
human_approval() |
中断执行 ,调用 interrupt() 等待人类审批;根据决策更新状态 |
status 改为 approved/rejected,或修改 planets |
tools |
ToolNode |
执行 call_for_human_input 工具(本例未实际触发) |
工具返回 ToolMessage |
dispatch_planets |
dispatch_planets() |
为每个行星创建一个 Send 对象,实现并行分发 |
返回 [Send("explore_planet", {...})] |
explore_planet |
explore_planet() |
模拟探测单个行星,返回探测报告 | planet_results 追加一条报告 |
final_report |
generate_final_report() |
汇总所有 planet_results,生成最终报告 |
final_report, status="completed" |
end |
lambda s: {} |
空节点,用于终止 | - |
4. 路由函数
| 函数 | 输入 | 输出 | 路由规则 |
|---|---|---|---|
should_continue |
MissionState |
"tools" / "human_approval" / "dispatch" / "__end__" |
• 如果有 tool_calls → tools • 若 status == "planning" → human_approval • 若 status == "approved" 且有 planets → dispatch • 其他 → END |
route_after_tools |
MissionState |
"mission_control" / "human_approval" / "__end__" |
如果最后消息是 ToolMessage → mission_control;否则 human_approval |
5. 边(Edges)与执行顺序
text
START → mission_control
mission_control → 条件边(should_continue)
├─ human_approval
├─ dispatch_planets
├─ tools
└─ END
human_approval → 条件边(根据 needs_approval)
├─ tools(如果 needs_approval=True)→ tools → route_after_tools → mission_control
└─ mission_control(如果 approval 已得到)→ 回到 mission_control
dispatch_planets → 动态生成多个 Send → 并行执行 explore_planet 节点
explore_planet → final_report
final_report → end → END
6. 关键高级功能体现
- 持久化检查点 :
SqliteSaver将每一步的状态保存到mission_checkpoints.db,支持故障恢复。 - 中断与恢复 :
interrupt()在human_approval节点暂停,等待人工输入后通过Command(resume=...)继续。 - Send API 并行 :
dispatch_planets返回多个Send,LangGraph 会并发执行explore_planet节点。 - Reducer 汇聚 :
planet_results使用自定义归约器将并行结果自动合并到主状态。 - 消息裁剪/摘要 :虽然本例未直接使用
trim_messages或SummarizationMiddleware,但预留了扩展点。
📊 完整流程图(Mermaid)
#mermaid-svg-LnjGm2u2AyeUWWtS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LnjGm2u2AyeUWWtS .error-icon{fill:#552222;}#mermaid-svg-LnjGm2u2AyeUWWtS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LnjGm2u2AyeUWWtS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LnjGm2u2AyeUWWtS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LnjGm2u2AyeUWWtS .marker.cross{stroke:#333333;}#mermaid-svg-LnjGm2u2AyeUWWtS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LnjGm2u2AyeUWWtS p{margin:0;}#mermaid-svg-LnjGm2u2AyeUWWtS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster-label text{fill:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster-label span{color:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster-label span p{background-color:transparent;}#mermaid-svg-LnjGm2u2AyeUWWtS .label text,#mermaid-svg-LnjGm2u2AyeUWWtS span{fill:#333;color:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS .node rect,#mermaid-svg-LnjGm2u2AyeUWWtS .node circle,#mermaid-svg-LnjGm2u2AyeUWWtS .node ellipse,#mermaid-svg-LnjGm2u2AyeUWWtS .node polygon,#mermaid-svg-LnjGm2u2AyeUWWtS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LnjGm2u2AyeUWWtS .rough-node .label text,#mermaid-svg-LnjGm2u2AyeUWWtS .node .label text,#mermaid-svg-LnjGm2u2AyeUWWtS .image-shape .label,#mermaid-svg-LnjGm2u2AyeUWWtS .icon-shape .label{text-anchor:middle;}#mermaid-svg-LnjGm2u2AyeUWWtS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LnjGm2u2AyeUWWtS .rough-node .label,#mermaid-svg-LnjGm2u2AyeUWWtS .node .label,#mermaid-svg-LnjGm2u2AyeUWWtS .image-shape .label,#mermaid-svg-LnjGm2u2AyeUWWtS .icon-shape .label{text-align:center;}#mermaid-svg-LnjGm2u2AyeUWWtS .node.clickable{cursor:pointer;}#mermaid-svg-LnjGm2u2AyeUWWtS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LnjGm2u2AyeUWWtS .arrowheadPath{fill:#333333;}#mermaid-svg-LnjGm2u2AyeUWWtS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LnjGm2u2AyeUWWtS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LnjGm2u2AyeUWWtS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LnjGm2u2AyeUWWtS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LnjGm2u2AyeUWWtS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LnjGm2u2AyeUWWtS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster text{fill:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS .cluster span{color:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LnjGm2u2AyeUWWtS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LnjGm2u2AyeUWWtS rect.text{fill:none;stroke-width:0;}#mermaid-svg-LnjGm2u2AyeUWWtS .icon-shape,#mermaid-svg-LnjGm2u2AyeUWWtS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LnjGm2u2AyeUWWtS .icon-shape p,#mermaid-svg-LnjGm2u2AyeUWWtS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LnjGm2u2AyeUWWtS .icon-shape .label rect,#mermaid-svg-LnjGm2u2AyeUWWtS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LnjGm2u2AyeUWWtS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LnjGm2u2AyeUWWtS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LnjGm2u2AyeUWWtS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} status=planning
有 tool_calls
status=approved & 有planets
其他
仍需要?
已审批
ToolMessage
其他
for each planet
自动累积结果
human_approval 节点
approve
reject
modify
interrupt 等待审批
用户决策
status=approved
status=rejected
修改planets, status=approved
mission_control 节点
分析用户输入
生成任务ID
确定行星列表
START
mission_control
should_continue
human_approval
tools
didispatch_planets
END
needs_approval?
route_after_tools
explore_planet
final_report
END
✅ 流程总结表
| 阶段 | 节点 | 动作 | 关键API |
|---|---|---|---|
| 1 | mission_control |
解析用户意图,生成任务 | - |
| 2 | human_approval |
中断并等待人工审批 | interrupt() / Command(resume=) |
| 3 | dispatch_planets |
为每个行星创建并行任务 | Send() |
| 4 | explore_planet |
并行执行行星探测 | reducer 自动合并结果 |
| 5 | final_report |
汇聚结果,生成报告 | - |
| 持久化 | 全流程 | 状态保存到 SQLite | SqliteSaver |
通过这个案例,您可以看到 LangGraph 如何优雅地实现中断恢复、并行执行、状态持久化 等高级特性。您可以直接运行代码(需配置 .env 中的 API Key),体验人工审批时的中断与恢复效果。