本文是「LangGraph 核心与实战」系列专栏的第 3 篇。专栏总览参见:《LangGraph 核心架构全景:用状态图构建可控 Agent 工作流》 。
顺序图(Sequential Graph)是 LangGraph 中结构最简单、但工程意义最基础的拓扑结构:节点按照确定的先后次序单向执行,没有分支分流,也没有环形回路。
在学习更复杂的动态路由、自纠循环或多 Agent 并行之前,必须先通过顺序图建立标准的开发习惯:如何设计阶段性数据契约、如何使用 add_edge 装配流水线,以及如何利用 stream 接口实时观测每个工位的状态演进。
业务需求:健康指标综合评估流水线 以一个面向用户的健康体检指标评估系统 为例,完整走通顺序图的设计与实现。
业务流水线包含 5 道工序:
参数校验(validate) :检查用户输入的身高与体重数值是否在生理合理区间内;
指标计算(calculate) :根据身高体重计算 BMI 指数( );
分级归类(categorize) :根据世界卫生组织(WHO)与国标标准,将 BMI 映射为健康分级(偏瘦、正常、超重、肥胖);
健康建议(advise) :根据健康等级匹配针对性的饮食与运动指导策略;
格式化报告(format) :汇聚全流程数据,生成可直接交付给前端的结构化报告文本。
流水线拓扑图如下:
flowchart TD
S([START 入口]) --> V["1. 校验输入 (validate)"]
V --> C["2. 指标计算 (calculate)"]
C --> K["3. 等级分类 (categorize)"]
K --> A["4. 生成建议 (advise)"]
A --> F["5. 格式化交付 (format)"]
F --> E([END 终点])
状态建模(State Modeling) 状态是整条流水线的数据载体。为了保证类型安全,明确划分“外部必填输入”、“中间计算字段”与“最终产出”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from typing import TypedDict, Optional , List class HealthAssessmentState (TypedDict ): user_name: str height_cm: float weight_kg: float is_valid: bool error_message: Optional [str ] bmi_value: Optional [float ] health_category: Optional [str ] advice_points: Optional [List [str ]] final_report: Optional [str ]
在状态建模中,不要将临时变量都作为顶层必填字段。中间字段设置默认初始为 None,允许流水线在启动时仅接收前 3 个用户输入参数。
工序节点实现(Node Implementation) 每个节点严格遵循单一职责与增量返回原则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def validate_node (state: HealthAssessmentState ) -> dict : h = state.get("height_cm" , 0 ) w = state.get("weight_kg" , 0 ) if h < 50 or h > 260 : return {"is_valid" : False , "error_message" : f"身高数据异常 ({h} cm),合理范围为 50~260cm" } if w < 10 or w > 300 : return {"is_valid" : False , "error_message" : f"体重数据异常 ({w} kg),合理范围为 10~300kg" } return {"is_valid" : True , "error_message" : None } def calculate_node (state: HealthAssessmentState ) -> dict : if not state.get("is_valid" ): return {} h_m = state["height_cm" ] / 100.0 w_kg = state["weight_kg" ] bmi = round (w_kg / (h_m * h_m), 1 ) return {"bmi_value" : bmi} def categorize_node (state: HealthAssessmentState ) -> dict : if not state.get("is_valid" ) or state.get("bmi_value" ) is None : return {} bmi = state["bmi_value" ] if bmi < 18.5 : cat = "偏瘦" elif 18.5 <= bmi < 24.0 : cat = "正常" elif 24.0 <= bmi < 28.0 : cat = "超重" else : cat = "肥胖" return {"health_category" : cat} def advise_node (state: HealthAssessmentState ) -> dict : if not state.get("is_valid" ): return {} cat = state.get("health_category" ) advice_map = { "偏瘦" : ["增加高蛋白与优质碳水摄入" , "加入抗阻力力量训练以提升肌肉量" ], "正常" : ["继续保持现有的均衡膳食结构" , "每周维持至少 150 分钟中等强度有氧运动" ], "超重" : ["减少精制糖和高脂食品摄入" , "增加快走、游泳等低关节冲击的有氧运动" ], "肥胖" : ["建议在临床医生或营养师指导下制定减重方案" , "严格控制每日总热量摄入" ] } return {"advice_points" : advice_map.get(cat, ["保持健康作息" ])} def format_node (state: HealthAssessmentState ) -> dict : name = state["user_name" ] if not state.get("is_valid" ): report = f"【体检评估失败】\n用户: {name} \n原因: {state.get('error_message' )} " return {"final_report" : report} advice_text = "\n" .join([f" - {p} " for p in state["advice_points" ]]) report = ( f"==============================\n" f" 健康指标综合评估报告\n" f"==============================\n" f"用户姓名: {name} \n" f"身体数据: 身高 {state['height_cm' ]} cm / 体重 {state['weight_kg' ]} kg\n" f"BMI 指数: {state['bmi_value' ]} (评估等级: {state['health_category' ]} )\n\n" f"健康建议:\n{advice_text} \n" f"==============================" ) return {"final_report" : report}
组装图结构与编译 使用 StateGraph 将 5 个工序用确定性边串接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from langgraph.graph import StateGraph, START, ENDbuilder = StateGraph(HealthAssessmentState) builder.add_node("validate" , validate_node) builder.add_node("calculate" , calculate_node) builder.add_node("categorize" , categorize_node) builder.add_node("advise" , advise_node) builder.add_node("format" , format_node) builder.add_edge(START, "validate" ) builder.add_edge("validate" , "calculate" ) builder.add_edge("calculate" , "categorize" ) builder.add_edge("categorize" , "advise" ) builder.add_edge("advise" , "format" ) builder.add_edge("format" , END) app = builder.compile ()
执行机制:批量与流式单步观测 1. 同步完整调用(app.invoke) 1 2 3 4 5 6 7 8 normal_input = { "user_name" : "张工程师" , "height_cm" : 175.0 , "weight_kg" : 68.0 } result = app.invoke(normal_input) print (result["final_report" ])
输出:
1 2 3 4 5 6 7 8 9 10 11 ============================== 健康指标综合评估报告 ============================== 用户姓名: 张工程师 身体数据: 身高 175.0cm / 体重 68.0kg BMI 指数: 22.2 (评估等级: 正常) 健康建议: - 继续保持现有的均衡膳食结构 - 每周维持至少 150 分钟中等强度有氧运动 ==============================
2. 流式单步追踪(app.stream) 在长程执行或异步网络调用中,通过 stream 能够精确捕捉到每一次状态演进:
1 2 3 4 print ("--- 启动流式追踪 ---" )for step_event in app.stream(normal_input): for node_name, delta in step_event.items(): print (f"节点 [{node_name:10 } ] -> 更新字段: {list (delta.keys())} " )
输出:
1 2 3 4 5 6 --- 启动流式追踪 --- 节点 [validate ] -> 更新字段: ['is_valid', 'error_message'] 节点 [calculate ] -> 更新字段: ['bmi_value'] 节点 [categorize] -> 更新字段: ['health_category'] 节点 [advise ] -> 更新字段: ['advice_points'] 节点 [format ] -> 更新字段: ['final_report']
每个字典元素即时反映了当前节点所贡献的增量输出。
初始 State 的边界处理原则 在调用 app.invoke() 时,许多开发者会有疑问:“输入参数是否必须包含 HealthAssessmentState 定义的所有键?”
答案是不需要 :
只传必需输入 :在调用入口处,仅需传入起点节点依赖的初始键(如 user_name、height_cm、weight_kg);
缺省键访问防崩溃 :在下游节点读取可能尚未生成的字段时,严禁使用直接下标读取 state["bmi_value"] ,应采用防御性读取:state.get("bmi_value");
不可预知的异常短路 :在顺序图中,即使输入异常(如输入身高 -10cm),由于没有条件分支,图依然会走完所有 5 个节点。本例通过 if not state.get("is_valid"): return {} 实现了业务层面的轻量穿透。在下一篇中,我们将用真正的**条件边(Conditional Edges)**在校验失败时直接跳过中间步骤直达终点。
系列导航与参考