Execução Paralela
Domine a execução paralela de nós em LangGraph, padrões de fan-out/fan-in, coordenação de ramos, segurança de thread e sincronização de resultados paralelos.
LangGraph suporta execução paralela de nós por meio de arestas de fan-out. Esta lição cobre quando e como paralelizar, coordenar ramos paralelos, mesclar resultados e tratar a segurança de threads.
Como Funciona a Execução Paralela
Quando um nó tem múltiplas arestas de saída, todos os nós de destino são executados simultaneamente em threads separados:
# Fan-out: B and C run in parallel after A
builder.add_edge("A", "B")
builder.add_edge("A", "C")Cada ramo recebe uma cópia do estado no ponto de fan-out. Quando todos os ramos terminam, as atualizações deles são mescladas antes da execução do nó de fan-in.
Fan-Out/Fan-In Básico
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
from typing import List
from operator import add
class ParallelState(TypedDict):
input_text: str
results: Annotated[List[str], add]
merged: str
def splitter(state: ParallelState) -> dict:
"""Fan-out: this node passes state to multiple parallel nodes."""
return {} # Passes state through unchanged
def process_a(state: ParallelState) -> dict:
return {"results": [f"Process A: {state['input_text'].upper()}"]}
def process_b(state: ParallelState) -> dict:
return {"results": [f"Process B: {state['input_text'].lower()}"]}
def process_c(state: ParallelState) -> dict:
return {"results": [f"Process C: {state['input_text'][::-1]}"]}
def merger(state: ParallelState) -> dict:
combined = "\n".join(state["results"])
return {"merged": combined}
builder = StateGraph(ParallelState)
builder.add_node("splitter", splitter)
builder.add_node("process_a", process_a)
builder.add_node("process_b", process_b)
builder.add_node("process_c", process_c)
builder.add_node("merger", merger)
builder.add_edge(START, "splitter")
builder.add_edge("splitter", "process_a")
builder.add_edge("splitter", "process_b")
builder.add_edge("splitter", "process_c")
builder.add_edge("process_a", "merger")
builder.add_edge("process_b", "merger")
builder.add_edge("process_c", "merger")
builder.add_edge("merger", END)
app = builder.compile()Os três nós de processamento (A, B, C) rodam em paralelo. O nó de mesclagem espera os três terminarem antes de executar.
Paralelismo Dinâmico
Faça fan-out dinamicamente com base no número de itens a processar:
def dispatch(state: State) -> dict:
items = state["items"]
# Dynamic dispatch creates parallel work
return {"item_count": len(items)}
# The splitter node and multiple parallel workers
# process different chunks simultaneouslyPara um fan-out verdadeiramente dinâmico, use um padrão map:
def map_node(state: State) -> dict:
items = state["items_to_process"]
results = []
# Process each item (can parallelize by fan-out)
for item in items:
result = process_item(item)
results.append(result)
return {"all_results": results}Segurança de Thread
Ramos paralelos compartilham o estado, mas são executados em threads separados. Cada ramo recebe um snapshot do estado no ponto de fan-out:
import threading
import time
def parallel_worker(state: State) -> dict:
thread_id = threading.get_ident()
print(f"Thread {thread_id} processing: {state.get('shared_value', 'N/A')}")
# Simulate work
time.sleep(1)
return {"worker_result": f"Thread {thread_id} done"}
# Each worker sees the same state snapshot
# Their updates are merged when all completeRamos paralelos não devem depender dos resultados uns dos outros. Cada ramo trabalha a partir do mesmo snapshot de estado. Use nós de fan-in para coordenação após a conclusão do trabalho paralelo.
Padrões de Redução (Fan-In)
Agregação Simples
def reducer(state: State) -> dict:
all_results = "\n".join(state.get("results", []))
return {"final": all_results}Fusão Ponderada
def weighted_merger(state: State) -> dict:
results = state.get("results", [])
weights = state.get("weights", {})
final = ""
for result in results:
source = result.split(":")[0]
weight = weights.get(source, 1.0)
if weight > 0.5:
final += f"[High confidence] {result}\n"
return {"merged": final}Fusão por Votação
def voting_merger(state: State) -> dict:
results = state.get("results", [])
from collections import Counter
answers = Counter(results)
most_common = answers.most_common(1)[0]
return {"consensus": most_common[0], "votes": most_common[1]}Execução Paralela de Ferramentas
Execute várias ferramentas em paralelo:
from langgraph.prebuilt import ToolExecutor
@tool
def search_web(q: str) -> str:
return f"Web: {q}"
@tool
def search_db(q: str) -> str:
return f"DB: {q}"
@tool
def search_api(q: str) -> str:
return f"API: {q}"
tools = [search_web, search_db, search_api]
tool_executor = ToolExecutor(tools)
def parallel_search(state: State) -> dict:
query = state["query"]
# Dispatch all three searches in parallel via fan-out
return {
"web_query": query,
"db_query": query,
"api_query": query
}
# Each search node runs in parallel
def web_search_node(state: State) -> dict:
result = tool_executor.invoke({"name": "search_web", "args": {"q": state["web_query"]}})
return {"web_result": str(result)}
# Same for db_search_node and api_search_node...
def aggregate_results(state: State) -> dict:
combined = f"Web: {state['web_result']}\nDB: {state['db_result']}\nAPI: {state['api_result']}"
return {"final": combined}Chamadas LLM Paralelas
Use fan-out paralelo para obter múltiplas perspectivas de LLM:
def perspective_a(state: State) -> dict:
response = llm.invoke(f"Analyze {state['topic']} from an economic perspective")
return {"perspectives": [f"Economic: {response.content}"]}
def perspective_b(state: State) -> dict:
response = llm.invoke(f"Analyze {state['topic']} from a social perspective")
return {"perspectives": [f"Social: {response.content}"]}
def perspective_c(state: State) -> dict:
response = llm.invoke(f"Analyze {state['topic']} from a technical perspective")
return {"perspectives": [f"Technical: {response.content}"]}
# All three run in parallel → merger synthesizesChamadas paralelas de LLM com prompts de sistema diferentes permitem reunir perspectivas diversas simultaneamente e depois sintetizá-las em uma resposta abrangente.
Tratamento de Erros em Ramos Paralelos
def safe_parallel_worker(state: State) -> dict:
try:
result = risky_operation(state["input"])
return {"results": [f"Success: {result}"]}
except Exception as e:
return {"errors": [f"Worker failed: {e}"]}
def error_aware_merger(state: State) -> dict:
results = state.get("results", [])
errors = state.get("errors", [])
if errors and not results:
return {"merged": f"All workers failed: {errors}"}
if errors:
return {"merged": f"Partial results: {results}\nErrors: {errors}"}
return {"merged": "\n".join(results)}Uma exceção em um ramo paralelo não cancela os demais ramos. Todos os ramos terminam; os resultados deles são mesclados, incluindo quaisquer campos de erro.
Considerações de Desempenho
Factor | Impacto | Mitigation |
|---|---|---|
Sobrecarga de threads | Iniciar threads tem custo | Paralelize apenas trabalho > 100ms |
Lock de estado compartilhado | Escritas precisam de sincronização | Use o reducer add para escritas |
Largura do fan-out | Muitos ramos = contenção | Limite a 5-10 ramos paralelos |
Memória | Cada ramo tem seu próprio contexto | Mantenha o estado pequeno |
Limites de taxa do LLM | Chamadas paralelas atingem os limites mais rápido | Adicione throttling |
Exemplo Completo de Processamento Paralelo
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, Annotated
from typing import List
from operator import add
class DataState(TypedDict):
data: str
chunks: List[str]
chunk_results: Annotated[List[str], add]
final_output: str
def split_data(state: DataState) -> dict:
# Split large text into chunks for parallel processing
chunk_size = len(state["data"]) // 3
chunks = [
state["data"][:chunk_size],
state["data"][chunk_size:2*chunk_size],
state["data"][2*chunk_size:]
]
return {"chunks": chunks}
def process_chunk_1(state: DataState) -> dict:
return {"chunk_results": [f"Chunk 1: {state['chunks'][0][:50]}..."]}
def process_chunk_2(state: DataState) -> dict:
return {"chunk_results": [f"Chunk 2: {state['chunks'][1][:50]}..."]}
def process_chunk_3(state: DataState) -> dict:
return {"chunk_results": [f"Chunk 3: {state['chunks'][2][:50]}..."]}
def merge_results(state: DataState) -> dict:
return {"final_output": "\n".join(state["chunk_results"])}
builder = StateGraph(DataState)
builder.add_node("split", split_data)
builder.add_node("chunk1", process_chunk_1)
builder.add_node("chunk2", process_chunk_2)
builder.add_node("chunk3", process_chunk_3)
builder.add_node("merge", merge_results)
builder.add_edge(START, "split")
builder.add_edge("split", "chunk1")
builder.add_edge("split", "chunk2")
builder.add_edge("split", "chunk3")
builder.add_edge("chunk1", "merge")
builder.add_edge("chunk2", "merge")
builder.add_edge("chunk3", "merge")
builder.add_edge("merge", END)
app = builder.compile()Perguntas de Prática (Practice Questions)
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
How does LangGraph execute nodes with multiple outgoing edges?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What does a fan-in node receive?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What state snapshot do parallel branches see?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What happens if one parallel branch raises an exception?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What is the recommended maximum number of parallel branches?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What reducer is ideal for collecting results from parallel branches?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
How can you parallelize LLM calls with different perspectives?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
When is parallel execution NOT beneficial?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
What pattern processes a large dataset by splitting, parallel processing, and merging?
Como responder
Questão de múltipla escolha com 4 opções. Selecione uma opção e escolha Verificar resposta.
How does the 'add' reducer help with parallel execution?
Principais Conclusões
- Arestas de fan-out criam ramos de execução paralela
- Cada ramo vê o mesmo snapshot de estado no fan-out
- Nós de fan-in esperam todos os ramos terminarem antes de executar
- Annotated[list, add] acumula resultados paralelos de forma limpa
- Trate erros por ramo usando try/except
- 5-10 ramos paralelos é um bom máximo para a maioria dos sistemas
- Chamadas paralelas de LLM com prompts diferentes reúnem perspectivas diversas
- Padrão Map-Reduce: dividir → processar em paralelo → mesclar
- Tarefas curtas (< 100ms) podem não se beneficiar da paralelização