LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 4 · Parallelization
1 4-1 Parallelization
2 4-2 Sub Graph
3 4-3 Map Reduce
4 4-4 Research Asst
HomeLangGraphModule 4 · Parallelization4-2 Sub Graph
📓 Notebook 📖 Explained
LANGGRAPH MODULE 4 · LESSON 2

Sub-graphs
The Art of Modularity and State Boundaries

Embed a compiled graph as a node in the parent graph - understand how subgraphs communicate, how to encapsulate complex logic,
build a truly scalable multi-Agent system.

1What is a sub-graph?

Sub-graph is a core advanced feature in LangGraph:Insert a compiled StateGraph directly into another parent graph as a node (Node). From the perspective of the parent graph, this subgraph is no different from an ordinary node—it still receives State, executes logic, and returns updates; but inside it is actually a complete independent graph with its own state, nodes, and edges.

Subgraphs can be understood as "pictures within graphs" or "super nodes with complete workflows in functions". It allows you to achieve true meaning in the world of LangGraphNested schema(Nested Architecture)。

Intuitive structure of subgraphs

START
clean_logs
Parent graph node: cleaning data
Subgraph A
get_failures
generate_summary
failure_analysis
Subgraph B
generate_summary
send_to_slack
question_summarization
END
one sentence definition

subgraph = a callcompile()The resulting StateGraph instance can be passed directly into the parent graph as a node.add_node(), thus becoming a step in the execution flow of the parent graph.

The specific way to add it is a line of code:

# 把编译好的子图作为节点加入父图
entry_builder.add_node("failure_analysis", fa_builder.compile())
entry_builder.add_node("question_summarization", qs_builder.compile())

herefa_builder.compile()What is returned is aCompiledStateGraphObject, which can be passed in like an ordinary functionadd_node, because LangGraph’s compiled graph implementsRunnable interface, has the same calling convention as ordinary Python functions.

2Why Subgraphs Are Needed: Modularity vs. Encapsulation

When you build a complex AI system, directly stacking all nodes in a large graph will cause a series of problems: logical confusion, state pollution, difficulty in testing, and inability to reuse. Subgraphs are created to solve these problems.

The three core values brought by sub-pictures

Modularity
  • Each subgraph is an independent functional unit
  • Can be independently developed, tested and debugged
  • The parent picture only cares about the "result" and not the "process"
Reusability
  • The same sub-picture can be inserted into multiple parent pictures.
  • Avoid writing the same logic repeatedly
  • Update the subgraph logic and it will automatically take effect in all places where it is used.
State encapsulation (Encapsulation)
  • Subgraphs have their own private state
  • Intermediate calculation results do not pollute the parent graph
  • Precisely control communication boundaries with overlapping keys
Teamwork
  • Different teams develop different subplots
  • Assemble the Agent system like microservices
  • Suitable for large-scale multi-agent architecture
Best Practices for Multi-Agent Systems

In a real production system, a "research assistant" may be composed of multiple specialized Agent subgraphs: search subgraph, summary subgraph, fact checking subgraph, formatting subgraph... Each subgraph focuses on one thing, and their execution sequence and data flow are coordinated through the parent graph. Subgraphs are a key abstraction for building maintainable multi-agent systems.

Dimensions No subgraph (all nodes in one graph) Use subgraphs
state design All intermediate variables are exposed in the parent graph state Private state of subgraphs is completely isolated
Testability The entire big picture must be run to test a piece of logic Subgraphs can be separateinvoke()test
code organization Dozens of nodes mixed together, making it difficult to read Each subgraph is an independent file/function with a clear structure
Parallel execution Manual coordination, error-prone Multiple subgraph nodes naturally support parallelism

3Example scenario for this lesson: Log analysis system

To demonstrate subgraphs, the notebook builds aLog analysis system, this system does three things:

First, notebook defines the data structure of the log:

from operator import add
from typing_extensions import TypedDict
from typing import List, Optional, Annotated

# 每一条日志的数据结构
class Log(TypedDict):
    id: str               # 日志 ID
    question: str         # 用户提问
    docs: Optional[List]  # 检索到的文档(可选)
    answer: str           # LLM 的回答
    grade: Optional[int]  # 评分,0 表示失败(可选)
    grader: Optional[str] # 评分者
    feedback: Optional[str] # 反馈信息

The test data contains two logs: a normal log (no score) and a failure log (a score of 0, indicating poor document retrieval quality):

