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-3 Map Reduce
📓 Notebook 📖 Explained
LANGGRAPH MODULE 4 · LESSON 3

Map-Reduce and Send API
Dynamic Parallel Fanout · Result Aggregation

Why can't static parallel edges handle runtime dynamic lists?
How does Send API achieve true "dynamic Worker dispatch" and result merging?

1What is Map-Reduce? The nature of the two stages

Map-Reduce is a classic parallel computing model widely used in the field of big data (such as Hadoop, Spark). LangGraph introduces this idea into the LLM workflow forDecompose a task into multiple subtasks for parallel processing, and then aggregate the results

It consists of two stages:

Map-Reduce two-stage diagram

Enter topic
topic = "animals"
generate_topics
Generate subtopic list
Send API dynamic fan-out ↓ ↓ ↓
Send("generate_joke", mammals)
Worker 1
mammals joke
Send("generate_joke", reptiles)
Worker 2
reptiles jokes
Send("generate_joke", birds)
Worker 3
birds joke
reducer automatically merges the jokes list
best_joke
Choose the best joke from all
END
core value

The greatest value of Map-Reduce mode in LLM workflow isHandle a dynamic number of parallel tasks. You don’t know in advance how many subtasks you need to process (for example, LLM will generate 3 or 5 subtopics based on the topic). Map-Reduce can dynamically determine the number of workers at runtime and wait for all workers to complete before aggregating.

Essential differences from Module 4-1 ordinary parallelism

Module 4-1 Learned and usedadd_edgeExecute multiple nodes in parallel, that isstatic parallelism——Which parallel branches are fixed at compile time. Map-Reduce isdynamic parallelism——The number of branches is determined based on data at runtime, and can be 1, 3 or 100 Workers, fully flexible.

2Limitations of static parallel edges: why they are not enough

In Module 4-1 (Parallelization), you learned to implement parallelism using ordinary edges:

# 静态并行:编译时固定有 3 个并行节点
graph.add_edge(START, "node_a")
graph.add_edge(START, "node_b")
graph.add_edge(START, "node_c")  # 永远是这 3 个,不多不少

The problem with this approach is:The number of parallel branches must be determined during graph compilation, they are "hard-coded" into the structure of the graph.

characteristic Static parallel edges (Module 4-1) Map-Reduce/Send API (this section)
Number of workers Fixed at compile time and cannot be changed Dynamically determined at runtime and elastically scalable
decide the time Compile time (graph construction phase) Runtime (graph execution phase)
Typical scenario Fixed number of parallel subtasks (e.g. calling 3 APIs simultaneously) Parallel processing of LLM-generated lists (undetermined number)
Input for each Worker Share the same State Each Worker has its own independent sub-State
Result collection All nodes write back the same OverallState field Automatically merge the output of each Worker into a list through reducer
core contradiction

If you want to call LLM in parallel to generate content for each of the subtopic lists generated by LLM (maybe 3 or 5),Static parallel edges cannot do this——Because the length of the list is not known at compile time and can only be known at runtime. This is where the Send API comes in.

A concrete example to illustrate the problem

Suppose you ask LLM to generate subtopics based on the topic "animals", and LLM returns["mammals", "reptiles", "birds"]. Next time you enter the topic "programming", LLM may return["Python", "Rust", "Go", "TypeScript", "Java"](5 pieces).

youImpossibleUse static parallel edges to handle this situation when writing code - because you don't know how many you have to writeadd_edge. The only solution is to dynamically generate workers at runtime, which is what the Send API does.

3Send API: the core mechanism for dynamically dispatching work

What is Send?

Sendis a special object provided by LangGraph for use at runtimeDynamically dispatch a task to a node, and also carries status data exclusive to the task.

from langgraph.types import Send

# Send 的语法
Send(
    node_name,  # str:目标节点的名称
    state       # dict:传给该节点的状态(可以不是 OverallState 的子集!)
)

Key features:

How is Send triggered? ——The conditional edge returns the Send list

SendThe object is not called directly, butReturned from the routing function of a conditional edge. When the routing function returns aSendwhen a list of objects, LangGraph willSendDistributed in parallel:

from langgraph.types import Send

# 路由函数:读取 subjects 列表,为每个 subject 创建一个 Send 对象
def continue_to_jokes(state: OverallState):
    # 返回一个 Send 对象的列表
    # LangGraph 看到 Send 列表,就知道要并行分发
    return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
    #         ↑ 目标节点名       ↑ 每个 Worker 独享的子 State

hypothesisstate["subjects"]Yes["mammals", "reptiles", "birds"], the above list comprehension will generate:

