Inicio / Artículos

Muchas manos, un bolígrafo: siete reglas para orquestar agentes de IA

Siete reglas para orquestar agentes de IA: cuando gana un flujo de trabajo sencillo, por qué el orquestador no cuenta con herramientas, los subagentes tipados y las condiciones de detención escritas.

Editor: REACTAPP.TOP10 min de lectura2339 palabras

Fuentes: Many Hands, One Pen: Orchestrating Agents Without Losing the Plot

Imagínese a la Marina de los EE. UU. en 1942, enfrentándose a un problema de coordinación que dice mucho sobre los softwares modernos. Los buques de guerra contaban con radar, sonar, interceptaciones de radio y observadores humanos, y cada informe llegaba al puente simultáneamente y en orden aleatorio. Un capitán que intentaba dirigir una batalla quedaba abrumado por señales sin procesar.

La solución de la Marina fue una sala sin ventanas llamada Centro de Información de Combate. Cada informe de los sensores se marcaba en un único gráfico común utilizando un formato acordado. Los operadores que leían los instrumentos no daban órdenes; su tarea era mantener el gráfico preciso, mientras que un solo oficial decidía el siguiente movimiento del barco. La información llegaba de forma generalizada, las decisiones se reducían a un punto concreto, y el formato estándar de los informes permitía comparar las entradas entre sí.

Los sistemas multiagente siguen redescubriendo esa habitación. La fase en la que “añadir otro agente” era la solución para todo ya pasó, y gran parte de 2026 se dedicó a medir cuánto costó esa solución. El patrón que sobrevive es el que adoptó la Marina: reunirse de forma paralela, informar con un formato disciplinado, decidir en un solo lugar y definir explícitamente qué significa haber terminado.

Un marco útil: la mayoría de los problemas multiagente en realidad no son problemas de agentes. Son problemas de contexto y de flujo de control disfrazados de problemas de agentes. Una ejecución suele fallar porque dos agentes toman decisiones incompatibles que ninguno de los dos puede ver, porque la información pierde su significado al cruzar una frontera, o porque nadie anotó cómo se ve algo cuando está terminado.

Siguen siete prácticas, cada una extraída de fallos en sistemas que ya estaban en uso. Juntas le ayudan a determinar cuándo un flujo de trabajo simple es mejor que un agente, cuándo un orquestador debe pagar por sus tokens, cómo dividir el trabajo según los límites del contexto, por qué el orquestador debe mantenerse alejado de las herramientas, cómo proporcionar a los subagentes un contrato de retorno que pueda validar y cómo finalizar una ejecución de forma intencionada, y no por capricho del modelo.

Usar flujos de trabajo simples por defecto; la autonomía debe ganarse

Qué sale mal. Tomemos un proceso de reembolso con cuatro resultados, cada uno ya cubierto por una regla escrita. Un equipo lo envuelve en un bucle autónomo, con seis herramientas y una cadena de objetivos. De repente, el ticket del martes sigue un camino diferente al del lunes, y cada ejecución cuesta entre diez y cincuenta veces más que el código anterior. La precisión se mantiene constante, porque ninguna parte del flujo es lo suficientemente ambigua como para requerir inteligencia artificial.

La solución. Dibuje primero el diagrama de flujo. Todo lo que pueda planificarse con antelación pertenece al flujo de control normal, por lo que el código sencillo puede manejar cada ramificación que sea posible nombrar. Llame al modelo solo en el paso en el que la entrada sea realmente susceptible de interpretación. Un agente solo debería tener autoridad sobre una ramificación cuando se haya demostrado que dicha ramificación no puede describirse por escrito.

REFUND_LIMIT = 50.00
def llm_step(prompt: str) -> str:
    """Called only where the written rules run out."""
    return model.complete(prompt).strip().lower()
def handle_refund(ticket):
    if ticket.days_since_purchase > 30:
        return deny(ticket, reason="outside window")
    if ticket.amount <= REFUND_LIMIT:
        return approve(ticket)          # a rule, not a judgement call
    intent = llm_step(
        f"Classify as fraud, defect, or remorse:\n{ticket.body}"
    )
    if intent == "fraud":
        return escalate(ticket, queue="risk")
    if intent == "defect":
        return approve(ticket)
    return route_to_human(ticket)

