用假模型测试节点、路由、Reducer 和错误分支,快速、离线、可重复。
让程序不只会回答,
还会走流程
你不需要先懂 Agent。接下来我们只跟着一个问题走:看它如何被清洗、分类、回答、检查、暂停、恢复,最后成长为一个可靠的学习教练。
不是所有代码,
都需要变成图
只有两三步时,普通函数最清楚。真正让人头疼的是:分支越来越多、失败要重试、流程会暂停、过几小时还要接着运行。
观察任务变复杂后,代码结构发生什么变化。
图不是为了“高级”,而是为了让复杂路线仍然看得见。
两步固定路线:直接调用函数最简单。此时没有必要引入 LangGraph。
我们不会背术语,
而是把一个程序逐步养大
每到一个新问题,才引入一个新概念。这样你永远知道“它为什么存在”,而不是记住一堆孤立名词。
你只需要带着一个问题:“当前这一步知道什么、要做什么、下一步去哪?”后面所有概念都只是这三个问题的不同答案。
用户只说了一句:
“什么是 State?”
程序接到这句话后,不能把所有事情挤在一个函数里。我们先把它拆成四个能独立解释的小步骤。
用嘴说清路线,比复制一段框架代码更重要。
黄色小球不是“数据本身”,而是帮助你想象:同一份共享状态正被一步步补充。
State 不是记忆魔法,
只是一张共享表单
流程走到任何一步,都要知道目前已经有哪些事实。State 就把这些事实放在有名字的字段里。
一开始由用户提供,例如 raw_text。
节点逐步补充,例如 clean_text、difficulty。
最后交还调用方,例如 answer。
后续节点需要知道,而且值得被保存的事实,才放进 State。
raw_text“ 什么是 State? ”clean_text等待清洗…difficulty等待分类…answer等待回答…现在只有用户输入;其他字段还没有事实。
Node 是一道工序:
读取 State,返回局部更新
清洗节点只负责清洗。它没有必要复制整张表单,更不应该偷偷修改全局变量。
def clean_node(state: State):
clean = state["raw_text"].strip()
return {"clean_text": clean}
Node 不交回整份 State,只交回“我刚改了什么”。
{
raw_text: " 问题 ",
clean_text: ""
}
{
clean_text: "问题"
}
Edge 不负责干活,
只回答:下一站是谁?
如果清洗完成后永远都要分类,那么这条路线就是固定 Edge。它像地铁轨道,不查看乘客内容。
builder.add_edge(START, "clean")
builder.add_edge("clean", "classify")
builder.add_edge("classify", END)
graph = builder.compile()
graph.invoke({"raw_text": "什么是 State?"})
到了岔路口,
路由函数只负责选方向
它读取当前 State,例如 difficulty,然后返回下一节点名称。它不生成答案,也不做数据库操作。
路由器只指路;真正回答问题的是 basic_answer 或 advanced_answer 节点。
等待选择输入。你认为黄色小球会走向哪条分支?
现在把三件事合起来:
State + Node + Edge
输入一个问题。页面会用纯 Python 规则模拟清洗、分类与回答;这里故意不接 LLM,避免同时面对两个未知数。
先证明路线和 State 正确,以后接入模型时,才知道错误来自流程还是模型。
如果你能解释每一步“读了什么、改了什么、去了哪里”,就已经真正理解了第一张图。
两个节点同时写一个字段,
到底该覆盖还是追加?
普通字段默认使用“新值覆盖旧值”。但日志、消息和并行结果通常需要保留双方内容,所以必须提前写清合并规则。
import operator
from typing import Annotated
class State(TypedDict):
results: Annotated[
list[str],
operator.add
]
Reducer 就像收件规则:新包裹来了,是替换旧包裹,还是排在后面?
{ results: ["A"] }{ results: ["B"] }规则
并行不是“大家随时互相看”,
而是同一轮各自工作,轮末统一交卷
同一个 super-step 中,Worker A 和 B 读取的是这一轮开始时的相同 State。它们看不到对方尚未提交的更新,直到步末由 Reducer 合并。
不要依赖并行任务谁先完成。需要稳定顺序时,给结果附带 order,再在汇总节点排序。
A 不需要 B 的结果,B 也不需要 A 的结果;它们的等待时间才能真正重叠。
“不合格就修改”很容易,
难的是确保一定能停
循环需要两个出口:内容已经通过,或者修改次数达到上限。revision_count 必须放进 State,因为路由、存档和恢复都要看到真实次数。
State 中维护最大修改次数,调用图时再设置 recursion_limit,避免任何意外死循环。
有时一个节点完成决定后,
需要同时说清“改什么”与“去哪”
这时返回 Command。它把状态更新和动态跳转放在同一个结果里,特别适合审批、交接和工具内部路由。
{ status: "approved" }先把审批结果写入共享状态。
goto="publish"再明确下一步直接进入发布节点。
不要给同一节点再添加一条普通静态 Edge。Command 已经动态跳转,再加固定路线可能让两条分支同时执行。
不是把流程“全交给 AI”,
而是让它只处理语义任务
能用确定规则写清的事情,Python 更快、更便宜、更稳定。只有理解含义、生成语言或评价质量时,才需要模型。
点击右侧任务,先在心里判断。蓝色代表 Python,珊瑚色代表 LLM。
已判断 0 / 6
模型只初始化一次,
节点只负责调用它
不要在每个节点里重复创建模型。统一模块便于切换供应商、设置超时与重试,也能避免配置散落。
def generate_answer(state: State):
started = time.perf_counter()
try:
response = model.invoke(
state["question"]
)
return {
"answer": response.content,
"latency_ms": elapsed(started)
}
except Exception as error:
return {"error": type(error).__name__}
记录输入摘要、输出摘要、耗时和异常类型;永远不要记录 API Key。
“解释 State”
842 ms
“State 是…”
TimeoutError
聊天不只是字符串,
每条消息都有角色与顺序
HumanMessage 表示用户说了什么,AIMessage 表示模型回答或提出工具调用,ToolMessage 把真实执行结果送回模型。
用户的意图与补充信息。
模型文本或结构化 tool_calls。
必须用 tool_call_id 匹配原调用。
对话是连续发生的事件,新消息应该追加,而不是覆盖上一轮。
300 分钟课程,每天学 60 分钟,需要几天?
safe_calculator(a=300, b=60, operation="divide")
5
每天学习 60 分钟,5 天可以完成。
程序不要解析“差不多”的话,
而要接收有类型的结果
让模型自由回答“我觉得这可能是进阶题,因为……”很适合人读,却很不适合程序路由。Schema 把允许字段、类型和枚举提前写清。
结构化输出只是让模型与程序之间的交接更稳定、更容易验证。
程序:我要 split 哪个符号?“也许”算不算确定?
先看任务依赖,
再选择串联、路由或并行
它们不是三种“高级程度”,只是三种不同依赖关系:下一步依赖上一步、只需选择一个方向、或者多个任务彼此独立。
后一步需要前一步结果,所以按顺序串联。
输入只进入最适合的一条路线。
三项任务互不依赖,可以同时开始,最后汇总。
Checkpoint 是存档,
thread_id 是存档槽编号
图在 super-step 边界保存 StateSnapshot。下次携带相同 thread_id,运行时才能找到这一段会话;换一个编号,就像打开全新的存档。
thread_id 是状态指针,不是用户权限、API Key,也不是副作用幂等键。
thread-A 保存:小明正在学习 State,已完成第 2 步。
Time travel 不是修改过去,
而是从旧快照重新出发
Replay 使用旧 checkpoint 的 config 重新执行后续节点;Fork 先用 update_state 创建新 checkpoint,再沿新状态继续。原历史不会被删除。
旧快照之后的 LLM、API 与 interrupt 都会重新执行,所以副作用仍要安全。
before_routeapprove
发布reject
重规划
同一次运行,
可以从三个角度观看
values 看每一步后的完整 State;updates 只看节点刚改了什么;messages 专门看模型 token 或消息片段。
适合做完整状态监视器。
最容易定位哪个节点改坏了字段。
面向用户实现逐字出现的回答。
它们只是同一条执行轨迹的不同投影。
工具不是神秘插件,
而是一个描述清楚的函数
模型要知道工具名称、用途和参数类型;执行层要知道如何验证输入、捕获错误并返回稳定结果。
a: float · b: float · operation: enumquery: str · level: optional[str]total_minutes: int · daily_minutes: int工具输入来自用户或模型;eval 会把“算式字符串”扩大成任意代码执行入口。
class CalculatorInput(BaseModel):
a: float = Field(ge=-1e9, le=1e9)
b: float = Field(ge=-1e9, le=1e9)
operation: Literal[
"add", "subtract",
"multiply", "divide"
]
@tool(args_schema=CalculatorInput)
def safe_calculator(...):
"""执行白名单内的四则运算。"""
...
模型不会直接执行工具,
它只会提出一个结构化申请
模型产生 tool_calls;工具节点从白名单中找到函数并执行;结果以 ToolMessage 返回;模型再决定回答还是继续调用。
选择工具并给出名称、参数和调用编号。
验证、执行、捕错,返回匹配编号的结果。
读取工具结果,组织最终自然语言回答。
最后一条 AIMessage 没有 tool_calls 时进入 END;同时必须设置最大调用次数。
“需要几天?”模型
tool_call计算器
返回 5模型
最终回答
模型可以做决定,
但安全边界必须是代码
未知工具、缺少参数、超时和工具异常都要返回明确的 ToolMessage,让模型知道发生了什么,而不是让整张图无提示崩溃。
查询与计算不改变外部世界;发送、保存、扣费等副作用要留到人工审批之后。
不要根据模型生成的名称动态 import 或执行同名函数。模型的输出只是候选请求,白名单才是最终边界。
程序不是卡死,
而是保存现场后等你回来
interrupt() 把可序列化提示交给外部,并让图暂停。Checkpointer 保存 State;恢复时使用相同 thread_id 和 Command(resume=...)。
Command(resume="小明") 中的“小明”,会成为节点里 interrupt() 的返回值。
State
人工决定不只改变路线,
有时还要修改 State
approve 保持原内容继续;edit 把人工内容写回 State 后继续;reject 记录原因并进入取消或重新规划分支。
人工输入也要验证:action 必须来自允许列表;edit 必须带 edited_draft;reject 最好带可执行的 reason。
恢复后节点会从头执行,
但发送、保存、扣费不能重复生效
幂等不是“绝不执行第二次”,而是相同业务操作即使被重试,最终外部结果仍然和执行一次相同。
校验和计算可以重复;真正副作用放在中断后或独立节点。
例如 send:plan-001:v1,同一业务重试时保持不变。
键已成功处理时,直接返回第一次收据。
连续点击右侧“发送”。你会看到函数被调用多次,但只有一张有效收据。
调用次数 0 · 外部有效发送 0
任务数量运行前不知道,
就让 Planner 动态发出N 份工作单
Planner 先产生子任务;每个 Send 包含目标 worker 和该次独立输入;worker 并行返回局部结果;Reducer 合并后再统一排序和总结。
def dispatch(state: OverallState):
return [
Send("worker", task)
for task in state["tasks"]
]
Map 是把大题拆成多份独立工作;Reduce 是把多份结果合成一个可靠答案。
产生 3 个 task
排序并汇总
等待 Planner。worker_results 使用追加 reducer。
当“生成 → 审核 → 修改”已经很复杂,
把它收进一个可独立测试的小图
父图只关心 topic 和最终 draft;review_feedback、revision_count 等内部细节留在子图。边界越清晰,修改内部流程时越不容易影响父图。
父子图都需要,例如 topic、draft。
只服务内部循环,例如 review_feedback。
用包装节点转换输入与输出,不依赖全局变量。
generate
review
revise 后返回审核
共享:topic、draft | 隔离:review_feedback、revision_count
Router 像医院分诊台:
先判断类型,再送往合适专家
一个明确的路由步骤对输入分类,可以选择一个专家,也可以把独立子问题并行分给多个专家,最后统一汇总。
领域边界清楚、预处理明确、每次请求相对独立。
长对话频繁换专家时,Router 本身通常不维护自然连续的角色状态。
控制权在路由步骤或工作流;专家只处理被分配的请求。
主 Agent 不交出方向盘,
只把明确任务作为工具委派
主 Agent 理解用户整体目标,决定调用哪个专家、给它多少上下文,再把专家返回的局部结论汇总成统一回答。
用户始终面对主 Agent;它拥有全局上下文。
子 Agent 通常只看到完成任务所需信息,返回后结束。
集中控制带来额外模型调用与延迟,不要无理由增加专家。
一直在主 Agent。专家不会直接接管用户后续对话。
Handoff 不是“问专家一句”,
而是把后续对话正式交接
例如售前确认需求后,把用户交给实施顾问;接下来几轮都由新角色负责。状态中的 active_agent 或 current_step 会持续影响行为。
目标、已完成步骤、承诺、限制和未决问题。
不需要每一轮都回到原主 Agent 汇总。
传少了信息断层,传多了又会膨胀和混乱。
从旧角色转移给新角色,并在 State 中跨轮保持。
handoff payload:目标=掌握 LangGraph;基础=会 Python;时间=每天 60 分钟;未决=选择练习项目。
真正可靠的系统通常是混合物:
代码守边界,Agent 做语义
用 LangGraph 明确校验、分类、Agent、并行、人工审批和副作用节点。每一步是普通函数、模型调用,还是完整 Agent,都由图设计者决定。
这是多数真实业务的推荐起点。先用明确 Workflow 固定风险边界,再在确实需要语义判断的位置放 Agent。
不要问“哪个最强”,
要问谁掌控下一步
点击场景。先自己判断,再看右侧答案与理由。
模式不是由 Agent 数量决定,而是由任务依赖、控制权与上下文需求决定。
Agent 测试要同时检查:
结果、路径与边界
一句回答看起来正确,不代表系统可靠。它可能调用了错误工具、绕过审批、循环太多次,或者把另一个 thread 的状态带进来。
计算题必须经过 safe_calculator;普通问候绝不能触发工具。
清晰度与完整性等语义质量,可用数据集、人工或 LLM judge 比较。
最低限度保留两条关键测试:一条证明“该调用工具时确实调用”,一条证明“不该调用时没有乱调用”。
Demo 会跑只是起点,
生产还要知道坏在哪里、如何恢复
持久化 checkpoint、身份隔离、超时重试、成本上限、日志脱敏、状态保留策略和副作用幂等,决定系统是否能长期运行。
记录节点输入摘要、输出、耗时、错误与工具轨迹。
生产不能依赖进程内存;还要定义加密和保留周期。
recursion、concurrency、timeout、retry、tool calls、成本预算。
API Key 不进代码、不进 State、不进 checkpoint、不进日志。
现在回到最初的问题,
把它升级成一个可靠学习教练
它理解目标、查询课程、计算时间、生成计划、接受人工修改、保存会话,还能在失败后解释原因并恢复。
毕业标准不是“用过很多 Agent”,而是能画出路径、解释 State、写出边界、验证轨迹,并在失败后定位和恢复。
你不需要背完 API,
只要永远能回答四个问题
当前知道什么?这一步做什么?下一步为什么去那里?失败或暂停后如何安全继续?
LangGraph 的核心不是让 AI 更自由,而是让复杂、长时间、会失败的 AI 流程仍然可理解、可控制、可恢复。