Ejecución Paralela
Domina la ejecución paralela de nodos en LangGraph, patrones de fan-out/fan-in, coordinación de ramas, seguridad de hilos y sincronización de resultados paralelos.
LangGraph admite la ejecución paralela de nodos mediante aristas de fan-out. Esta lección cubre cuándo y cómo paralelizar, coordinar ramas paralelas, fusionar resultados y tratar la seguridad de los hilos.
Cómo Funciona la Ejecución Paralela
Cuando un nodo tiene múltiples aristas de salida, todos los nodos de destino se ejecutan simultáneamente en hilos separados:
# Fan-out: B and C run in parallel after A
builder.add_edge("A", "B")
builder.add_edge("A", "C")Cada rama recibe una copia del estado en el punto de fan-out. Cuando todas las ramas terminan, sus actualizaciones se fusionan antes de que se ejecute el nodo 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()Los tres nodos de procesamiento (A, B, C) se ejecutan en paralelo. El nodo de fusión espera a que los tres terminen antes de ejecutarse.
Paralelismo Dinámico
Haz fan-out dinámicamente según el número de elementos a procesar:
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 un fan-out verdaderamente dinámico, usa un patrón 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}Seguridad de Hilos
Las ramas paralelas comparten el estado, pero se ejecutan en hilos separados. Cada rama recibe una instantánea del estado en el punto 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 completeLas ramas paralelas no deben depender de los resultados de las demás. Cada rama trabaja a partir de la misma instantánea de estado. Usa nodos de fan-in para coordinar una vez terminado el trabajo paralelo.
Patrones de Reducción (Fan-In)
Agregación Simple
def reducer(state: State) -> dict:
all_results = "\n".join(state.get("results", []))
return {"final": all_results}Fusión 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}Fusión por Votación
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]}Ejecución Paralela de Herramientas
Ejecuta varias herramientas en 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}Llamadas LLM Paralelas
Usa fan-out paralelo para obtener múltiples 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 synthesizesLas llamadas paralelas a LLM con distintos prompts de sistema te permiten reunir perspectivas diversas simultáneamente y luego sintetizarlas en una respuesta integral.
Manejo de Errores en Ramas Paralelas
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)}Una excepción en una rama paralela no cancela las demás ramas. Todas las ramas terminan; sus resultados se fusionan, incluidos los campos de error.
Consideraciones de Rendimiento
Factor | Impacto | Mitigation |
|---|---|---|
Sobrecarga de hilos | Iniciar hilos tiene costo | Paraleliza solo trabajo > 100ms |
Bloqueo de estado compartido | Las escrituras requieren sincronización | Usa el reducer add para las escrituras |
Amplitud del fan-out | Demasiadas ramas = contención | Limita a 5-10 ramas paralelas |
Memoria | Cada rama tiene su propio contexto | Mantén el estado pequeño |
Límites de tasa del LLM | Las llamadas paralelas alcanzan los límites más rápido | Añade limitación de tasa |
Ejemplo Completo de Procesamiento 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()Preguntas de Práctica (Practice Questions)
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How does LangGraph execute nodes with multiple outgoing edges?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What does a fan-in node receive?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What state snapshot do parallel branches see?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What happens if one parallel branch raises an exception?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What is the recommended maximum number of parallel branches?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What reducer is ideal for collecting results from parallel branches?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How can you parallelize LLM calls with different perspectives?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
When is parallel execution NOT beneficial?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
What pattern processes a large dataset by splitting, parallel processing, and merging?
Cómo responder
Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.
How does the 'add' reducer help with parallel execution?
Conclusiones Clave
- Las aristas de fan-out crean ramas de ejecución paralela
- Cada rama ve la misma instantánea de estado en el fan-out
- Los nodos de fan-in esperan a que todas las ramas terminen antes de ejecutar
- Annotated[list, add] acumula resultados paralelos de forma limpia
- Maneja errores por rama usando try/except
- 5-10 ramas paralelas es un buen máximo para la mayoría de los sistemas
- Las llamadas paralelas a LLM con distintos prompts reúnen perspectivas diversas
- Patrón Map-Reduce: dividir → procesar en paralelo → fusionar
- Las tareas cortas (< 100ms) pueden no beneficiarse de la paralelización