El problema. Es posible llegar a un laberinto de condicionales anidados que codifican en silencio reglas de juicio que en realidad no se pueden aplicar. Cuando se escribe una quinta condición para texto libre, esa es la ambigüedad para la cual existe el modelo.

Omite el orquestador cuando el problema ya se conoce

Qué sale mal. Un pipeline de informes de cuatro pasos queda envuelto en un bucle del orquestador con un límite de quince iteraciones. Cada iteración realiza una llamada de planificación para decidir qué hacer a continuación, y cada llamada llega a la misma respuesta que ya se había determinado: obtener datos, extraer información, resumir y formatear. Eso suma quince llamadas de razonamiento, cada una con unos pocos miles de tokens, para redescubrir una lista que se podría escribir en cinco líneas.

La solución. Utilice una regla rápida antes de codificar cualquier cosa. Cuando se conozca la lista completa de subtareas antes de que comience la ejecución, úselas en un bucle simple con distribución fija y guarde los tokens para otro propósito. La orquestación impulsada por modelos pertenece a aquellos casos en los que la descomposición en sí es desconocida, o debe evolucionar según lo que hayan encontrado los pasos anteriores.

# Before: up to 15 planning calls to rediscover a fixed list
def run_orchestrated(doc):
    state = {"doc": doc}
    for _ in range(15):
        decision = orchestrator.plan(state)      # 1 LLM call per pass
        if decision.action == "finalize":
            break
        state = WORKERS[decision.worker](state)
    return state
# After: 0 planning calls, same four workers, same result
PIPELINE = [fetch, extract, summarize, format_report]
def run_static(doc):
    state = {"doc": doc}
    for step in PIPELINE:                        # 0 LLM calls here
        state = step(state)
    return state

El problema. Este consejo tiene un lado negativo. Un proceso que crea un caso especial nuevo cada mes, o en el que la salida del segundo paso decide realmente si se ejecuta el tercer paso, está realizando una planificación real. Eliminar al planificador solo traslada esa toma de decisiones a algún archivo de configuración del que nadie se ocupará.

Divida a los agentes por contexto, no por título del puesto

Qué sale mal. La versión basada en organigrama del enfoque multiagente parece ordenada: planificador, implementador, probador, revisor, trabajando una función por orden. En la práctica, cada transferencia de tareas implica suposiciones que nadie anotó. El probador verifica comportamientos que el implementador nunca tuvo en mente, y el revisor se opone a una elección hecha deliberadamente por el planificador. Paga cuatro veces más por esta degeneración, y un solo agente que realizara las cuatro tareas habría hecho un trabajo mejor.

La solución. Corte a lo largo de las líneas de contexto. Antes de agregar un segundo agente, haga una pregunta al diseño: ¿podría esta subtarea completarse dentro de una ventana de contexto que no tenga nada en común con la del agente vecino? Si la respuesta es no, ambos pertenecen al mismo agente, y esa es precisamente la razón por la cual el agente que desarrolla una función también debe escribir las pruebas correspondientes.

from dataclasses import dataclass
@dataclass
class Subtask:
    name: str
    needs: set[str]        # facts this subtask must read
    produces: set[str]     # facts this subtask decides
def should_split(a: Subtask, b: Subtask) -> bool:
    """Split only when neither side needs what the other decides."""
    shared = (a.needs & b.produces) | (b.needs & a.produces)
    return not shared
implement = Subtask("implement", {"spec"}, {"api_shape", "error_semantics"})
test = Subtask("test", {"spec", "api_shape", "error_semantics"}, {"cases"})
assert not should_split(implement, test)     # one agent writes both

El problema. Las subtareas verdaderamente aisladas son más raras de lo que implica un organigrama, por lo que la prueba a menudo indicará “un agente”, y esa es la respuesta correcta, no una falta de imaginación. Dividir las tareas es útil en trabajos de amplio alcance, como cuando cinco agentes leen cinco fuentes no relacionadas, ya que lo que decide uno no altera lo que necesita otro.

Deje que el orquestador genere una decisión y nunca invoque una herramienta

Qué sale mal. El orquestador recibe una herramienta de búsqueda para emergencias. Pronto deja de asignar tareas y comienza a responder a las subtareas por su cuenta. La salida bruta de la herramienta se acumula en su ventana, por lo que su planificación empeora justo cuando la duración del proceso aumenta y la planificación es lo más importante. La trazabilidad también se ve afectada, ya que no queda claro qué agente generó qué hallazgo.

