
📚 系列:《Agent 从入门到上手》· 30 天连载 · 第 17 篇(框架篇第 3 篇) 🎯 本篇目标:把 State / Node / Edge / Conditional Edge 四个词讲到你能在白板前给别人画出来;亲手画一张「会回头」的图;用实测数字看清「图 vs 裸 Agent」的成本账;最后给图装上人工审核闸口、玩一把时间旅行 ⏱️ 阅读时长:约 30 分钟 🧪 特别说明:本篇所有运行结果都在 langgraph 1.2.18 上真机跑过,依旧零 API Key、零网络 (沿用上一篇那个「按剧本说话的假模型」)。顺带一提:Day 16 你用
create_agent搭的那个 Agent,底层跑的就是今天的主角 LangGraph ------ 上篇是租房子,这篇带你参观地基
一、开场:三种活儿,链子串不住
上一篇结尾我说了一句:Chain 是流水线,Agent 是自由职业者,LangGraph 是给自由职业者画的施工图。今天来验证这句话。
先看三个活儿,你品品哪个能用 Day 16 的链条串起来:
活儿一(直线的): 把用户输入翻译成英文 → 润色 → 存档。三步,永远这个顺序,谁也不等谁。这是链的主场,一根竖线 | 串完收工。
活儿二(看情况的): 帮我查资料写行业观察。问题来了 ------ 查几次才够? 不知道。查完第一次,得有人看一眼说"不够,再查";查完第二次,还是有人说"不够";第三次终于说"够了,写吧"。这条流程会回头。
活儿三(要等人的): 帮我起草一份对外发的公告。写完初稿不能直接发,得等主管审核 ------ 而主管可能在开会,可能在午休,可能请了年假。这条流程会中途停下来等人,等到了还得按人的答复决定走哪条路。
用一张表总结:
| 活儿的类型 | 例子 | 链能干吗 | 裸 Agent 能干吗 |
|---|---|---|---|
| 直线活 | 翻译 → 润色 → 存档 | ✅ 主场 | 能干,但杀鸡用牛刀 |
| 看情况的活 | 查资料 → 判断够不够 → 不够再查 | ❌ 没有回头路 | ✅ 但路径失控、成本失控 |
| 要等人的活 | 写稿 → 等审核 → 发 / 重写 | ❌ 停不下来 | ⚠️ 能停,但停了状态就丢了 |
🍜 比喻:链是绿皮火车,图上怎么走全写死在时刻表里;裸 Agent 是没有司机的网约车,方向它自己定,你想去哪儿全凭它心情;图(LangGraph)是地铁 ------ 路线可以分岔、可以换乘,但每一条轨道都是画好的,你永远出不了这张网。
所谓编排(Orchestration),翻译成人话就是:给"会拐弯"的流程画一张地图,让灵活性长在边框里。 今天就干这个。
二、四个词讲透:State / Node / Edge / Conditional Edge
打开 LangGraph 文档,扑面而来的是 Graph、StateGraph、Pregel、Channel、Reducer、Checkpointer...... 别慌,真正需要记住的只有四个,剩下全是这四个的装配件。

