Langgraph学习

安装依赖

pip install -U langchain langchain-openai langgraph-checkpoint-sqlite
!apt-get install -y graphviz graphviz-dev
!pip install pygraphviz

State 与 Checkpoint

在构建复杂的 AI Agent(智能体)时,很多开发者会被 LangGraph 中的两个概念绕晕:State(状态)Checkpoint(检查点)

其实,用一个“单机游戏”的比喻,你瞬间就能秒懂。


1. State(状态):Agent 的“实时大脑”

State 是 Agent 运行时的“共享笔记”或“当前属性”。

想象你在玩一款 RPG 游戏,你的角色当前有多少血量(HP)、背囊里有几把剑、正站在地图的哪个坐标——这些实时变化的数据,就是 State

  • 它的作用:负责节点间的通信。比如“路由节点”在 State 里写下“建议去搜索”,随后的“搜索节点”才能看到这条指令并开始干活。
  • 它的寿命:它存在于内存中。如果你不刻意保存,一旦程序关掉(游戏断电),State 就会消失。

2. Checkpoint(检查点):Agent 的“存档按钮”

Checkpoint 是对 State 在某一刻的“快照”或“存档”。

还是那个游戏。当你走到一个存档点,按下“保存游戏”时,系统会把那一刻所有的 State(血量、装备、位置)打包存入硬盘。这个存下来的文件,就是 Checkpoint

  • 它的作用:负责持久化容错
    • 断点续传:如果程序崩溃了,你可以读档,从上次停下的地方继续,不用从头再来。
    • 时光倒流:如果 AI 在第 5 步聊歪了,你可以强行让它回到第 3 步的存档,重新尝试。
    • 人工审批:Agent 打算执行危险操作(如删除文件)时先存档并暂停,等你点完“同意”,它再从存档中复活继续执行。

3. 一张表看清区别

特性 State (状态) Checkpoint (检查点)
形象比喻 游戏角色的当前状态 游戏的存档文件
存储位置 内存 (瞬时,快,易失) 硬盘/数据库 (持久,稳,可追溯)
核心目的 让节点 A 告诉节点 B 该干什么 让今天的 Agent 记得昨天聊了什么
消失时机 程序停止运行即消失 除非手动删除,否则永远存在

总结

  • 没有 State,Agent 是支离破碎的,各部分无法协作。
  • 没有 Checkpoint,Agent 是健忘的,无法处理复杂的长任务。

State 保证了 Agent “能干活”,而 Checkpoint 保证了 Agent “干活稳”。 只有两者结合,我们才能打造出真正聪明且可靠的 AI 助手。

流程控制与交互特征

前置
申请api-key:https://cloud.siliconflow.cn

API_KEY ="sk-xxxx"
from langchain.chat_models import init_chat_model
from IPython.display import Image, display
# === LLM 初始化 ===
model = init_chat_model(
    "Qwen/Qwen3-8B",
    model_provider="openai",
    base_url="https://api.siliconflow.cn/v1",
    api_key= API_KEY,
    temperature=0.0
)

def display_graph(app):
  # 使用 Graphviz 渲染(Colab 最稳定的方案)
  try:
      display(Image(app.get_graph(xray=True).draw_png()))
  except Exception as e:
      print(f"Graphviz 渲染失败: {e}")
      print("\n使用 Mermaid 文本方式显示:")
      print(app.get_graph(xray=True).draw_mermaid())

条件路由与循环

1.1 条件路由回顾

在第1课中,我们学习了基本的条件边。现在深入学习更复杂的路由模式。

提示:先看图的构建,在看具体每个节点是在做什么。

基本条件路由:
其实就是,加一个判断节点(add_conditional_edges),分叉成不同的分支。

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    score: float
    decision: str

def evaluate(state: State) -> dict:
    """评估并打分"""
    return {"score": 0.75}

def route_by_score(state: State) -> Literal["high", "medium", "low"]:
    """根据分数路由"""
    score = state["score"]
    if score > 0.8:
        return "high"
    elif score > 0.5:
        return "medium"
    else:
        return "low"

def handle_high(state: State) -> dict:
    return {"decision": "自动通过"}

def handle_medium(state: State) -> dict:
    return {"decision": "人工审核"}

