子图
图中嵌图——用模块化方式构建复杂智能体系统
一个节点 = 一个完整的图,组合拼装成更大的系统图越来越复杂
单个图有 30 个节点、20 条边——没人能看懂,也没人敢改。
维护困难
一个巨大的图里塞了研究、写作、审核、发布四个流程。改审核逻辑时误伤了写作流程。
无法复用
团队 A 写了一个"代码审查"图,团队 B 也需要一个。只能复制粘贴,各自维护。
协作冲突
三个团队在同一个图文件上开发。每天都在解决合并冲突,Git 状态一团糟。
开始之前
前置知识
- Ch04-10 的所有核心概念
- 特别是 持久化(checkpointer 传播)
- 理解 StateSchema 和状态共享
学完后你应该能
- 区分两种子图通信方式
- 在节点内调用子图(状态不同)
- 将子图直接作为节点添加(状态共享)
- 理解子图的持久化模式
什么是子图
子图就是作为另一个图中节点使用的图

图:父图与子图的关系
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个大圆角矩形代表"父图",内部包含 3 个节点 - 节点 1:"研究子图"(蓝色,内部可见 2-3 个小节点) - 节点 2:"写作子图"(绿色,内部可见 2-3 个小节点) - 节点 3:"审核节点"(橙色,普通节点无内部结构) - 节点之间有箭头连接:研究→写作→审核 - 左侧箭头进入父图,标注"输入" - 右侧箭头离开父图,标注"输出" - 右下角注释框:"子图 = 一个完整的图,可以有自己的节点和边" - 各元素关系:父图包含子图作为节点(组合关系),子图内部有独立的节点和边;父图的边连接到子图节点,子图执行完毕后将结果返回给父图 - 关键标注:父图边框标注"父图",子图内标注各自的内部节点名称,右侧注释说明子图的定义 - 配色建议:浅灰背景,父图浅色边框,子图各有不同柔和色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
适用场景
- 多智能体系统:不同团队各负责一个子图
- 复用组件:一组节点在多个图中重复使用
- 分布式开发:不同团队独立开发、独立测试
两种通信
关键决策:父子图的状态是否共享?
| 模式 | 适用场景 | 做法 |
|---|---|---|
| 在节点内调用 | 父子图有不同状态(无共享键) | 写包装函数映射状态 |
| 直接作为节点 | 父子图共享状态键 | 直接传入编译后的子图 |