# 正常日志:只有问答,无评分
question_answer = Log(
    id="1",
    question="How can I import ChatOllama?",
    answer="To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'",
)

# 失败日志:评分为 0,表示检索到的文档与问题不相关
question_answer_feedback = Log(
    id="2",
    question="How can I use Chroma vector store?",
    answer="To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).",
    grade=0,
    grader="Document Relevance Recall",
    feedback="The retrieved documents discuss vector stores in general, but not Chroma specifically",
)

raw_logs = [question_answer, question_answer_feedback]
NOTE: This is a toy example

in notebookgenerate_summaryand other functions directly return hardcoded strings (such as"Poor quality retrieval of Chroma documentation."), in order to focus on demonstrating the subgraph architecture and omit real LLM calls. In production, real AI models are called in these nodes to generate summaries.

4Define and compile subgraphs: FailureAnalysisState and QuestionSummarizationState

Each subgraph needs to: ① define its own state type; ② write node functions; ③ useStateGraphAssembly; ④ Callcompile(). The subgraph is defined in exactly the same way as a normal graph, except that it will eventually be "mounted" to the parent graph.

Failure Analysis Sub-graph

from langgraph.graph import StateGraph, START, END

# ① 子图的完整状态(包含私有字段 failures)
class FailureAnalysisState(TypedDict):
    cleaned_logs: List[Log]   # 与父图共享的输入键
    failures: List[Log]       # 子图私有:过滤出的失败日志
    fa_summary: str           # 与父图共享的输出键
    processed_logs: List[str]  # 与父图共享的输出键

# ② 输出状态(仅包含需要"发布"给父图的键)
class FailureAnalysisOutputState(TypedDict):
    fa_summary: str
    processed_logs: List[str]

# ③ 节点:从 cleaned_logs 中筛选出有 grade 字段(即失败)的日志
def get_failures(state):
    cleaned_logs = state["cleaned_logs"]
    failures = [log for log in cleaned_logs if "grade" in log]
    return {"failures": failures}

# ④ 节点:根据失败日志生成摘要(生产中替换为 LLM 调用)
def generate_summary(state):
    failures = state["failures"]
    fa_summary = "Poor quality retrieval of Chroma documentation."
    return {
        "fa_summary": fa_summary,
        "processed_logs": [f"failure-analysis-on-log-{failure['id']}" for failure in failures]
    }

# ⑤ 构建并编译子图,指定 output_schema 限制输出
fa_builder = StateGraph(
    state_schema=FailureAnalysisState,
    output_schema=FailureAnalysisOutputState   # 关键:限制输出键
)
fa_builder.add_node("get_failures", get_failures)
fa_builder.add_node("generate_summary", generate_summary)
fa_builder.add_edge(START, "get_failures")
fa_builder.add_edge("get_failures", "generate_summary")
fa_builder.add_edge("generate_summary", END)

Question Summarization Sub-graph

# ① 子图状态(qs_summary 是私有中间字段,不会发布到父图)
class QuestionSummarizationState(TypedDict):
    cleaned_logs: List[Log]   # 与父图共享的输入键
    qs_summary: str           # 子图私有:中间摘要
    report: str               # 与父图共享的输出键
    processed_logs: List[str]  # 与父图共享的输出键

# ② 输出状态:只发布 report 和 processed_logs 给父图
class QuestionSummarizationOutputState(TypedDict):
    report: str
    processed_logs: List[str]

# ③ 节点:生成问题摘要
def generate_summary(state):
    cleaned_logs = state["cleaned_logs"]
    summary = "Questions focused on usage of ChatOllama and Chroma vector store."
    return {
        "qs_summary": summary,
        "processed_logs": [f"summary-on-log-{log['id']}" for log in cleaned_logs]
    }

# ④ 节点:把摘要发送到 Slack(此处模拟)
def send_to_slack(state):
    qs_summary = state["qs_summary"]
    report = "foo bar baz"   # 生产中:调用 Slack API
    return {"report": report}

qs_builder = StateGraph(QuestionSummarizationState, output_schema=QuestionSummarizationOutputState)
qs_builder.add_node("generate_summary", generate_summary)
qs_builder.add_node("send_to_slack", send_to_slack)
qs_builder.add_edge(START, "generate_summary")
qs_builder.add_edge("generate_summary", "send_to_slack")
qs_builder.add_edge("send_to_slack", END)
Comparison of two sub-pictures