La solución. Cualquier plan que no deba ejecutarse no debe obtener herramientas, por lo que se le dan al coordinador cero herramientas. Lo único que emite es una decisión compacta y tipada: delegar, replanificar o finalizar, junto con el trabajador a utilizar y el texto de la tarea. Un bucle circundante lee esa decisión, ejecuta al trabajador designado y almacena el resultado en el estado compartido, listo para la próxima decisión.

from typing import Literal
from pydantic import BaseModel
class OrchestratorDecision(BaseModel):
    next_action: Literal["delegate", "replan", "finalize"]
    target_worker: str | None = None
    task_description: str | None = None
    reasoning: str
def orchestrator_step(state):
    d = decide(state)                    # model has zero tools attached
    if d.next_action == "finalize":
        return finalize(state, d.reasoning)
    if d.next_action == "replan":
        return state.reset_plan(d.reasoning)
    worker = WORKERS[d.target_worker]    # workers own every tool
    result = worker.run(d.task_description)
    return state.record(d.target_worker, result)

El problema. Sin herramientas, el orquestador solo puede ver lo que los trabajadores le envían. Un trabajador que devuelve un resumen escueto deja al orquestador sin información para planificar. En lugar de ofrecer acceso en tiempo real a herramientas, proporcione una ventana de solo lectura al estado compartido, y asegúrese de que los datos devueltos por los trabajadores estén lo suficientemente estructurados como para poder planificar a partir de ellos.

Exigir que cada subagente cumpla con un contrato de retorno predefinido

Qué sale mal. Imagine un subagente de investigación que devuelve un diario de 2,500 palabras con todo lo que intentó, incluidos los caminos fallidos. Si hay siete trabajadores, alrededor de 20,000 tokens llegan a la ventana del agente principal antes de que comience cualquier síntesis. La delegación debía proteger esa ventana, pero en realidad la exploración se paga dos veces, y el agente principal sigue las instrucciones de forma aún peor justo cuando más se necesita.

La solución. Considere el retorno como un límite de API, con un esquema de respuesta en cada subagente. Su mensaje de cierre es un JSON que contiene campos con nombres: hallazgos, fuentes, preguntas sin respuesta y un estado de finalización. Revíselo al llegar y rechace cualquier dato mal formado o demasiado largo para que se intente nuevamente, de modo que nunca llegue al agente padre.

from pydantic import BaseModel, Field, ValidationError
class SubagentResult(BaseModel):
    findings: list[str] = Field(max_length=5)   # hard cap, one sentence each
    sources: list[str]
    open_questions: list[str] = []
    completion_status: str                      # complete | partial | blocked
def parse_or_retry(worker, task, attempts=2):
    for _ in range(attempts):
        raw = worker.run(task, response_format=SubagentResult)
        try:
            return SubagentResult.model_validate_json(raw)
        except ValidationError as err:
            task = f"{task}\n\nRejected: {err}\nReturn only JSON in the schema."
    raise RuntimeError(f"{worker.name} returned no valid result")

El problema. Los esquemas establecen un mínimo y nada más. Un trabajador aún puede incluir un párrafo de 400 palabras en una sola cadena de resultados y entregar datos redundantes presentados como JSON. Es necesario limitar tanto la cantidad de elementos como la longitud de cada campo, y que el texto de rechazo indique el campo que superó el límite, para que el intento de reenvío sepa qué corregir.

Paralelizar las lecturas y canalizar las escrituras a través de un único agente

Qué sale mal. Dos agentes trabajan en la misma función al mismo tiempo y cada uno informa éxito. Uno dibujó un fondo al estilo de Super Mario mientras que el otro dibujó un sprite de Flappy Bird, y ambos pasan las pruebas escritas para su propia versión del diseño. La fusión no puede resolver las opciones que ninguno de los agentes conocía, por lo que se descarta un resultado, o ambos.

La solución. La lectura puede realizarse en paralelo; la escritura, no. Limita cada trabajador a una lista de herramientas de solo lectura, lo que permite que la fase de recopilación sea tan amplia como sea necesario, y nunca le des a ningún trabajador herramientas de escritura. El resultado final se genera entonces en una sola llamada sin condiciones que recibe los resultados de todos los trabajadores al mismo tiempo.

