Por qué dejé de usar un agente y en su lugar construí una canalización de múltiples agentes

Desarrollamos una aplicación de texto a SQL. Era una arquitectura simple de un solo agente que toma una consulta de usuario en lenguaje natural y después de pasar el punto de control de validaciones genera una consulta SQL para ella. Después de completar el desarrollo, comenzamos a probarlo y nos dimos cuenta de que la arquitectura de un agente no era suficiente para nuestra aplicación. Funcionó para consultas simples donde solo se requerían 1 o 2 operaciones, pero comenzó a fallar cuando el usuario comenzó a realizar consultas complejas, incluidas múltiples operaciones y escaneos de esquemas.

Después de realizar pruebas exhaustivas de la aplicación, nos dimos cuenta de que un solo agente no podía realizar todas las tareas. El agente intentaba analizar la intención, asignarla a un esquema, generar un SQL válido y validar su propia salida, todo de una sola vez. En el tercer intento, el contexto estaba tan saturado de intentos fallidos y autocorrecciones que el agente empezó a contradecirse.

Después de eso, decidimos revisar nuestra arquitectura e implementamos un sistema multiagente con agentes especializados para cada tarea en lugar de depender de un solo agente para hacer todo.

En este artículo

Por qué los agentes individuales luchan con tareas complejas Qué significa realmente la arquitectura multiagente en la práctica Cómo diseñar agentes especializados y conectarlos entre sí Orquestación y gestión de estado con LangGraph Cómo se implementan realmente los nodos de agente El circuito de retroalimentación y la lógica de reintento Qué se interrumpe en la producción Cuándo no debería usar esto Conclusión

Por qué un solo agente tiene dificultades

Cuando se comienza a construir con LLM, se supone que si el modelo es lo suficientemente capaz, un buen mensaje puede hacerlo todo. Esto se puede lograr para tareas más simples, pero a medida que la tarea crece en complejidad, no solo le pide al modelo que piense más, sino que le pide que mantenga múltiples modelos mentales en competencia en una única ventana de contexto simultáneamente.

Tomemos como ejemplo el problema de texto a SQL. Para convertir una pregunta en lenguaje natural en una consulta SQL correcta, debe:

Analizar lo que el usuario realmente quiere (descomposición de intenciones) Mapear esas intenciones con el esquema real (conocimiento del esquema) Escribir SQL sintácticamente correcto (generación de consultas) Verificar el SQL de salida (validación)

La descomposición de intenciones es una tarea de PNL y requiere que el agente comprenda la consulta del usuario y analice su significado oculto. El mapeo de esquemas requiere una base fáctica precisa de la base de datos para mapear la intención con los esquemas de tabla correctos. La generación de consultas exige un esquema completo y conocimiento sintáctico. La validación necesita una lente crítica y contradictoria para juzgar el SQL generado.

Un solo agente que intente hacer los cuatro hará cada uno de ellos con mediocridad, pero ninguno bien. Las fallas son muy básicas, como una condición de unión que es casi correcta pero incorrecta para el contexto del usuario, un filtro que ignora una restricción de tiempo o un agregado que parece correcto hasta que verifica los casos extremos y falla.

Cada intento fallido queda en la memoria. El modelo ve lo que intentó antes y hace ajustes cada vez más pequeños, en lugar de dar un paso atrás y repensar. Después de tres intentos, no obtendrás un nuevo intento, sino una revisión de un primer borrador incorrecto.

Lo que realmente significa multiagente

Un sistema multiagente es un conjunto de agentes, cada uno con una responsabilidad específica, coordinados por un orquestador que gestiona el flujo entre ellos. En lugar de que un agente haga todo con resultados mediocres, tiene varios agentes, cada uno de los cuales se especializa en una cosa.

En la práctica, existen dos formas de crear un sistema multiagente. El primero es secuencial, donde los agentes se ejecutan uno tras otro, y cada uno recibe la salida del anterior como entrada. El segundo es paralelo, donde los agentes ejecutan simultáneamente subtareas independientes y sus resultados son fusionados por un agregador.

La mayoría de los sistemas reales utilizan ambos. Nuestra canalización de texto a SQL es secuencial por diseño: no puede asignar el esquema hasta que haya analizado la intención y no puede validarlo hasta que tenga una consulta. Pero dentro de esa secuencia, puede ejecutar búsquedas paralelas en diferentes secciones del esquema simultáneamente.

Una cosa que hay que entender es que el propio orquestador puede ser un agente LLM o puede ser una lógica de enrutamiento determinista. Para una tubería donde el flujo está bien definido y es predecible, el enrutamiento determinista es casi siempre la mejor opción. No necesita un LLM para decidir "ejecutar el mapeo de esquemas después del análisis de intenciones", esa decisión nunca cambia.