Both subplots havecleaned_logs(input) andprocessed_logs(Output) These two keys are shared with the parent graph. But their respective private intermediate states (failuresqs_summary) do not interfere with each other and will not appear in the state of the parent picture.

5State sharing mechanism: Overlapping Keys are a bridge for communication

The communication between the subgraph and the parent graph is the most important part of Sub-graph to understand in depth.There is only one core rule: communication is achieved through overlapping key names.

When entering the sub-picture (parent picture → sub-picture)

When the parent graph is executed to the child graph node, LangGraph will automatically compare the current state of the parent graph with the state definition of the child graph.Key-value pairs with the same nameExtract it and pass it in as the input state of the subgraph.

raw_logs: [...]
cleaned_logs: [log1, log2]
fa_summary: ""
report: ""
processed_logs: []
Parent picture status (before entering child picture)
cleaned_logs: [log1, log2]
failures: []
fa_summary: ""
processed_logs: []
Subgraph initial state (only overlapping keys are passed in)

cleaned_logsIt is a key defined by both the parent and child graphs, so its value automatically flows from the parent graph to the child graph. And the father picture’sraw_logsThe key is not recognized by the subgraph (it is not in the State definition of the subgraph), so it will not be passed in.

When exiting the sub-picture (sub-picture → parent picture)

After the subgraph execution is completed, LangGraph will output the subgraph in the stateA key-value pair with the same name as the parent graph stateWrite back to parent graph state. A key private to the subgraph (e.g.failuresqs_summary) will not appear in the parent diagram.

fa_summary: "Poor quality..."
processed_logs: ["failure-analysis-on-log-2"]
(The failures field inside the subgraph is not output)
Subgraph output (filtered by output_schema)
raw_logs: [...]
cleaned_logs: [...]
fa_summary: "Poor quality..."
report: ""
processed_logs: ["failure-analysis-on-log-2"]
Parent graph status (updated after child graph is completed)
The relationship between parallel subgraphs and reducers

When two subgraphs are run in parallel, they will both outputprocessed_logskey. If the key is in the parent graph stateNo reducer, the outputs of two parallel subgraphs will conflict (whose overwrites whom?). There are two solutions:

Plan A: For the father pictureprocessed_logsaddAnnotated[List, add]reducer, merges the result lists of two subgraphs.

Option B (adopted by notebook): Set for each subgraphoutput_schema, let different subgraphs outputdifferent keys(such asfa_summary vs report) to avoid conflicts. Butprocessed_logsBoth subgraphs output, so the parent graph still requires a reducer.

Complete mapping of status keys

Key name Parent graph status Failure analysis subgraph Problem summary subgraph illustrate
raw_logs Yes None None The parent image is private and the child image is not visible.
cleaned_logs Yes Yes (input) Yes (input) Overlapping keys: parent graph → two child graphs
failures None Yes (private) None Subgraph A private intermediate state
qs_summary None None Yes (private) Subgraph B private intermediate state
fa_summary Yes Yes (output) None Overlapping key: child picture A → parent picture
report Yes None Yes (output) Overlapping key: subgraph B → parent graph
processed_logs Yes (with reducer) Yes (output) Yes (output) Both subgraphs are output, and the parent graph usesaddmerge

6Add child graph to parent graph: add_node with parallel execution

After understanding the state sharing mechanism, adding the subgraph to the parent graph is very intuitive. The key steps are: define the parent graph state, write the parent graph node, and useadd_nodeMount the compiled subgraph.

Parent graph state definition

from operator import add
from typing import Annotated

# 父图的完整状态
class EntryGraphState(TypedDict):
    raw_logs: List[Log]      # 输入:原始日志
    cleaned_logs: List[Log]  # 清洗后的日志(传入两个子图)
    fa_summary: str          # 来自失败分析子图
    report: str              # 来自问题总结子图
    # 两个子图都会输出 processed_logs,用 add Reducer 合并列表
    processed_logs: Annotated[List[str], add]
Why does cleaned_logs not require a reducer?

Because the two subgraphs are justread cleaned_logs, does not modify or output it (there is no such key in output_schema). Only when multiple parallel nodes are going to the same keywritereducer is only needed when.

Clean node of parent graph

# 父图中的普通节点:对原始日志做清洗
def clean_logs(state):
    raw_logs = state["raw_logs"]
    cleaned_logs = raw_logs   # 生产中:做实际清洗处理
    return {"cleaned_logs": cleaned_logs}