2.1 State:所有节点共享的一块白板
先想一个问题:工地上三个工人分工干活,他们怎么知道彼此的进度?靠一块大家都能看、都能写的白板。 State 就是这块白板。
import operator
from typing import Annotated, TypedDict
class DocState(TypedDict):
notes: Annotated[list, operator.add] # 有归约器:只往上贴
revision: int # 没归约器:后写的覆盖先写的
draft: str
人话翻译:State 就是"这个任务到目前为止的所有进展"。它的每个字段有两种命运,这也是 LangGraph 第一个容易翻车的地方:
| 字段写法 | 行为 | 适合装什么 |
|---|---|---|
notes: Annotated[list, operator.add] |
新值追加到旧值后面 | 查到的资料、日志、多轮意见 |
revision: int(无归约器) |
新值直接覆盖旧值 | 当前第几稿、当前主题 |
那个写在 Annotated 里的 operator.add,学名叫 Reducer(归约器)------ 规定"两个节点都想写这个字段时,怎么合并"。
人话翻译:Reducer 就是白板上贴的便签。 没写归约器的字段是一块黑板,后写的人直接把前面的擦了;写了归约器的字段是一面便签墙,只许往上贴,不许撕。我实测把两种都跑了一遍:
notes (有 operator.add 归约器)→ 累积了 3 条资料 ✅
revision (无归约器,最后写的赢)→ 9 (节点1 写 1,节点2 写 9,1 被冲掉)
title (无归约器,最后写的赢)→ 「被覆盖的标题」 (「初稿标题」没了 ❌)
更刺激的是并行分支同时写同一个字段 。两条分支干完活,同时往没有归约器的 winner 字段上写,LangGraph 当场把桌子掀了:
当场抛错:InvalidUpdateError
报错原文:At key 'winner': Can receive only one value per step.
Use an Annotated key to handle multiple values.
↑ LangGraph 不让你悄悄丢数据,它当场拦下来 ------ 但注意:
只有在「同一步里两个节点都写」时才拦得住;不同步写,它照样覆盖。
给 winner 也补上 Annotated[list, operator.add] 再跑,两条意见都稳稳贴在墙上:winner=['A', 'B'] ✅。
🏗️ 比喻:两个实习生同时往同一张便利贴上写字,字必然叠罗汉。 与其指望他们自觉排队,不如干脆人手一张便利贴(各自 append 到列表),最后由你来汇总 ------ 归约器就是"人手一张"这句话在代码里的写法。
2.2 Node / Edge / Conditional Edge:工人、箭头、放行章
Node(节点)就是一个普通 Python 函数 ,签名长这样:def node(state) -> dict。吃进整个 State,吐出一个字典 ------ 注意,吐的是"增量"不是"全量":你说"我要把 notes 加一条、revision 加一",LangGraph 负责帮你合并到白板上。
Edge(边)就是传送带 :g.add_edge("research", "judge"),意思是 research 干完必去 judge。起点终点是两个内置常量 START 和 END。
Conditional Edge(条件边)是岔路口的指示牌 ------ 这是四件套里唯一的新东西,也是整个 LangGraph 的灵魂:
def route(state):
if len(state["notes"]) >= 2:
return "draft" # 资料够了 → 去写稿
return "research" # 不够 → 回头再查
g.add_conditional_edges("judge", route, {"research": "research", "draft": "draft"})
人话翻译:条件边 = 质检员的放行章。 质检员(route 函数)看一眼白板上的进度,合格的盖"放行"章送下一道,不合格的盖"回炉"章推回上一道。注意一个微妙但重要的事实:做判断的是你写的普通 Python 代码,不是模型。 模型可以在节点里干活,但"走哪条路"这个决定权,你随时可以收回自己手里 ------ 这就是"有边界的自由"。
三、最小图跑通:让流程第一次学会「回头」
四件套备齐,搭一张最小的图。任务:查资料 → 判断够不够 → 不够回头 → 够了成稿。
from langgraph.graph import StateGraph, START, END
g = StateGraph(DocState)
g.add_node("research", research) # 工人一:查资料
g.add_node("judge", judge) # 工人二:判断够不够
g.add_node("draft", draft) # 工人三:写稿
g.add_edge(START, "research") # 上班:先去查资料
g.add_edge("research", "judge") # 查完必送检
g.add_conditional_edges("judge", route, # 质检员决定去留
{"research": "research", "draft": "draft"})
g.add_edge("draft", END) # 写完下班
app = g.compile() # 编译成可执行对象
out = app.invoke({"notes": [], "rounds": 0, "draft": ""})
20 行以内,一个"会自己回头"的流程就跑起来了。 实测节点轨迹(门槛设为 2 条资料):
节点轨迹: research(第1轮) → judge → research(第2轮) → judge → draft
收集 2 条 / 共 2 轮
成稿(60 字):2026 年 Agent 岗位同比增长 61%,企业落地以客服、文档、数据分析为主,
失败主因是需求边界不清与缺少评测集。
看到中间那次 judge → research 了吗?那就是回头。 第一轮 judge 说不够,流程自己拐回 research 又查了一轮。然后把门槛从 2 改成 3(只改 route 里一个数字),其他一行不动:
节点轨迹: research#1 → research#2 → research#3 → draft
它自己回头查了 2 次,凑够 3 条才动笔 ✅
对比一下三种写法在这个任务上的表现:
| 链(Chain) | 裸 Agent | 图(LangGraph) | |
|---|---|---|---|
| 能不能回头 | ❌ 轨道是直的 | ✅ 模型自己决定 | ✅ 但条件写在你代码里 |
| 回头几次 | 0 次(写死的) | 不确定,可能 19 次 | 恰好需要的次数(你封顶) |
| 谁在控制 | 你(写死的) | 模型 | 你 + 模型分工 |
| 出了错怎么查 | 第几步很清楚 | 翻消息轨迹猜 | judge 的每次放行都有记录 |
🔀 比喻:链是单行道,走错只能到终点认栽;裸 Agent 是路痴网约车,它知道要回头,但它可能绕三圈;图是带掉头车道的立交桥 ------ 回头可以,但只能在画好的虚线口掉头。
「回头」这件事,链做不到、裸 Agent 做得到但管不住,图是唯一让回头既受控又可观测的写法 ------ 回头的条件就是 route 里那个 return,白纸黑字,可测试、可审计。
四、图 vs 裸 Agent:这笔成本账,实测给你看
上面说裸 Agent"管不住",空口无凭。Day 16 我们算过一次模型调用次数,今天算得更狠一点:同一个任务,两边分别把多少字喂给模型(喂进去的字数 ≈ 烧掉的钱,这个等式在 API 计费下基本成立)。
任务的设定:要收集 N 条资料(每条 600 字符,检索返回的真实文档片段就是这个量级),然后成稿。
- 裸 Agent 硬跑 :每一轮都把完整历史原样回喂 ------ 这不是它抠门,是 Day 13 讲的原理:模型无状态,历史必须每次重发。第 1 轮 48 字符,第 2 轮 678,第 3 轮 1308......到第 8 轮已经 4458,成稿那一轮 5088。每一轮都在变胖。
- 图编排 :research 节点把原始资料压成 120 字符的要点贴到 State 上;judge 只看要点(每次固定 147 字符);draft 最后才看一次全文。当然要诚实:压缩本身也要花一次模型调用,这笔账我算进去了。
实测结果(langgraph 1.2.18 真机数据):
| 收集轮数 | 裸 Agent | 图(理想) | 图(诚实版) | 诚实版节省 | 倍数 |
|---|---|---|---|---|---|
| 1 | 726 | 294 | 921 | −26.9% | 0.79× |
| 2 | 2,034 | 561 | 1,815 | 10.8% | 1.12× |
| 3 | 3,972 | 828 | 2,709 | 31.8% | 1.47× |
| 5 | 9,738 | 1,362 | 4,497 | 53.8% | 2.17× |
| 8 | 23,112 | 2,163 | 7,179 | 68.9% | 3.22× |

