想象你拿到一套 200 平米的毛坯房,要在两个月内交付精装。如果只派一个全能师傅从水电做到油漆,他要么累垮、要么返工、要么拖期。聪明的装修公司会怎么做?把工程拆成几个专业小队——水电组、瓦工组、木工组、油漆组,每组只干自己最擅长的事,项目经理统一排期协调。组与组之间只需要交接“电管布好了、地面找平了”这样的标准接口,谁先干谁后干清清楚楚。这就是模块化拆分的魔力:大工程变清晰,小队可复用,哪个环节出问题就只修哪块。 LangGraph 里的子图(Subgraph)干的就是这件事。你面对的不是毛坯房,而是复杂的 Agent 流程——比如一个客服 Agent 要完成意图识别、知识库检索、情绪安抚、工单生成、人工交接……如果把所有逻辑塞进一张大图,就像让一个全能师傅干完所有工种,图会变得臃肿难维护。而 LangGraph 的 Subgraph 机制,让你能把大流程像装修工程一样拆成独立的小图,各自定义状态、各自编译、再像模组一样拼进主图。这篇文章会带你从概念、定义、嵌套、传参、复用一路讲到最佳实践,让你真正学会用子图拆分复杂 Agent。

岗位关联

对 AI 从业者来说,子图是 LangGraph 架构师的核心工具。学会它,你就能把千行大图拆成可维护、可测试、可复用的模块,这在大模型应用工程化落地中几乎是必修课。

目录
  1. 可视化演示
  2. 子图核心概念
  3. 子图定义方式
  4. 子图嵌套层级
  5. 子图传参通信
  6. 子图复用策略
  7. 模块化最佳实践
  8. 实际应用场景
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示子图嵌套结构、状态传递映射机制及子图复用架构,展示LangGraph模块化拆分Agent流程的核心设计模式。

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

子图嵌套结构 子图本身是完整LangGraph图,含独立节点边和State 子图封装为单个节点嵌入父图使用 支持多层嵌套,实现复杂Agent模块化拆分 ParentGraph = Node₁ → SubGraph₁ → Node₂ → SubGraph₂ 子图嵌套实现复杂Agent的模块化设计

子图核心概念

回到装修的比喻。你拿到 200 平米毛坯房,如果把它当成一个“超级大任务”,让一个师傅从头干到尾,那这个“全能师傅”要同时懂水电、泥瓦、木工和油漆——他很容易出错,而且一旦中途有人离职,整个工程就瘫痪了。子图的思路很简单:把大任务拆成几个小任务,每个小任务由一支专业小队负责,每支小队有自己独立的施工图纸(State 定义)、独立的验收标准(入口/出口),而项目经理(主图)只负责排期和协调。

在 LangGraph 里,“子图”指的就是一个编译完成的 StateGraph,它本身是一张完整的图,有自己独立的节点、边和状态定义。当一个图被编译后,它可以被当作一个普通节点嵌入到另一张更大的图中。这种“图里有图”的设计,本质上是把复杂 Agent 的工作流变成了乐高积木——每一块积木内部再复杂,对外只露几个插孔。

为什么需要拆分?核心原因是降低复杂度。假设一张大图有 N 个节点,节点间的连线可能达到 O(N²) 的量级,任何一次修改都可能牵一发动全身。而拆成 k 个子图后,每个子图内部节点数为 Nᵢ,子图间只需维护少量接口连线,整体复杂度大大降低。

理解了拆分逻辑,你会发现一个深刻的道理:子图的本质不是“切碎代码”,而是“定义接口”。就像装修公司不关心水电工用什么牌子的电钻,主图也不关心子图内部是怎么推理的,只关心它能不能按时交付标准结果。这种“外部只认接口、内部自由发挥”的特性,正是子图成为大型 Agent 架构基石的原因。