def handle_low(state: State) -> dict:
    return {"decision": "自动拒绝"}

# 构建图
graph = StateGraph(State)
graph.add_node("evaluate", evaluate)
graph.add_node("high", handle_high)
graph.add_node("medium", handle_medium)
graph.add_node("low", handle_low)

graph.add_edge(START, "evaluate")
graph.add_conditional_edges(
    "evaluate",
    route_by_score,
    {
        "high": "high",
        "medium": "medium",
        "low": "low"
    }
)

for node in ["high", "medium", "low"]:
    graph.add_edge(node, END)

app = graph.compile()
display_graph(app)

image

1.2 LLM 驱动的路由

使用 LLM 做决策:

from langchain.messages import HumanMessage, SystemMessage
import json

class AgentState(TypedDict):
    messages: list
    next_action: str


def llm_router(state: AgentState) -> Literal["search", "calculate", "respond", "clarify"]:
    """LLM 决定下一步行动"""
    messages = state["messages"]
    last_message = messages[-1].content

    routing_prompt = f"""
分析用户的请求,决定下一步行动:

用户请求:{last_message}

可选行动:
- search: 需要搜索信息
- calculate: 需要计算
- respond: 可以直接回复
- clarify: 需要澄清用户意图

只返回行动名称,不要解释。
"""

    response = model.invoke([HumanMessage(content=routing_prompt)])
    action = response.content.strip().lower()

    # 验证返回值
    valid_actions = ["search", "calculate", "respond", "clarify"]
    return action if action in valid_actions else "clarify"

def router_node(state: AgentState):
    return state

def search_node(state: AgentState) -> dict:
    """搜索节点"""
    print("🔍 执行搜索...")
    return {"next_action": "search"}

def calculate_node(state: AgentState) -> dict:
    """计算节点"""
    print("🧮 执行计算...")
    return {"next_action": "calculate"}

def respond_node(state: AgentState) -> dict:
    """回复节点"""
    print("💬 生成回复...")
    return {"next_action": "respond"}

def clarify_node(state: AgentState) -> dict:
    """澄清节点"""
    print("❓ 请求澄清...")
    return {"next_action": "clarify"}


graph = StateGraph(AgentState)
graph.add_node("oracle", router_node)
graph.add_node("calculate",calculate_node)
graph.add_node("search", search_node)
graph.add_node("respond", respond_node)
graph.add_node("clarify", clarify_node)

# 3. 明确入口
graph.add_edge(START, "oracle")

graph.add_conditional_edges(
    "oracle",
    llm_router,# 使用这个函数决定去哪
    {
       "search": "search",
        "calculate": "calculate",
        "respond": "respond",
        "clarify": "clarify"
    }
)
for node in ["search", "calculate", "respond", "clarify"]:
    graph.add_edge(node, END)

app = graph.compile()
display_graph(app)


# 运行
result = app.invoke({
    "messages": [HumanMessage(content="帮我查询北京今天的天气")],
})
print("============")
result = app.invoke({
    "messages": [HumanMessage(content="帮我计算1+1")],
})

image
🔍 执行搜索...
🧮 执行计算...

1.3 多级路由

就是add_conditional_edges内还有add_conditional_edges
先粗筛后细分:

class MultiLevelState(TypedDict):
    content_type: str
    urgency: str
    assigned_team: str

def classify_content_type(state: MultiLevelState) -> Literal["technical", "business", "other"]:
    """第一级:内容类型分类"""
    # 实际应用中使用 LLM
    return "technical"

def classify_urgency(state: MultiLevelState) -> Literal["urgent", "normal"]:
    """第二级:紧急程度分类"""
    return "urgent"

def assign_technical_urgent(state: MultiLevelState) -> dict:
    return {"assigned_team": "技术紧急响应组"}

def assign_technical_normal(state: MultiLevelState) -> dict:
    return {"assigned_team": "技术支持组"}

# 构建多级路由图
graph = StateGraph(MultiLevelState)

# 第一级分类
graph.add_node("classify_type", lambda s: {})
graph.add_node("technical_branch", lambda s: {})
graph.add_node("business_branch", lambda s: {})