Diseñando a los agentes

diagrama de arquitectura multiagente de texto a SQL

Para un sistema de texto a SQL, el desglose del agente se asigna directamente a los modos de falla que vimos con un solo agente. Le hicimos una pregunta para "extraer a los principales clientes en cada categoría y comparar su tendencia de compra con el promedio de la categoría durante los últimos 12 meses" y nos ayudó a evaluar a cada agente en nuestro sistema.

El agente analizador de intenciones toma la pregunta sin procesar del usuario y la descompone en intenciones discretas. La pregunta sobre los principales clientes contiene al menos tres intenciones: clasificar a los clientes por categoría, calcular su tendencia de compra y comparar esa tendencia con una línea base a nivel de categoría. Un solo agente que hace esto en línea tiende a descomponerse parcialmente y luego generar SQL para una interpretación incompleta. El único trabajo de este agente es descomponer la consulta del usuario en diferentes intenciones.

El Agente de esquema recibe las intenciones descompuestas y las asigna a nombres de tablas, nombres de columnas, condiciones de unión y tipos de datos reales del esquema de su base de datos. Aquí es donde falla un sistema de un solo agente porque sin una base de esquema explícita, el agente inventa nombres de columnas que parecen plausibles pero no existen. Por ejemplo, customer_purchase_value parece una columna real en la base de datos de transacciones del cliente, pero esta columna no existe en nuestra base de datos. Mantener esto como un agente separado con el esquema inyectado directamente en su contexto resuelve el problema limpiamente.

El agente Query Builder toma las intenciones asignadas al esquema y genera SQL. Para cuando este agente se ejecuta, la ambigüedad ya está resuelta y está realizando una tarea de generación enfocada, no de interpretación. La diferencia en la calidad del resultado en comparación con un solo agente que hace todo esto al mismo tiempo es significativa.

El Agente Crítico es el que la mayoría de los equipos omiten y luego se arrepienten. El generador de consultas producirá una consulta sintácticamente válida, pero eso es diferente de ser semánticamente correcta. El agente crítico recibe la consulta generada y la evalúa de forma independiente con respecto a las intenciones originales: ¿esto realmente responde a lo que se preguntó? ¿Hay casos extremos que faltan? ¿El filtro de ventana de tiempo coincide con lo que especificó el usuario? No se puede hacer esto de manera creíble en el mismo contexto que la generación porque el modelo se ancla en lo que acaba de escribir y racionalizará su propia producción en lugar de cuestionarla. Un agente independiente con un contexto nuevo y un mensaje de sistema explícitamente contradictorio detecta cosas que el constructor nunca detectaría.

El Agente de respuesta formatea la consulta final para el usuario, agrega una explicación en lenguaje sencillo de lo que hace y muestra cualquier suposición hecha durante la generación.

Cada uno de estos agentes tiene un mensaje de sistema diferente, una función diferente y, lo más importante, una nueva ventana de contexto.

Cableado junto con LangGraph

LangGraph encaja bien aquí porque le brinda control explícito sobre el estado y los bordes. Usted define el gráfico, los nodos y exactamente cómo fluyen los datos entre ellos. Nada se abstrae de ti.

El objeto de estado recorre todo el oleoducto. Cada agente lee y escribe en él:

al escribir import TypedDict, List, clase opcional PipelineState(TypedDict): user_query: str intents: Opcional[List[dict]] esquema_mapping: Opcional[dict] generate_query: Opcional[str] critique: Opcional[dict] final_response: Opcional[str] retry_count: int Failure_source: Opcional[str] # rastrea qué agente causó una falla

Es muy importante incluir el campo Failure_source en el estado del agente. Cuando algo sale mal en producción, necesita saber si el crítico rechaza porque el generador de consultas falló o porque el analizador de intenciones envió basura en sentido posterior.

La estructura del gráfico en sí es muy fácil y directa de implementar (lógica simplificada para facilitar la lectura):

desde langgraph.graph import StateGraph, END def debería_retry(state: PipelineState) -> str: critique = state.get("critique", {}) if critique.get("passed"): return "respond" if state["retry_count"] >= 3: return "respond" # mejor intento de superficie, no hacer un bucle para siempre return "rebuild" graph = StateGraph(PipelineState) Graph.add_node("parse_intent", intent_parser_node) Graph.add_node("map_schema", esquema_agent_node) Graph.add_node("build_query", query_builder_node) Graph.add_node("critique", critic_agent_node) Graph.add_node("respond", respuesta_agent_node) Graph.set_entry_point("parse_intent") Graph.add_edge("parse_intent", "map_schema") Graph.add_edge("map_schema", "build_query") Graph.add_edge("build_query", "critique") Graph.add_conditional_edges("critique", debería_retry, { "rebuild": "build_query", "respond": "respond" }) graph.add_edge("responder", FIN) aplicación = graph.compile()