T整体=∑i=1kT(Ni)+C编排\displaystyle T_{\text{整体}} = \sum_{i=1}^{k} T(N_i) + C_{\text{编排}}
这句话的意思是:一个大任务的复杂度,等于若干个小任务复杂度之和,再加上一个小小的协调成本。因为 C 远小于直接管理一个大图的成本,所以拆分总是划算的。
  1. 求和符号\displaystyle \sum_{i=1}^{k} 表示把 k 个子任务各自的复杂度全部加起来。
  2. 每项 T(N_i)T(N_i) 是第 i 个子图的复杂度,N_i 是它内部的节点数,节点越少复杂度越低。
  3. 编排成本 CC 是主图协调这些子图的开销,只要接口设计得好,C 通常很小。

子图定义方式

装修公司在报价时,不会让一个施工队“什么都干”,而是把工程拆成“水电改造套餐”“泥瓦工程套餐”“整体木作套餐”——每个套餐都有明确的服务边界和交付清单。LangGraph 里定义子图的方式,就是先画一张独立的小图,再把它“打包成套餐”挂到主图上。

定义一个子图最直接的方式:先创建一个 StateGraph,给它加上节点和边,然后调用 compile()。编译后的图就能作为一个节点函数嵌入父图。此时父图节点函数像这样:

你只需要在父图的 StateGraph.add_node("child", call_child) 里挂载它,一张子图就正式成为大图的一员了。这个“编译后当节点用”的设计,让子图结构完全内部自治。

定义子图时还有几个关键点(即“边界节点”)。子图需要明确的入口(set_entry_point)和出口(set_finish_point),父图只跟这两个“门”打交道。为了让子图能配合父图工作,子图的 State 结构通常需要与父图对接——要么共享同一个 State 类型,要么在调用时显式传参数。当你用 LangGraph 的 add_node 把一个编译好的图放进去时,LangGraph 会自动识别它的输入输出字段,就像装修验收只看交付结果一样。

计算公式上并没有新的数学,但从接口设计的角度,子图的“契约”可以形式化描述为:父图对子图的调用等于一个函数复合。

想记住“子图定义方式”的精髓,可以拿装修公司做类比:每个子图就是一支有营业执照的施工队,它有资质(State 类型)、有报价单(接口文档)、有验收标准(入口/出口)。主图招投标(add_node)时只看资质不看流水线,这就是“编译后当节点用”的工程智慧。

父图输出=F父(S入口, F子(S请求))\displaystyle \text{父图输出} = F_{\text{父}}( S_{\text{入口}},\ F_{\text{子}}( S_{\text{请求}} ) )
这个式子说:父图把一部分状态作为“请求”发给子图,子图处理完返回“响应”,父图再用响应继续推进自己的流程。
  1. F_子(S_请求)子图是一个函数,吃进请求状态,产出结果。
  2. F_父(S_入口,……)父图是更大的函数,它把自己的入口状态和子图结果一起处理。
  3. 函数嵌套整个表达式是一个嵌套调用,对应主图“调用一个子图节点”的动作。
python2 行
def call_child(state):    return child_app.invoke(state)

子图嵌套层级

装修界有总包和分包。总包接了整个楼盘的外墙工程,又把外墙拆成东区、西区、北区三个标段;东区标段又分包给三个班组。子图嵌套也是这个道理——子图内部还可以再挂子图,形成“根图 → 子图 → 孙图”的层级结构,每一层只跟相邻层通信。

LangGraph 的嵌套没有深度硬限制,但实际工程中常见的是两到三层。根图(Root Graph)是总入口,负责定义整体流程。它调用的子图负责某个子流程,子图内部又可以调用更小的子图。嵌套的层级越高,状态映射(请求/响应字段的对应关系)就越复杂,所以实践中要克制。

嵌套时最核心的机制是“请求/响应映射”。父图调用子图时,需要显式说明把父状态的哪些字段传给子图,子图返回后把哪些字段写回父状态。LangGraph 官方推荐用这个模式:

这里的关键是:子图运行在自己的状态空间里,它完全不知道父图还有别的字段,也拿不到父图的全部状态。这种隔离让嵌套变得更加安全——修改孙图不会波及根图。

