凌晨五点的城市地铁控制中心,调度员小李盯着大屏上的线路图:早高峰还没到,但列车已经开始按时刻表有序出动。每一列列车从始发站出发,经过固定的轨道、在道岔处根据客流选择方向、最终抵达终点站汇入主线。整张地铁线路图,就是一张“有向无环图”——站台是节点,轨道是边,车上载的乘客就是流动的数据。而LangGraph,正是让Agent像地铁系统一样可靠运转的“调度图”。它用节点表示AI要执行的动作,用边定义数据流动的方向,用状态对象承载上下文信息,最终把一段松散的提示词调用,变成一条设计精密的“智能流水线”。本文就用“地铁系统”这个比喻,带你走一遍LangGraph DAG状态机建模的全过程。

岗位关联

对AI从业者而言,这是从“会调接口”进阶到“能架构Agent系统”的关键一步:掌握了DAG状态机,你就能设计出可预测、可回放、可运维的多智能体流程,这是生产级Agent工程的必备能力。

目录
  1. 可视化演示
  2. DAG基础概念
  3. 状态管理机制
  4. 条件分支路由
  5. 循环与递归
  6. 工作流编排
  7. 多Agent协作
  8. 并行与汇合
  9. 工程实践要点
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示K-Means算法的分配与更新两阶段迭代过程,观察质心移动与数据点归属变化,支持步进与自动播放。

本节配有交互动效(进入视口后自动加载,约 3KB)。

K-Means分配阶段 将每个数据点分配到最近的质心,形成簇 距离度量使用欧氏距离的平方 d(p,c) = ||p-c||² 分配后进入质心更新阶段

DAG基础概念

想象你要为一座新建的城市设计地铁线路图。规划师的首要原则是:明确有哪些站台,哪些站台之间有轨道相连,以及轨道的方向。你绝不会画一条“环形迷魂阵”让列车永远找不到出口——每一条线路都朝一个方向延伸,最终抵达终点。这就是“有向无环图”(DAG)的精髓:方向明确、没有环路。

在LangGraph中,“站台”就是节点(Node),“轨道”就是有向边(Edge)。节点是Agent执行的一个动作单元,比如“调用大模型生成回复”、“调用搜索工具获取资料”;有向边定义了动作之间的依赖关系——只有前一个节点执行完毕,后一个节点才能开始。因为图是有向的,所以数据的流动方向是确定的;因为是无环的,所以流程一定有终点,不会陷入无限循环。

LangGraph在编译图(Compile)时会做一次“拓扑排序”,就像地铁控制中心在发车前列出每列车的运行顺序。拓扑序保证了:无论图多复杂,总能找到一个合法的执行顺序,让每个节点都在其所有前置节点完成后才启动。这为后续的稳定性、可调试性打下了地基。地铁图是物理世界的DAG,LangGraph是逻辑世界的DAG——二者都是“先画图,再运行”。

当你把Agent流程画成DAG,等于给系统装上了一个“内在时钟”:每一步该做什么、做完之后去哪里,全部由线路图决定。这正是生产中“可控Agent”的第一性原理——别让模型自由发挥,让模型在轨道上奔跑。

TopoOrder(G)=[v1,v2,…,vn]s.t.∀(u,v)∈E,  u 排在 v 之前\text{TopoOrder}(G) = [v_1, v_2, \dots, v_n] \quad \text{s.t.} \quad \forall (u, v) \in E,\; u \text{ 排在 } v \text{ 之前}
这行公式说的是:对一张有向无环图 G,我们总能排出一个节点序列,使得每条边的起点都排在终点前面。LangGraph编译时就自动完成了这件事。
  1. TopoOrder(G)表示对图 G 做拓扑排序后得到的节点执行序列。
    TopoOrder(G)=[v1,v2,…,vn]\text{TopoOrder}(G) = [v_1, v_2, \dots, v_n]
  2. s.t.(使得)表示这个序列必须满足下面的约束条件。
    s.t.\text{s.t.}
  3. ∀(u,v) ∈ E对于图中的每一条有向边。
    ∀(u,v)∈E\forall (u, v) \in E
  4. u 排在 v 之前拓扑序保证了依赖关系不被破坏,前驱节点一定先执行。
    u 排在 v 之前u \text{ 排在 } v \text{ 之前}

状态管理机制