# 第二级分类(technical 分支)
graph.add_node("classify_urgency", lambda s: {})
graph.add_node("tech_urgent", assign_technical_urgent)
graph.add_node("tech_normal", assign_technical_normal)

graph.add_edge(START, "classify_type")
graph.add_conditional_edges(
    "classify_type",
    classify_content_type,
    {
        "technical": "technical_branch",
        "business": "business_branch",
        "other": END
    }
)

graph.add_edge("technical_branch", "classify_urgency")
graph.add_conditional_edges(
    "classify_urgency",
    classify_urgency,
    {
        "urgent": "tech_urgent",
        "normal": "tech_normal"
    }
)

graph.add_edge("tech_urgent", END)
graph.add_edge("tech_normal", END)
graph.add_edge("business_branch", END)

app = graph.compile()
display_graph(app)

image

1.4 循环模式

就是首尾相连。
Agent 的思考-行动循环:

from typing import Annotated
from langgraph.graph.message import add_messages

class AgentLoopState(TypedDict):
    messages: Annotated[list, add_messages]
    iterations: int
    max_iterations: int
    final_answer: str

def should_continue(state: AgentLoopState) -> Literal["continue", "end"]:
    """决定是否继续循环"""
    # 检查是否有最终答案
    if state.get("final_answer"):
        return "end"

    # 检查是否超过最大迭代次数
    if state["iterations"] >= state["max_iterations"]:
        return "end"

    return "continue"

def think_node(state: AgentLoopState) -> dict:
    """思考节点"""
    print(f"🤔 思考中... (迭代 {state['iterations'] + 1})")

    # LLM 推理
    messages = state["messages"]
    response = model.invoke(messages)

    # 检查是否有答案
    if "最终答案:" in response.content:
        return {
            "final_answer": response.content,
            "iterations": state["iterations"] + 1
        }

    return {
        "messages": [response],
        "iterations": state["iterations"] + 1
    }

def act_node(state: AgentLoopState) -> dict:
    """行动节点(执行工具)"""
    print("⚡ 执行工具...")

    # 执行工具,获取结果
    tool_result = "工具执行结果..."

    return {
        "messages": [HumanMessage(content=f"观察:{tool_result}")]
    }

# 构建循环图
loop_graph = StateGraph(AgentLoopState)
loop_graph.add_node("think", think_node)
loop_graph.add_node("act", act_node)

loop_graph.add_edge(START, "think")
loop_graph.add_conditional_edges(
    "think",
    should_continue,
    {
        "continue": "act",
        "end": END
    }
)
loop_graph.add_edge("act", "think")  # 形成循环

loop_app = loop_graph.compile()

# 运行
result = loop_app.invoke({
    "messages": [HumanMessage(content="帮我查询北京今天的天气")],
    "iterations": 0,
    "max_iterations": 5,
    "final_answer": ""
})

display_graph(loop_app)

image
🤔 思考中... (迭代 1)
⚡ 执行工具...
🤔 思考中... (迭代 2)
⚡ 执行工具...
🤔 思考中... (迭代 3)
⚡ 执行工具...
🤔 思考中... (迭代 4)
⚡ 执行工具...
🤔 思考中... (迭代 5)

2. Streaming:流式输出

2.1 为什么需要 Streaming?

在长时间运行的任务中,用户希望看到实时进度:

  • ✅ 更好的用户体验
  • ✅ 实时反馈
  • ✅ Token-by-Token 输出
  • ✅ 监控任务进度

2.2 三种 Streaming 模式

模式 1:values - 完整状态

“给我看最新的完整剧本。”

  • 传输内容:返回图的当前完整 State(状态)。
  • 触发时机:每个节点(Node)执行结束后。
  • 数据格式:包含 State 中定义的所有字段(即使某些字段没变,也会一起返回)。

适用场景:

  • 你需要前端页面始终同步展示所有的上下文信息。
  • 调试时想看每一步执行完后,整体状态变成了什么样。

缺点: 如果 State 非常大(比如存了上百轮对话历史),每次都传输全量数据会浪费带宽。

# 第1步返回:
{"messages": [HumanMessage(...)], "user_info": {...}}
# 第2步返回(包含第1步的内容):
{"messages": [HumanMessage(...), AIMessage(...)], "user_info": {...}}

模式 2:updates - 状态更新

