A node splits into multiple parallel branches, runs simultaneously, and then converges back to one node.
This is Fan-out / Fan-in - also the basic ability to build multi-agent research assistants.
In the first three modules of LangGraph, the graphs we study arelinear: After node A is executed, node B starts. After node B is executed, node C starts. This "single lane" execution method is simple and predictable, but has a natural bottleneck:Each step must wait for the previous step to complete。
In real-life scenarios, many tasks areindependent of each otherYes, it can be done at the same time. For example:
If these independent tasks are executed serially, the total time consumption = task 1 time + task 2 time + ...; and ifParallel execution, the total time taken ≈ the time of the slowest task. When each subtask needs to wait for LLM or network IO, the speedup effect brought by parallelization is very significant.
The goal of Module 4 is to build aMulti-agent research assistant. Parallel execution is the most critical basic capability - allowing the assistant to retrieve information from multiple data sources at the same time and then hand it over to LLM for comprehensive analysis. This lesson (Lesson 1) first explains the principles and API of parallel execution thoroughly.
These two terms come from circuit design and have the following meanings in LangGraph:
| Terminology | meaning | LangGraph implementation |
|---|---|---|
| Fan-out | The output of a node is "split" into multiple parallel branches, triggering multiple downstream nodes at the same time | Starting from the same node,add_edgeto multiple different nodes |
| Fan-in | The outputs of multiple parallel branches are "converged" to a single node, which waits for all upstreams to complete before running | Pass through multiple nodesadd_edgePoint to the same downstream node |
The notebook first uses a linear graph for baseline comparison. Each node puts its own valuecoverdropstateFields (note: no reducer here):
from typing import Any, List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
# 注意:没有 Reducer,默认行为是"覆盖"
state: List[str]
class ReturnNodeValue:
def __init__(self, node_secret: str):
self._value = node_secret
def __call__(self, state: State) -> Any:
print(f"Adding {self._value} to {state['state']}")
return {"state": [self._value]} # 直接覆盖,不追加
# 构建线性图:a → b → c → d
builder = StateGraph(State)
builder.add_node("a", ReturnNodeValue("I'm A"))
builder.add_node("b", ReturnNodeValue("I'm B"))
builder.add_node("c", ReturnNodeValue("I'm C"))
builder.add_node("d", ReturnNodeValue("I'm D"))
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("b", "c")
builder.add_edge("c", "d")
builder.add_edge("d", END)
graph = builder.compile()
The running results are as follows. Because every nodecoverThe value of the previous node, only the value of D is left in the final state:
Now change b and c to be executed in parallel. The key changes are onlyEdge connection method——FromaConnect simultaneouslybandc, thenbandcAll even tod:
# Fan-out:a → b 且 a → c(两条边同时从 a 出发)
builder.add_edge("a", "b")
builder.add_edge("a", "c")
# Fan-in:b → d 且 c → d(两条边汇聚到 d)
builder.add_edge("b", "d")
builder.add_edge("c", "d")
Parallel execution of LangGraphDeclared entirely through edges, does not require any special parallelism API or thread management code. You only need to add edges that "start from the same node and connect to multiple nodes", and LangGraph will automatically run these nodes in parallel.
If the parallel graph directly uses State without reducer,Will report an error. This is an important security mechanism of LangGraph, and understanding it is critical.
In parallel steps,bandc Execute simultaneously within the same "step", each of them has tostateWrite a value to the field. Without the reducer, LangGraph doesn't know which one to keep - the two writes conflict with each other, so it throws an exception instead of quietly discarding one of them.
from langgraph.errors import InvalidUpdateError
try:
graph.invoke({"state": []})
except InvalidUpdateError as e:
print(f"An error occurred: {e}")
InvalidUpdateError: At key 'state': Can receive only one value per step.What this error says is: within the same execution step, State'sstateThe field received updates from multiple nodes, but no way to handle multiple values was configured (i.e. no reducer). The solution is to add this fieldAnnotated + reducer function。
LangGraph divides the execution of the graph into "steps". In a parallel scenario, multiple nodes may run concurrently in the same step. When these nodes all update the same State field, LangGraph needs an explicit rule to decide how to merge these updates - this rule isReducer。
reducer is one attached to the State fieldmerge function. When multiple nodes write values to the same field in the same step, LangGraph will use the reducer function to combine all written values into a final value.
operator.addas reducerPython standard libraryoperator.addApplying to a list is equivalent to list splicing (list1 + list2). Using it as a reducer, the output lists of all parallel nodes will be concatenated in sequence:
import operator
from typing import Annotated
class State(TypedDict):
# Annotated 的第二个参数是 Reducer 函数
# operator.add 作用于列表时 = 列表拼接(追加)
state: Annotated[list, operator.add]
# 其余代码(节点定义、边连接)与之前完全相同
builder = StateGraph(State)
builder.add_node("a", ReturnNodeValue("I'm A"))
builder.add_node("b", ReturnNodeValue("I'm B"))
builder.add_node("c", ReturnNodeValue("I'm C"))
builder.add_node("d", ReturnNodeValue("I'm D"))
# Fan-out + Fan-in
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "d")
builder.add_edge("c", "d")
builder.add_edge("d", END)
graph = builder.compile()
Now it runs successfully:
Note that C is printed before B in the output, but the final state order is B before C. This means:The execution order of parallel nodes is uncontrollable(Determined by LangGraph's internal scheduling), but the result after the reducer merge is still deterministic (explained in detail in the next section).
state: Annotated[list, operator.add]The meaning of this line:
- Annotated: A type annotation extension for Python that allows additional metadata
- first parameterlist: The data type of this field
- second parameteroperator.add: reducer function, used by LangGraph to merge concurrent writes
In actual scenarios, multiple parallel branches often have different lengths—for example, one branch has only one node, and another branch has two nodes connected in series. How does LangGraph handle this situation?
In this example, fromaFan-out has two branches: branch one is b → b2 (two nodes connected in series), branch two is c (only one node). Both branches are Fan-in.d:
builder = StateGraph(State)
builder.add_node("a", ReturnNodeValue("I'm A"))
builder.add_node("b", ReturnNodeValue("I'm B"))
builder.add_node("b2", ReturnNodeValue("I'm B2"))
builder.add_node("c", ReturnNodeValue("I'm C"))
builder.add_node("d", ReturnNodeValue("I'm D"))
builder.add_edge(START, "a")
builder.add_edge("a", "b") # 支路一:a → b → b2
builder.add_edge("a", "c") # 支路二:a → c
builder.add_edge("b", "b2")
# 列表语法:等 b2 和 c 都完成后,才触发 d
builder.add_edge(["b2", "c"], "d")
builder.add_edge("d", END)
graph = builder.compile()
builder.add_edge(["b2", "c"], "d")is a concise way of writing, which is equivalent to writing separatelyadd_edge("b2", "d")andadd_edge("c", "d"). It clearly states"dneedb2andcare completed", the semantics are clearer.
Execution result:
As can be seen from the output:
aexecutebandc At the same timeexecution (since they are bothadirect successor)b2execute(btriggered after completion), whilecCompleted waitingdExecution——only ifb2andcTriggered after everything is completedLangGraph guarantees: Fan-in nodes (i.e.d)Will only run after all upstream parallel nodes have completed. Even if each branch has different lengths and takes different times, LangGraph will automatically wait for the slowest path to complete. You don't need to write any synchronization code.
use operator.addWhen merging parallel output, the order in which each node writesDetermined by LangGraph internal scheduling, unpredictable. Sometimes we want the merged results to have a certain order, which requiresCustom reducer。
Output from previous section["I'm A", "I'm B", "I'm C", "I'm B2", "I'm D"], C comes before B2, but we probably want the results to be sorted alphabetically.
def sorting_reducer(left, right):
"""合并两个列表并按字母顺序排序"""
# 确保 left 和 right 都是列表
if not isinstance(left, list):
left = [left]
if not isinstance(right, list):
right = [right]
# 合并后排序
return sorted(left + right, reverse=False)
class State(TypedDict):
# 使用自定义 Reducer
state: Annotated[list, sorting_reducer]
The other code (definition of nodes and edges) is the same as the previous section. Just replace the reducer in the State definition:
Pay attention to the end result["I'm A", "I'm B", "I'm B2", "I'm C", "I'm D"]Alphabetically sorted - even if C is printed before B during execution, the sorting reducer ensures that the final result is ordered.
| parameter | meaning | Example |
|---|---|---|
left |
The existing value of this field in the current State (accumulated) | ["I'm A", "I'm B"] |
right |
The value written by the new node (this update) | ["I'm C"] |
| return value | The new value after merging is used as the next state of the field | ["I'm A", "I'm B", "I'm C"](after sorting) |
If you need more fine-grained control over the merge order, you can use a three-step strategy:① The parallel node writes the output to the temporary field(instead of the final field);② Use a "convergence node"Read all temporary fields and merge and sort according to custom rules;③ Clear temporary fields. This method is more flexible than reducer and is suitable for scenarios with strict sequence requirements.
The previous examples all used simple strings to demonstrate the parallel mechanism. Now let’s use a real AI scenario to demonstrate the practical value of parallel execution:Data is retrieved from Wikipedia and Web search engines at the same time, and then LLM combines the two data to answer user questions.。
import operator
from typing import Annotated
from typing_extensions import TypedDict
class State(TypedDict):
question: str # 用户问题(不需要 Reducer,只写一次)
answer: str # LLM 的最终回答
context: Annotated[list, operator.add] # 检索到的资料(需要 Reducer 追加)
contextThe field will besearch_webandsearch_wikipediaTwo parallel nodes write at the same time, so they must be addedoperator.add Reducer。questionandanswerWill only be written by one node, no reducer is required.
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.document_loaders import WikipediaLoader
from langchain_tavily import TavilySearch
from langchain_deepseek import ChatDeepSeek
llm = ChatDeepSeek(model="deepseek-v4-pro", temperature=0)
# ──── 节点 1:Web 搜索 ────
def search_web(state):
"""从 Web 搜索引擎检索资料"""
tavily_search = TavilySearch(max_results=3)
data = tavily_search.invoke({"query": state['question']})
search_docs = data.get("results", data)
# 格式化为带 URL 的文档字符串
formatted = "\n\n---\n\n".join([
f'<Document href="../../../langGraph/module-4/{doc["url"]}">\n{doc["content"]}\n</Document>'
for doc in search_docs
])
return {"context": [formatted]} # 返回列表,Reducer 会追加
# ──── 节点 2:Wikipedia 搜索 ────
def search_wikipedia(state):
"""从 Wikipedia 检索资料"""
search_docs = WikipediaLoader(
query=state['question'], load_max_docs=2
).load()
# 格式化为带来源的文档字符串
formatted = "\n\n---\n\n".join([
f'<Document source="{doc.metadata["source"]}">\n{doc.page_content}\n</Document>'
for doc in search_docs
])
return {"context": [formatted]} # 返回列表,Reducer 会追加
# ──── 节点 3:LLM 综合回答 ────
def generate_answer(state):
"""基于检索到的上下文,让 LLM 回答问题"""
context = state["context"] # 已合并的两份资料
question = state["question"]
answer_template = "Answer the question {question} using this context: {context}"
instructions = answer_template.format(question=question, context=context)
answer = llm.invoke([
SystemMessage(content=instructions),
HumanMessage(content="Answer the question.")
])
return {"answer": answer}
builder = StateGraph(State)
builder.add_node("search_web", search_web)
builder.add_node("search_wikipedia", search_wikipedia)
builder.add_node("generate_answer", generate_answer)
# 从 START 同时 Fan-out 到两个搜索节点(并行)
builder.add_edge(START, "search_wikipedia")
builder.add_edge(START, "search_web")
# 两个搜索节点都完成后,Fan-in 到 generate_answer
builder.add_edge("search_wikipedia", "generate_answer")
builder.add_edge("search_web", "generate_answer")
builder.add_edge("generate_answer", END)
graph = builder.compile()
result = graph.invoke({"question": "How were Nvidia's Q2 2025 earnings"})
print(result['answer'].content)
Web search and Wikipedia searchsimultaneously, the total time taken is approximately equal to the slower of the two, rather than the sum of the two.
The results of both search nodes are passedoperator.addreducer automatically splices intocontextlist,generate_answerWhat the node receives isTwo complete documents, no need to merge manually.
LLM's web-based timely content + Wikipedia's authoritative knowledge base provide comprehensive answers from two sources, which are more comprehensive and accurate than a single source.
The constructed parallel graph can be directly displayed in the notebookinvoke, can also be deployed to LangGraph API Server and called asynchronously through HTTP interface or SDK.
existmodule-4/studio/Execute in the directory:
# 在终端里运行(不是 Python 代码)
langgraph dev
Open the Studio UI address in the browser and you can visually see the graph structure, real-time execution status, and input/output of each node, which is very useful when debugging complex parallel graphs.
from langgraph_sdk import get_client
client = get_client(url="http://127.0.0.1:2024")
# 创建新线程(每个线程有独立的状态)
thread = await client.threads.create()
input_question = {"question": "How were Nvidia Q2 2025 earnings?"}
# 流式订阅执行过程,stream_mode="values" 表示每个步骤后推送完整 State
async for event in client.runs.stream(
thread["thread_id"],
assistant_id="parallelization", # 对应 studio 里的图名称
input=input_question,
stream_mode="values"
):
# 检查 answer 字段是否已经出现
if event.data is not None:
answer = event.data.get('answer', None)
if answer:
print(answer['content'])
stream_mode="values": Whenever the graph completes a step (a batch of parallel nodes are completed), the current complete State value is pushed. Suitable for scenarios where intermediate progress needs to be displayed in real time. Also"updates"(Only push the State change part of this step) and"events"(push more fine-grained events) and other modes.
| Way | Suitable for the scene | Main limitations |
|---|---|---|
directinvoke() |
Local development, testing, one-off scripts | Blocking call, does not support persistence, does not support multiple clients |
| LangGraph API Server | Production environment, Web services, multi-user concurrency | The service needs to be started and there are certain operation and maintenance costs. |
Annotated[list, operator.add]