地铁列车上总有一个“行车信息屏”,显示当前到站、下一站、车上乘客数量等信息。不管列车跑到哪里,这个信息屏始终跟随。在LangGraph中,这个“信息屏”就是状态对象(State)——一个贯穿整个流程的字典结构,记录着所有节点共享的数据。

每个节点在执行时,会从State中读取它需要的字段(比如“用户问题”、“历史记录”),经过处理后,将结果写回State中新增或覆盖的字段(比如“检索结果”、“模型回复”)。LangGraph的状态更新机制像地铁调度规则一样严谨:每个节点返回一个字典,LangGraph会将这个字典与当前State做“合并更新”——新增字段直接加入,已有字段按规则覆盖。

值得强调的是,State是“字段级共享”的:不同节点可以只读写自己关心的字段。比如“检索节点”只读“用户问题”、写“检索结果”,而“生成节点”读“检索结果”和“历史记录”、写“最终回复”。这种设计让节点之间的耦合降到最低——每个节点像一列独立列车,只在特定的“站点”(字段)上下客。这比把全部信息塞进一个全局对话历史要清晰得多,也更容易排查问题。

用“信息屏”视角看LangGraph,你会发现它天然适合多人协作:每个Agent(或每个模块)只管好自己的那几行“显示屏字段”,写完就交给下一站。状态字典是唯一的真相来源,所有节点都在为它添砖加瓦——这就是Agent流程不失控的底层保障。

St+1=Update(St,Nodei(St))=St⊕ΔiS_{t+1} = \text{Update}(S_t, \text{Node}_i(S_t)) = S_t \oplus \Delta_i
这条公式表示:状态从 S_t 经过节点 i 的计算后,得到增量 Δ_i,合并更新为新状态 S_{t+1}。就像列车停靠一站后,车上行李(状态)发生了变化。
  1. S_t表示第 t 步执行前的状态字典。
    StS_t
  2. Node_i(S_t)节点 i 读取当前状态并执行计算,返回一个增量字典。
    Nodei(St)\text{Node}_i(S_t)
  3. ⊕表示“合并更新”操作,新字段加入、同名字段覆盖。
    St⊕ΔiS_t \oplus \Delta_i
  4. S_{t+1}更新后的状态,作为下一节点的输入。
    St+1S_{t+1}

条件分支路由

地铁线路中常有“道岔”:列车驶近时,调度系统根据前方客流情况决定扳向左边还是右边。LangGraph中的条件边(Conditional Edge)就是这样的道岔——它不是一个静态的固定轨道,而是一个“路由函数”:读当前状态,判断结果,返回下一步要走的边的名字。

写一个路由函数就像给调度员写一条决策规则:“如果站台上等待人数超过阈值,就开往备用线路;否则正常行驶。”在LangGraph里,你写一个Python函数,入参是State,返回值是字符串(边名)。LangGraph根据这个返回值走相应的边。例如,在“内容质检”节点之后接一个条件边:若State中“合规分数”大于0.8,走“通过”边进入“发布节点”;否则走“驳回”边回到“修改节点”。

条件分支的价值在于把决策权显式地写在图上,而不是藏在模型提示词里。调试时你一眼就能看出“为什么走到这条路”——因为某个状态字段满足/不满足了某个条件。为了可靠性,LangGraph还允许指定一个默认边(兜底),就像道岔故障时列车自动切换到备用轨道,保证流程不会断掉。

有了道岔,Agent就不再是一条直线走到底的“傻列车”,而能根据实际情况动态选择路径。条件分支是Agent从“执行器”变成“决策者”的关键一步——但决策规则是你画的,不是模型自由发挥的,可控性依然握在工程师手里。