Assemble the parent graph: mount the child graph as a node

entry_builder = StateGraph(EntryGraphState)

# 挂载普通节点
entry_builder.add_node("clean_logs", clean_logs)

# 挂载子图节点:compile() 返回的 CompiledGraph 直接传入
entry_builder.add_node("question_summarization", qs_builder.compile())
entry_builder.add_node("failure_analysis", fa_builder.compile())

# 定义执行顺序:先清洗,然后两个子图并行执行
entry_builder.add_edge(START, "clean_logs")
entry_builder.add_edge("clean_logs", "failure_analysis")       # 并行分支 1
entry_builder.add_edge("clean_logs", "question_summarization")   # 并行分支 2
entry_builder.add_edge("failure_analysis", END)
entry_builder.add_edge("question_summarization", END)

graph = entry_builder.compile()

A node hastwo exit edgesPoint to different nodes, LangGraph willAutomatic parallel executionthese two nodes. hereclean_logsAfter completion,failure_analysisandquestion_summarizationwill run at the same time, and will be summarized when they finally reach END.

Parent graph execution process (including parallel subgraphs)

START
clean_logs
Parent graph normal node
Subfigure A: failure_analysis
get_failures → generate_summary
Output: fa_summary, processed_logs
Subfigure B: question_summarization
generate_summary → send_to_slack
Output: report, processed_logs
The two subgraphs execute in parallel, and LangGraph automatically waits for both to complete.
END

Visualize nested structures: xray=1

LangGraph provides a particularly useful debugging parameter: when callingget_graph()when passed inxray=1, you can "see through" the internal structure of the sub-picture and draw the entire nested diagram completely.

# xray=1 表示"透视":显示子图的内部节点,而不只是显示一个黑盒
display(Image(graph.get_graph(xray=1).draw_mermaid_png()))

Not addedxrayWhen , the subgraph is only displayed as a rectangular node; addedxray=1, you can see the detailed connection relationships of all nodes and edges within the subgraph, which is very suitable for debugging multi-layer nested complex graphs.

7Output Schema: Precisely control the return of subgraphs

in subgraphoutput_schemaParameters are a very important design decision. They determine which keys the subgraph "releases" to the parent graph, effectively implementing the state encapsulation of the subgraph.

Problems when output_schema is not used

If not specifiedoutput_schema, the subgraph will convert itscomplete stateAs output - includes all private intermediate fields.

The problem is: when two subgraphs are run in parallel, they both havecleaned_logsfields, and the values are the same (both are read from the parent graph). Both sub-picturescleaned_logsWriting back to the parent picture will cause conflicts, and you need to give the parent picturecleaned_logsAdd a reducer (e.g.operator.add) to merge - but this adds the same log list twice!

# 不用 output_schema 时,父图需要给 cleaned_logs 加 Reducer
class EntryGraphState(TypedDict):
    raw_logs: List[Log]
    # 因为两个子图都会"回写" cleaned_logs,需要 Reducer
    cleaned_logs: Annotated[List[Log], add]  # 会产生重复数据!
    fa_summary: str
    report: str
    processed_logs: Annotated[List[str], add]

Elegant solution using output_schema

By defining for each subgraphoutput_schema, precisely controlling which keys it publishes to the parent graph, completely avoidingcleaned_logsConflict issues:

# 失败分析子图只发布这两个键
class FailureAnalysisOutputState(TypedDict):
    fa_summary: str            # 父图有,回写到父图
    processed_logs: List[str]  # 父图有,回写到父图
    # cleaned_logs 和 failures 不在这里 → 不会回写父图

# 问题总结子图只发布这两个键
class QuestionSummarizationOutputState(TypedDict):
    report: str               # 父图有,回写到父图
    processed_logs: List[str]  # 父图有,回写到父图
    # cleaned_logs 和 qs_summary 不在这里

# 于是父图的 cleaned_logs 不需要 Reducer 了!
class EntryGraphState(TypedDict):
    raw_logs: List[Log]
    cleaned_logs: List[Log]    # 普通字段即可,无需 Reducer
    fa_summary: str
    report: str
    processed_logs: Annotated[List[str], add]  # 仍然需要,因为两个子图都输出它
The essence of output_schema