从数学上看,嵌套的深度 L 与每层的状态映射维度相关。假设每一层保留的接口字段数为 m,则状态传递的“信息流”可写成一个递推关系。

在实际项目中,嵌套层级越深,排错越难——就像总包都不清楚东区标段的三号班组用的什么砂浆。所以最好的实践是:能两层解决就不上三层,每一层的传参显式写清楚,不靠隐式共享。记住,嵌套的本质是“层级隔离”,每一层只对上一层负责。

S请求(l+1)=P(S响应(l)),l=0,1,…,L−1\displaystyle S^{(l+1)}_{\text{请求}} = \mathcal{P}\left( S^{(l)}_{\text{响应}} \right),\quad l=0,1,\dots,L-1
公式含义是:每一层的请求都是从上一层的响应里“投影”出来的,就像总包只把“里程碑节点”传给分包,细节自己留档。
  1. 投影函数 P\mathcal{P} 表示从上层状态筛选出本层需要的字段,这是一个映射操作。
  2. 下标 ll 表示当前嵌套的层数,l=0 是根图,l=1 是第一层子图。
  3. 递推关系整个式子描述了状态在嵌套层级间逐层传递的路径。
python7 行
def call_child(state):    child_request = {        "query": state["user_query"],        "history": state["conversation"]    }    result = child_app.invoke(child_request)    return {"subgraph_answer": result["answer"]}

子图传参通信

装修队之间的配合靠“交接单”:水电组干完,在墙上留一张单子写明“电管走向已标注、水管打压完成”,瓦工组照单施工。谁也不会把整个装修图纸搬过去。子图传参通信的精髓正是这种“交接单文化”——父图给子图一张“请求单”,子图回一张“结果单”。

LangGraph 中传参通信有两条路:一是共享同一个 State 类型(图结构定义时用同一个 Schema),二是调用时显式构造请求/响应字典。显式传参更推荐,因为它把“交接单”的内容摆在了明面上。子图内部有自己独立的 State,即使和父图 Schema 相同,运行时也是隔离的两份数据,不会互相污染。

具体来说,父图节点里调用子图时,LangGraph 会用你返回的字典关键字段去覆盖父状态里同名字段。这既是通信通道,也是数据更新的方式——你返回 {"subgraph_answer": ...},父图就把 subgraph_answer 更新为新值。这种 State 覆盖机制让信息流动变得可追踪:每一步改了什么字段一目了然。

用集合的语言来描述这种通信:父图的完整状态集合为 P,子图的应答集合为 R,一次通信本质上是字段投影的复合。

把通信理解成“交接单”,你就能避免两个常见错误:一是别指望子图自动拿到父图的全部状态(谁也不会把图纸全给水电工);二是别让子图直接改父图的深层嵌套字段(交接单只写结果不写过程)。显式、小巧、命名清晰——这是传参通信的黄金法则。当你的按钮按下,整栋大厦的电路由一根根独立的管线串联起来时,你才知道当年那句“接口比实现更重要”有多正确。

S父子通信={(k,v)∣k∈P∩R}⏟交集字段⊕{(k,v)∣k∈R∖P}⏟新增字段\displaystyle S_{\text{父子通信}} = \underbrace{\{ (k,v) \mid k \in P \cap R \}}_{\text{交集字段}} \quad\oplus\quad \underbrace{\{ (k,v) \mid k \in R \setminus P \}}_{\text{新增字段}}
这个式子把通信拆成两类:一类是父图和子图都认识的“公共字段”,另一类是子图新产生的“专属字段”,两者合起来构成一次完整的传参。
  1. 交集字段P ∩ R 表示两个状态集合中名字相同的字段,这些字段直接映射传递。
  2. 集合差 R \ P表示子图独有的输出字段,父图需要“吸收”这些新信息。
  3. 直和符号\oplus 表示两组字段合并成一个完整的状态更新包。

子图复用策略

装修公司最赚钱的能力不是施工,而是“复制经验”:这套水电工艺在 A 小区用过,在 B 小区几乎原样再来一遍。子图复用也是同一个商业逻辑——同一张子图可以在主图的不同分支、不同位置反复挂载,每次调用都是独立运行,互不干扰。