El límite máximo de retry_count no es negociable aquí. Sin él, un canal que sigue fracasando en la crítica se cerrará indefinidamente. Tres reintentos significan cuatro intentos en total, y si el sistema no ha producido una consulta aceptable para entonces, muestre el mejor intento y márquelo para su revisión en lugar de quemar tokens en espiral.

Cómo se implementan realmente los nodos de agente

La definición del gráfico anterior muestra la estructura, pero el trabajo real ocurre dentro de cada uno de los nodos.

Cada nodo es una función que toma el estado actual, hace algo con él y devuelve los campos que desea actualizar. Así es como se ve en la práctica el analizador de intenciones y el agente crítico (lógica simplificada para facilitar la lectura):

de langchain_core.messages importar SystemMessage, HumanMessage de langchain_google_vertexai importar ChatVertexAI importar json llm = ChatVertexAI( model="gemini-2.5-flash", temperatura=0, max_tokens=None ) def intent_parser_node(state: PipelineState) -> dict: system = SystemMessage(content="""Eres una intención especialista en descomposición. Su único trabajo es dividir una pregunta en lenguaje natural en intenciones analíticas discretas. Devolver una lista JSON. Cada intención debe tener: 'descripción', 'métrica', 'filtros', 'intervalo de tiempo'. No haga referencia a ningún esquema de base de datos.""") human = HumanMessage(content=state["user_query"]) respuesta = llm.invoke([system, human]) intents. json.loads(response.content) return {"intents": intents} def critic_agent_node(state: PipelineState) -> dict: system = SystemMessage(content="""Usted es un crítico de consultas SQL. Su trabajo es encontrar problemas. Dadas las intenciones originales y la consulta SQL generada, evalúe: 1. ¿La consulta responde a todas las intenciones, o solo a algunas de ellas? 2. ¿Faltan filtros, agregaciones incorrectas, o uniones incorrectas? 3. ¿El manejo del rango de tiempo coincide con lo solicitado? Devuelve JSON con: 'aprobado' (bool), 'problemas' (lista de cadenas), 'gravedad' (baja/media/alta).""") human = HumanMessage(content=f""" Intentos originales: {json.dumps(state['intents'], indent=2)} Consulta generada: {state['generated_query']} """) respuesta = llm.invoke([sistema, humano]) crítica = json.loads(response.content) return { "critica": crítica, "retry_count": estado["retry_count"] + 1, "failure_source": "query_builder" si no crítica["aprobado"] else Ninguno}

Algunas cosas a tener en cuenta: el mensaje del sistema del analizador de intenciones dice explícitamente no generar SQL y no hacer referencia a ningún esquema de base de datos. Esto es intencional porque sin esas restricciones el modelo se desviará y comenzará a hacer suposiciones de esquema incluso cuando no le haya proporcionado información del esquema.

El bucle de retroalimentación y la lógica de reintento

El borde condicional del gráfico dirige la canalización en función de lo que devuelve el crítico. Pero cuando el generador de consultas recibe un reintento, necesita saber por qué se le solicita que reconstruya; simplemente regresar a él con el mismo estado no es suficiente.

Cuando el crítico falla en una consulta, el constructor en la siguiente pasada necesita acceder a la crítica (una de las claves de estado). Entonces el Estado lo lleva adelante:

def query_builder_node(state: PipelineState) -> dict: anterior_critique = state.get("critique") system_content = """Usted es un especialista en generación de consultas SQL. Dadas las intenciones asignadas al esquema, genere una única consulta SQL correcta. Devuelva solo el SQL. Sin explicación.""" # En un reintento, adjunte los comentarios del crítico si la crítica_anterior y no la crítica_anterior.get("aprobado"): issues = "n".join(previous_critique.get("problemas",[])) system_content += f"nnEl intento anterior fue rechazado. Problemas encontrados:n{problemas}nAborde estos específicamente." system = SystemMessage(content=system_content) human = HumanMessage(content=f""" Intentos: {json.dumps(state['intents'], indent=2)} Mapeo de esquema: {json.dumps(state['schema_mapping'], indent=2)} """) respuesta = llm.invoke([system, human]) return {"generated_query": respuesta.content.strip()}