[
    Send("generate_joke", {"subject": "mammals"}),   # Worker 1
    Send("generate_joke", {"subject": "reptiles"}),  # Worker 2
    Send("generate_joke", {"subject": "birds"}),     # Worker 3
]
Key understanding: Send is not an ordinary "next step"

The routing function of ordinary conditional edges returns a string (node name), indicating "which node to go to next". AndWhen a routing function returns a Send object (or Send list), LangGraph understands it as "starting multiple parallel tasks at the same time", each task has its own independent status data. This is the most exquisite design of Send API.

Worker nodes can use private State

This is another powerful thing about the Send API: Worker nodes (generate_joke) does not need to be usedOverallState, which can define its own private state structure:

# Worker 节点的私有 State——只有 subject 一个字段
class JokeState(TypedDict):
    subject: str

# Worker 节点使用 JokeState 而不是 OverallState
def generate_joke(state: JokeState):
    prompt = joke_prompt.format(subject=state["subject"])  # 读私有 subject
    response = model.with_structured_output(Joke).invoke(prompt)
    # 但返回写入 OverallState 的 jokes 字段!
    return {"jokes": [response.joke]}

Send passed in when dispatching{"subject": s}that isJokeStateThe content of LangGraph will be used to populate the input State of the Worker node. The output of the Worker ({"jokes": [...]}) will be written backOverallStateThe corresponding fields are merged by reducer.

4State Design: How Annotated reducer Aggregates Results

If multiple workers write to the same field at the same time, will there be a conflict?

This is a very important question. In Map-Reduce, each of the three Worker nodes generates a joke and all need to be written.OverallStateofjokesfield. If you use the default "overwrite" semantics, only the results of the last written Worker will be retained!

The solution is to useAnnotated+ reducer function, changes the write semantics of the field from "overwrite" to "append merge":

import operator
from typing import Annotated
from typing_extensions import TypedDict
from pydantic import BaseModel

# 结构化输出的 Pydantic 模型
class Subjects(BaseModel):
    subjects: list[str]   # LLM 输出的子话题列表

class BestJoke(BaseModel):
    id: int               # 最佳笑话的索引

class OverallState(TypedDict):
    topic: str                                    # 输入:用户给定的大话题
    subjects: list                                # Map 节点生成的子话题列表
    jokes: Annotated[list, operator.add]          # ← 关键!Reducer 追加合并
    best_selected_joke: str                       # Reduce 节点选出的最佳笑话
The meaning of Annotated[list, operator.add]

Annotated[list, operator.add]Tell LangGraph: when multiple nodes write at the same timejokesfield, do not overwrite it, but useoperator.add(i.e. list splicing+) merges all written lists into one big list. This is what "reducer" does.

State field life cycle

topic: "animals"
subjects: []
jokes: []
best_selected_joke: ""
initial input
topic: "animals"
subjects: ["mammals","reptiles","birds"]
jokes: []
best_selected_joke: ""
after generate_topics
jokes: []
3 Workers execute in parallel...
Each writes ["JokeX"]
Worker is executing
jokes: ["Why mammals...", "Why alligators...", "Why birds..."]
reducer (operator.add) automatic merge
After all Workers are completed

Why does Worker wrap jokes into lists when writing them?

def generate_joke(state: JokeState):
    ...
    return {"jokes": [response.joke]}  # 注意:是 [joke],而不是 joke
    # 因为 jokes 字段的类型是 list
    # Reducer operator.add 对 list 做的是列表拼接:
    #   [] + ["joke1"] + ["joke2"] + ["joke3"] = ["joke1","joke2","joke3"]

Each Worker returns a list containing only one element. reduceroperator.addConcatenate these single-element lists to get a complete list of all jokes. This is a very subtle design - keep the output format of the Worker node consistent and let the reducer handle the merging automatically.

5Complete example: Joke generation and selection system

The example in the notebook is a complete Map-Reduce application: given a large topic, multiple subtopics are automatically generated, and LLM generates a joke for each subtopic in parallel, and finally the LLM selects the best one from all the jokes.

Step One: Prepare LLM and Prompt Words

from langchain_deepseek import ChatDeepSeek

# 三个提示词模板
subjects_prompt = """Generate a list of 3 sub-topics that are all related to this overall topic: {topic}."""
# → 根据大话题生成 3 个子话题(LLM 决定具体是哪 3 个)

joke_prompt = """Generate a joke about {subject}"""
# → 根据子话题生成一则笑话

