18.1 星系案例:多智能体宇宙探索系统(学习langgraph 的存储知识)

🎯 课程核心知识点梳理

第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 中断)
  • 多行星并行探索(使用 Send API)
  • 结果汇聚与最终报告
  • 持久化检查点(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_callstools • 若 status == "planning"human_approval • 若 status == "approved" 且有 planetsdispatch • 其他 → END
route_after_tools MissionState "mission_control" / "human_approval" / "__end__" 如果最后消息是 ToolMessagemission_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_messagesSummarizationMiddleware,但预留了扩展点。

📊 完整流程图(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),体验人工审批时的中断与恢复效果。

相关推荐
m沐沐1 小时前
【深度学习】PyTorch CNN 手写数字识别(卷积神经网络)
人工智能·pytorch·python·深度学习·机器学习·pycharm·cnn
garmin Chen1 小时前
Prompt工程入门:让AI按你的要求工作(3)--Prompt工程与提示词安全评测概述
java·人工智能·python·安全·prompt
lizhihai_991 小时前
股市学习心得-六月的股市怎么应对
大数据·人工智能·科技·学习·区块链
nanawinona1 小时前
只会用 K 线算期货信号下一步怎么接到交易
python·区块链
数智工坊1 小时前
机器人控制总线深度解析:CAN与EtherCAT,谁在决定机器人的稳定性?
嵌入式硬件·学习·机器人
叫我:松哥1 小时前
基于卷积神经网络的静态手势语识别算法,在测试集上的识别准确率达到97.5%
人工智能·python·深度学习·神经网络·算法·cnn
不考研当牛马1 小时前
Django 框架 深度学习 第二课程
后端·python·django
Dust-Chasing1 小时前
Claude Code源码剖析 - ShellTool与真实动作
人工智能·python·ai
韭菜钟2 小时前
Windows下编译coal
windows