图:两种子图通信方式对比
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面分为左右两部分,用虚线分隔 - 左侧标题"节点内调用"(蓝色): - 父图状态 {foo: "..."} → 包装函数 {bar: state.foo} → 子图执行 → 结果 {bar: "hi! ..."} → 映射回 {foo: "hi! ..."} - 标注:"状态不同,需要手动转换" - 右侧标题"直接作为节点"(绿色): - 父图状态 {foo: "..."} → 直接传入子图 → 子图返回 {foo: "hi! ..."} → 自动合并 - 标注:"状态共享,无需转换" - 各元素关系:左侧"节点内调用"需要包装函数做状态映射(父状态→子状态→结果→父状态);右侧"直接作为节点"状态自动传递,无需映射 - 关键标注:左侧每步标注转换逻辑({foo}→{bar}→{bar:result}→{foo:result}),右侧标注"共享状态键 foo,自动合并" - 配色建议:浅灰背景,左侧蓝色调,右侧绿色调 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
节点内调用
父子图状态不同时,在节点函数内调用子图并转换状态
// 子图 — 有自己的状态 { bar }
const SubgraphState = new StateSchema({ bar: z.string() });
const subgraph = new StateGraph(SubgraphState)
.addNode("subgraphNode1", (state) => {
return { bar: "hi! " + state.bar };
})
.addEdge(START, "subgraphNode1")
.compile();
// 父图 — 状态是 { foo },和子图的 { bar } 不同
const State = new StateSchema({ foo: z.string() });
const graph = new StateGraph(State)
.addNode("node1", async (state) => {
// 手动映射:foo → bar
const output = await subgraph.invoke({ bar: state.foo });
// 手动映射回:bar → foo
return { foo: output.bar };
})
.addEdge(START, "node1")
.compile();
直接作为节点
父子图共享状态键时,直接传入编译后的子图
// 父子图共享 { foo } 状态
const State = new StateSchema({ foo: z.string() });
const subgraph = new StateGraph(State)
.addNode("subgraphNode1", (state) => {
return { foo: "hi! " + state.foo };
})
.addEdge(START, "subgraphNode1")
.compile();
// 直接传入子图作为节点 — 无需包装函数
const graph = new StateGraph(State)
.addNode("node1", subgraph) // 直接传!
.addEdge(START, "node1")
.compile();
子图持久化
子图的检查点行为取决于它的 checkpointer 配置
| 模式 | checkpointer | 行为 |
|---|---|---|
| 每次调用(默认) | 不指定 | 每次调用从新开始,继承父 checkpointer 支持中断 |
| 每线程 | 指定 | 状态在同一线程的多次调用间累积 |
| 无状态 | false | 无检查点,像普通函数调用 |
功能对比
| 功能 | 每次调用 | 每线程 | 无状态 |
|---|---|---|---|
| 中断 (HITL) | ✅ | ✅ | ❌ |
| 多轮记忆 | ❌ | ✅ | ❌ |
| 多次调用(不同子图) | ✅ | ⚠️ | ✅ |
| 状态检查 | ⚠️ | ✅ | ❌ |
查看子图状态
用 subgraphs: true 选项查看子图内部状态
// 查看当前调用的子图状态
const subgraphState = (await graph.getState(config, { subgraphs: true }))
.tasks[0].state;
// 流式输出子图结果
for await (const chunk of await graph.stream(
{ foo: "foo" },
{ subgraphs: true, streamMode: "updates" }
)) {
console.log(chunk);
}
subgraphs: true 后,可以查看子图内部的执行细节——包括中间节点的状态和输出。
排错指南
状态键不匹配
症状:子图读不到父图的数据,或返回的数据父图不认识。
修复:检查父子图的 StateSchema。使用"直接作为节点"时,两者的键名必须完全一致。
checkpointer 没传播
症状:子图中的 interrupt 不暂停。
修复:确认在父图编译时传入了 checkpointer。默认情况下子图自动继承父图的 checkpointer。
忘记 compile
症状:传入子图时报类型错误。
修复:子图必须先 .compile() 才能作为节点使用。未编译的 StateGraph 不是可执行图。
动手练习
构建一个包含两个子图的父图
- 子图 A(研究):状态为
{ topic, findings },包含"搜索"和"整理"两个节点 - 子图 B(写作):状态为
{ findings, article },包含"起草"和"润色"两个节点 - 父图状态为
{ topic, findings, article } - 用节点内调用模式连接两个子图
参考答案
// 子图 A:研究
const ResearchState = new StateSchema({
topic: z.string(),
findings: z.array(z.string()).optional(),
});
const researchGraph = new StateGraph(ResearchState)
.addNode("search", async (state) => {
// 模拟搜索
return { findings: [`Found info about ${state.topic}`] };
})
.addNode("organize", (state) => {
return { findings: state.findings?.map(f => `Organized: ${f}`) };
})
.addEdge(START, "search")
.addEdge("search", "organize")
.compile();
// 子图 B:写作
const WritingState = new StateSchema({
findings: z.array(z.string()).optional(),
article: z.string().optional(),
});
const writingGraph = new StateGraph(WritingState)
.addNode("draft", (state) => {
return { article: `Draft based on ${state.findings?.join(", ")}` };
})
.addNode("polish", (state) => {
return { article: `Polished: ${state.article}` };
})
.addEdge(START, "draft")
.addEdge("draft", "polish")
.compile();
// 父图 — 用节点内调用连接
const ParentState = new StateSchema({
topic: z.string(),
findings: z.array(z.string()).optional(),
article: z.string().optional(),
});
const parentGraph = new StateGraph(ParentState)
.addNode("research", async (state) => {
const out = await researchGraph.invoke({ topic: state.topic });
return { findings: out.findings };
})
.addNode("write", async (state) => {
const out = await writingGraph.invoke({ findings: state.findings });
return { article: out.article };
})
.addEdge(START, "research")
.addEdge("research", "write")
.addEdge("write", END)
.compile();
📄 完整代码:11_multi_level_subgraph.ts
本章回顾
到这里,你已经能
- 区分两种子图通信方式
- 在节点内调用子图(状态不同时)
- 将子图直接作为节点(状态共享时)
- 理解子图的三种持久化模式
- 查看和流式输出子图内部状态
延伸阅读
进阶方向
- LangSmith 调试 — 可视化追踪子图执行
- 工具集成 — 网页搜索、数据库查询、API 调用
- 重试逻辑 — 指数退避实现失败重试
- 测试策略 — 单元测试和集成测试
官方资源
- LangGraph.js 文档
- LangSmith Studio — 可视化原型开发
- LangGraph 部署