Day 40 / 共 68 天 · 阶段 7 编排框架
LangGraph① 把 Agent 画成图
昨天(Day 39)你列出了手写循环的痛点清单。今天主角登场:用 LangGraph 最核心的三个概念——StateGraph(状态图)、add_node(加节点)、add_edge(连边),把一个流程画成一张图并跑起来。先掌握"直线流程"这一半;明天(Day 41)再补"条件分支 + 循环 + 人在环路"另一半。
📍 你在阶段 7(编排框架 D39-44)的位置
D39 为什么要框架→
D40 LangGraph①→
D41 LangGraph②→
D42 CrewAI→
D43 eino→
D44 选型
💡 用一个类比兜住今天(今天全程沿用「画一张地铁线路图」的世界观)
用 LangGraph 搭 Agent = 画一张地铁线路图。StateGraph = 整张线路图(你在图纸上规划);节点 node = 一个个车站(每站干一件事:检索站、写稿站、审校站);边 edge = 站与站之间的轨道(规定列车怎么开);START / END = 始发站和终点站;state 状态 = 乘客随身带的一个行李箱,每到一站往里放东西、也从里拿东西,一路带到终点;compile 编译 = 线路图审核通过、正式通车;invoke 调用 = 发一趟车,乘客(输入)从始发站坐到终点站。今天你先修一条"直达线"(没有换乘分支),明天再修支线。
L01
三个核心词,一次记住
🤔 痛点LangGraph 文档一上来一堆名词,零基础容易劝退。其实入门只要三个词就能跑通第一个图。
💡 本质就三样:①State=行李箱(定义图里传的数据长啥样)、②node=车站(一个函数,读行李箱、往里写东西)、③edge=轨道(规定从哪站开到哪站)。搭图=定义行李箱→摆几个车站→用轨道连起来→通车→发车。
图注:今天要搭的就是这条直达线。看懂这张图,代码就是把它"翻译"成几行调用。
👶 装一下练习前先装:
pip install langgraph(在你的虚拟环境里,回忆 Day05)。它是 LangChain 团队出的、专门画 Agent 流程图的库。L02
第一步:定义 State(行李箱里装什么)
🤔 痛点各车站要共享数据(Day37 的"共享黑板"),那得先说清楚这个"行李箱"里有哪些格子、装什么类型。
💡 本质用一个
TypedDict(带类型标注的字典)声明 state 的结构——就是给行李箱贴上分格标签:哪格放城市、哪格放天气、哪格放消息列表。特别地,像"消息列表"这种要追加不覆盖的格子(Day37 讲过!),用 Annotated[list, add] 标一下,框架就会自动帮你 +=。from typing import TypedDict, Annotated
from operator import add # add 就是"相加/拼接",用来标记"这格要追加"
class State(TypedDict): # 定义行李箱的分格
city: str # 覆盖型:直接存最新值
weather: str # 覆盖型
messages: Annotated[list, add] # 追加型:每站往里加,旧的保留(Day37 的"追加")
TypedDict 你就当"带说明书的字典"看。用它的好处是:编辑器会提示你有哪些格子、类型对不对,少打错字。Annotated[list, add] 读作"这是个 list,更新时请用 add(追加)而不是覆盖"。
L03
add_node:一个函数就是一个车站
🤔 痛点怎么定义"每一站干什么活"?会不会又要学一堆继承、类、装饰器?
💡 本质一个节点就是一个普通函数:参数是
state(进站时的行李箱),返回一个字典(这一站要往行李箱里写/改的格子)。框架会自动把你返回的字典合并进 state(覆盖型直接换、追加型自动加)。就这么朴素——和昨天手写的"环节函数"几乎一样。def 检索(state: State): # 一个车站 = 一个函数,收 state
city = state["city"]
info = f"{city} 今天 5℃,有风" # 假装查到的资料
# 只返回"这一站要改的格子",框架自动合并进行李箱
return {"weather": info, "messages": [f"检索到:{info}"]}
def 写稿(state: State):
advice = f"根据「{state['weather']}」,建议穿厚外套 🧥"
return {"messages": [advice]} # messages 是追加型 → 自动接在后面
📝 举个例子:节点返回后,行李箱怎么变
进"检索"站前行李箱是
{city:"北京", messages:[]};检索返回 {weather:"...5℃...", messages:["检索到:..."]}。框架合并后:weather 是覆盖型→直接填上;messages 是追加型→变成 ["检索到:..."]。你只操心"这站产出啥",合并规则由行李箱的分格标签(Annotated)决定。👶 关键直觉节点函数不用改整个 state,只返回"我这站动了哪几格"。剩下的合并框架包办。这比昨天手写"整个 state 传来传去"更省心、更不易错。
L04
add_edge:用轨道把车站连起来
🤔 痛点车站摆好了,列车怎么知道先到哪站、再到哪站、哪站是终点?
💡 本质用
add_edge(甲, 乙) 铺一条"甲→乙"的轨道。特殊两站:START(始发,列车从哪进)和 END(终点,到这趟车结束)。今天先只连直线:START→检索→写稿→END。明天(Day41)才学"看情况走哪条轨"的条件边。from langgraph.graph import StateGraph, START, END
g = StateGraph(State) # 拿出图纸,声明这张图用哪种行李箱
g.add_node("检索", 检索) # 摆上"检索站"(名字, 对应的函数)
g.add_node("写稿", 写稿) # 摆上"写稿站"
g.add_edge(START, "检索") # 始发 → 检索站
g.add_edge("检索", "写稿") # 检索站 → 写稿站
g.add_edge("写稿", END) # 写稿站 → 终点
👶 小白:节点名为什么要用字符串 "检索",直接用函数不行吗?
👨🏫 老师:名字(字符串)是这个站的站牌,连边、画图、看轨迹时都用它指代。函数是这站干的活。分开的好处:一个站可以改活(换函数)但站牌不变,连边不受影响;画流程图和看 trace(Day39 讲的)时,显示的也是这些好懂的站名,而不是一堆函数地址。
L05
compile & invoke:通车 & 发车
🤔 痛点图画完了,怎么让它真跑起来?怎么把用户输入喂进去、把结果拿出来?
💡 本质两步:
app = g.compile() = 线路图审核通过、正式通车(生成一个能用的对象);app.invoke(初始行李箱) = 发一趟车,给它一个装了初始数据的 state,列车从 START 一路跑到 END,把终点时的完整行李箱还给你。app = g.compile() # 通车:把图纸变成可运行的 app
result = app.invoke({"city": "北京", "messages": []}) # 发车:塞入初始行李箱
print(result["messages"]) # 拿到终点时的完整行李箱
# → ['检索到:北京 今天 5℃,有风', '根据「北京 今天 5℃,有风」,建议穿厚外套 🧥']
print(result["weather"]) # → 北京 今天 5℃,有风
想"看图"?编译后的 app 能导出流程图(如 app.get_graph().draw_mermaid() 生成一段可渲染的图代码)——昨天说的"可视化"痛点,框架顺手就解决了。想接断点续跑,compile(checkpointer=...) 传个存档器即可(Day37 的概念,明天会用到)。
L06
完整可跑示例:把前几讲拼起来
💡 本质下面就是一个从头到尾能跑的最小 LangGraph 程序。对照昨天(Day38)手写的 60 行看:你不再自己写循环、自己合并 state——只声明"有哪些站、怎么连",跑循环和合并行李箱的活,框架全包了。
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
# 1) 行李箱
class State(TypedDict):
city: str
weather: str
messages: Annotated[list, add]
# 2) 车站(节点函数:收 state,返回要改的格子)
def 检索(state):
info = f"{state['city']} 今天 5℃,有风"
return {"weather": info, "messages": [f"检索到:{info}"]}
def 写稿(state):
return {"messages": [f"根据「{state['weather']}」,建议穿厚外套 🧥"]}
# 3) 画图:摆站 + 连轨
g = StateGraph(State)
g.add_node("检索", 检索)
g.add_node("写稿", 写稿)
g.add_edge(START, "检索")
g.add_edge("检索", "写稿")
g.add_edge("写稿", END)
# 4) 通车 + 发车
app = g.compile()
out = app.invoke({"city": "北京", "messages": []})
for m in out["messages"]:
print(m)
# 检索到:北京 今天 5℃,有风
# 根据「北京 今天 5℃,有风」,建议穿厚外套 🧥
📝 举个例子:想加一个"翻译成英文"的站,多简单
写个
🔗 想吃透 StateGraph / 节点 / 边 的更多细节和源码?去看《LangGraph 20 天精讲》→(学完回来继续 Day 41)
def 翻译(state): return {"messages": ["EN: dress warmly"]},再 g.add_node("翻译", 翻译)、把 g.add_edge("写稿", END) 改成 g.add_edge("写稿","翻译") + g.add_edge("翻译", END)。加一站、挪一条轨,流程就变了——这正是 Day39 说的"图比 if 迷宫好改"。L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- LangGraph 入门的三个核心词是什么?(State 行李箱 / node 车站 / edge 轨道)
- 节点函数长啥样?它返回的是整个 state 还是"只返回改动的格子"?
Annotated[list, add]是什么意思?对应 Day37 哪个概念?(追加不覆盖)START / END是什么?compile和invoke分别干嘛?(通车 / 发车)- 对照昨天手写的 60 行,框架替你省掉了哪两件事?(自己写循环 / 自己合并 state)
✋ 动手 10 分钟:跑通你的第一张图,再加一站
装好后把 L06 完整代码敲进 day40.py 跑通,然后动手改造:
# 任务:在"写稿"之后加一个"润色"站,把建议加上一句鼓励的话
def 润色(state):
last = state["messages"][-1] # 取最后一条(写稿的输出)
return {"messages": [last + " 出门注意保暖,加油!☀️"]}
g.add_node("润色", 润色) # 摆新站
# 把原来的 写稿→END 改成 写稿→润色→END:
g.add_edge("写稿", "润色")
g.add_edge("润色", END)
# 重新 compile 再 invoke,观察 messages 多了润色那句
# 进阶:试试 print(app.get_graph().draw_ascii()) 把你的线路图打印出来看看
体会:改流程 = 加个函数 + 挪几条边。这就是"编排"的手感。
明日预告 · Day 41:今天修的是"直达线"。但真实 Agent 要看情况拐弯、要循环、要中途等人。明天 LangGraph② 学三样杀器:
add_conditional_edges(条件边——到路口按行李箱内容决定走哪条轨)、循环(让列车绕回某站,做出昨天手写的那种"想→动手→再想"主循环)、interrupt(Day37 的"中途等人"真正落地)。把今天的直达线,升级成会思考、会拐弯的智能线网。