import asyncio
READ_TOOLS = ["search_repo", "read_file", "fetch_docs"]
async def gather_context(subtasks):
    workers = [Agent(name=t.name, tools=READ_TOOLS) for t in subtasks]
    return await asyncio.gather(
        *(w.run(t.prompt) for w, t in zip(workers, subtasks))
    )
async def build_feature(spec, subtasks):
    findings = await gather_context(subtasks)     # wide, parallel, read-only
    writer = Agent(name="writer", tools=["write_file", "apply_patch"])
    return await writer.run(spec, context=findings)   # one writer, one pass

El problema. Ese único escritor constituye un cuello de botella y debe manejar los resultados de todos los trabajadores al mismo tiempo, por lo que te encontrarás con el límite de tokens mucho antes de llegar a un límite lógico. Si el artefacto realmente se divide de manera limpia, como ocurre con tres archivos que no comparten ninguna interfaz, asigna a cada archivo su propio escritor y mantén la misma disciplina dentro de cada uno.

Escribe las condiciones de terminación

Qué sale mal. Una ejecución finaliza, no aparece ningún error y el trabajo queda incompleto. Un análisis de 1,600 trazas de múltiples agentes en siete frameworks determinó que el 12.4% de los fallos se debían a que los agentes no conocían sus condiciones de parada, y otro 6.2% se detenía prematuramente. Por lo tanto, casi uno de cada cinco fallos se manifiesta como una salida ordenada que incluye un resultado a medio terminar.

La solución. Convierta la línea de meta en algo concreto en lugar de en una sensación. Asigne a cada tarea una lista de verificación explícita para su finalización, y coloque una marca de aprobación, basada en el objetivo original, en el único camino hacia el estado final. Según esa misma investigación, la puerta de verificación aumentó el éxito de ChatDev en un 9,4%, mientras que la revisión de los objetivos de alto nivel lo elevó en un 15,6%.

CHECKLIST = [
    "every requested section exists",
    "each claim cites a retrieved source",
    "open questions are listed, or explicitly none",
]
def verify_complete(objective, checklist, output) -> bool:
    for item in checklist:
        verdict = judge(f"Objective: {objective}\nCheck: {item}\n\n{output}")
        if not verdict.passed:
            log.info("termination blocked by: %s", item)
            return False
    return judge(f"Does this satisfy the objective?\n{objective}\n\n{output}").passed
def finish(state):
    if verify_complete(state.objective, CHECKLIST, state.draft):
        return state.done()
    return state.keep_working(reason="checklist not satisfied")

El problema. Si dejas que el modelo productor evalúe su propia lista de verificación, aprobará su trabajo sin problemas. Prefiere elementos mecánicos siempre que sea posible, como la existencia de un archivo, que un campo esté completado o que una cita se haya resuelto. Una barrera que nunca se abre constituye también un tipo de fallo, por lo que limita los intentos y genera un error una vez se agotan.

Por dónde empezar

Si solo puedes cambiar una cosa, elige la más económica: define por escrito qué significa que algo esté terminado. Una lista de verificación más un paso de verificación antes de llegar al estado final toma solo una tarde, funciona tanto para un agente como para todo un grupo, y elimina toda una categoría de fallos silenciosos. Haz que sea la tarea de esta semana.

A continuación viene la eliminación. Examine su orquestador y pruebe si sus opciones se reducen a una lista fija y una función de agregación. Si es así, úselo; el gasto en tokens y la latencia disminuyen y los resultados se mantienen estables. Dado que es fácil deshacerlo, constituye una primera eliminación segura.

Guarde la maquinaria más compleja para cuando sea realmente necesario. Los contratos de retorno por teclado son rentables a partir de unos cinco trabajadores, y la lista de permisos de solo lectura es importante cuando dos agentes escriben realmente simultáneamente. La excepción es el orquestador sin herramientas: casi gratuito al inicio, pero difícil de añadir posteriormente.

Las configuraciones que seguirán funcionando en 2026 no cuentan con la mayor cantidad de agentes. En ellas, las conexiones entre agentes son la parte más pequeña y explícita del diseño. Menos participantes, contratos más firmes, un solo instrumento.

Leer más

  • El ajuste fino ahora es económico, pero los agentes siguen necesitando un mecanismo para los pesos — Cómo convertir las lecciones almacenadas de un agente en pesos mediante auto-distilación dentro de la política, cuatro criterios de promoción, y por qué repetir el ciclo tres veces lo daña.