Day 41 / 共 68 天 · 阶段 7 编排框架
LangGraph② 条件边、循环、人在环路
昨天(Day 40)你修好了一条"直达线"。但真 Agent 得看情况拐弯、能循环重来、危险处还要等人拍板。今天补齐另一半:add_conditional_edges(条件边)、循环(把手写的"想→动手→再想"用图实现)、interrupt(Day37 的"中途等人"真正落地)。学完这两天,你就能用 LangGraph 搭出有脑子的 Agent 了。
📍 你在阶段 7(编排框架 D39-44)的位置
D39 为什么要框架→
D40 LangGraph①→
D41 LangGraph②→
D42 CrewAI→
D43 eino→
D44 选型
💡 用一个类比兜住今天(继续沿用「地铁线路图」的世界观,今天修支线和道口)
昨天修的是直达线。今天让线网有脑子:条件边(add_conditional_edges) = 一个岔路口,列车到这看行李箱里的情况,决定走 A 线还是 B 线;路由函数 = 岔路口的扳道员(读行李箱,喊一声"往左!"还是"往右!");循环 = 一段环线,列车可以绕回上一站再来一圈(这就是昨天手写的"想→动手→再想");interrupt(人在环路) = 一道道口栏杆,危险路段落杆停车、等调度员按钮放行才继续。今天你从"修直路"进阶成"修带岔道、环线和道口的智能线网"。
L01
为什么直线不够用
🤔 痛点昨天的图是写死的一条道:检索→写稿→结束。可现实是:检索没查到东西怎么办?写完稿审校不合格要不要打回重写?这些"看情况"的岔口,直线图表达不了。
💡 本质需要两种新能力:①条件分支——到路口按当前 state 决定走哪条边;②循环——某条边能绕回前面的站再走一遍。有了这俩,昨天手写循环里的
if 和 while,就都能用"图"优雅地表达出来,而且一眼看得懂、还能被监控。👶 一句话普通边(add_edge)= "永远走这条";条件边(add_conditional_edges)= "到这看情况再决定走哪条"。今天的主角就是后者。
L02
路由函数:岔路口的扳道员
🤔 痛点"看情况走哪条边"——这个"看情况"的判断逻辑写哪?
💡 本质写一个路由函数(扳道员):它和节点函数一样收
state,但不改行李箱,只返回一个字符串——"下一站该去哪"。框架拿这个字符串去查"该走哪条边"。扳道员只管指路,不搬货。# 扳道员:读行李箱,返回"下一步去哪"的暗号(一个字符串)
def 审校结果如何(state):
if state["passed"]: # 审校通过
return "通过" # 返回暗号"通过"
if state["retries"] >= 3: # 已重写3次,别死磕
return "放弃"
return "回炉" # 否则打回重写
# 注意:它没有 return {...} 改 state,只吐一个字符串指方向
📝 举个例子:同一个扳道员,行李箱不同就指不同方向
行李箱
{passed:true} → 扳道员喊 "通过";{passed:false, retries:1} → 喊 "回炉";{passed:false, retries:3} → 喊 "放弃"。同一个路口、同一个扳道员,列车走哪条全看此刻行李箱里的内容——这就是"看情况拐弯"。路由函数返回的字符串是你自己定的"暗号"(如 "通过"/"回炉"/"放弃"),下一讲会把每个暗号映射到一个真实的站名。暗号和站名分开,读起来更清楚。
L03
add_conditional_edges:把岔路口接上图
🤔 痛点扳道员写好了,怎么告诉图"从审校站出来,按扳道员的话去不同的站"?
💡 本质用
add_conditional_edges(出发站, 扳道员, 暗号→站名的对照表)。意思是:列车离开"出发站"时,让"扳道员"看行李箱喊个暗号,再按对照表把暗号翻译成真正要去的站。一个路口,几条出路,清清楚楚。from langgraph.graph import END
# 从"审校"站出来,让"审校结果如何"扳道员决定去哪
g.add_conditional_edges(
"审校", # 从这个站出发
审校结果如何, # 扳道员函数:返回 "通过"/"回炉"/"放弃"
{ # 暗号 → 真实站名 的对照表
"通过": END, # 喊"通过" → 到终点
"回炉": "写稿", # 喊"回炉" → 绕回写稿站(这就形成了循环!)
"放弃": END, # 喊"放弃" → 也结束(兜底,别死循环)
},
)
图注:一个条件边,就把"审校不过就回炉重写"这种循环逻辑,画成了任何人都看得懂的图。
L04
循环:让列车绕回去,就是 Agent 主循环
🤔 痛点昨天手写 Agent 的核心是"想→动手→看→再想"的 while 循环。用图怎么表达这种"转圈"?
💡 本质循环 = 一条绕回去的条件边。经典 Agent 图长这样:有个"大脑"站(LLM 决策)和"工具"站,大脑决定"要调工具"就走边到工具站,工具站干完绕回大脑站再想;直到大脑说"完事"就走边到 END。这正是昨天手写的循环,只不过画成了图——而且步数上限、状态存档这些护栏,框架都替你兜着。
图注:大脑站用条件边——"要调工具"就去工具站(工具站再无条件绕回大脑),"完事"就去 END。昨天的 while,今天成了这张环线图。
👶 小白:绕回去会不会像昨天担心的那样无限转、烧钱?
👨🏫 老师:会,所以要留兜底。框架有个递归上限(默认转到一定圈数就自动停,类似昨天的 max_steps),你也可以在扳道员里判断"转够 N 次就走去 END"。凡是有循环的图,都必须有一条"通往 END 的出路 + 一个上限",这是铁律,面试也爱问。
L05
interrupt:道口栏杆,落杆等人放行
🤔 痛点Day37 说过,危险动作(发邮件、下单、删数据)要"暂停等人拍板"。在图里怎么让列车停在某站、等人点头再继续?
💡 本质编译时告诉图"在某站前落杆":
compile(checkpointer=存档器, interrupt_before=["发邮件"])。列车开到"发邮件"站前会自动停下并存档(靠 Day37 的 checkpointer!),把控制权交回给你。人看过后,用同一个 thread_id 再 invoke 一次(通常传 None),列车就从道口继续开。暂停能原样恢复,全因为状态被存住了——这就是"人在环路"落地。from langgraph.checkpoint.memory import MemorySaver # 最简单的内存存档器
app = g.compile(
checkpointer=MemorySaver(), # 有存档才能"停了还能续"(Day37)
interrupt_before=["发邮件"], # 在"发邮件"站前落杆暂停
)
cfg = {"configurable": {"thread_id": "u1"}} # 存档栏位编号,区分不同会话
app.invoke({"draft": "尊敬的客户..."}, cfg) # 跑到"发邮件"前停住、已存档
# —— 这里把草稿拿给人看,人点了"同意" ——
app.invoke(None, cfg) # 传 None:从道口继续,真正发出
👶 记住这条链能"停在半路等人、还能原样续跑",靠的是三件事串起来:state(行李箱记着现场)+ checkpointer(存档)+ interrupt(落杆)。Day37 的三个词,今天在这一段全用上了——这就是学得扎实的回报。Day54 讲"审批闸/护栏"会再深化。
🔗 想看条件边/循环/interrupt 的完整实战与源码?去看《LangGraph 20 天精讲》→(学完回来继续 Day 42)
L06
完整示例:一个会"回炉重写"的写作 Agent
💡 本质把今天的条件边 + 循环拼成一个能跑的小图:写稿→审校,不过就回炉,最多 3 次。为聚焦流程,审校用"字数够不够"这种简单规则代替真 LLM(真项目里换成模型打分即可)。
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
draft: str # 当前稿子
passed: bool # 审校过没
retries: Annotated[int, add] # 重写次数(追加型:每回炉一次+1)
def 写稿(state):
n = state.get("retries", 0)
return {"draft": f"这是第 {n+1} 版稿子,内容更充实了……" * (n+1)}
def 审校(state):
ok = len(state["draft"]) >= 40 # 简单规则:够长就算过
return {"passed": ok}
def 扳道员(state): # 决定:过了/回炉/放弃
if state["passed"]: return "通过"
if state.get("retries", 0) >= 2: return "放弃" # 最多再回炉,别死循环
return "回炉"
g = StateGraph(State)
g.add_node("写稿", 写稿); g.add_node("审校", 审校)
g.add_edge(START, "写稿")
g.add_edge("写稿", "审校")
g.add_conditional_edges("审校", 扳道员,
{"通过": END, "放弃": END, "回炉": "回炉计数"}) # 回炉先去计数站
g.add_node("回炉计数", lambda s: {"retries": 1}) # +1 次后回写稿
g.add_edge("回炉计数", "写稿")
app = g.compile()
out = app.invoke({"draft": "", "passed": False})
print("最终稿:", out["draft"][:30], "…")
print("重写次数:", out["retries"], "|通过:", out["passed"])
📝 举个例子:它会自己转几圈
第 1 版太短→审校不过→回炉计数(+1)→回写稿写第 2 版……直到稿子够长审校通过、走 END。你没写一个 while,循环却在图里自然发生了——而且这张图能画出来、能被监控。
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 普通边和条件边(
add_conditional_edges)有什么区别? - 路由函数(扳道员)和节点函数有什么不同?(它只返回字符串指路,不改 state)
- 循环在图里怎么表达?为什么有循环就必须有"通往 END 的出路 + 上限"?
interrupt_before怎么让列车停下等人?它依赖 Day37 的哪个东西?(checkpointer)- "能暂停还能原样续跑"靠哪三件事串起来?(state + checkpointer + interrupt)
✋ 动手 10 分钟:给写作 Agent 加一道"人工审批"
在 L06 的图上,把"通过"之后先别直接 END,而是加一个"发布"站,并在发布前落杆等人:
from langgraph.checkpoint.memory import MemorySaver
def 发布(state): return {"draft": state["draft"] + "(已发布)"}
g.add_node("发布", 发布)
# 把"通过"的去向从 END 改成 "发布",再 发布→END
# {"通过": "发布", ...} 、 g.add_edge("发布", END)
app = g.compile(checkpointer=MemorySaver(), interrupt_before=["发布"]) # 发布前落杆
cfg = {"configurable": {"thread_id": "t1"}}
app.invoke({"draft": "", "passed": False}, cfg) # 跑到"发布"前停住
print("暂停,等人审批……当前稿:", app.get_state(cfg).values["draft"][:20])
app.invoke(None, cfg) # 人点头 → 继续,真正发布
观察:第一次 invoke 停在发布前;第二次 invoke(None) 才真发布。你就亲手做出了"人在环路"。
明日预告 · Day 42:LangGraph 是"自己画图、控制力强"的路子。明天换个风格——CrewAI:它不让你画图,而是让你像组建一个团队那样声明"有哪几个角色(Agent)、每人负责哪个任务(Task)、按什么流程(Process)协作",框架自动帮他们接力。同样是编排,一个像"画电路图",一个像"带团队",体会两种思路的差别。