Ésta es la diferencia entre un ciclo de reintento que realmente mejora la consulta y uno que simplemente ejecuta la misma generación una y otra vez.

Lo que se rompe en la producción

Sangrado de contexto entre agentes. LangGraph pasa el estado completo entre nodos. Cuando se ejecuta el crítico, tiene visibilidad de todo: consulta original, intenciones, mapeo de esquema y SQL generado. Eso es intencional, pero también significa que una descomposición de intención sutilmente incorrecta al principio del proceso se propaga silenciosamente hacia abajo. El crítico está evaluando la consulta frente a las intenciones, no reevaluando si las intenciones mismas eran correctas o no. Agregue un paso de validación ligero después del análisis de intenciones para verificar que las intenciones descompuestas realmente cubran la pregunta original antes de seguir adelante.

Los agentes de esquemas alucinan con esquemas grandes. Si su base de datos tiene 200 tablas, no es práctico inyectar todo el esquema en el contexto. El agente de esquema necesita incrustaciones recuperadas de las tablas relevantes en función de las intenciones analizadas. Una búsqueda vectorial sobre descripciones de tablas y columnas, que devuelva las 15 a 20 tablas más relevantes, mantendrá el contexto manejable.

El bucle de reintento oculta el análisis con malas intenciones. Cuando el crítico rechaza una consulta y regresa al constructor, parece que el sistema está haciendo lo correcto, pero si la causa raíz es una descomposición con malas intenciones, el constructor no puede solucionarlo. Está funcionando con una especificación incorrecta y el crítico seguirá rechazándolo por las mismas razones subyacentes hasta que alcance el límite de reintentos.

Los costos de los tokens se acumulan rápidamente. Cinco agentes, cada uno con un mensaje del sistema y un contexto, ejecutándose secuencialmente, agregan un ciclo de reintento y ahora ha realizado seis llamadas LLM por solicitud como mínimo, varias de ellas con un contexto sustancial. Perfile el uso de su token por nodo antes de implementarlo en cualquier volumen, especialmente el crítico porque recibe tanto las intenciones como la consulta completa, lo que suma y aumenta la longitud del contexto. Usar modelos más pequeños para tareas de validación donde el razonamiento no necesita ser sofisticado es una optimización razonable.

El análisis de la salida estructurada falla silenciosamente. Varios nodos anteriores devuelven JSON que se analiza directamente. Si el LLM devuelve JSON con formato incorrecto, toda la canalización falla en el paso de análisis. Envuelva cada llamada json.loads() en el manejo de errores y defina un respaldo.

Cuando no deberías hacer esto

Una arquitectura multiagente agrega una sobrecarga real, más puntos de falla, una depuración más difícil, costos de token más altos y una administración del estado más compleja. Sólo debe usarse cuando un solo agente no puede hacer bien el trabajo.

Si sus consultas SQL son simples y su esquema es pequeño, un agente único bien activado con un ciclo de reintento superará a esta canalización en todas las métricas importantes: latencia, costo y mantenibilidad. No cree un sistema de cinco agentes para responder preguntas que se asignan claramente a dos o tres tablas.

Primero cree el agente único y ejecútelo en consultas reales hasta que falle de una manera que un mensaje mejor no solucionará. Ese error le indica exactamente dónde introducir la especialización y podrá crear la versión multiagente con una cabeza mucho más clara.

Conclusión

El problema de texto a SQL es una lente útil para una arquitectura de múltiples agentes porque los modos de falla de un solo agente son muy concretos. Puede señalar la consulta exacta en la que se equivocó y explicar exactamente que falló porque el modelo intentaba simultáneamente comprender la pregunta, conocer el esquema, escribir SQL y evaluar su propio resultado.

Dividir eso en agentes especializados no hace que ningún agente individual sea más inteligente. Hace que el sistema sea más confiable al limitar de qué es responsable cada agente. Una ventana de contexto centrada en una tarea produce mejores resultados que la misma ventana de contexto extendida a lo largo de cuatro. LangGraph le brinda el ecosistema para crear esto sin ocultar cómo funciona. Cuando algo se rompe, puedes rastrear exactamente qué nodo falló y qué estado recibió. Esa transparencia importa más de lo que parece cuando estás depurando un proceso de producción a medianoche.

La arquitectura descrita aquí es un punto de partida, no un producto terminado. Las implementaciones reales necesitarán recuperación de esquemas para bases de datos grandes, manejo de resultados mejor estructurado, herramientas de observabilidad y una cuidadosa presupuestación de tokens. Pero la estructura: agentes especializados, estado explícito, enrutamiento condicional y un bucle crítico es la base adecuada sobre la que construir.