“告诉我刚刚发生了什么变化。”

  • 传输内容:返回刚刚执行完的那个节点所产出的输出(即对 State 的修改量)。
  • 触发时机:每个节点(Node)执行结束后。
  • 数据格式:一个字典,Key 是节点名称,Value 是该节点的输出结果。

适用场景:

  • 构建多 Agent 系统时,想知道具体是“哪个 Agent”在干活,以及它产出了什么。
  • 节省带宽,只传输变化的数据。
  • 前端只需要追加显示新产生的消息,而不需要刷新整个页面。
# 只有"chatbot"节点刚运行完,只返回它的更新
{"chatbot": {"messages": [AIMessage(content="Hello")]}}

模式 3:messages - Token 流

“打字机效果:别等说完,想到一个字就告诉我一个字。”

  • 传输内容:LLM 生成的Token(词元)以及相关的元数据。
  • 触发时机:在节点执行过程中,LLM 生成内容的瞬间(实时)。
  • 数据格式:通常是 (message_chunk, metadata) 的元组。
  • 核心区别: 前两种模式都要等一个节点彻底跑完代码才返回数据(Step-by-step),而 messages 是在节点内部一边跑一边返回(Token-by-token)。

适用场景:

  • ChatGPT 式的打字机效果:让用户感觉 AI 响应很快,不用等整个回复生成完。
  • 实时反馈:在长文本生成任务中提供视觉反馈。

返回示例:

# 连续快速返回
("H", {...}), ("el", {...}), ("lo", {...}), ("!", {...})

注意

LangGraph 允许你同时开启多种模式。例如,你可能既想要 updates 来知道是哪个 Agent 在说话,又想要 messages 来实现打字机效果

# 同时获取更新和 Token 流
async for chunk in graph.astream(inputs, stream_mode=["updates", "messages"]):
    # chunk 可能是更新对象,也可能是 token 消息,需要根据类型处理
    print(chunk)

2.3 实战:进度条显示

import time
from typing import TypedDict, Annotated
from operator import add

class ProcessingState(TypedDict):
    total_items: int
    processed_items: Annotated[int, add]
    current_step: str
    progress_pct: float

def step1(state: ProcessingState) -> dict:
    """数据加载"""
    time.sleep(1)
    return {
        "processed_items": 20,
        "current_step": "数据加载",
        "progress_pct": 20.0
    }

def step2(state: ProcessingState) -> dict:
    """数据处理"""
    time.sleep(1.5)
    return {
        "processed_items": 50,
        "current_step": "数据处理",
        "progress_pct": 70.0
    }

def step3(state: ProcessingState) -> dict:
    """保存结果"""
    time.sleep(0.5)
    return {
        "processed_items": 30,
        "current_step": "保存结果",
        "progress_pct": 100.0
    }

graph = StateGraph(ProcessingState)
graph.add_node("load", step1)
graph.add_node("process", step2)
graph.add_node("save", step3)

graph.add_edge(START, "load")
graph.add_edge("load", "process")
graph.add_edge("process", "save")
graph.add_edge("save", END)

app = graph.compile()
display_graph(app)

# 实时显示进度
print("\n处理任务...")
for state in app.stream({"total_items": 100, "processed_items": 0}, stream_mode="values"):
    step = state.get("current_step", "初始化")
    progress = state.get("progress_pct", 0)
    processed = state.get("processed_items", 0)

    bar_length = 30
    filled = int(bar_length * progress / 100)
    bar = "█" * filled + "░" * (bar_length - filled)

    print(f"\r{step}: [{bar}] {progress:.0f}% ({processed}/100)", end="", flush=True)

print("\n✅ 完成!")

/会看到打字机效果的进度条/
处理任务...
保存结果: [██████████████████████████████] 100% (100/100)
✅ 完成!

2.4 Streaming + Checkpoint

from langgraph.checkpoint.memory import MemorySaver

# 带 checkpoint 的 streaming
checkpointer = MemorySaver()
app_with_checkpoint = graph.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "task-1"}}

print("\n=== Streaming with Checkpoint ===")
for chunk in app_with_checkpoint.stream(
    {"total_items": 100, "processed_items": 0},
    config,
    stream_mode="updates"
):
    print(f"节点更新: {chunk}")