LangGraph 中的复用有几种常见姿势。第一,多实例复用:同一张编译好的子图,在主图的多个节点上调用,每个 Node 的调用彼此独立,子图运行状态不会串。第二,多图组合:多张子图可以组合成一张更大的图,子图之间像搭积木一样串联或并联。第三,条件入口:主图根据当前状态动态选择调用哪张子图,相当于给施工队派单时按户型分流。第四,工厂函数:写一个返回子图的函数,根据参数生成不同配置的子图实例——就像装修公司针对大平层和复式楼分别定制施工方案。

复用带来的收益可以用一个简单的数学模型衡量。假设开发一张子图的固定成本是 C_dev,单次运行成本是 c_run,如果重复开发 m 次不复用,总成本是 m × (C_dev + c_run);而复用只需要开发一次,后续每次只付运行成本。

复用子图最大的价值在于一致性——同一个水电施工队在 100 个工地干出来的活标准一样,Agent 也一样:同一个子图在不同流程中表现完全一致,消除了“同一逻辑在不同地方有不同行为”的口碑危机。当然,复用要克制。如果两张子图只有 10% 的共性,硬凑一张反而绑手绑脚;装修队也不是一支队伍干所有活,专家队的复制才有意义。

复用收益=(m−1)×Cdev+(m×Cdev′+cmin⁡)其中 Cdev′→0\displaystyle \text{复用收益} = (m-1) \times C_{\text{dev}} + (m \times C_{\text{dev}}' + c_{\min}) \quad \text{其中 } C_{\text{dev}}' \to 0
这句话是说:当同一张图被复用的次数越多,省下的开发成本就越可观——这也是“一次开发,处处运行”的工程学意义。
  1. 开发成本项(m-1) × C_dev 表示省下了 m-1 次重复开发的开销。
  2. 趋近于 0 的 C_dev'复用后每次接入的边际开发成本趋近于零。
  3. 运行成本 c_min唯一不可省的是每次运行的资源开销,但这是必需支出。

模块化最佳实践

装修干得多了,优秀的项目经理会总结出一套“施工标准答辩手册”:图纸统一编号、材料统一采购、每个工种进场前先看前一道工序的验收单。模块化编程的最佳实践,本质上就是这些工程经验的数字化。

在 LangGraph 子图拆分中,最有价值的四个实践是:

第一个是依赖注入。子图不应该在内部硬编码外部资源(比如 API Key、模型名),而应通过入参或配置传入,就像装修队不自己烧砖,而是由总包统一供料。这样同一个子图换一套配置就能适配不同环境。

第二个是关注点分离。每个子图只做一件事:检索子图只检索,生成子图只生成,不要在一个子图里既检索又生成。检查你拆分的粒度是否合适,就看“这个子图你会不会给它起个动词开头的名字”。

第三个是命名规范。节点名、子图绑定的 key 都要语义清晰,比如 rag_retrieve、escalate_to_human,不要用 node1、node2。装修队的对讲机里喊“张三李四”还是喊“水电三组”完全影响现场效率。

第四个是接口收敛。子图对外暴露的 State 字段越少越好——请求给两个字段、返回一个字段,就是最理想的迷你接口。对外暴露的字段一旦增多,复用的阻力就变大。

这个“接口数量”的哲学可以量化:假设子图对外暴露的字段数为 p,每增加一个字段,主图与子图间的熵耦合就多一分。耦合度可以写成:

装修界有句老话:“好木匠不换工具,换工具的不是好木匠。”模块化最佳实践的核心,是让子图成为一把“好工具”——内部复杂但外部简单。依赖注入让它适应不同环境,关注点分离让它职责单一,命名规范让协作不吃力,接口收敛让它随处可嵌。有了这些实践,复杂 Agent 才能像装修流水线一样,每个模块都靠谱、每个接口都干净。

耦合度=1−p共享p总,p共享≤p总\displaystyle \text{耦合度} = 1 - \frac{p_{\text{共享}}}{p_{\text{总}}} \quad,\quad p_{\text{共享}} \leq p_{\text{总}}
意思是:子图对外暴露的字段里,真正被多个调用方共享的越少,耦合度越低,这个子图就越“独立自主”。
  1. p_共享指多个父图节点都会读写的字段数量,这类字段造成耦合。
  2. p_总子图对外暴露的全部字段总数。
  3. 比值共享字段占比越小,比值越接近 0,耦合度越低。

实际应用场景

当一支训练有素的装修工程队走上正轨,它能接的活儿就远不止一套毛坯房了。LangGraph 的子图模块化一旦落地,复杂 Agent 的场景几乎都能“一键接入”。下面看三个典型场景。

第一个是智能客服工单全流程。整套系统拆成四张子图:意图识别子图(听懂用户说什么)、知识库检索子图(找标准答案)、情绪安抚子图(态度决定天)、工单生成子图(处理不了自动建单)。主图负责调度:先走意图识别,再按分支决定走检索还是情绪安抚,最终汇聚到工单生成。传统做法里这些逻辑全写在一个大图里,改一处全线回归测试;拆成子图后,团队分成四人组各自维护一张图,改情绪安抚词库完全不影响检索逻辑。

第二个是数据分析流水线。数据分析 Agent 拆成数据采集子图、数据清洗子图、统计分析子图、报告生成子图。其中“数据清洗子图”尤其值得复用——它像一个标准工程队,无论是跑电商销售数据还是跑服务器日志,它都能干;主图换掉输入输出映射,同一张清洗图就能服务完全不搭界的业务线。

第三个是内容审阅发布流水线。这个场景把流程拆成初稿生成子图、合规检查子图、风格润色子图、多平台发布子图。合规检查子图常常是独立部署的——因为它的审核标准和发布业务全无关系,独立成图后甚至可以发布给其他团队复用。这就是子图模块化的终极形态:不是某个 Agent 的一部分,而是可供整个公司调用的公共设施。

这些场景揭示了同一个规律:找到了 30% 的公共子图,就找到了 70% 的复用杠杆。子图拆分的是代码,撬动的是工程效率和团队协作。

AI 应用场景

智能客服工单全流程

阶段:流程编排与模块化

解决的问题:传统客服 Agent 将所有逻辑塞进一张大图,遇到需求变更(比如新增“退款申请”流程)要改动主链路,风险高、测试慢。用子图拆分后,每个环节独立迭代互不干扰,工单生成逻辑调整不需要回归整个客服系统。

具体案例:某电商平台客服 Agent 拆为“意图识别、检索、情绪安抚、工单生成”四张子图。某次大促前紧急新增“仅退款”政策,团队只改了工单生成子图的语义规则,两小时内完成上线,其余子图零改动。

数据周报自动生成系统

阶段:数据管道编排与复用

解决的问题:数据分析 Agent 最怕“数据源换了一个,全部脚本重写”。抽取公共子图后,数据清洗和统计计算模块可以跨业务线复用,新的数据源只需写一个采集子图接入。

具体案例:某公司为三条业务线分别搭建周报 Agent,两期后发现清洗和统计逻辑高度重合,于是抽成公共子图库,两条新业务线的开发周期从两周缩短到三天。

媒体内容审阅发布流水线

阶段:业务链路模块化分工

解决的问题:内容平台的“生成-审核-发布”链路涉及产运、法务、编辑多个团队,如果一张大图被所有团队共同修改,权限和变更管理都是灾难。子图拆分后,每个团队只管自己那张子图,通过接口对接。

具体案例:某 MCN 机构把选题生成、合规审核、多平台发布拆成三张子图。合规审核子图单独维护,法务更新规则只发布那一个子图版本,发布子图毫不知情,整个系统稳定运行零事故九个月。

Python 演示

python116 行
# 简易 LangGraph 子图拆分演示(纯 Python 实现核心思路)class GraphState(dict):    """状态容器,类似 LangGraph 的 State"""    pass class StateGraph:    """简化版 StateGraph,支持子图嵌入"""    def __init__(self, state_schema=None):        self.nodes = {}        self.edges = []        self.entry = None        self.finish = None        self.schema = state_schema or GraphState     def add_node(self, name, fn):        self.nodes[name] = fn        return self     def add_edge(self, start, end):        self.edges.append((start, end))        return self     def set_entry(self, name):        self.entry = name        return self     def set_finish(self, name):        self.finish = name        return self     def compile(self):        """编译后返回可调用对象,即'子图'"""        return CompiledGraph(self) class CompiledGraph:    """编译后的图,可被父图当作节点调用"""    def __init__(self, graph):        self.graph = graph        self.node_map = graph.nodes     def invoke(self, state):        cur = self.graph.entry        while cur != self.graph.finish:            fn = self.node_map[cur]            state = GraphState(state)            result = fn(state)            if result:                state.update(result)            nxt = None            for s, e in self.graph.edges:                if s == cur:                    nxt = e                    break            cur = nxt        return dict(state) # ============ 装修公司案例:总图调用三支"施工队"子图 ============def make_water_node():    """水电子图"""    g = StateGraph()    g.add_node("lay_pipes", lambda s: {"pipe_status": "done"})    g.add_node("check_leak", lambda s: {"leak_check": "passed"})    g.add_edge("lay_pipes", "check_leak")    g.set_entry("lay_pipes")    g.set_finish("check_leak")    return g.compile() def make_tile_node():    """瓦工子图"""    g = StateGraph()    g.add_node("level_floor", lambda s: {"floor_level": "ok"})    g.add_node("polish", lambda s: {"tile_status": "done"})    g.add_edge("level_floor", "polish")    g.set_entry("level_floor")    g.set_finish("polish")    return g.compile() def make_paint_node():    """油漆子图"""    g = StateGraph()    g.add_node("prime_wall", lambda s: {"prime": "done"})    g.add_node("coat", lambda s: {"paint_status": "finished"})    g.add_edge("prime_wall", "coat")    g.set_entry("prime_wall")    g.set_finish("coat")    return g.compile() def build_master_graph():    """主图:编排三支施工队"""    water_sub = make_water_node()    tile_sub = make_tile_node()    paint_sub = make_paint_node()     def call_water(state):        return water_sub.invoke(state)     def call_tile(state):        return tile_sub.invoke(state)     def call_paint(state):        return paint_sub.invoke(state)     master = StateGraph()    master.add_node("water_team", call_water)    master.add_node("tile_team", call_tile)    master.add_node("paint_team", call_paint)    master.add_edge("water_team", "tile_team")    master.add_edge("tile_team", "paint_team")    master.set_entry("water_team")    master.set_finish("paint_team")    return master.compile() if __name__ == "__main__":    app = build_master_graph()    result = app.invoke({"project": "200平毛坯房"})    print("装修结果:", result)
目的:用纯 Python 模拟 LangGraph 子图嵌入机制——主图(总包)依次调用水电、瓦工、油漆三张子图,每张子图内部有独立的节点链,对外表现为一个函数。运行后输出装修全流程的最终状态。
  1. class StateGraph:定义简化版状态图类,add_node 挂载节点,add_edge 连边,模拟 LangGraph 的图构建 API。
  2. def compile(self): return CompiledGraph(self)编译后返回一个新对象,这个对象就是“子图”,可被父图当成普通节点。
  3. def invoke(self, state):子图的核心入口:接受一个状态字典,按边拓扑执行,返回更新后的状态。
  4. state = GraphState(state)每次调用生成新的状态容器,实现了子图运行时的状态隔离。
  5. result = fn(state); state.update(result)节点函数的返回值会覆盖更新状态,这正是 State 覆盖通信机制的精髓。
  6. make_water_node()工厂函数:每调用一次就生成一张全新的“水电子图”编译实例,多个父图互不干扰。
  7. master.add_node("water_team", call_water)主图把整张编译好的子图包装成节点函数挂载,对应 LangGraph 的 add_node("child", child_graph.invoke)。
  8. build_master_graph()组装总图:主图只认识 water_team / tile_team / paint_team 三个接口,对子图内部一无所知。

常见误区

误区编译完的子图既然是个 Python 对象,那我随时可以调用 `add_node` 再往里面塞一个节点。

正解编译后的 CompiledGraph 与构建时的 StateGraph 是隔离的,compile() 之后子图结构已冻结。想要不同的结构,要么改源码重新编译,要么用工厂函数生成不同配置的子图实例。这就像装修队进场后不能临时换图纸,要改方案得回到设计部重新出图。

为何常见:初学者把“编译”理解成 Java 的运行时对象,以为编译只是加了一层壳,忽略了 LangGraph 中“编译后成为节点”这个身份转变的不可逆性。

误区子图是用父图的状态 Schema 定义的,所以调用时父图整个状态都会被塞进子图。

正解LangGraph 只传递你显式构造的字段。你在父图节点函数里 child_app.invoke({"query": ...}),子图就只知道 query。这是一个防错设计:子图的输入输出越窄,耦合越少。总包不会把 200 页图纸全给水电工,只给水电专项图。

为何常见:因为从代码看父图和子图用的是同一个字典类型,新手容易把“类型相同”误解为“数据互通”,忽略了显式传参这一层。

误区为了能传参,子图必须 import 父图的那个 State 类,否则字段对不上。

正解这恰恰是反向最优实践。子图最好定义自己独立的 State 接口,用请求/响应字段映射对接。LangGraph 支持任意可序列化的状态字典作为子图输入输出,你完全可以给子图另起一套精简的字段名。接口越独立,子图越容易被其他项目复用。

为何常见:共享 Schema 在写第一个 Demo 时最省事,于是被误当成了标准做法。真正的模块化设计恰恰要摆脱“共享结构”的惯性。

误区每张子图在父图里出现一次,就像每个工种只干一个岗位。

正解同一张编译好的子图可以在父图的不同分支、不同节点反复调用。每次 invoke 都是独立的状态空间,运行结果互不影响。你可以在改写产品文案和改写营销邮件两个分支里调用同一个“风格润色子图”,两份文案各自走各自的流程,互不串扰。

为何常见:把“Python 函数被调用多次”的直觉丢了——明明函数能复用,编译后的子图却被默认为单次资源。

误区模块化嘛,把每两个节点之间的逻辑都单独抽成子图,绝对是最优雅的架构。

正解每次子图调用都有通信成本和状态映射成本,拆分过细会让主图变成一张纯调度网——主图上全是“调A、调B、调C”的胶水节点,真正的业务逻辑反而淹没在接口转换里。好的拆分粒度是“一张子图对应一个业务阶段”,比如装修只分水电、瓦工、木作、油漆四段,而不是把“贴砖”再细分成“拌浆”“抹平”“放砖”“敲实”四个子图。

为何常见:常见“为了模块化而模块化”的冲动,忽略了模块化的目的是降低总复杂度,而不是把复杂度转移到接口对接上。

面试题

Q1. 在 LangGraph 中,一个编译完成的 StateGraph 可以被如何处理?

单选题 · easy

  • A.作为节点嵌入父图← 正确答案
  • B.只能在单独进程中运行
  • C.不能再被任何图引用
  • D.只能作为根图运行

答案:A。编译后的子图本质是一个可调用的图对象,LangGraph 允许你把它作普通节点挂到父图上。就像一支装修队可以承接任何总包派发的活。B/C/D 都违反了“子图即节点”的核心特性。

Q2. 下列关于子图传参的说法正确的是?

单选题 · easy

  • A.父图状态全量自动传入子图
  • B.子图只能返回空字典
  • C.需显式构造请求字段传给子图← 正确答案
  • D.子图与父图必须共享内存

答案:C。LangGraph 子图通过显式传参通信,只传递你构造的字段。A 错在“全量自动”,C 对,D 完全不符合状态隔离设计。这就像总包只给施工队发交接单,而不是把所有图纸全塞过去。

Q3. 子图嵌套时,请求/响应映射的主要作用是?

单选题 · mid

  • A.提高子图运行速度
  • B.将上层状态投影为子图需要的字段← 正确答案
  • C.自动合并所有状态字段
  • D.删除父图的冗余数据

答案:B。嵌套层级间通过投影函数 P 将上层响应映射为下层请求,实现状态隔离与定向传参。B 正是“投影”的含义。A/D 与传参无关,C 违反了“显式传参”原则。

Q4. 一张子图在父图里被复用的最佳方式是什么?

单选题 · mid

  • A.每次复制源码重新编译
  • B.同一编译实例多次 invoke← 正确答案
  • C.为每个调用方重写一次
  • D.只能在条件分支里用一次

答案:B。复用应直接复用编译好的子图实例,每次 invoke 独立状态空间互不干扰。A/C 都重复造轮子,D 限制了复用场景。这就像装修队承接多个楼盘的不同标段,同一个队进场 N 次。

Q5. 下面哪些属于子图模块化的最佳实践?

多选题 · mid

  • A.依赖注入外部配置← 正确答案
  • B.子图内做所有业务逻辑
  • C.对外暴露最少字段← 正确答案
  • D.命名采用 node1/node2

答案:AC。A 和 C 是标准最佳实践,分别保证可配置性和低耦合度。B 违背关注点分离,D 违背命名规范。好接口讲究“小而清晰”,就像交接单只写必要项。

Q6. 关于子图内部状态与父图状态的关系,正确的理解是?

单选题 · hard

  • A.两者是同一份数据的引用
  • B.子图内部有独立状态空间← 正确答案
  • C.子图可以修改父图全部字段
  • D.共享 Schema 意味着数据互通

答案:B。子图拥有独立状态空间,通过返回值与父图交互。A 错在引用共享,C 错在“全部字段”,D 是典型误区——共享类型不代表共享数据。

Q7. 在设计复杂 Agent 时,哪些信号说明应该引入子图?

多选题 · hard

  • A.单张图节点数超过几十个← 正确答案
  • B.存在独立可复用的业务阶段← 正确答案
  • C.多个团队需要并行维护不同模块← 正确答案
  • D.图中所有逻辑一次性执行完毕

答案:ABC。A 图规模大、B 有可拆分阶段、C 有协作需求,都是拆分子图的信号。D 恰好相反——流程过于简单线性时引入子图反而是过度设计。

Q8. LangGraph 中子图定义完成后,父图如何接入它?

单选题 · easy

  • A.直接用 apply 方法连接
  • B.通过 add_node 注册子图函数← 正确答案
  • C.必须修改子图源码
  • D.重新写一张合并大图

答案:B。父图用 add_node("child", child_app.invoke) 注册后加边即可。B 是标准接入方式。A 是 API 误记,C/D 都是绕远路,好比总包拿到施工队电话直接调度。

Q9. 状态覆盖机制中,子图返回 {"result": x} 后发生了什么?

单选题 · hard

  • A.父图所有状态被清空
  • B.父图状态里 result 字段被更新← 正确答案
  • C.子图内部状态被覆盖
  • D.父图状态完全不变

答案:B。子图返回的字典以字段为键覆盖父状态中同名字段。B 是正确的 State 更新语义。A/C/D 错得离谱,这就像施工队交回验收单,总包只登记单上写的项目。

Q10. 关于子图复用收益的表述,下列哪些是正确的?

多选题 · extreme

  • A.复用消除了多份逻辑不一致的风险← 正确答案
  • B.复用能省去每次的运行成本
  • C.固有一定成本后边际接入成本趋零← 正确答案
  • D.任何子图复用次数越多越划算

答案:AC。A 对,复用保证同一逻辑各处行为一致。B 错,每次运行仍有资源成本。C 对,边际开发成本趋零正是复用模型的核心。D 错,共性低的子图强行复用反增耦合——就像让油漆队去通下水道,省不了钱还坏事。