这张表有三个必须盯住的读数:
- 只查 1 条就成稿,图反而贵 26.9%。 因为多了"压摘要"这道工序,省下的还没花的多。短流程别上图 ------ 这是我见过的最容易被神化的框架误区,图不是级别更高的存在,它是个工具。
- 轮数越多,差距越夸张 :3 轮省 32%,8 轮省 69%。原因很简单:裸 Agent 的账单是轮数的平方(每轮都背着之前所有轮的历史),图的账单接近线性(judge 只看定长的要点)。
- "平方账单"就是 Day 16 那个"平均调用 19 次"翻车现场的数学解释。 19 轮的历史回喂,你付的不是一个任务的价钱,是 19 个越来越大的任务的价钱。
🍱 比喻:裸 Agent 吃自助餐,每拿一盘都把之前吃过的重新端上来再吃一遍 ------ 撑的是它,掏钱的是你。图的每个节点只吃自己盘子里那一口。
顺便说一句测量口径:这里用"字符数"做代理指标(真实计费按 token,中文约 1.5~2 字符一个 token,比例结论不变 )。想要精确账单,把假模型换成真模型,读回调里的 token_usage 即可。
五、人工审核:interrupt + Checkpointer,图的绝活
现在上强度:活儿三那种"写到一半停下来等人"的流程。这是链和裸 Agent 的绝对盲区,却是图的看家本领,一共就三行核心代码。
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
def human_review(state):
answer = interrupt({ # ← 在这里喊一声"停"
"question": "这份稿子能发吗?(approve / reject:原因)",
"doc": state["doc"],
"revision": state["revision"],
})
return {"decision": "approve" if str(answer).startswith("approve") else "reject",
"feedback": str(answer).split(":", 1)[1] if ":" in str(answer) else ""}
app = g.compile(checkpointer=InMemorySaver()) # ← 存档器,暂停全靠它
out = app.invoke(state, config={"configurable": {"thread_id": "report-001"}})
interrupt() 一被调用,整张图当场挂起 ,invoke 的返回值里带着待办事项,程序干干净净地停住(不是死循环占着 CPU,是真的"下班了")。人来之后,一行恢复:
app.invoke(Command(resume="approve"), config=cfg) # 从暂停处接着跑
实测完整跑一遍"启动 → 打回 → 修改 → 通过 → 发布":
① 启动 → 程序主动停下来
卡在节点:('human_review',)
待办事项:这份稿子能发吗?(approve / reject:原因)
此时 published = '' ← 空着,还没发
② 人工点「打回」,并捎一句意见:数据来源要标注
[节点 human_review] 人工回复:reject: 数据来源要标注
[节点 draft] 出第 2 稿......现在又卡在:('human_review',)
③ 人工点「通过」
[节点 publish] ✅ 已发布
注意一个细节:中间停了多久,代码完全不知道也不在乎。 停 3 秒和停 3 天对它没区别 ------ 因为状态被 InMemorySaver 按 thread_id 完整存档了(生产环境换成 SqliteSaver / PostgresSaver,就是断电也不怕)。这个 thread 一共留下了 7 个检查点,每个都记录了当时白板上写了什么、下一步去哪:
thread「report-001」共留下 7 个检查点(从旧到新):
[0] 下一站=('__start__',) 第---稿 decision=---
[1] 下一站=('draft',) 第0稿 decision=---
[2] 下一站=('human_review',) 第1稿 decision=--- ← 第一次卡住
[3] 下一站=('draft',) 第1稿 decision=reject
[4] 下一站=('human_review',) 第2稿 decision=reject ← 第二次卡住
[5] 下一站=('publish',) 第2稿 decision=approve
[6] 下一站=() 第2稿 decision=approve ← 跑完了