# 查看最终 checkpoint
final_state = app_with_checkpoint.get_state(config)
print(f"\n最终状态: {final_state.values}")

=== Streaming with Checkpoint ===
节点更新: {'load': {'processed_items': 20, 'current_step': '数据加载', 'progress_pct': 20.0}}
节点更新: {'process': {'processed_items': 50, 'current_step': '数据处理', 'progress_pct': 70.0}}
节点更新: {'save': {'processed_items': 30, 'current_step': '保存结果', 'progress_pct': 100.0}}
最终状态:{'total_items': 100, 'processed_items': 100, >'current_step': '保存结果', 'progress_pct': 100.0}

3. Interrupts:Human-in-the-Loop

什么是 Interrupt?
Interrupt 允许图在特定节点暂停,等待外部输入(通常是人工干预)。

注意:
Interrupts在 LangGraph 中必须结合 Checkpoint,如果只有interrupts而没有 Checkpoint,中断就只是“崩溃”

Interrupt 负责“停下来等”,Checkpoint 负责“记得等谁”。

场景 只有 Interrupt Interrupt + Checkpoint
程序状态 程序直接退出,内存清空。 程序退出,但数据在数据库里“睡着了”。
能否恢复 不能。必须从 START 重新开始。 。可以从中断的那一秒精准复活。
应用场景 几乎没有实际意义。 人工审核、打回重写、交互式绘图

“如果说 State 是 Agent 的大脑,那么 Checkpoint 就是它的备忘录,而 Interrupt 则是它的闹钟。

闹钟(Interrupt)响了,Agent 停下工作去睡觉。如果没有备忘录(Checkpoint),它醒来后会完全忘记昨天干了什么;只有配合备忘录,它才能在醒来后,拍拍脑袋,接着昨天没写完的代码继续写。”

使用场景:

  • ✅ 人工审核
  • ✅ 确认决策
  • ✅ 提供额外信息
  • ✅ 修改和重试

3.2 使用 interrupt()

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt

class ReviewState(TypedDict):
    content: str
    approved: bool
    feedback: str

def generate_content(state: ReviewState) -> dict:
    """生成内容"""
    print("📝 生成内容...")
    return {"content": "这是生成的内容..."}

def review_node(state: ReviewState) -> dict:
    """人工审核节点"""
    content = state["content"]

    print(f"\n{'='*50}")
    print("⏸️  等待人工审核...")
    print(f"内容: {content}")
    print(f"{'='*50}\n")

    # 暂停并等待人工输入
    approval = interrupt({
        "question": "是否批准此内容?",
        "options": ["approve", "reject", "request_changes"]
    })

    # 根据审核结果返回
    if approval == "approve":
        return {"approved": True, "feedback": "已批准"}
    elif approval == "reject":
        return {"approved": False, "feedback": "已拒绝"}
    else:
        return {"approved": False, "feedback": "需要修改"}

def publish_node(state: ReviewState) -> dict:
    """发布内容"""
    print("🚀 发布内容...")
    return {}

def reject_node(state: ReviewState) -> dict:
    """拒绝内容"""
    print("❌ 内容已拒绝")
    return {}

def should_publish(state: ReviewState) -> Literal["publish", "reject"]:
    """根据审核结果路由"""
    return "publish" if state.get("approved", False) else "reject"

# 构建图(必须使用 checkpointer)
graph = StateGraph(ReviewState)
graph.add_node("generate", generate_content)
graph.add_node("review", review_node)
graph.add_node("publish", publish_node)
graph.add_node("reject", reject_node)

graph.add_edge(START, "generate")
graph.add_edge("generate", "review")
graph.add_conditional_edges(
    "review",
    should_publish,
    {
        "publish": "publish",
        "reject": "reject"
    }
)
graph.add_edge("publish", END)
graph.add_edge("reject", END)

# 必须使用 checkpointer
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)
display_graph(app)

# 运行
config = {"configurable": {"thread_id": "review-1"}}

print("\n=== 第一次调用(会中断) ===")
result = app.invoke({"content": "", "approved": False}, config)
print(f"状态: {result}")

# 检查是否中断
state = app.get_state(config)
print(f"\n中断状态: {state.next}")  # 应该显示等待继续的节点