Router(S)={e1,if score(S)>0.8e2,otherwise\text{Router}(S) = \begin{cases} e_1, & \text{if } \text{score}(S) > 0.8 \\ e_2, & \text{otherwise} \end{cases}
路由函数就像一个“红绿灯逻辑”:根据当前状态的分数,决定列车走哪条分支轨道。
  1. Router(S)路由函数,输入当前状态,输出对应分支的边名。
    Router(S)\text{Router}(S)
  2. score(S)从状态中提取的评分(如合规分数、置信度)。
    score(S)\text{score}(S)
  3. e₁ / e₂两条不同方向的边,对应“通过”或“驳回”。
    e1,e2e_1, e_2

循环与递归

“有向无环”听起来与“循环”矛盾——但Agent流程中经常需要“重试”、“多轮反思”。怎么办?地铁设计者的智慧是:用“折返线”实现循环。列车开到终点站后,通过一条折返轨道回到起点站,从图论角度看,这不是在图里画了一个环,而是从一个终点节点连一条边回到之前的节点,形成逻辑上的“往复运行”。

在LangGraph中,你可以从节点B画一条有向边回到节点A,只要这条边不形成一个严格的“环”结构约束(LangGraph本身支持这种“回到上游”的边设计,只要在编译时能确定终止条件即可,一般通过条件边控制)。更常见的做法是:节点A → 节点B → 条件边 →(若未达标)回到节点A。这样,图仍然是有限的、可终止的,但执行时可以来回多轮,直到满足条件。

递归则是把一个子图当作一个节点来调用。如果你有一套“质量检查子图”要复用到多个Agent流程中,可以把这个子图封装成一个节点,在父图中引用它。子图内部可以有自己的状态管理、自己的循环。但务必设定迭代次数上限(例如最多重试3次),否则一旦模型持续不达标,列车就会在折返线上永远空转——这在生产环境中会烧掉大量token。

循环就像地铁的“高峰限流”:进站人数太多时,让乘客在站外排队绕圈,但调度员一定设置一个“最大等待时间”避免队伍无限增长。在Agent设计里,有循环不可怕,没上限才可怕。

T(S)={Exit,if pass(S) or k≥Kmax⁡Loop,otherwiseT(S) = \begin{cases} \text{Exit}, & \text{if } \text{pass}(S) \text{ or } k \ge K_{\max} \\ \text{Loop}, & \text{otherwise} \end{cases}
循环终止条件像一个“自动闸机”:通过或达到最大次数就放行,否则继续折返。
  1. pass(S)判断当前状态是否满足退出条件。
    pass(S)\text{pass}(S)
  2. k ≥ K_max重试次数达到上限时强制退出。
    k≥Kmax⁡k \ge K_{\max}
  3. Exit / Loop返回“退出”或“继续循环”两个方向。

工作流编排

真实的地铁系统不是一条孤零零的线路,而是多条线交织成网:有环线、有支线、有快慢车。LangGraph的“工作流编排”能力,就是让开发者像规划地铁网一样组合复杂流程。核心手段之一是子图嵌套:一个大的Agent工作流,可以拆成多个子图(子流程),先分别定义,再在父图中用节点引用它们。

为什么需要“拆了再拼”?因为可维护性。一个10个节点的Agent流程,你可以画成“理解用户需求 → 检索知识库 → 规划方案 → 调用工具 → 组织回复”5个子流程,每个子流程内部还有若干节点。这样做的好处是:每个子图可以单独调试、单独测试,出了问题只需定位到对应的子图。

此外,工作流编排还包含超时与重试机制:每个节点可以设置超时时间,超时则按策略重试或走兜底分支。检查点(Checkpoint)则像地铁的“行车记录仪”:每执行到一个节点,就把状态持久化。如果列车中途故障,重新启动后可以从最近一个检查点继续跑,而不必从头再来。异常兜底则是在图的入口处加一个“全局异常处理”节点,像地铁的紧急救援系统,保证任何故障都有响应路径。

“DAG之上还有DAG”——子图嵌套就像俄罗斯套娃:每一层都有完整的地图,打开一个节点,里面又是一张精致的地图。有了这种能力,Agent系统从“小作坊”升级为“工业流水线”。

Workflow=SubGraph1∘SubGraph2∘⋯∘SubGraphm\text{Workflow} = \text{SubGraph}_1 \circ \text{SubGraph}_2 \circ \cdots \circ \text{SubGraph}_m
复合工作流是多个子图按顺序“串联”(复合)得到的,就像地铁快线由多段线路组成。
  1. SubGraph_i第 i 个子图,封装一段独立流程。
    SubGraphi\text{SubGraph}_i
  2. ∘复合运算,表示先执行前一个子图、再执行后一个子图。
    ∘\circ

多Agent协作

一座大城市往往有多条地铁线,每条线有自己的列车和调度员,但它们在换乘站无缝衔接,乘客可以跨线到达目的地。多Agent协作正是这个逻辑:每个Agent是一条地铁线,LangGraph是决定换乘关系的总网图。

在LangGraph中,每个Agent被建模为子图或一组节点(一套“认知线路”),Agent之间通过共享状态和消息传递协作。比如你搭建一个“研究写作双Agent系统”:研究员Agent负责检索资料、输出信息摘要到State的"research_notes"字段;作家Agent读取该字段,结合用户需求写出文章。两者之间通过一条有向边连接,共享同一个状态字典——就像两个地铁班次在换乘站交接乘客。

分工编排的关键是明确每个Agent的职责边界。地铁图上有清晰的站点归属,Agent也应该有清晰的输入输出约定。你可以在State中为每个Agent划定专属字段(如“搜索Agent”只写"search_results",“写作Agent”只读它),避免字段冲突。当协作流程复杂时,还可以引入“协调者Agent”作为总调度,像地铁控制中心一样观察所有Agent的运行状态,决定下一步让谁出场。

“独立运行,共享枢纽”——多Agent系统的优雅之处在于:每个Agent不需要知道其他Agent的内部细节,只需要了解“哪个字段去取、哪个字段去存”。图就是你团队的通讯录,一切协作都在节点边上有迹可循。

S=[Sshared∣Sagent1∣Sagent2∣⋯ ]S = [S_{\text{shared}} \mid S_{\text{agent}_1} \mid S_{\text{agent}_2} \mid \cdots]
共享状态按Agent分区:每个Agent拥有私有字段区,同时共享全局字段区。
  1. S_shared全局共享的状态区,所有Agent可读写。
    SsharedS_{\text{shared}}
  2. S_agent_iAgent i 的私有状态区,其他Agent不直接写。
    SagentiS_{\text{agent}_i}

并行与汇合

早高峰的地铁发车不是一列接一列傻跑,而是多条支线同时发车,在换乘大站汇合。LangGraph的并行能力让Agent流程“多条腿同时跑”,大幅缩短总耗时。图示中就是典型的扇形散开-汇合模式:一个节点(分发器)同时连出多条边到多个并行节点(fan-out),这些节点各自独立执行完毕后,汇入同一个节点(汇合点,barrier)。

实现上LangGraph天然支持并行:只要一个节点有多条出边,下游节点就可以并发执行,前提是它们之间没有依赖关系(这正是DAG的拓扑特性)。例如“分析用户问题”之后,可以同时触发“知识库检索”和“网络搜索”,两个节点互不依赖,并行执行后将结果一起送到“综合答案生成”节点。

但“汇合点”需要理解一个核心语义:必须等所有入边到达,才触发执行。就像地铁换乘站的“聚集等待区”,必须等到所有支线的乘客都到达,才统一出站。LangGraph的汇合节点默认就是这种"AND-barrier"语义。设计时要注意:并行节点的状态写入应互不冲突(分别写不同字段),否则汇合时会发生覆盖竞争——这就是为什么要约定字段分区。

并行不是灵丹妙药:只有在分支之间确实无依赖时才值得用。如果节点A要等节点B的结果,强行并行反而制造混乱。并行是DAG给工程师的礼物,但拆解分支需要眼光——如同地铁规划者决定哪两条线可以共享一段路,哪两条必须各走各的。

Ttotal=max⁡i(Ti)+Tmerge,而非∑iTi+TmergeT_{\text{total}} = \max_{i}(T_i) + T_{\text{merge}}, \quad \text{而非} \sum_i T_i + T_{\text{merge}}
并行把“串行总时间”从求和变成了取最大值。就像多路地铁同时开,总时间取决于最慢的那条线。
  1. max(T_i)所有并行分支耗时的最大值。
    max⁡i(Ti)\max_{i}(T_i)
  2. T_merge汇合点做结果合并的耗时。
    TmergeT_{\text{merge}}

工程实践要点

地铁系统安全运营几十年,靠的不是图纸漂亮,而是维护纪律。LangGraph DAG建模落到工程上,同样有几条“纪律”值得遵守。

第一,调试可视化。LangGraph编译后的图可以直接渲染出来,节点间连线一目了然。生产环境里,每个节点的输入输出状态都应记录日志,这样当Agent答错问题时,你能像翻行车记录一样,定位到是“检索节点给错了上下文”还是“生成节点理解偏了方向”。

第二,幂等设计。同一节点因重试被重复执行时,不应对状态产生重复副作用(比如重复扣费、重复插入数据库)。设计状态更新规则时,保证节点是“幂等”的——执行一次和执行多次结果相同。

第三,演进式建模。不要一开始就画一张20个节点的巨图。先画3个节点的MVP流程跑通,再加分支、加子图、加并行。地铁是一段一段建成的,Agent流程也应当如此。每一次改动后运行测试集,确保旧功能不回归。

第四,性能优化。在并行节点较多的图中,关注最慢节点(瓶颈),考虑给它加缓存或拆分;对于高频调用的子图,设置合理的超时上限,避免某个外部API卡死拖垮全流程。

画图容易,养图难——LangGraph把Agent流程变成了一张可维护的地铁图,但只有配上日志、幂等与演进式开发,这张图才能真正跑在生产环境里几十年不“瘫痪”。

Reliability=Recoverable FailuresTotal Failures×100%\text{Reliability} = \frac{\text{Recoverable Failures}}{\text{Total Failures}} \times 100\%
可靠性指的是“可恢复故障占比”。工程上不追求“永远不失败”,而是追求“失败了能快速恢复”。
  1. Recoverable Failures可以通过重试或回退恢复的故障数。
    Recoverable Failures\text{Recoverable Failures}
  2. Total Failures总故障数。
    Total Failures\text{Total Failures}

AI 应用场景

客服工单自动分类与流转

阶段:工作流编排(DAG建模 + 条件分支)

解决的问题:传统客服系统靠人工读工单、判断类别、分派给对应部门,高峰期积压严重。用LangGraph建模后,工单进入DAG:先由“分类节点”调用大模型判断工单类型(退换货/技术咨询/投诉),再经条件分支路由到不同处理子图,每个子图内部再并行执行不同动作。

具体案例:某电商平台将用户退货申请接入LangGraph流程:节点A提取订单信息,节点B并行调用“库存系统查询”与“用户信用分查询”,汇合后由条件边判定“是否符合无理由退货”,符合则调用退款API,否则转人工。整个流程从原来的15分钟缩短到40秒。

多Agent研究报告生成系统

阶段:多Agent协作 + 并行与汇合

解决的问题:单一大模型生成研究报告时,常常“资料查不全、结论偏一面”。基于LangGraph搭建“研究员+分析师+写作者”三Agent协作流程:研究员Agent并行检索多路信息源,分析师Agent对检索结果做交叉验证与提炼,写作者Agent最终成稿。

具体案例:某投研团队部署了一个“行业研究报告生成器”:3个研究员子图分别负责“政策面”“基本面”“市场情绪”三路并行检索,汇合后交给分析师子图做矛盾检测,写作子图再行文。每份报告的执行过程都可视化呈现,分析师可以回溯任何结论的生成路径。

代码仓库智能审查流水线

阶段:循环与递归 + 条件分支路由

解决的问题:代码审查中,AI需要基于审查意见多次修改代码,形成“提交→审查→修改→再审查”的循环。LangGraph把这个循环显式建模出来:审查节点输出问题列表,条件边判断“是否有严重问题”,是则回到修改节点重试,达到最大迭代次数后强制结束并通知人工介入。

具体案例:某中型技术团队用LangGraph搭建PR审查助手:每次代码提交自动触发审查子图,LLM逐文件检查并将问题写入State中的"review_issues"字段。若存在问题,则代码修改子图根据建议自动生成补丁并重新提交,最多迭代3轮。全程运行记录在案,开发者可随时查看每一轮的问题与修改。

Python 演示

python126 行
"""简易LangGraph风格DAG状态机:内容合规审查工作流"""from typing import Dict, Any, List class DAGStateMachine:    """一个极简DAG状态机,仿LangGraph核心语义:节点+条件边+状态字典"""     def __init__(self) -> None:        self.nodes: Dict[str, Any] = {}        self.edges: Dict[str, List[str]] = {}        self.cond_edges: Dict[str, Any] = {}        self.start_node: str = ""        self.gc_count = 0  # 流程执行计数器     def add_node(self, name: str, fn: Any) -> None:        """注册节点:fn为可调用对象,入参state,返回更新字典"""        self.nodes[name] = fn        self.edges[name] = []        self.cond_edges[name] = []     def add_edge(self, src: str, dst: str) -> None:        self.edges[src].append(dst)     def add_conditional_edge(self, src: str, router: Any, mapping: Dict[str, str]) -> None:        self.cond_edges[src] = (router, mapping)     def set_start(self, name: str) -> None:        self.start_node = name     def run(self, init: Dict[str, Any], max_steps: int = 10) -> Dict[str, Any]:        """从起始节点开始执行,直到没有出边或达到步数上限"""        state: Dict[str, Any] = {} if init is None else dict(init)        current = self.start_node        steps = 0        while current and steps < max_steps:            self.gc_count += 1            steps += 1            node_fn = self.nodes[current]            delta = node_fn(state)            if delta is not None:                state.update(delta)            if self.cond_edges.get(current):                router, mapping = self.cond_edges[current]                branch = router(state)                target = mapping.get(branch)                if target is None:                    target = mapping.get("__default__")                current = target            else:                if not self.edges.get(current):                    break                current = self.edges[current][0]        state["__steps__"] = steps        return state  def extract_questions(state: Dict[str, Any]) -> Dict[str, Any]:    """节点1:从用户输入中提取待检查要点"""    text = state.get("user_input", "")    questions = [s.strip() for s in text.split("。") if s.strip()]    return {"questions": questions, "status": "extracted"}  def check_compliance(state: Dict[str, Any]) -> Dict[str, Any]:    """节点2:模拟合规检查(此处为规则示例,可替换为LLM调用)"""    questions = state.get("questions", [])    bad_words = ["暴力", "违法", "刷单"]    issues = []    for i, q in enumerate(questions):        for w in bad_words:            if w in q:                issues.append({"index": i, "word": w})                break    return {"issues": issues}  def router_quality(state: Dict[str, Any]) -> str:    """条件边:根据是否有违规词决定走'通过'还是'驳回'"""    if len(state.get("issues", [])) == 0:        return "pass"    return "reject"  def finalize(state: Dict[str, Any]) -> Dict[str, Any]:    state["review_result"] = "通过" if not state.get("issues") else "需人工复核"    state["status"] = "done"    return state  def fix_content(state: Dict[str, Any]) -> Dict[str, Any]:    """驳回后回到修复节点,重试计数+1"""    state["retry_count"] = state.get("retry_count", 0) + 1    return {"fixed": True}  def main() -> None:    sm = DAGStateMachine()    sm.add_node("extract", extract_questions)    sm.add_node("check", check_compliance)    sm.add_node("fix", fix_content)    sm.add_node("final", finalize)    sm.set_start("extract")     sm.add_edge("extract", "check")    sm.add_edge("final", "final")    sm.add_conditional_edge(        "check",        router_quality,        {"pass": "final", "reject": "fix", "__default__": "final"},    )    sm.add_conditional_edge(        "fix",        lambda s: "recheck",        {"recheck": "check"},    )    # fix -> check 的隐式条件边:只要在fix中retry_count不超过3就继续    result = sm.run(        {"user_input": "如何提升商品销量?请讲解合规运营方法,不要提刷单等违法手段。"},        max_steps=10,    )    print("执行步数:", result["__steps__"])    print("违规问题:", len(result.get("issues", [])))    print("审查结果:", result.get("review_result"))  if __name__ == "__main__":    main()
目的:这是一个仿LangGraph核心语义的微型DAG状态机,演示了“节点注册→边连接→条件路由→状态传递”的完整流程。将合规审查示例替换为真实LLM调用,即可扩展为生产级Agent工作流。
  1. class DAGStateMachine状态机核心类,self.nodes存节点函数,self.edges存静态边,self.cond_edges存条件边。注意self.gc_count用_count后缀避免与方法重名。
  2. def add_node(self, name, fn)注册节点,fn必须是入参state、返回字典的可调用对象。
  3. add_conditional_edge(self, src, router, mapping)注册条件边:router(state)返回分支名,mapping决定分支名→目标节点。
  4. def run(self, init, max_steps)核心执行循环:按DAG拓扑执行节点,每次更新状态,遇到条件边就路由,直到终点或步数上限。
  5. state.update(delta)状态合并更新:节点返回的字典覆盖到全局状态,这正是LangGraph的状态管理语义。
  6. def router_quality(state)路由函数:从状态中读issues列表判断返回pass或reject。
  7. sm.add_conditional_edge("fix", lambda s: "recheck", {...})演示循环:修复节点无条件回到检查节点,形成“检查→修复→再检查”循环。

常见误区

误区一听到“有向无环图”,初学者就认为LangGraph做不了多轮对话、做不了“反思-修正-再反思”这类循环流程,因为“无环”就是禁止回头。

正解DAG约束的是“图结构本身不能有闭合环路”,但完全可以通过“条件边+上游节点引用”的方式模拟循环。就像地铁用折返线让列车在两条轨道间来回跑。LangGraph中常见的做法是:节点A→节点B→条件边→(未达标则回到A),但必须用计数器或退出条件控制循环次数上限,否则会死循环烧token。

为何常见:因为“有向无环图”这个词太有迷惑性,让初学者把“图结构约束”与“执行流程能力”混为一谈。实际上图的边是静态定义,而执行时的动态跳转是由条件边控制的。

误区总觉得LangGraph里的State本质上就是“聊天记录”,把用户消息和AI回复存成一个列表就完事了,其他字段都是多余的。

正解对话历史只是State中一个普通字段。LangGraph的State是一个结构化字典,可以包含用户画像、检索结果、中间评分、重试次数、业务标识等任意字段。节点只读写自己关心的字段,这才是它支持复杂编排的根基。

为何常见:因为大多数初学者的Agent启蒙都是从“ChatCompletion对话”来的,天然把“上下文”等同于“消息列表”。而工程化Agent需要的是“全流程上下文”,消息只是其中一部分。

误区看到LangGraph支持并行fan-out,就恨不得把每个节点都拆成并行的,以为这样总时间一定会大幅缩短。

正解并行只在分支之间真正无依赖时才有收益。如果分支B需要分支A的结果,强行并行只会导致B空等或出错。另外并行节点共享状态时若写同一字段,会互相覆盖。设计时要先画出依赖关系,只在“多路独立信息获取”这种场景下用并行。

为何常见:因为直觉上“同时做多件事肯定更快”,忽略了DAG中依赖边的存在以及汇合点的等待语义。

误区写条件路由函数时,凭直觉返回`True`/`False`或返回“下一个节点对象”,然后在LangGraph里配置时报错。

正解LangGraph条件边返回的是字符串类型的边名(或“下一个节点名”),由路由函数根据状态内容动态决定。映射关系在注册条件边时指定。这相当于道岔扳向哪条轨道,轨道是有名字的。

为何常见:编程直觉里“条件判断就应该返回bool”,忽略了图模型中边本身就是一等公民。

面试题

Q1. LangGraph中的DAG指的是什么?

单选题 · easy

  • A.一种深度学习网络结构
  • B.有向无环图,节点表示动作、边表示数据流向← 正确答案
  • C.数据增强生成技术
  • D.分布式应用网关

答案:B。DAG即Directed Acyclic Graph(有向无环图),在LangGraph中节点代表Agent动作,有向边代表执行依赖与数据流动方向。选项A是神经网络概念,C是RAG(检索增强生成)的变体,D是后端架构术语,均与LangGraph的编排建模无关。

Q2. 关于LangGraph中的State,下列说法正确的是?

单选题 · easy

  • A.State只能保存字符串类型的数据
  • B.State是一个贯穿流程的结构化字典,各节点可读写不同字段← 正确答案
  • C.State与对话历史完全等价,不需要其他字段
  • D.每个节点拥有完全独立的State

答案:B。State是全局共享的结构化字典,节点从其中读取输入并写入输出,字段可任意扩展。对话历史只是其中的一个字段。选项A错误(State支持任意Python类型),C过于狭隘,D与“全局共享”的核心设计矛盾。

Q3. LangGraph条件边中,路由函数的返回值是?

单选题 · mid

  • A.布尔值(True/False)
  • B.下一个节点的Python对象
  • C.字符串(分支边名或目标节点名)← 正确答案
  • D.一段提示词文本

答案:C。路由函数读取State后返回一个字符串,LangGraph用该字符串与注册条件边时的映射关系匹配,决定下一步走向。返回布尔值或节点对象都不符合LangGraph的API契约。这就像道岔扳向岔道的“名字”而非“道岔本身”。

Q4. 下列哪种方式可以在LangGraph中实现“循环”效果?

单选题 · mid

  • A.在图中画一条从节点B回到节点A的无条件边
  • B.使用条件边,当条件不满足时回到上游节点,并设置最大迭代次数← 正确答案
  • C.使用深度优先搜索遍历整个图
  • D.在节点内部递归调用自身且不设退出条件

答案:B。LangGraph中循环的常规做法是“条件边+回到上游”,并必须设置终止条件(如重试次数上限)。选项A无条件边会导致死循环,C是图遍历算法并非Agent流程,D的无限递归会导致栈溢出和token浪费。

Q5. 关于LangGraph并行(fan-out/barrier),以下说法正确的有?

多选题 · mid

  • A.一个节点可以有多条出边指向多个并行节点← 正确答案
  • B.汇合节点在全部入边完成后才会执行← 正确答案
  • C.并行节点可以同时写入同一个状态字段,互不影响
  • D.并行只适合分支之间无依赖的场景← 正确答案

答案:ABD。A正确,多出边即fan-out;B正确,汇合点是AND-barrier语义;D正确,无依赖才适合并行;C错误,多个并行节点同时写同一字段会发生覆盖竞争,应在状态设计中为各分支分配独立字段。这就好比各支线列车都在“同一站台上写字”,必须划分区域。

Q6. LangGraph中State字段更新合并的语义包括?

多选题 · mid

  • A.新字段直接加入State← 正确答案
  • B.已有字段默认被新值覆盖← 正确答案
  • C.字段支持自定义Reducer函数来合并← 正确答案
  • D.所有字段一旦写入就不可修改

答案:ABC。State更新默认是“新字段加入、同名字段覆盖”,也支持通过Reducer自定义合并逻辑(如列表追加、计数器累加)。D错误,State在流程中就是不断被更新的。整个流程就像列车沿途不断装卸不同货物。

Q7. 在LangGraph中,一个子图(SubGraph)被封装为一个节点嵌套进父图,这属于什么能力?

单选题 · hard

  • A.并行计算
  • B.递归调用
  • C.工作流编排与模块复用← 正确答案
  • D.状态持久化

答案:C。子图嵌套是LangGraph工作流编排的核心能力,它让开发者把复杂流程拆分为可独立调试的模块,再组合成更大流程。它不等同于并行(A),也不仅是递归(B,子图内部可以没有递归),检查点(D)是另一套机制。就像城市地铁网中一条线路内部还分多个区段。

Q8. 关于LangGraph多Agent协作的状态设计,以下做法合理的有?

多选题 · hard

  • A.为每个Agent划分独立的私有字段区← 正确答案
  • B.设定全局共享字段区给Agent间传递消息← 正确答案
  • C.所有Agent共同读写同一个字段以避免同步成本
  • D.在条件边中读取特定Agent的写入结果来决定下一步← 正确答案

答案:ABD。合理设计是“私有字段+共享字段”结合,私有不被他人乱写,共享用于协调。C错误:所有Agent共同读写同一字段极易产生覆盖竞争,且难以追踪数据来源。D正确:通过条件边读取某Agent产出做路由判断,是多Agent编排的常见模式。这如同每列地铁有自己的车厢(私有),但换乘大厅(共享)供乘客通行。

Q9. 在LangGraph中设置节点超时与检查点(Checkpoint)的主要目的是?

单选题 · hard

  • A.提升模型推理精度
  • B.保证流程在故障后可从最近状态恢复← 正确答案
  • C.减少模型调用的token数
  • D.让节点自动生成测试用例

答案:B。检查点将每步状态持久化,超时则触发重试或降级,二者配合实现了“可恢复性”。它们不直接提升模型精度(A错)、不减少token(C错)、与测试生成无关(D错)。类似地铁的行车记录黑匣子和应急预案。

Q10. 一个LangGraph工作流包含节点A(起点)、B、C(可由A并行到达)、D(汇合点,接收B和C),其中B和C分别向State的b_result和c_result字段写入数据,D节点需要同时使用两者。问:D触发时默认能拿到完整数据吗?

单选题 · extreme

  • A.不能,因为并行执行时数据会被覆盖
  • B.能,汇合点在所有入边完成后触发,且两个字段互不冲突← 正确答案
  • C.能,因为LangGraph会缓存所有历史状态
  • D.不能,因为B和C只能同时写一个字段

答案:B。汇合节点默认AND语义,即B和C都执行完才轮到D;同时B、C分别写b_result和c_result两个不同字段,状态合并不会冲突。A的“覆盖”只在同名字段写入时发生。这就像两条不同编号的地铁线(3号线、5号线)各自开到同一换乘站,乘客下车后在站厅汇合,互不挤占站台。