人话翻译:这套机制 = 游戏存档。 interrupt() 是"暂停菜单",checkpointer 是存档文件,thread_id 是存档槽位。你随时退出、随时读档,甚至(下一节)从任意一个存档点开新档。
但这个绝活有两个必须刻在脑子里的坑,都是实测踩出来的:
| # | 坑 | 后果 | 解法 |
|---|---|---|---|
| 1 | 恢复运行时,被中断的节点会从头再跑一遍 (不是从 interrupt() 那一行续跑) |
节点里写在 interrupt() 之前的发邮件、扣款、写库操作会被重复执行 |
副作用全部放在 interrupt() 之后,或做成幂等(重复执行无害) |
| 2 | 人工可能永远点「打回」 | 流程无限回头,负责人放假一个月,稿子改了一千稿 | 图里自己加封顶(如"第 3 稿强制送审")+ 外面再配 recursion_limit 兜底 |
🛵 比喻:interrupt 就是外卖到了放门口。 骑手不会在你家门口扎帐篷等你(挂起不占资源),你三小时后回来,外卖(State)还在门口(Checkpointer 里),而且你知道是放在 1 号门还是 2 号门(thread_id)。
六、两个进阶招:时间旅行与并行扇出
6.1 时间旅行:回到过去改一笔,历史不会被抹掉
既然每个检查点都是完整存档,那能不能回到三步之前,换个决定重新跑?能,两行:
hist = list(app.get_state_history(cfg)) # 拿到全部检查点
target = ... # 选中「大纲已定、正文未写」那个
fork_cfg = app.update_state(target.config, # 就地改一笔
{"outline": "大纲:① 落地场景(提到最前)② ..."})
branch = app.invoke(None, config=fork_cfg) # 从分叉点继续跑
实测:
原线跑完:正文(按「大纲:① 岗位增长 ② 落地...」展开)
选中检查点:下一站=('body_node',)
改写 outline → 生成新分叉
分叉重跑结果:正文(按「大纲:① 落地场景(提到最前...」展开)
回到原线终点看一眼:正文(按「大纲:① 岗位增长 ② 落地...」展开)
↑ 检查点一个都没删,只是多了条岔路
最后那行是精髓:原线的结果原封不动还在。 这不是"撤销重做",是分叉 ------ Git 的时间线,不是橡皮擦。
🎮 比喻:这是游戏里的"读档开新档",不是"重开游戏"。 你读三小时前的档试了另一种打法,原来的存档还在列表里躺着 ------ 两个结局都是真实的,随时可以回去看。
这个能力在真实业务里就是"灰度实验":同一条生产流程,从某个检查点分出去一条支线换个模型、换个提示词跑一遍,两条线的每一步都摆在台面上比。没有 Checkpointer 的框架,这种事你只能靠复制粘贴改代码。
6.2 并行扇出:Send,三个工人同时开工
最后一把快刀。写行业观察要起草三个章节,每个 300ms(一次真实的模型调用耗时)。串行是排队打饭,Send 是开三个窗口:
from langgraph.types import Send
def fanout(state): # 条件边的返回值不是"下一站",是一串任务
return [Send("write_one", {"topic": s}) for s in SECTIONS]
g.add_conditional_edges("plan", fanout, ["write_one"])
实测:
并行(Send 扇出 3 份):302.3 ms
串行(老实排队 3 份):901.4 ms
⚡ 提速 2.98×(理想 3×,差的是调度开销)
注意两个细节:① 三个并行节点往 chapters 字段里写结果,靠的还是 2.1 节的归约器 ------ 没有归约器,并行就是事故 ,四件套是环环相扣的;② Send 允许给每个分支塞不同的输入(这里塞了不同的章节主题),这是它比普通"并行边"更强的地方,map-reduce 就靠它。
七、泼盆冷水:三个信号,提醒你先别上图
讲了这么多优点,必须补一节"反向说明书"。图不是 Agent 开发的毕业证书,它是流程复杂度税 ------ 你提前支付的每一张节点、每一条边,之后都要维护。出现下面三个信号之一,请把手从 StateGraph 上拿开:
| 信号 | 典型现场 | 更划算的选择 |
|---|---|---|
| 流程里没有任何"看情况"的分支 | 翻译 → 润色 → 存档,永远三步 | LCEL 链,一根竖线串完(Day 16) |
| 判断本身就是模糊的、全权交给模型的 | 开放式调研、头脑风暴 | 裸 Agent + 三道护栏(Day 16 第六节) |
| 任务短到不需要等人、不需要回头 | 单轮问答、格式转换 | 直接 model.invoke(),框架都省了 |
反过来,出现下面任何一条,图就值得上:
- 流程会回头,且回头条件你能说清楚(哪怕说不全);
- 流程要等人 ,或者中途要碰不可逆操作(发邮件、付款、删库);
- 一次运行烧的钱多到值得留档(出了问题要能回放、能对账);
- 多个步骤天然能并行,且你在乎时间。
📐 比喻:图是工程图纸,而工程图纸是要有人审、有人改、有人存档的。 家里挂幅画需要图纸吗?不需要。但盖楼需要 ------ 区别不在于画图帅不帅,在于这栋楼塌了会不会砸到人。你的 Agent 流程"塌了"会砸到什么?答案决定你要不要上图。
一个我在真实项目里反复验证的经验法则:先写最土的版本跑通(链或裸 Agent),等它第一次因为"没有分支/没有存档/没人审批"出事故,再把那一段换成图。 让事故告诉你图的边界画在哪,比让架构癖告诉你准得多。
八、动手环节:六个演示,全部真机跑过
脚本在 temp/day17_langgraph_lab.py,零 API Key、零网络(真 LangGraph + 假模型),换台机器照样跑。六个演示的实测输出摘要:
演示 1|State 与归约器
notes (有 operator.add 归约器)→ 累积了 3 条资料 ✅
revision (无归约器,最后写的赢)→ 9 (节点1 写 1,节点2 写 9,1 被冲掉)
并行两分支同写 winner(无归约器)→ InvalidUpdateError: At key 'winner':
Can receive only one value per step. Use an Annotated key to handle multiple values.
补上归约器后 → log=['A 干完了', 'B 干完了'] winner=['A', 'B'] ✅
演示 2|最小图与回头(见第三节,两种门槛的完整轨迹)
演示 3|成本对照(见第四节表格:1 轮 −26.9% / 3 轮省 31.8% / 8 轮省 68.9%)
演示 4|interrupt 与 7 个检查点(见第五节完整输出)
演示 5|时间旅行分叉
检查点清单(从新到旧,共 4 个):
[0] 下一站=() outline=大纲:① 岗位增长 ② ...
[1] 下一站=('body_node',) outline=大纲:① 岗位增长 ② ... ← 选中这个
[2] 下一站=('outline_node',) outline=
[3] 下一站=('__start__',) outline=None
分叉重跑结果:正文(按「大纲:① 落地场景(提到最前...」展开)
原线终点仍是:正文(按「大纲:① 岗位增长 ② 落地...」展开)
演示 6|并行与护栏
并行(Send 扇出 3 份):302.3 ms 串行:901.4 ms 提速 2.98×
回头上限 ≤3:tries=3,正常收工 ✅
拿掉回头上限 + recursion_limit=8 → 抛 GraphRecursionError
⚠️ 诚实的边界说明 :演示 3 的成本数字基于"字符数 ≈ token"的代理计量,模型行为用剧本假模型固定;真实计费按各家 token 折算,比例结论(平方 vs 线性)不变。
InMemorySaver重启即失忆,生产必须换持久化的 Saver。
跑完这六段,你应该有体感了:图的四件套解决"流程怎么拐弯",interrupt 解决"流程怎么等人",Checkpointer 解决"人不在怎么办",Send 解决"活儿怎么分"。 这四个答案合起来,就是 Day 22 要讲的"多智能体协作"的地基 ------ 一个团队,无非是多个节点 + 更复杂的图。
九、踩坑清单:这六条能省你一个周末
| # | 症状 | 真正的原因 | 解法 |
|---|---|---|---|
| 1 | InvalidUpdateError: Can receive only one value per step |
并行节点同时写无归约器的字段 | 给该字段加 Annotated[list, operator.add] |
| 2 | 并行分支"偶尔"丢数据 | 不同步的写入不会报错,直接覆盖 | 共享进度类字段一律配归约器;状态字段(如"当前第几稿")才允许裸写 |
| 3 | 恢复后副作用执行了两次 | 被中断的节点从头重跑 | 副作用放 interrupt() 之后,或做幂等 |
| 4 | 死循环图跑不停 | 回头条件永远是真 | 状态里自己计回头次数封顶 + config={"recursion_limit": N} 兜底 |
| 5 | 重启后 thread_id 里的状态全没了 |
InMemorySaver 只存在内存里 |
生产换 SqliteSaver / PostgresSaver |
| 6 | 简单任务上图后成本反升 | 压缩/摘要工序的开销 > 节省 | 3 轮以内的直线活,链更划算(见第四节第 1 行) |
🧰 比喻:这六条是地铁司机手册的六页。 不用会修列车,但"这个灯亮了先减速、那个灯亮了立刻报调度"得刻在脑子里。
十、本篇小结
- 🧱 四件套 :State(共享白板)、Node(工人函数,吐增量)、Edge(传送带)、Conditional Edge(质检员的放行章)。第四个是灵魂 ------ "拐不拐弯"的条件写在你的代码里,而不是模型的临时起意里。
- 📝 Reducer 是并发的保命符 :
Annotated[list, operator.add]是便签墙(只贴不撕),裸字段是黑板(后写覆盖先写)。并行写同一个裸字段,LangGraph 会当场掀桌(InvalidUpdateError)------ 这是好事,总比悄悄丢数据强。 - 🔀 "回头"是图存在的第一理由 :链没有回头路,裸 Agent 会回头但管不住,图让回头受控 (条件是代码)且可观测(每次转弯都有检查点)。
- 💸 成本账实测 :裸 Agent 的历史回喂是轮数的平方 (8 轮烧 23,112 字符),图编排接近线性(7,179,省 68.9%)。但只查 1 条的短流程,图反而贵 26.9% ------ 短流程别上图。
- ⏸️ interrupt + Checkpointer = 图的绝活 :三行代码实现"跑一半停下等人",状态按
thread_id存档,人挂三天回来接着跑。链和裸 Agent 在这个需求面前是零分。 - ⚠️ 恢复 = 节点重跑 :
interrupt()之前的副作用会被重复执行,要么后置要么幂等。外加recursion_limit兜底 ------ 图不会无限跑,但它只会摔给你看。 - 🎮 时间旅行是分叉不是撤销 :
update_state到历史检查点再invoke(None, ...),开一条新时间线,原线完好。灰度实验、A/B 对比、事故回放,全靠它。 - ⚡ Send 是 map-reduce 的钥匙 :给每个分支塞不同输入、并行执行,三章起草实测提速 2.98 倍 ------ 前提还是那句:字段要有归约器。
- 🚇 一句话定位 :链是绿皮火车,裸 Agent 是没有司机的网约车,图是地铁 ------ 灵活长在边框里。
十一、课后作业
-
动手题(必做) :跑通
temp/day17_langgraph_lab.py,然后做三个改造 ------ ① 给演示 2 的图加一个bad_note字段(无归约器),让 research 和 judge 同时 往里写,亲眼看看那个InvalidUpdateError长什么样,再补上归约器修复它; ② 给演示 4 的人工审核加一条规则:"第 3 稿之后无论谁打回都自动发布",然后构造一个永远点「打回」的场景,验证你的封顶逻辑真的生效; ③ 把InMemorySaver换成SqliteSaver(pip install langgraph-checkpoint-sqlite),跑完演示 4 后杀掉进程重新连上同一个thread_id,看看稿子是不是还在。 -
思考题 :你要做一个"招聘简历初筛"流程:读简历 → 打分 → 分数够的发面试邀请(发邮件是不可逆操作! )→ 不够的进人才库。请回答: ① 哪一步必须是
interrupt节点?为什么不能让 Agent 自己判断"该不该发邮件"? ② 每个节点的副作用(发邮件、写人才库)应该放在函数的哪一半?用第五节坑 1 的原理解释; ③ 画出这张图的节点和边(纸上画即可),标出哪条边是 Conditional Edge、它的判断函数返回哪几种值。 -
进阶题 :把演示 3 的成本模型扩展成"真实计费版" ------ ① 查一下你所用模型的 input token 单价,把字符数折算成钱; ② 补充一个现实因素:图的 research 节点除了压缩还要去重(同一份资料查两次只算一次),重新算 8 轮的账; ③ 回答:在什么任务特征下,"上图"从省钱变成纯粹增加复杂度?给出你的三条判断标准。
🔮 下集预告
Day 18|LlamaIndex:数据多的场景,它是真香
这两篇我们一直在"编排流程",但有个前提没细说 ------ research 节点查到的那些资料,是从哪儿查来的? Day 09/10 我们手写过 RAG:自己切分、自己算相似度、自己排重排。两百行代码跑通了原理,但真到"公司有 3,000 份文档"的场景,手写就开始冒烟了。
下一篇的主角 LlamaIndex,就是专门吃这碗饭的:
- 📚 索引类型:Vector Store Index 之外,还有摘要索引、树索引、知识图谱索引 ------ 什么数据配什么索引,选错直接白干
- 🔍 Query Engine:Day 09 手写的那 200 行,它一个方法调用搞定,而且每一层都能拆开换零件
- 🤝 Agent + RAG 深度结合:让 Agent 自己决定"这个问题要查哪个索引、查几层" ------ 和今天讲的图正好接上
- 🧪 实测对照:同一批文档,Day 09 的手写流水线 vs LlamaIndex,检索质量和代码量各差多少
剧透一句:LangChain 是建材市场,LlamaIndex 更像精装图书馆 ------ 你带数据进去,它连书架带检索系统一起给你配好。代价是出了它家大门,你得学它的逛法。明天见。😉
🗺️ 系列导航
| 阶段 | 篇目 | 主题 |
|---|---|---|
| 认知篇 | Day 01-06 | Agent 是什么、四大件、ReAct、Function Calling、概念辨析(✅ 已完结) |
| 能力篇 | Day 07-14 | 提示词、记忆、RAG、向量库、规划、自愈、上下文工程、人设(✅ 已完结) |
| 框架篇 | Day 15-21 | 选型、LangChain、LangGraph、LlamaIndex、手写 mini Agent、Dify/Coze、本地部署(📍 本篇第 3 篇) |
| 多智能体篇 | Day 22-27 | 多智能体架构、MCP、A2A、人机协同 |
| 工程化篇 | Day 28-33 | 评测、可观测、成本优化、安全护栏、生产落地、收官实战 |
已更新 :Day 01|Agent 到底是什么? · Day 02|LLM 和 Agent 的区别 · Day 03|解剖一只 Agent:四大件 · Day 04|ReAct 循环:想→做→看→再想 · Day 05|Function Calling 从零讲透 · Day 06|四个概念辨析与选型 · Day 07|提示词工程(Agent 版) · Day 08|记忆系统 · Day 09|RAG 检索增强生成 · Day 10|向量数据库入门 · Day 11|规划与任务分解 · Day 12|错误自愈与重试机制 · Day 13|上下文工程 · Day 14|人设与边界感 · Day 15|框架全家桶怎么选 · Day 16|LangChain 快速上手
每天 12:00 更新一篇。 如果今天这篇帮你少画了一张错误的架构图,点个收藏 ------ 等你真要给流程加"人工审核"的时候,你会回来抄第五节那三行代码。 有问题欢迎评论区留言,典型问题会写进后续的加餐篇。