best_joke_prompt = """Below are a bunch of jokes about {topic}. Select the best one!
Return the ID of the best one, starting 0 as the ID for the first joke.
Jokes: \n\n  {jokes}"""
# → 从笑话列表中选出最佳(返回序号)

# 使用 DeepSeek 模型(temperature=0 确保输出稳定)
model = ChatDeepSeek(model="deepseek-v4-pro", temperature=0)

Step 2: Define all State and Pydantic models

import operator
from typing import Annotated
from typing_extensions import TypedDict
from pydantic import BaseModel

# LLM 结构化输出的类型定义
class Subjects(BaseModel):
    subjects: list[str]   # 子话题列表

class Joke(BaseModel):
    joke: str             # 单则笑话文本

class BestJoke(BaseModel):
    id: int               # 最佳笑话的索引(0-based)

# 整图共享的 Overall State
class OverallState(TypedDict):
    topic: str                           # 用户输入的大话题
    subjects: list                       # generate_topics 写入,默认覆盖
    jokes: Annotated[list, operator.add] # Worker 各自追加,Reducer 合并
    best_selected_joke: str              # best_joke 节点写入最终结果

# Worker 节点专用的私有 State(只有 subject 一个字段)
class JokeState(TypedDict):
    subject: str

Step 3: Define three node functions

### 节点 1:generate_topics(Map 的"前置"节点)
# 读取用户输入的大话题,让 LLM 生成子话题列表
def generate_topics(state: OverallState):
    prompt = subjects_prompt.format(topic=state["topic"])
    response = model.with_structured_output(Subjects).invoke(prompt)
    return {"subjects": response.subjects}
    # 写入 subjects 字段,触发后续的 Send 扇出

### 节点 2:generate_joke(Map 阶段的 Worker 节点)
# 接受 JokeState(私有 State),生成一则笑话写回 OverallState.jokes
def generate_joke(state: JokeState):   # ← 注意:使用 JokeState,不是 OverallState
    prompt = joke_prompt.format(subject=state["subject"])
    response = model.with_structured_output(Joke).invoke(prompt)
    return {"jokes": [response.joke]}    # ← 单元素列表,由 Reducer 合并

### 节点 3:best_joke(Reduce 节点)
# 所有 Worker 完成后,从合并后的 jokes 列表中选最佳
def best_joke(state: OverallState):
    jokes = "\n\n".join(state["jokes"])  # 将所有笑话拼成一段文本
    prompt = best_joke_prompt.format(topic=state["topic"], jokes=jokes)
    response = model.with_structured_output(BestJoke).invoke(prompt)
    return {"best_selected_joke": state["jokes"][response.id]}

Step 4: Define the routing function (Send trigger point)

from langgraph.types import Send

# 路由函数:从 subjects 列表中为每个 subject 创建一个 Send 对象
def continue_to_jokes(state: OverallState):
    return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
    # 返回 Send 列表 → LangGraph 并行执行所有 generate_joke Worker

6Line-by-line analysis: graph construction and special usage of conditional edges

Complete graph construction code

from langgraph.graph import END, StateGraph, START

# ① 以 OverallState 为蓝图创建图
graph = StateGraph(OverallState)

# ② 注册所有节点
graph.add_node("generate_topics", generate_topics)  # Map 前置节点
graph.add_node("generate_joke", generate_joke)        # Worker 节点(会被多次并行实例化)
graph.add_node("best_joke", best_joke)                # Reduce 节点

# ③ START → generate_topics:固定边,图从 generate_topics 开始
graph.add_edge(START, "generate_topics")

# ④ 核心!条件边:generate_topics → [Send, Send, ...] → generate_joke
graph.add_conditional_edges(
    "generate_topics",     # 源节点
    continue_to_jokes,      # 路由函数(返回 Send 列表)
    ["generate_joke"]       # 可能的目标节点列表(告诉图 generate_joke 是合法目标)
)

# ⑤ generate_joke → best_joke:所有 Worker 完成后汇聚到 best_joke
graph.add_edge("generate_joke", "best_joke")

# ⑥ best_joke → END
graph.add_edge("best_joke", END)

# ⑦ 编译
app = graph.compile()
The third parameter of add_conditional_edges

When using the Send API,add_conditional_edgesA third parameter is required: a list of strings declaring which nodes the routing function may route to. This is so that LangGraph knows the structure of the graph at compile time (even though the actual number of distributions is determined at runtime). write here["generate_joke"]Means "all Send targets are generate_joke nodes".

The complete topology of the graph structure

Node and edge relationships of Map-Reduce graph

