Inicio / Artículos / Crear un agente de investigación ReAct en LangGraph: Cerebro, Manos, Enrutador

Crear un agente de investigación ReAct en LangGraph: Cerebro, Manos, Enrutador

Aprender cómo implementar el bucle ReAct de razonar-actuar-observar como un sub-gráfico en LangGraph, con reflexión forzada, presupuestos de iteración e investigación paralela de tipo scatter-gather.

4678 palabras

Una sola llamada al LLM no puede investigar una pregunta sobre la que no sabe nada. Un agente de investigación útil debe buscar, leer lo que recibe como respuesta, decidir qué sigue faltando y buscar de nuevo hasta contar con suficiente información para responder. El patrón ReAct da a este comportamiento una estructura precisa, y LangGraph permite expresarlo como un grafo pequeño y explícito en lugar de un enredo de bucles while. Al final de esta guía, comprenderá cada nodo de un subgrafo de investigación ReAct funcional, sabrá dónde puede fallar y dispondrá de una lista de mejoras que garantizan su funcionamiento seguro en entornos de producción.

El diseño sigue el componente de investigación de un proyecto de código abierto llamado deep-research-agent. A nivel general, un ciclo de ejecución se desarrolla de la siguiente manera:

  1. El usuario plantea una pregunta de investigación.
  2. El LLM, que actúa como el “cerebro”, analiza la pregunta y elige una herramienta a utilizar.
  • Un ejecutor de herramientas, las “manos”, ejecuta dicha herramienta y devuelve una observación.
  • Un enrutador examina el último mensaje del cerebro para determinar si se solicitaron más llamadas a herramientas.
  • Si es así, el control vuelve al paso 2.
  • De lo contrario, la investigación acumulada se comprime en una respuesta final.
  • Qué es realmente el patrón ReAct

    ReAct es la abreviatura de “Reason and Act”. Proviene del artículo de investigación “ReAct: Synergizing Reasoning and Acting in Language Models” (publicado por primera vez en 2022 y presentado en ICLR 2023). La observación central es que los modelos de lenguaje rinden mejor cuando alternan dos tipos de pasos: pensar en qué hacer a continuación y utilizar una herramienta para obtener información real. Cada uno de estos pasos por sí solo falla. Un modelo que solo razona inventará hechos con confianza, ya que nada lo sustenta. Un modelo que solo actúa utilizará las herramientas de forma mecánica sin interpretar lo que estas devuelven.

    La guía de arquitectura de Google Cloud describe este patrón como un bucle de pasos en lenguaje natural que continúa hasta que se cumple una condición de salida. En la práctica, se divide en tres fases repetitivas:

    1. Pensamiento. El modelo analiza todo lo recopilado hasta el momento y decide si la solicitud ya ha sido respondida o qué hacer a continuación.
    2. Acción. Basándose en ese razonamiento, llama a una herramienta para obtener más datos o escribe una respuesta final que pone fin al ciclo.
    3. Observación. La salida de la herramienta vuelve a aparecer y se mantiene en la conversación. Dado que las observaciones anteriores siguen visibles, el modelo puede basarse en ellas en lugar de repetir búsquedas o olvidar el contexto.

    Esto se asemeja mucho a la forma en que un ingeniero experimentado investiga un problema desconocido: busca información, lo analiza, busca más datos y solo entonces escribe la conclusión. La documentación de uso de herramientas de Anthropic describe el mismo mecanismo desde el lado de la API: el modelo responde con una solicitud de uso de herramienta, tu aplicación la ejecuta y envía el resultado, y el proceso se repite. El ciclo es idéntico independientemente de que el modelo subyacente sea Claude, GPT o Gemini.

    Ese es el punto importante que hay que recordar: ReAct es un patrón, no una función de la biblioteca. LangGraph ofrece una forma ordenada de expresarlo mediante un StateGraph, pero se podría implementar el mismo ciclo con cualquier modelo y cualquier código de orquestación. El proyecto deep-research-agent lo empaqueta como un sub-gráfico de LangGraph, lo que lo hace componible: un sistema más grande puede llamarlo como una sola unidad. Si desea repasar los conceptos antes de profundizar en el código, consulte nuestro resumen sobre cómo los agentes de IA combinan el razonamiento con acciones del mundo real.

    Los tres componentes y el estado que comparten

    El bucle del investigador está compuesto por tres funciones pequeñas, cada una con una tarea específica. El cerebro (llm_call) lee la conversación actual y responde ya sea con texto plano o solicitudes de llamada a herramientas. Las manos (tool_node) ejecutan las solicitudes de llamada a herramientas y devuelven sus resultados. El enrutador (should_continue) decide si se necesita otra ronda. Al mantener estas responsabilidades separadas, es posible probar cada una de forma unitaria, cambiar el modelo o las herramientas de manera independiente, y comprender cómo funciona el bucle al analizar tres funciones cortas.

    Definir el estado del investigador

    Todo lo que fluye a través de un grafo LangGraph existe dentro de un objeto de estado, declarado como TypedDict. El estado del investigador cuenta con cinco campos. researcher_messages almacena la conversación en curso; está envuelto en Annotated con el reductor add_messages, que indica a LangGraph que fusione los nuevos mensajes en la lista existente en lugar de sobrescribirla. tool_call_iterations cuenta los ciclos de bucle, research_topic registra qué está investigando el agente, compressed_research recibe el resumen final, y raw_notes recopila notas utilizando operator.add como su reductor, de modo que las listas devueltas por diferentes nodos se concatenan.

    # Define the state that flows through the entire ReAct loop
    from typing import Annotated, Sequence, List, TypedDict
    from langgraph.graph.message import add_messages
    from langchain_core.messages import BaseMessage
    import operator
    
    class ResearcherState(TypedDict):
        # The message history accumulates as the loop runs
        researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
        # Tracks how many tool call iterations have occurred
        tool_call_iterations: int
        # The topic this researcher is investigating
        research_topic: str
        # The final compressed output after the loop ends
        compressed_research: str
        # Raw notes collected during research
        raw_notes: Annotated[List[str], operator.add]
    

    Los reducers son los que hacen funcionar el bucle. Cada vez que el “cerebro” emite un mensaje o las manos devuelven resultados, un nodo solo devuelve los nuevos mensajes, y el reducer los agrega. Sin add_messages, cada nodo reemplazaría el historial, y el modelo perdería todo lo que había aprendido en iteraciones anteriores.

    El proyecto también declara un esquema de salida más restringido. Este controla qué campos salen del sub-gráfico cuando un gráfico padre lo llama.

    # Output schema controls what the parent graph sees
    class ResearcherOutputState(TypedDict):
        compressed_research: str
        raw_notes: Annotated[List[str], operator.add]
        researcher_messages: Annotated[Sequence[BaseMessage], add_messages]
    

    Separar el estado interno del estado de salida es una buena práctica en LangGraph. La contabilidad, como tool_call_iterations, solo es relevante dentro del bucle, por lo que el gráfico padre nunca la ve. El padre recibe la información de investigación comprimida, las notas en bruto y los mensajes, lo que mantiene la interfaz entre los gráficos simple y bien estructurada.

    ¿Por qué usar TypedDict en lugar de un diccionario normal? Documenta con exactitud qué datos se transfieren a través del grafo y permite que los verificadores de tipos detecten claves escritas incorrectamente. El reductor add_messages añade funcionalidades adicionales: agrega nuevos mensajes y, cuando un mensaje entrante lleva el ID de uno ya presente en la lista, reemplaza ese mensaje en lugar de duplicarlo, manteniendo así la orden y la identidad consistentes.

    El cerebro: llm_call

    El cerebro es donde tiene lugar el razonamiento. Construye una instrucción a partir de un mensaje del sistema más toda la historia de mensajes y le pregunta al modelo qué hacer a continuación. Tenga en cuenta que las etiquetas originales indican que este fragmento es de JavaScript; en realidad es de Python.

    # The "brain" of the researcher: analyzes current state and decides next action
    from langchain_core.messages import SystemMessage
    
    def llm_call(state: ResearcherState):
        # Invoke the LLM with the system prompt and full conversation history
        return {
            "researcher_messages": [
                model_with_tools.invoke(
                    [SystemMessage(content=research_agent_prompt.format(date=get_today_str()))]
                    + state["researcher_messages"]
                )
            ]
        }
    

    Aquí ocurren tres cosas. Primero, se crea un SystemMessage a partir de research_agent_prompt, con la fecha de hoy insertada mediante get_today_str() para que el modelo pueda determinar cuán actual es su información. Segundo, ese mensaje del sistema se añade al principio de todo lo que está en state["researcher_messages"], de modo que el modelo siempre vea el contexto completo. Tercero, model_with_tools.invoke() envía todo esto a un modelo al que se le han asignado herramientas. La respuesta puede ser texto plano, lo cual indica que la investigación ha finalizado, o una o más solicitudes de llamada a herramientas, lo cual indica que se necesita más información. La función devuelve la respuesta envuelta en una lista bajo researcher_messages, y el reducer la agrega.

    El prompt del sistema se reconstruye en cada llamada en lugar de almacenarse en el estado. Eso mantiene un historial limpio y garantiza que las instrucciones siempre tengan prioridad, incluso después de muchas iteraciones.

    model_with_tools se crea durante la configuración. En lugar de importar una clase de un proveedor como ChatOpenAI, el proyecto utiliza init_chat_model() de LangChain, que acepta una cadena de modelo con prefijo de proveedor.

    # Initialize the model using LangChain's provider-agnostic helper
    from langchain.chat_models import init_chat_model
    
    # The project uses different models for different tasks
    model = init_chat_model(model="openai:gpt-4o")
    
    # Bind the research tools so the model knows what actions are available
    model_with_tools = model.bind_tools([tavily_search, think_tool])
    

    La ventaja del prefijo del proveedor es que pasar de "openai:gpt-4o" a un modelo de Anthropic (una cadena del formato "anthropic:claude-sonnet-4-20250514") constituye un cambio de configuración, no uno de importación. Verifique la lista actual de modelos de su proveedor, ya que los identificadores cambian con el tiempo. Luego, .bind_tools() informa al modelo sobre las acciones disponibles. Se vinculan dos herramientas: tavily_search, que realiza búsquedas en la web a través de la API de Tavily, y think_tool, una herramienta de reflexión que se describe más abajo.

    Las manos: tool_node

    Las manos toman las llamadas a herramientas del último mensaje enviado por el “cerebro” y las ejecutan. Este fragmento también está escrito en Python, a pesar de su etiqueta de JavaScript.

    # The "hands" of the researcher: executes all tool calls from the brain
    from langchain_core.messages import ToolMessage
    
    def tool_node(state: ResearcherState):
        # Get the tool calls from the last message (the brain's output)
        tool_calls = state["researcher_messages"][-1].tool_calls
        observations = []
    
        # Execute each tool call and collect raw results
        for tool_call in tool_calls:
            tool = tools_by_name[tool_call["name"]]
            observations.append(tool.invoke(tool_call["args"]))
    
        # Convert raw results into properly formatted ToolMessage objects
        tool_outputs = [
            ToolMessage(
                content=str(observation),
                name=tool_call["name"],
                tool_call_id=tool_call["id"]
            )
            for observation, tool_call in zip(observations, tool_calls)
        ]
    
        return {"researcher_messages": tool_outputs}
    

    La función lee tool_calls del mensaje más reciente, busca cada herramienta solicitada por nombre en el diccionario tools_by_name y la invoca con los argumentos proporcionados por el modelo. La segunda parte es la que determina la corrección: cada resultado bruto se envuelve en un ToolMessage que cuenta con tres campos. content almacena el resultado en formato de cadena, name registra qué herramienta lo generó, y tool_call_id relaciona el resultado con la solicitud específica que lo provocó. Las APIs de modelo requieren ese ID; un resultado de herramienta que no pueda asociarse a una solicitud es rechazado, y una solicitud sin resultado correspondiente deja la conversación en un estado inválido.

    Se utiliza la asignación tools_by_name, pero nunca se define en el cuaderno de notas del proyecto. Tendrías que crearla tú mismo, por ejemplo como {"tavily_search": tavily_search, "think_tool": think_tool}, o con una comprensión de diccionario sobre la lista de herramientas para que los nombres siempre estén sincronizados con lo que se ha definido.

    Si el “cerebro” pide a tavily_search que busque “la última investigación en IA”, las “manos” ejecutan la consulta y devuelven un mensaje con este formato:

    ToolMessage(content="Search results for 'latest AI research': ...", name="tavily_search", tool_call_id="call_abc123")
    

    Dado que las llamadas a las herramientas se ejecutan secuencialmente en un bucle for simple, una sola ejecución con varias búsquedas tarda tanto como todas ellas juntas. Esto es aceptable para una demostración; más adelante veremos cómo hacer que este nodo sea más robusto.

    El enrutador: should_continue

    El enrutador es la función más sencilla y la que controla todo el bucle. Examina el último mensaje y elige el siguiente nodo.

    # The "router": determines whether to loop again or finish
    from typing import Literal
    
    def should_continue(state: ResearcherState) -> Literal["tool_node", "compress_research"]:
        # Check the last message in the conversation
        messages = state["researcher_messages"]
        last_message = messages[-1]
    
        # If the brain requested tool calls, continue the loop
        if last_message.tool_calls:
            return "tool_node"
    
        # If no tool calls, the brain is done researching
        return "compress_research"
    

    Si el último mensaje del cerebro contiene tool_calls, el enrutador devuelve "tool_node" y el bucle continúa. Si el cerebro solo generó texto, devuelve "compress_research", lo que hace que el bucle termine y se pase a la fase de resumen. La decisión se basa exclusivamente en la salida del modelo; el enrutador en sí no tiene opinión sobre si la investigación es lo suficientemente buena.

    La anotación de retorno Literal["tool_node", "compress_research"] indica a LangGraph qué destinos son posibles. LangGraph la utiliza para conocer las ramas del enrutador (por ejemplo, al dibujar el grafo o cuando no se proporciona una asignación explícita), por lo que va más allá de ser simplemente documentación. Sin embargo, esto no impide que la función devuelva otra cadena en tiempo de ejecución; eso se manifestaría como un error cuando se seleccione esa rama.

    ¿Por qué enrutar hacia un paso de compresión en lugar de terminar directamente? Porque este sub-grafo está diseñado para ser llamado por un agente supervisor. El supervisor necesita una respuesta concisa y estructurada, no un registro largo de búsquedas, reflexiones y datos de las herramientas. Comprimir los datos dentro del sub-grafo mantiene el contexto del supervisor reducido.

    Conexión del bucle con un StateGraph

    Una vez establecidas las tres funciones, el siguiente paso es conectarlas. En lugar de escribir el flujo de control a mano, se declaran los nodos y las aristas, y LangGraph ejecuta el grafo. La ejecución comienza en el cerebro, pasa por el router y ya sea que vaya a las manos (después de lo cual siempre regresa al cerebro) o sale a través de compress_research.

    Montaje y compilación del grafo

    A pesar de la etiqueta de JavaScript, este fragmento también está escrito en Python.

    # Build the ReAct loop as a LangGraph StateGraph
    from langgraph.graph import StateGraph, START, END
    
    # Initialize the graph with both input state and output schema
    agent_builder = StateGraph(ResearcherState, output_schema=ResearcherOutputState)
    
    # Add the three nodes to the graph
    agent_builder.add_node("llm_call", llm_call)                       # The brain
    agent_builder.add_node("tool_node", tool_node)                     # The hands
    agent_builder.add_node("compress_research", compress_research)     # The exit point
    
    # Wire the entry point: execution starts at the brain
    agent_builder.add_edge(START, "llm_call")
    
    # Wire the router: after the brain thinks, decide what to do next
    agent_builder.add_conditional_edges(
        "llm_call",
        should_continue,
        {
            "tool_node": "tool_node",
            "compress_research": "compress_research",
        },
    )
    
    # Wire the loop: after the hands act, always go back to the brain
    agent_builder.add_edge("tool_node", "llm_call")
    
    # Wire the exit: after compression, end the graph
    agent_builder.add_edge("compress_research", END)
    
    # Compile the graph into a runnable agent
    researcher_agent = agent_builder.compile()
    

    Al leerlo de arriba hacia abajo:

    1. StateGraph(ResearcherState, output_schema=ResearcherOutputState) crea el grafo con el estado interno completo y el esquema de salida restringido que verán los grafos padre.
    2. Se registran tres nodos: llm_call, tool_node y compress_research.
  • add_edge(START, "llm_call") hace que el cerebro sea el punto de entrada.
  • add_conditional_edges conecta el enrutador a la salida del cerebro, mediante un diccionario que asocia cada valor de retorno posible a un nodo.
  • Un enlace fijo desde tool_node de vuelta a llm_call cierra el bucle.
  • add_edge("compress_research", END) finaliza el grafo una vez que se ha escrito el resumen.
  • .compile() convierte la declaración en un objeto ejecutable. Se le da el nombre de researcher_agent en lugar de simplemente agent porque, en el proyecto completo, es un sub-gráfico invocado por el supervisor.

    La asignación pasada a add_conditional_edges merece atención. Sus claves, "tool_node" y "compress_research", deben coincidir exactamente con lo que devuelve should_continue, y sus valores deben ser nombres reales de nodos. La compilación verifica que los destinos asignados existan, por lo que un error tipográfico en el nombre de un nodo provoca un fallo temprano en lugar de a mitad del proceso. Por otro lado, un enrutador que devuelve un valor que no está en el mapa solo falla cuando se ejecuta esa rama, por lo que es necesario probar el enrutador en ambas ramas. Aun así, esto es mucho más fácil de validar que un bucle while implementado manualmente, donde una rama incorrecta simplemente se comportará de forma errónea.

    Visualización del ciclo

    El grafo compilado genera este flujo:

    START
      │
      ▼
    llm_call (Brain reasons about the query)
      │
      ├── has tool_calls? ──► tool_node (Hands execute tools)
      │                            │
      │                            └──► llm_call (Back to brain)
      │
      └── no tool_calls? ──► compress_research (Summarize and exit)
    

    Este es el ciclo clásico de ReAct. El cerebro y las manos pueden alternarse tantas veces como el modelo siga solicitando herramientas. Cada paso agrega observaciones al estado, por lo que cada nueva decisión se toma con más información que la anterior.

    Agregar puntos de verificación

    El ejemplo hasta ahora es un agente independiente que opera en memoria. Para cualquier proceso de larga duración, se debe añadir un punto de verificación en tiempo de compilación para que el estado del grafo se guarde después de cada paso.

    # Production: add checkpointing for fault tolerance
    from langgraph.checkpoint.memory import MemorySaver
    
    checkpointer = MemorySaver()
    agent = agent_builder.compile(checkpointer=checkpointer)
    

    MemorySaver almacena los puntos de control en la memoria del proceso, lo cual es ideal para el desarrollo y las pruebas, pero desaparece al reiniciar. Para entornos de producción, LangGraph ofrece herramientas de almacenamiento persistente como PostgresSaver y SqliteSaver, que permiten reanudar una ejecución interrumpida desde el último paso guardado y registrar cada transición de estado. Un detalle práctico: una vez que un grafo cuenta con un mecanismo de punto de control, cada llamada invoke necesita un identificador de hilo en su configuración (por ejemplo {"configurable": {"thread_id": "..."}}) para que LangGraph sepa qué conversación guardada cargar y actualizar.

    Reforzando el bucle: reflexión, presupuestos y paralelismo

    Un bucle ReAct básico presenta dos modos de fallo que se manifiestan rápidamente. Puede entrar en bucle sin converger, gastando tokens y créditos de API en búsqueda tras búsqueda. O bien puede realizar búsquedas rápidamente sin procesarlas realmente, generando resultados superficiales. El proyecto aborda ambos problemas y luego escala el bucle con tres adiciones.

    Reflexión forzada con think_tool

    La adición más interesante es una herramienta que no realiza ninguna acción externa. Su único propósito es hacer que el modelo se detenga y piense por escrito.

    # A tool that forces the agent to pause and reflect
    from langchain_core.tools import tool
    
    @tool(parse_docstring=True)
    def think_tool(reflection: str) -> str:
        """Tool for strategic reflection on research progress and decision-making.
    
        Use this tool after each search to analyze results and plan next steps
        systematically. This creates a deliberate pause in the research workflow
        for quality decision-making.
    
        Args:
            reflection: Your detailed reflection on research progress, findings,
                        gaps, and next steps.
    
        Returns:
            Confirmation that reflection was recorded for decision-making.
        """
        return f"Reflection recorded: {reflection}"
    

    think_tool recibe una cadena de reflection y la devuelve precedida por una confirmación. La extensa documentación no es un adorno: con parse_docstring=True, LangChain extrae la descripción de la herramienta y la descripción de los argumentos de dicha documentación, y ese texto es el que lee el modelo al elegir entre las herramientas. El efecto proviene del prompt del sistema, que indica al agente que llame a think_tool después de cada búsqueda. Esto obliga al modelo a indicar qué acaba de aprender, qué aún falta y qué planea hacer a continuación.

    Sin este paso, los agentes tienden a realizar búsquedas sucesivas sin sintetizar nada en el proceso. El equipo de ingeniería de Anthropic ha hecho una observación relacionada: los agentes que pueden revisar y corregir su propio resultado son más fiables, ya que detectan errores antes de que se agraven y pueden corregir su rumbo cuando se desvían. think_tool incorpora un pequeño punto de control de auto-revisión en cada iteración. Es económico, ya que solo requiere los tokens de la reflexión en sí más un viaje adicional, y mantiene al modelo enfocado en su objetivo.

    Supongamos que, después de una búsqueda, el agente reflexiona sobre que ha encontrado tres enfoques de indexación RAG pero aún carece de pruebas de rendimiento para compararlos. La herramienta simplemente reproduce esa reflexión junto con su prefijo de confirmación:

    Reflection recorded: The search results show three approaches to RAG indexing. I still need to find benchmarks comparing them.
    

    Esa cadena se almacena como un ToolMessage, por lo que en la siguiente iteración el sistema lee de nuevo su propio plan. ¿Por qué usar una herramienta en lugar de simplemente pedirle al modelo que “piense paso a paso”? Una llamada a herramienta es un evento discreto y visible en el registro de actividades, se registra en la historia de mensajes en un formato predecible, y el prompt puede solicitarla en un punto específico del bucle.

    Controles de presupuesto en el prompt del sistema

    La segunda adición limita cuántas búsquedas puede realizar el agente:

    Budget rules embedded in the system prompt:
    
    - Simple queries: 2 to 3 search calls maximum
    - Complex queries: up to 5 search calls maximum
    - Always stop after 5 calls if sources are not found
    

    Estos límites se encuentran en el prompt del sistema, no en el código. Se indica al modelo cuántas búsquedas son adecuadas para una consulta simple o compleja y cuándo debe rendirse. Es una elección pragmática: sin contadores ni lógica adicional de grafos, solo instrucciones en las que se confía que el modelo seguirá.

    El compromiso es real. La guía de Google Cloud señala que el estilo iterativo genera mayor latencia en comparación con una sola consulta, y que los resultados dependen en gran medida de la calidad del modelo. Un presupuesto de búsqueda limita directamente esa latencia al restringir la cantidad de rondas que pueden realizarse.

    No obstante, las limitaciones basadas en prompts son flexibles. Un modelo puede malinterpretar la complejidad o simplemente ignorar la instrucción. El estado ya cuenta con un campo tool_call_iterations, por lo que es sencillo establecer un techo estricto que el router haga cumplir. Una configuración sólida utiliza ambos enfoques: los prompts dan forma al comportamiento normal, mientras que el código garantiza un límite máximo. Nuestro artículo sobre bucles agenciales limitados para el uso de herramientas LLM explora la misma idea en TypeScript.

    Recopilación dispersa con un supervisor

    La tercera adición trata todo el bucle ReAct como un trabajador reutilizable. Un agente supervisor divide una pregunta amplia en subpreguntas y ejecuta un subgráfico de investigador separado para cada una, de forma paralela.

    Supervisor receives: "Compare the economic impact of AI on healthcare vs. education"
    
    Supervisor creates two parallel research tasks:
    ├── ReAct Agent 1: Research AI impact on healthcare
    └── ReAct Agent 2: Research AI impact on education
    
    Both agents run their ReAct loops independently.
    Results are gathered and synthesized by the supervisor.
    

    Este es el patrón de dispersión y recolección: se distribuye el trabajo entre trabajadores independientes y luego se recogen y fusionan sus resultados. Cada investigador tiene su propio estado, herramientas y presupuesto, por lo que el historial de búsquedas largo de una subpregunta nunca contamina el contexto de otra. El supervisor solo ve las salidas comprimidas.

    El supervisor es en sí mismo un pequeño gráfico. En lugar de un bucle for fijo sobre las subpreguntas, se basa en el tipo de retorno Command de LangGraph, lo que permite a un nodo actualizar su estado y designar al siguiente nodo en un solo paso:

    Supervisor sub-graph nodes:
    ├── supervisor          (LLM decides what to do next)
    ├── supervisor_tools    (executes supervisor-level tools like ConductResearch)
    ├── red_team            (attacks draft logic to find flaws)
    └── context_pruner      (clears raw notes to manage context size)
    
    The supervisor_tools node uses Command to route dynamically:
      - If research is needed → spawns researcher sub-graphs via ConductResearch tool
      - If critique is needed → routes to red_team node
      - If context is bloated → routes to context_pruner node
      - If research is complete → routes to END
    

    Para el supervisor, el investigador es una caja negra. Él llama a la herramienta ConductResearch, y esa herramienta invoca el sub-gráfico del investigador compilado, el cual ejecuta el bucle completo de ReAct de forma independiente. Alrededor de él se encuentran otros nodos especializados: un nodo red_team que ataca el razonamiento del borrador para encontrar debilidades, y un context_pruner que elimina las notas en bruto cuando el contexto se vuelve demasiado grande.

    El uso de Command en lugar de bordes estáticos brinda flexibilidad al supervisor durante la ejecución. Después de cada paso, puede decidir lanzar más investigadores, enviar un borrador para su revisión, reducir el contexto o finalizar, según el estado actual. La desventaja es que la lógica de enrutamiento pasa a formar parte del código de los nodos, por lo que la estructura del gráfico no resulta tan evidente solo a partir de las declaraciones de bordes; por eso se vuelven más importantes un buen registro y el seguimiento de las operaciones.

    Rastreo de una ejecución completa

    Para ver cómo funcionan las piezas juntas, proporcione al agente una pregunta moderadamente compleja. La fuente etiqueta esta llamada como texto plano; en realidad es Python.

    # Run the agent with a research question
    result = agent.invoke({
        "researcher_messages": [
            HumanMessage(content="What are the main approaches to reducing hallucination in RAG systems?")
        ]
    })
    

    Se requieren dos pequeñas correcciones si lo ejecuta usted mismo. El grafo compilado anteriormente se llama researcher_agent, así que utilice ese nombre (o el agent guardado en punto de control del apartado anterior). Y si utiliza la versión guardada en punto de control, proporcione un thread_id en la configuración como se describió anteriormente.

    Una traza de ejecución ilustrativa se ve así. Se trata de un registro, no de código Python.

    --- Iteration 1 ---
    [Brain] Reasoning: I need to search for approaches to reducing RAG hallucination.
    [Brain] Tool call: search_tool(query="reducing hallucination in RAG systems approaches")
    [Hands] Executing search_tool...
    [Hands] Results: Found 5 relevant articles about RAG hallucination reduction.
    [Router] Last message has tool_calls? No (think_tool was called)
    
    --- Iteration 2 ---
    [Brain] Tool call: think_tool(reflection="The search results mention three main
    approaches: better chunking strategies, re-ranking retrieved documents, and
    adding citation verification. I should search for specific implementations.")
    [Hands] Executing think_tool...
    [Router] tool_calls present -> route to tool_node
    
    --- Iteration 3 ---
    [Brain] Tool call: search_tool(query="citation verification RAG pipeline implementation")
    [Hands] Executing search_tool...
    [Router] tool_calls present -> route to tool_node
    
    --- Iteration 4 ---
    [Brain] Tool call: think_tool(reflection="I now have solid coverage of the three
    main approaches with implementation details. I have enough information to
    provide a comprehensive answer.")
    [Hands] Executing think_tool...
    [Router] tool_calls present -> route to tool_node
    
    --- Iteration 5 ---
    [Brain] No tool calls. Generating final response.
    [Router] No tool_calls -> route to compress_research
    [Compress] Summarizing all findings into structured output.
    

    Lo que muestra la traza:

    1. El agente realizó dos búsquedas y dos llamadas de reflexión, todo dentro del límite establecido en la instrucción inicial.
    2. Cada reflexión resumía lo aprendido y preparaba la siguiente búsqueda, que es exactamente lo que pretende lograr la regla forced-reflection.
  • El agente se detuvo por sí mismo una vez que determinó tener suficiente información: cuatro rondas de llamada a herramientas seguidas de una respuesta final por texto.
  • El enrutador envió la ejecución a tool_node siempre que había llamadas a herramientas y a compress_research cuando no las había.
  • La salida final fue un resumen comprimido en lugar de la conversación original.
  • Lea el registro como un boceto, no como la salida literal del programa. Se denomina a la herramienta de búsqueda search_tool aunque la herramienta real es tavily_search, y la línea del enrutador en la primera iteración indica que no se realizaron llamadas a herramientas a pesar de que se solicitó una búsqueda, lo cual en realidad debería redirigir a tool_node. La estructura general, que alterna búsqueda y reflexión hasta que el modelo responde en texto plano, es lo importante de recordar.

    Llevar el bucle a producción

    El sub-gráfico del investigador constituye una base sólida, pero cinco mejoras lo hacen mucho más fiable en implementaciones reales:

    1. Aplicar el presupuesto en el código. Aumentar tool_call_iterations en cada iteración y hacer que should_continue dirija la ejecución a compress_research una vez se alcance el límite máximo, independientemente de lo que solicite el modelo. Esta es la red de seguridad detrás de los límites flexibles del prompt.
    2. Mostrar el progreso a los usuarios. La función .astream_events() de LangGraph emite eventos para las transiciones de nodos y las llamadas a herramientas, de modo que una interfaz de usuario pueda mostrar mensajes como “Buscando...” o “Analizando resultados...” mientras se ejecuta el bucle, en lugar de un indicador de carga.
  • Recuperarse de errores en las herramientas. Hoy en día, un tiempo de espera de red o un error de límite de velocidad dentro de una herramienta podría hacer que la ejecución fallara. Envuelva cada llamada en try/except y devuelva un ToolMessage que describa el error. De esta manera, el sistema detecta el fallo y puede intentarlo de nuevo con argumentos diferentes o cambiar de enfoque. La documentación de Anthropic recomienda reportar los errores de las herramientas al modelo de esta forma.
  • Mantener activas las ejecuciones de investigación prolongadas. Utilice PostgresSaver para tareas que abarcan muchas iteraciones, de modo que una ejecución interrumpida pueda reanudarse desde donde se detuvo, y cada paso de razonamiento y llamada a herramienta sigan siendo auditables.
  • Añada puntos de interrupción con intervención humana. LangGraph puede detenerse en esos puntos e esperar la aprobación. En investigaciones de alto riesgo, deténgase cada N iteraciones, muestre a una persona lo encontrado y permítale continuar, redirigir o detener al agente.
  • Puntos clave

    • ReAct es un ciclo de pensamiento, acción y observación; es independiente del marco utilizado, y LangGraph simplemente hace que este ciclo sea explícito e inspeccionable.
    • Tres nodos de propósito específico (cerebro, manos, enrutador) junto con un estado respaldado por un reductor son suficientes para tener un agente de investigación funcional.
    • Siempre associe el resultado de cada herramienta con su tool_call_id, y separe el estado interno de lo que expone el sub-gráfico.
    • Una herramienta de reflexión sin acción real es una forma económica y visible de forzar la síntesis entre búsquedas.
    • Los límites de los prompts influyen en el comportamiento, pero solo un límite a nivel de código garantiza la terminación.
  • Una vez que el bucle se convierte en un subgráfico compilado, un supervisor puede distribuirlo de forma paralela y tratar a cada investigador como una caja negra.
  • Lecturas relacionadas