output_schemaEquivalent to the "external interface" or "public API" of the subgraph. There can be any number of private intermediate states inside a subgraph, but onlyoutput_schemaOnly the keys listed in will "cross the boundary" and be passed back to the parent graph. This is the key mechanism to achieve true state encapsulation.

Comparison of two methods

aspects Do not use output_schema Use output_schema
Output content The complete state of the subgraph (all keys) Only keys specified in output_schema
Parallel conflict handling All shared keys in the parent graph require reducer Only keys that are actually output by multiple subgraphs require a reducer
Encapsulation level Low, interior details exposed High, private fields are completely isolated
Parent graph state complexity High, need to include the middle field of all subgraphs Low, the parent graph only cares about the final result

8Complete running demonstration and result interpretation

Run all the code together and observe the final output:

# 准备测试数据
question_answer = Log(
    id="1",
    question="How can I import ChatOllama?",
    answer="To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'",
)
question_answer_feedback = Log(
    id="2",
    question="How can I use Chroma vector store?",
    answer="To use Chroma, define: rag_chain = create_retrieval_chain(...).",
    grade=0,
    grader="Document Relevance Recall",
    feedback="The retrieved documents discuss vector stores in general, but not Chroma specifically",
)
raw_logs = [question_answer, question_answer_feedback]

# 运行图,传入原始日志
result = graph.invoke({"raw_logs": raw_logs})

actual output result

{
  'raw_logs': [
    {'id': '1', 'question': 'How can I import ChatOllama?', ...},
    {'id': '2', 'question': 'How can I use Chroma vector store?', ..., 'grade': 0, ...}
  ],
  'cleaned_logs': [
    {'id': '1', ...},  # 清洗节点处理后(本例等于原始数据)
    {'id': '2', ..., 'grade': 0}
  ],
  'fa_summary': 'Poor quality retrieval of Chroma documentation.',
                # ↑ 来自失败分析子图
  'report': 'foo bar baz',
            # ↑ 来自问题总结子图
  'processed_logs': [
    'failure-analysis-on-log-2',  # 来自子图 A(只处理了有 grade 的日志)
    'summary-on-log-1',           # 来自子图 B(处理了所有日志)
    'summary-on-log-2'
  ]
}

Interpret field by field

output fields source illustrate
raw_logs invoke passed in The original input is retained in the parent graph state throughout the process
cleaned_logs clean_logs node The cleaned log is read by two subgraphs; this example is the same as raw_logs
fa_summary Failure analysis subgraph Only the failure log with id=2 (grade=0) was analyzed and a summary was generated.
report Problem summary subgraph send_to_slack node generates and "sends" reports
processed_logs Two subgraphs (add merge) Subgraph A generates 1 item + Subgraph B generates 2 items, and the reducer merges them into 3 items.
merge order of processed_logs

Outputtingfailure-analysis-on-log-2appear in the first twosummary-on-log-*Previously, this reflected the order in which the two subplots were completed. Since it is executed in parallel, the specific order may vary depending on the operating environment, but reducer (operator.add) will collect all results.

Summary: Complete steps for building a subgraph system

Subgraph vs. Normal Function Nodes: When to use subgraphs?

If a piece of logicThere are multiple steps internally, require its own intermediate state, or may need to be reused independently., using subgraphs. If it is just a simple data conversion or a single call, use ordinary function nodes. In addition, you can also encapsulate the subgraph into afunction node(called inside the functionsubgraph.invoke(state)), this method is more flexible and can manually map state keys before passing them in, but it loses LangGraph's native tracking and visualization support for nested graphs.

Extension: Calling subgraphs in function node mode

# 另一种方式:把子图包在函数里,手动做状态映射
compiled_fa = fa_builder.compile()

def run_failure_analysis(state):
    # 手动提取需要传给子图的字段
    sub_input = {"cleaned_logs": state["cleaned_logs"]}
    # 调用子图
    sub_output = compiled_fa.invoke(sub_input)
    # 手动把子图输出映射回父图状态
    return {
        "fa_summary": sub_output["fa_summary"],
        "processed_logs": sub_output["processed_logs"]
    }

# 使用方式与普通节点完全一样
entry_builder.add_node("failure_analysis", run_failure_analysis)

This approach is suitable forParent and child graphs have no overlapping keyssituations, or scenarios that require additional processing before and after calling subgraphs. The disadvantage is that tools such as LangGraph Studio cannot directly visualize the internal subgraph structure.