# 提供审核决策并继续
print("\n=== 提供审核决策并继续 ===")
app.update_state(config, {"approved": True})  # 批准
result = app.invoke(None, config)  # 继续执行
print(f"最终结果: {result}")

image

=== 第一次调用(会中断) ===
📝 生成内容...
==================================================
⏸️  等待人工审核...
内容: 这是生成的内容...
==================================================
状态: {'content': '这是生成的内容...', 'approved': False, '__interrupt__': [Interrupt(value={'question': '是否批准此内容?', 'options': ['approve', 'reject', 'request_changes']}, id='33c935134236b74e7aa2482c9f0c81d1')]}
中断状态: ('review',)
=== 提供审核决策并继续 ===
==================================================
⏸️  等待人工审核...
内容: 这是生成的内容...
==================================================
最终结果: {'content': '这是生成的内容...', 'approved': True, '__interrupt__': [Interrupt(value={'question': '是否批准此内容?', 'options': ['approve', 'reject', 'request_changes']}, id='5268d390988a42ba18a80d0728a006c5')]}

结构化Interrupt

一般来说Interrupt是需要前端展示的,所以,我们一般在Interrupts中填写和前端交互的所有信息。
核心区别:简单选项是面向代码逻辑的“必填项”,而结构化选项是面向前端交互的“配置协议”。


维度 简单选项 (Simple List) 结构化选项 (Structured Objects)
信息含量 极简:仅包含操作的 ID 或原始值 丰富:包含 ID、显示标签、详细描述及元数据
前后端解耦 :按钮文案和显示逻辑通常需要硬编码在前端 :后端动态控制 UI 内容,前端仅作为渲染容器
维护效率 一般:修改按钮名称或增加描述需前后端协同 极高:仅通过后端代码即可动态更新整个审批面板
适用场景 逻辑分支测试、个人脚本、简单的命令行交互 正式商业产品、企业级后台、多语言协作系统
def review_with_options(state: ReviewState) -> dict:
    """带多个选项的审核"""
    content = state["content"]

    # 提供结构化的选项
    decision = interrupt({
        "type": "review",
        "content": content,
        "options": [
            {"id": "approve", "label": "批准", "description": "内容无问题,批准发布"},
            {"id": "minor_edit", "label": "小修改", "description": "需要小幅修改"},
            {"id": "major_edit", "label": "大修改", "description": "需要重写"},
            {"id": "reject", "label": "拒绝", "description": "内容不合适,拒绝"}
        ]
    })

    return {"decision": decision}

4. Time Travel:时间旅行

什么是 Time Travel?

Time Travel 允许你:

  • 查看完整的执行历史
  • 回溯到任意历史状态
  • 从历史状态创建新的分支

使用场景:

  • ✅ 调试和分析
  • ✅ A/B 测试
  • ✅ 审计追踪
  • ✅ 撤销操作

4.2 查看历史

from langgraph.checkpoint.memory import MemorySaver

class HistoryState(TypedDict):
    count: int
    history: list

def increment_node(state: HistoryState) -> dict:
    new_count = state.get("count", 0) + 1
    return {
        "count": new_count,
        "history": state.get("history", []) + [f"Incremented to {new_count}"]
    }

graph = StateGraph(HistoryState)
graph.add_node("inc1", increment_node)
graph.add_node("inc2", increment_node)
graph.add_node("inc3", increment_node)

graph.add_edge(START, "inc1")
graph.add_edge("inc1", "inc2")
graph.add_edge("inc2", "inc3")
graph.add_edge("inc3", END)

app = graph.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "history-demo"}}

# 运行
result = app.invoke({"count": 0, "history": []}, config)
print(f"最终结果: {result}")

# 查看历史
print("\n=== 执行历史 ===")
history = app.get_state_history(config)

for i, checkpoint in enumerate(history):
    print(f"\nCheckpoint {i + 1}:")
    print(f"  State: {checkpoint.values}")
    print(f"  Next: {checkpoint.next}")
    print(f"  Checkpoint ID: {checkpoint.config['configurable'].get('checkpoint_id')}")
posted @ 2026-04-10 13:33  kingwzun  阅读(79)  评论(0)    收藏  举报