图的力量——LangGraph 思维模型与第一个图 — LangGraph 实战——构建跨平台爆款图文 Agent 第1篇
第1章:图的力量——LangGraph 思维模型与第一个图
本章目标
读完本章你会:
- 能用自己的话解释"为什么 Agent 需要图而不是线性链条"
- 能定义一个
StateGraph,编写 node 函数,用 edge 把它们连起来 - 能编译(
compile)图并调用(invoke)它得到结果 - 能用 Mermaid 图可视化你构建的图结构
知识讲解
从一个生活例子开始
想象你走进一个快递分拣中心。
一个包裹从卡车卸下,它的旅程是这样的:
包裹到达 → 扫码分类 → 按地区分拣 → 装车 → 出发
每个环节是一个分拣站,站与站之间靠传送带连接。包裹在每个站被处理完后,上面的标签会被更新——比如从"未分类"变成"华东区"——然后顺着传送带滑向下一个站。
关键点来了:有些包裹的下一站不是固定的。 如果一个包裹标签写着"易碎品",传送带会把它分流到"易碎品专用通道";如果是"加急件",会直接跳到"优先装车站"。这个决策是在运行时根据标签内容做出的,不是预先定死的。
LangGraph 就是把这种"图"搬到了代码里:
| 快递中心 | LangGraph | 说明 |
|---|---|---|
| 包裹上的标签 | State(状态) | 记录当前所有信息,在节点间流转 |
| 分拣站 | Node(节点) | 一个 Python 函数,读标签、做处理、更新标签 |
| 传送带 | Edge(边) | 连接两个节点,决定数据往哪走 |
| 整个分拣系统 | StateGraph | 把所有节点和边组织起来的容器 |
| 启动前的线路检查 | compile() | 验证图结构没有死循环、没有断头路 |
| 把包裹放进系统 | invoke() | 传入初始数据,启动执行 |
思考一下: 如果包裹的下一站完全由当前标签内容决定(比如"易碎品走专用通道"),这对应 LangGraph 里的什么概念?答案稍后揭晓。
工作原理
为什么线性链条不够用?
假设你正在用代码写一个简单的"内容分析助手"。如果不使用 LangGraph,你可能会这样写:
def analyze_content(url: str) -> str:
raw = fetch_content(url) # 1. 抓取内容
cleaned = clean_html(raw) # 2. 清洗 HTML
summary = summarize(cleaned) # 3. 生成摘要
return summary
这是一个线性流程——管道式的,执行完 1 才能到 2,再到 3。对于简单的 ETL(提取-转换-加载)任务,这足够了。
但现在加一个需求:如果摘要质量不够好,需要重新生成。 线性代码立刻变得别扭:
def analyze_content(url: str) -> str:
raw = fetch_content(url)
cleaned = clean_html(raw)
summary = summarize(cleaned)
if quality_score(summary) < 0.7: # 不满意?
summary = summarize(cleaned) # 再来一次
if quality_score(summary) < 0.7: # 还不满意?
summary = summarize(cleaned) # 第三次...
return summary
问题显而易见:你在用 if 嵌套模拟循环,代码可读性差,而且实际需求里"不满意就重试"可能要重试 3 次、5 次,甚至需要人工介入——用线性代码去硬写这种逻辑,很快会变成一团乱麻。
更糟的是:如果需求变成"抓取完之后,根据内容类型走不同分支——视频内容走视频分析流程,图文内容走图文分析流程",嵌套的 if 会迅速失控。
这就是 LangGraph 要解决的问题:让你用"图"的方式描述复杂工作流——包括循环、分支、并行——而不是用 if/else 硬写。
StateGraph 是怎么工作的?
LangGraph 的核心模型极其简单,只有三个要素:
1. State(状态)——一个数据结构,所有节点共享。每个节点可以读取它、返回部分更新来修改它。它就像一个背包,在工作流的每个环节之间传递。
2. Node(节点)——一个 Python 函数,签名是 (state: State) -> dict。它接收当前完整状态,返回一个部分更新字典——只包含你想修改的字段。LangGraph 会自动把返回值合并到状态中。
3. Edge(边)——决定"从 A 节点之后去哪"。固定边(add_edge)意味着总是从 A 走到 B;条件边(add_conditional_edges)意味着根据状态内容动态选择下一站。
一个最小的 LangGraph 程序长这样:
START → node_a → node_b → END
用"启动→处理→汇总→结束"来理解:
- 传入初始数据给
START node_a做第一轮处理node_b做第二轮处理END结束
思考一下: 上面这个图有没有循环?没有——它只是一个顺序流程。那什么时候会出现循环?当某个条件边的返回值指向了前面的节点时,就形成了循环。比如"质量检查不通过 → 回到生成节点"——这是第 2 章要深入的内容。
执行流程:invoke 到底做了什么?
当你写 graph.invoke(initial_state) 时,LangGraph 内部发生了这些事:
- 从
START出发,找到第一条边连接的目标节点 - 执行该节点的函数,传入当前状态
- 把这个函数的返回值(一个 dict)合并到状态中
- 根据边的定义,找到下一个节点
- 重复 2-4,直到到达
END - 返回最终状态
每一步都是确定性的——同一个输入、同一个图,会产生同一个输出。这种确定性对于调试和测试非常关键。
代码实战
环境准备
在开始之前,打开终端安装 LangGraph:
pip install -U langgraph
检查安装: 安装完成后运行
python -c "import langgraph; print(langgraph.__version__)"确认成功。本章代码基于 LangGraph 0.3+ 版本。
基础版:最简两节点图
我们从整个教程中最简单的图开始——两个节点,一条边,不带任何分支。
打开你的编辑器,新建文件 chapter01_hello_graph.py:
"""
第 1 章 基础演示:最简两节点 LangGraph
LocalTrend 项目起点——一个能跑通的空骨架
"""
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# ============================================================
# Step 1: 定义 State(图的状态数据结构)
# ============================================================
class LocalTrendState(TypedDict):
"""LocalTrend 项目的全局状态。
所有节点共享这个结构,每个节点只返回自己需要更新的字段。
"""
raw_content: str # 原始内容(模拟抓取结果)
summary: str # 分析摘要
# ============================================================
# Step 2: 定义 Node 函数(图的处理单元)
# ============================================================
def fetch_content(state: LocalTrendState) -> dict:
"""模拟从平台抓取内容。
接收当前状态,返回一个部分更新 dict。
"""
print("[fetch_content] 正在抓取内容...")
# 实际项目中这里会调用 requests.get() 或网页抓取工具
# 本章先用模拟数据建立图的结构直觉
return {
"raw_content": "LangGraph 是构建 AI Agent 的强大框架,它用图来编排复杂工作流。"
}
def generate_summary(state: LocalTrendState) -> dict:
"""对抓取到的内容生成摘要。"""
print(f"[generate_summary] 正在分析内容: {state['raw_content'][:30]}...")
content = state["raw_content"]
# 实际项目中这里会调用 LLM,第 2 章会讲
summary = f"分析结果:这篇内容的核心主题是'{content[:20]}'"
return {"summary": summary}
# ============================================================
# Step 3: 构建 StateGraph,添加节点和边
# ============================================================
def build_graph() -> StateGraph:
"""构建 LocalTrend 的最简图骨架。"""
# 创建图容器,指定状态类型
builder = StateGraph(LocalTrendState)
# 添加节点:给每个函数起一个名字
builder.add_node("fetch_content", fetch_content)
builder.add_node("generate_summary", generate_summary)
# 添加边:定义执行顺序
# START → fetch_content → generate_summary → END
builder.add_edge(START, "fetch_content")
builder.add_edge("fetch_content", "generate_summary")
builder.add_edge("generate_summary", END)
# 编译:验证图结构并返回可执行对象
return builder.compile()
# ============================================================
# Step 4: 运行
# ============================================================
if __name__ == "__main__":
graph = build_graph()
# invoke() 接收初始状态,返回最终状态
# 注意:raw_content 和 summary 都传空字符串作为初始值
result = graph.invoke({"raw_content": "", "summary": ""})
print("\n=== 最终状态 ===")
print(f"raw_content: {result['raw_content']}")
print(f"summary: {result['summary']}")
在你自己的终端里运行它:
python chapter01_hello_graph.py
你应该看到类似这样的输出:
[fetch_content] 正在抓取内容...
[generate_summary] 正在分析内容: LangGraph 是构建 AI Agent 的强大框架...
=== 最终状态 ===
raw_content: LangGraph 是构建 AI Agent 的强大框架,它用图来编排复杂工作流。
summary: 分析结果:这篇内容的核心主题是'LangGraph 是构建 AI Age'
逐行解析
class LocalTrendState(TypedDict)
这就是我们的"包裹标签"。TypedDict 是 Python 标准库提供的一种类型提示方式——它定义了字典应该有哪些键、每个键是什么类型。LangGraph 用它来确保节点之间传递的数据结构是一致的。
注意我们只定义了两个字段:raw_content 和 summary。State 越精简越好——字段多了意味着每个节点都要理解更多上下文,增加心智负担。随着教程推进,我们会逐步给 State 添加新字段。
def fetch_content(state) -> dict
这是第一个节点。它的签名很关键:接收完整 State,返回 dict。返回的 dict 只包含你想更新的字段——不需要返回整个 State。LangGraph 会自动把你返回的 {"raw_content": "..."} 合并到全局状态中。
⚠️ 常见坑:初学者容易在节点里直接修改
state["raw_content"] = "xxx",然后什么都不返回。这样不会生效。 LangGraph 要求你显式return {"raw_content": "xxx"},框架才会追踪这个变更。直接修改 state 对象在 LangGraph 中是无效操作。
builder = StateGraph(LocalTrendState)
创建图容器。这一行告诉 LangGraph:"我要构建一个图,它的状态结构是 LocalTrendState。" 之后所有节点的读写都会对照这个结构。
builder.add_node("fetch_content", fetch_content)
把函数注册为节点,名字叫 "fetch_content"。节点名字很重要——之后添加边、定义条件路由都需要用这个名字来引用。名字可以任意取,但建议和函数名保持一致,减少理解成本。
builder.add_edge(START, "fetch_content")
START 是 LangGraph 内置的特殊常量,代表图的入口。这条边的意思是"图的执行从 fetch_content 节点开始"。如果你忘记加这条边,编译时会报错——LangGraph 不知道从哪里开始执行。
builder.add_edge("generate_summary", END)
END 同样是内置常量。连到 END 的节点执行完后,图就结束了。
builder.compile()
这一行做了很多事:检查所有边连接的目标节点是否存在、检查是否有不可达的节点、检查循环是否合理。如果图结构有问题,它会在这里抛出明确的错误,而不是等到运行时才崩溃。编译通过 = 图结构正确。
graph.invoke({"raw_content": "", "summary": ""})
invoke() 是启动图执行的方法。传入的字典必须是完整的初始 State——所有 TypedDict 定义的字段都要提供。执行完毕后,invoke() 返回最终状态的完整字典。
可视化你的图
LangGraph 内置了 Mermaid 图生成能力。在 chapter01_hello_graph.py 的 if __name__ == "__main__": 块中,graph = build_graph() 之后加入:
# 生成 Mermaid 图并保存
mermaid_code = graph.get_graph().draw_mermaid()
with open("chapter01_graph.md", "w", encoding="utf-8") as f:
f.write(mermaid_code)
print("图已保存到 chapter01_graph.md")
这段代码会把图的 Mermaid 描述保存到 Markdown 文件。在 VS Code 中安装 "Markdown Preview Mermaid Support" 插件后,可以直接预览图形。你应该看到:
START → fetch_content → generate_summary → END
养成习惯:每次修改图结构之后,先画出来看一眼——很多问题肉眼就能发现。
扩展版:增加预处理节点
上面的基础版只有一个"抓取→生成摘要"的流程。但实际项目中,抓取到的内容通常需要先清洗——去除 HTML 标签、截断过长文本、处理乱码等。
我们来给 LocalTrend 加一个清洗节点,同时演示多节点串行:
"""
第 1 章 扩展演示:三节点串行处理流水线
在基础版上增加一个内容清洗节点,展示多节点图的构建模式
"""
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class LocalTrendState(TypedDict):
raw_content: str
cleaned_content: str # 新增:清洗后的内容
summary: str
def fetch_content(state: LocalTrendState) -> dict:
"""模拟抓取——返回含 HTML 标签的原始内容"""
print("[fetch_content] 正在抓取...")
return {
"raw_content": "<html><body><h1>LangGraph 教程</h1><p>这是一个<strong>很棒的</strong>框架。</p></body></html>"
}
def clean_content(state: LocalTrendState) -> dict:
"""清洗内容:去除 HTML 标签。"""
raw = state["raw_content"]
print(f"[clean_content] 清洗中... 原始长度: {len(raw)} 字符")
# 简易 HTML 去标签——实际项目建议用 BeautifulSoup
import re
cleaned = re.sub(r"<[^>]+>", "", raw).strip()
print(f"[clean_content] 清洗后长度: {len(cleaned)} 字符")
return {"cleaned_content": cleaned}
def generate_summary(state: LocalTrendState) -> dict:
"""基于清洗后的内容生成摘要"""
# 优先用清洗后的内容
content = state.get("cleaned_content") or state["raw_content"]
print(f"[generate_summary] 分析内容: {content[:40]}...")
return {"summary": f"摘要:{content[:50]}..."}
def build_graph() -> StateGraph:
builder = StateGraph(LocalTrendState)
builder.add_node("fetch_content", fetch_content)
builder.add_node("clean_content", clean_content)
builder.add_node("generate_summary", generate_summary)
# 三节点串行流水线
builder.add_edge(START, "fetch_content")
builder.add_edge("fetch_content", "clean_content")
builder.add_edge("clean_content", "generate_summary")
builder.add_edge("generate_summary", END)
return builder.compile()
if __name__ == "__main__":
graph = build_graph()
result = graph.invoke({
"raw_content": "",
"cleaned_content": "",
"summary": "",
})
print(f"\n=== 最终状态 ===")
print(f"raw: {result['raw_content'][:60]}...")
print(f"cleaned: {result['cleaned_content']}")
print(f"summary: {result['summary']}")
运行后你会看到三个节点按顺序执行。关键观察:clean_content 读取了 fetch_content 写入的 raw_content,而 generate_summary 读取了 clean_content 写入的 cleaned_content。这就是图的威力——节点之间通过 State 传递数据,每个节点只关心自己的输入和输出,不需要知道上游和下游的细节。
本章小结
- LangGraph 把复杂工作流建模为图——节点做处理,边控制流向,State 在它们之间传递数据。
- State 是
TypedDict定义的数据结构,所有节点共享。节点通过返回 dict(而非直接修改 state)来更新它。 StateGraph是图容器;add_node注册节点函数;add_edge定义固定流向。START和END是内置常量,标记图的入口和出口。忘记START边会导致图无法启动。compile()验证图结构,invoke()同步执行图。先编译后执行——编译通过表示结构正确。- 每个节点只返回自己修改的字段——LangGraph 自动合并部分更新到全局 State。
get_graph().draw_mermaid()可以可视化图结构——养成"画出来看看"的习惯。
关键术语
| 术语 | 释义 |
|---|---|
| StateGraph | LangGraph 的核心类,用于定义、构建、编译一个有向图工作流 |
| State | 图中所有节点共享的数据结构(TypedDict / Pydantic),在节点间流转 |
| Node(节点) | 一个 (state) -> dict 签名的 Python 函数,读取 State 返回部分更新 |
| Edge(边) | 定义节点之间的流向,固定边(add_edge)总是从 A 到 B |
| START | 内置常量,标记图的入口。必须有一条边从 START 出发 |
| END | 内置常量,标记图的出口。到达 END 的边意味着执行结束 |
| compile() | 验证图结构(边连通性、节点存在性)并返回可执行 Runable |
| invoke() | 传入初始 State 同步执行图,返回最终 State |

浙公网安备 33010602011771号