__start__
↓ add_edge
generate_topics
Generate subjects list
add_conditional_edges(continue_to_jokes) → Send list
Send(mammals)
generate_joke
JokeState {subject: mammals}
Send(reptiles)
generate_joke
JokeState {subject: reptiles}
Send(birds)
generate_joke
JokeState {subject: birds}
reducer operator.add combines all jokes
best_joke
Reduce: Select the best
↓ add_edge
__end__

generate_joke → waiting mechanism for best_joke

There is an ordinary edge in the graphadd_edge("generate_joke", "best_joke"). when multiplegenerate_jokeWhen Worker instances run in parallel, LangGraph willWait for all Workers to completeonly afterbest_jokenode. This is LangGraph's built-in "Fan-in" synchronization mechanism, which does not require developers to manually handle waiting logic.

Fan-out and Fan-in

fan out: Send API dynamically distributes the work of one node to multiple parallel workers - this is the Map stage.fan in: LangGraph automatically waits for all Workers to complete, the reducer merges the results, and then triggers the downstream nodes - this is the Reduce stage. The two work together to form a complete Map-Reduce life cycle.

7Execution Process Tracking: From Topic to Best Joke

Run chart

# 以流式方式运行,实时观察每个节点的输出
for s in app.stream({"topic": "animals"}):
    print(s)

Actual output (from Notebook)

# 第一步:generate_topics 节点执行,生成 3 个子话题
{'generate_topics': {'subjects': ['mammals', 'reptiles', 'birds']}}

# 第二步:3 个 generate_joke Worker 并行执行(顺序可能不同)
{'generate_joke': {'jokes': ["Why don't mammals ever get lost? Because they always follow their 'instincts'!"]}}
{'generate_joke': {'jokes': ["Why don't alligators like fast food? Because they can't catch it!"]}}
{'generate_joke': {'jokes': ["Why do birds fly south for the winter? Because it's too far to walk!"]}}

# 第三步:best_joke 节点执行,选出最佳
{'best_joke': {'best_selected_joke': "Why don't alligators like fast food? Because they can't catch it!"}}

Breaking down the execution process step by step

State final value

topic: "animals"
subjects: ["mammals", "reptiles", "birds"]
jokes: [
"Why don't mammals ever get lost? Because they always follow their 'instincts'!",
"Why don't alligators like fast food? Because they can't catch it!",
"Why do birds fly south for the winter? Because it's too far to walk!"
]
best_selected_joke: "Why don't alligators like fast food? Because they can't catch it!"
Final OverallState (graph execution completed)

8Summary of application scenarios and design patterns

What scenarios is Map-Reduce suitable for?

scene Map stage (fan-out) Reduce phase (aggregation)
research assistant LLM generates multiple search queries, searches in parallel Merge all search results and generate a comprehensive report
Document analysis Divide long documents into segments and extract key information from each segment in parallel Summarize the extraction results of all paragraphs and generate a summary
Multi-angle assessment Evaluate code in parallel from different dimensions (security, performance, readability) Combine scores from each dimension to give comprehensive recommendations
content generation Generate content for multiple target audiences (technology/product/market) in parallel Choose the most suitable version
Data validation Call LLM validation in parallel for each record of the dataset Statistically verify results and generate quality reports

Summary of Send API design points

Map-Reduce vs other parallel methods

Way Parallel number Worker input Applicable scenarios
Static parallel edges (Module 4-1) Fixed at compile time Share full OverallState Fixed number of parallel subtasks
Send API (this section) Dynamic decision at runtime Each Worker has an exclusive private sub-State Process each element in a dynamic list in parallel
Subgraph (Module 4-2) fixed (number of subgraph instances) Sharing a subset of OverallState Encapsulation and reuse of complex sub-workflows
The core conclusion of this section

Send API is implemented by LangGraphDynamic Map-Reduce patternkey mechanism. It allows a routing function to dynamically create any number of parallel Workers based on data at runtime. Each Worker has an independent private State, and finally merges all results back to the OverallState through the reducer. This design allows LangGraph to elegantly handle the extremely common need in AI applications such as "parallel processing of dynamic lists generated by LLM".

Contact with Module 4-4

The next section (4-4) will build a complete multi-agent research assistant, which will comprehensively use the Map-Reduce model in this section: first let LLM decompose the research problem into multiple subqueries, let multiple Worker Agents collect information in parallel, and finally aggregate it into a complete research report. Map-Reduce is the core parallel engine of this research assistant.

Send API dynamic parallelism Map-Reduce Reducer Annotated operator.add Fan-out Fan-out Fan-in Private Worker State conditional edge routing