Execução Paralela
Avançado⏱35 min📖3 min de leituraLição 8 de 10

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:

python
# Fan-out: B and C run in parallel after A builder.add_edge("A", "B") builder.add_edge("A", "C")
100%

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

python
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()
ℹ️Nota

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:

python
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 simultaneously

Para um fan-out verdadeiramente dinâmico, use um padrão map:

python
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:

python
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 complete
📌Importante

Ramos 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

python
def reducer(state: State) -> dict: all_results = "\n".join(state.get("results", [])) return {"final": all_results}

Fusão Ponderada

python
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

python
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:

python
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:

python
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 synthesizes
💡Dica

Chamadas 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

python
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)}
⚠️Aviso

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

python
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)

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?

Questão de Prática
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?


✅Sucesso

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
Progresso80%