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)

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")],
})
🔍 执行搜索...
🧮 执行计算...
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)

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)
🤔 思考中... (迭代 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}")

=== 第一次调用(会中断) ===
📝 生成内容...
==================================================
⏸️ 等待人工审核...
内容: 这是生成的内容...
==================================================
状态: {'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')}")



浙公网安备 33010602011771号