La lección anterior terminó con una pregunta que nuestro flujo de un solo paso no puede responder: "¿mi proyecto Atlas supera el límite de tareas de mi plan?". Hacen falta dos consultas a fuentes distintas — la documentación para el límite del plan, la API para las tareas actuales — y una comparación que solo tiene sentido cuando ambos datos están sobre la mesa. Ningún guion fijo escrito de antemano resuelve esto con elegancia, porque qué consultar y en qué orden depende de la pregunta. La solución es dejar que el modelo dirija: llamarlo en bucle, dándole en cada vuelta los resultados de lo que pidió, hasta que decida que ya puede responder. Ese bucle — razonar, elegir herramienta, ejecutar, observar, repetir — es lo que convierte un LLM en un agente, y es la palabra más sobrecargada del sector explicada en veinte líneas de Python. En esta lección las escribimos: agente.py, con sus condiciones de salida, sus tres herramientas (una de las cuales será el RAG del módulo 4, reciclado como herramienta), su primera acción de escritura con confirmación humana, y sus trazas. Y cerraremos con la pregunta más importante y menos formulada: cuándo no usar un agente.

Contenido

  1. Qué convierte un LLM en agente: el bucle razonar-actuar-observar
  2. El bucle en Python: agente.py y sus dos condiciones de salida
  3. Las tres herramientas de DocuBot (y el RAG convertido en herramienta)
  4. Acciones de escritura: crear_ticket_soporte y la confirmación humana
  5. Trazas: ver lo que el agente hace paso a paso
  6. Cuándo NO usar un agente: pipeline fijo vs bucle agéntico

Qué convierte un LLM en agente: el bucle razonar-actuar-observar

En 05-01 el flujo era una partitura: primera llamada, ejecutar herramienta, segunda llamada, fin. Funcionaba porque el guion lo escribimos nosotros. Un agente invierte el reparto de papeles: nosotros escribimos el bucle y el modelo escribe el guion — decide en cada iteración si necesita otra herramienta, cuál, con qué argumentos, y cuándo ha terminado. La estructura es siempre la misma:

flowchart TD
    P[Petición del usuario] --> L["Llamada al LLM<br/>(historial completo + herramientas)"]
    L --> D{stop_reason}
    D -- "end_turn:<br/>el modelo da por resuelta la tarea" --> R[Respuesta final al usuario]
    D -- "tool_use:<br/>el modelo pide una o más herramientas" --> E["Tu código ejecuta<br/>(validando, con mínimo privilegio)"]
    E --> T["tool_result(s) añadidos al historial"]
    T --> C{"¿iteraciones < máximo?"}
    C -- "sí" --> L
    C -- "no" --> S["Salida de seguridad:<br/>informar del límite alcanzado"]

Cada vuelta del bucle es un ciclo razonar → actuar → observar: el modelo razona sobre el historial (que crece con cada resultado), actúa pidiendo una herramienta, y observa el tool_result en la siguiente llamada. La memoria del agente entre pasos es simplemente la lista messages — la misma mecánica de conversación de 03-03, solo que aquí los turnos los genera el propio proceso en vez de un humano.

Dos consecuencias de este diseño que conviene interiorizar antes de escribir código:

  • El agente es no determinista por partida doble: en el texto que genera (eso ya lo sabíamos) y en el camino que recorre. Dos ejecuciones de la misma pregunta pueden usar las herramientas en distinto orden, o en distinto número. Esto es su fuerza (flexibilidad) y su factura (previsibilidad), y da forma a toda la sección final.
  • Cada iteración es una llamada completa a la API con el historial acumulado: el coste y la latencia crecen con cada vuelta (y el historial engorda con cada tool_result). Un agente que da cinco vueltas cuesta, como mínimo, cinco llamadas — recuérdalo cuando revisitemos la proyección de costes de 03-04.

El bucle en Python: agente.py y sus dos condiciones de salida

El bucle completo. Es sorprendentemente corto — la robustez la heredamos, como siempre, de las capas anteriores (llamar_con_reintentos de 03-02, ejecutar_herramienta de 05-01):

# agente.py — el bucle agéntico de DocuBot
import json
from cliente import MODELO, extraer_texto
from reintentos import llamar_con_reintentos
from prompts import SYSTEM_DOCUBOT_AGENTE
from herramientas import HERRAMIENTAS, ejecutar_herramienta
from trazas import registrar_paso

MAX_ITERACIONES = 8   # cinturón de seguridad: nunca en bucle infinito

MENSAJE_LIMITE = (
    "No he podido completar la petición en el número máximo de pasos permitido. "
    "Prueba a formularla de forma más concreta o divídela en partes."
)

def bucle_agente(pregunta: str,
                 herramientas: list[dict] = HERRAMIENTAS,
                 max_iteraciones: int = MAX_ITERACIONES) -> str:
    """Razonar -> actuar -> observar, hasta que el modelo termine o se agote el límite."""
    messages = [{"role": "user", "content": pregunta}]

    for iteracion in range(1, max_iteraciones + 1):
        # RAZONAR: el modelo ve todo el historial y las herramientas
        respuesta = llamar_con_reintentos(
            model=MODELO,
            max_tokens=1024,
            system=SYSTEM_DOCUBOT_AGENTE,
            tools=herramientas,
            messages=messages,
        )

        # Condición de salida natural: el modelo da la tarea por terminada
        if respuesta.stop_reason != "tool_use":
            registrar_paso(iteracion, "respuesta_final", None)
            return extraer_texto(respuesta)

        # ACTUAR: ejecutar TODOS los tool_use del turno (puede haber varios)
        messages.append({"role": "assistant", "content": respuesta.content})
        resultados = []
        for bloque in respuesta.content:
            if bloque.type != "tool_use":
                continue                      # los bloques de texto no se ejecutan
            registrar_paso(iteracion, bloque.name, bloque.input)
            try:
                salida = ejecutar_herramienta(bloque.name, bloque.input)
                resultados.append({
                    "type": "tool_result",
                    "tool_use_id": bloque.id,
                    "content": json.dumps(salida, ensure_ascii=False),
                })
            except Exception as e:
                resultados.append({
                    "type": "tool_result",
                    "tool_use_id": bloque.id,
                    "content": str(e),
                    "is_error": True,        # el modelo decidirá cómo reaccionar
                })

        # OBSERVAR: los resultados entran al historial para la siguiente vuelta
        messages.append({"role": "user", "content": resultados})

    # Condición de salida de emergencia: el cinturón de seguridad
    registrar_paso(max_iteraciones, "limite_alcanzado", None)
    return MENSAJE_LIMITE

Las dos condiciones de salida son la anatomía del bucle y merecen nombre propio:

Salida Mecanismo Qué significa
Natural stop_reason != "tool_use" El modelo, tras observar los resultados, decidió que ya puede responder. Es el final feliz y el habitual.
De emergencia max_iteraciones agotadas Algo va mal: el modelo entra en círculos (reintenta una herramienta que falla, o "duda" entre dos), y sin límite el bucle giraría para siempre, quemando dinero en cada vuelta.

El valor de MAX_ITERACIONES se elige mirando tus casos reales: para DocuBot, con tres herramientas y preguntas que rara vez necesitan más de 2-3 pasos, 8 da holgura sin permitir desastres. Es el mismo espíritu que el circuit breaker de 03-02 — un mecanismo que ojalá nunca salte, y que no es opcional.

Falta el sistema. SYSTEM_DOCUBOT_AGENTE extiende el de 05-01 con las reglas de cuándo usar qué, y recupera nuestras dos frases sagradas:

# prompts.py (añadido en 05-02)
SYSTEM_DOCUBOT_AGENTE = """Eres DocuBot, el asistente interno de Nubelia. Solo respondes sobre Nubelia.

Herramientas y cuándo usarlas:
- Preguntas sobre cómo funciona Nubelia (planes, permisos, integraciones, API...):
  usa SIEMPRE buscar_documentacion antes de responder, y cita las fuentes.
- Datos en tiempo real de proyectos del usuario: usa consultar_proyecto.
- Crear un ticket de soporte: usa crear_ticket_soporte SOLO si el usuario lo pide
  explícitamente. Nunca lo crees por iniciativa propia.

Reglas:
- Si buscar_documentacion no devuelve nada relevante, responde exactamente:
  "No encuentro esa información en la documentación disponible."
- Nunca inventes datos de proyectos ni contenidos de la documentación.
- Responde en el idioma del usuario, con tono profesional y conciso."""

Las tres herramientas de DocuBot (y el RAG convertido en herramienta)

El agente estrena arsenal: a consultar_proyecto (05-01) se suman dos herramientas nuevas. La primera es un momento importante del curso: el pipeline RAG del módulo 4 se convierte en herramienta. No reescribimos nada — buscar_documentacion es un envoltorio fino sobre recuperar() y construir_documentacion() de rag.py:

# herramientas.py (ampliado en 05-02)
from rag import recuperar, construir_documentacion, UMBRAL_RELEVANCIA
import tickets

def buscar_documentacion(consulta: str) -> dict:
    """RAG como herramienta: recuperar chunks y devolverlos con sus fuentes."""
    chunks = recuperar(consulta)                     # k=4, colección docs_nubelia
    if not chunks or chunks[0]["similitud"] < UMBRAL_RELEVANCIA:
        return {"resultado": "sin_informacion_relevante", "fragmentos": ""}
    return {
        "resultado": "ok",
        "fragmentos": construir_documentacion(chunks),   # numerados y con fuente
        "fuentes": sorted({c["url"] for c in chunks}),
    }

Fíjate en el cambio de perspectiva respecto a 04-04: allí, responder_con_rag() era el dueño del flujo (recuperar → aumentar → generar). Aquí la generación la orquesta el bucle, así que la herramienta solo recupera y aumenta — devuelve los fragmentos y deja que el agente decida qué hacer con ellos (quizá necesite además consultar la API antes de redactar). El guardarraíl del umbral sigue en pie: si nada es relevante, la herramienta lo dice con una señal inequívoca (sin_informacion_relevante), y el sistema ordena al modelo responder con la frase anti-alucinación de siempre. Las defensas de 04-04 no se pierden; cambian de sitio.

Las definiciones de las tres herramientas, con sus JSON Schema:

HERRAMIENTAS = [
    {
        "name": "buscar_documentacion",
        "description": (
            "Busca en la documentación oficial de Nubelia y devuelve los fragmentos "
            "más relevantes con sus fuentes. Úsala SIEMPRE antes de responder "
            "preguntas sobre el funcionamiento de Nubelia: planes, límites, permisos, "
            "facturación, integraciones, API. Si devuelve 'sin_informacion_relevante', "
            "no inventes la respuesta."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "consulta": {"type": "string",
                             "description": "Consulta de búsqueda, específica y autocontenida."},
            },
            "required": ["consulta"],
        },
    },
    {   # sin cambios desde 05-01
        "name": "consultar_proyecto",
        "description": "Consulta el estado EN TIEMPO REAL de un proyecto del usuario...",
        "input_schema": {
            "type": "object",
            "properties": {"nombre": {"type": "string"}},
            "required": ["nombre"],
        },
    },
    {
        "name": "crear_ticket_soporte",
        "description": (
            "Crea un ticket en el sistema de soporte de Nubelia. Es una ACCIÓN DE "
            "ESCRITURA: úsala solo cuando el usuario pida explícitamente abrir un "
            "ticket, nunca por iniciativa propia. Resume el problema en el título "
            "y aporta el detalle en la descripción."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "titulo": {"type": "string", "description": "Resumen breve del problema."},
                "descripcion": {"type": "string", "description": "Detalle del problema y contexto."},
                "prioridad": {"type": "string", "enum": ["baja", "media", "alta"]},
            },
            "required": ["titulo", "descripcion", "prioridad"],
        },
    },
]

Y el despachador crece con las dos nuevas entradas (con un dict de funciones, que escala mejor que la cadena de if de 05-01):

_REGISTRO = {
    "buscar_documentacion": lambda args: buscar_documentacion(args["consulta"]),
    "consultar_proyecto":   lambda args: api_nubelia.obtener_proyecto(args["nombre"]),
    "crear_ticket_soporte": lambda args: tickets.crear_con_confirmacion(args),
}

def ejecutar_herramienta(nombre: str, argumentos: dict):
    if nombre not in _REGISTRO:
        raise ValueError(f"Herramienta desconocida: {nombre}")
    return _REGISTRO[nombre](argumentos)

Acciones de escritura: crear_ticket_soporte y la confirmación humana

Hasta ahora todas las herramientas de DocuBot eran de lectura: si el modelo se equivocaba, el daño era una respuesta mala. crear_ticket_soporte cruza una línea nueva — modifica el mundo (crea algo en otro sistema, molesta a un equipo humano, no se deshace con refrescar la página). Para acciones así, dos principios que ya apuntamos en 05-01 dejan de ser consejo y pasan a ser diseño:

  • Mínimo privilegio: la credencial de esta herramienta solo puede crear tickets — no leerlos todos, no cerrarlos, no tocar nada más. Si mañana un prompt malicioso engaña al agente (el ataque estrella de 06-01), el radio de la explosión es el que tú acotaste hoy.
  • Confirmación humana (human-in-the-loop): el modelo propone la acción con todos sus argumentos, pero un humano la aprueba antes de que ocurra. La decisión de actuar sobre el mundo es siempre humana; el agente prepara, no dispara.

El patrón, en su versión mínima de consola:

# tickets.py — acción de escritura con confirmación humana
_SIGUIENTE_ID = 1001

def crear_con_confirmacion(args: dict) -> dict:
    """Muestra la acción propuesta y solo la ejecuta si un humano la aprueba."""
    print("\n--- El agente propone crear un ticket ---")
    print(f"  Título:      {args['titulo']}")
    print(f"  Prioridad:   {args['prioridad']}")
    print(f"  Descripción: {args['descripcion']}")
    if input("¿Confirmar creación? (s/n): ").strip().lower() != "s":
        # El rechazo NO es un error del sistema: es información para el modelo
        return {"resultado": "cancelado",
                "detalle": "El usuario no ha autorizado la creación del ticket."}
    return _crear_ticket(args)                 # aquí iría la llamada real a la API

def _crear_ticket(args: dict) -> dict:
    global _SIGUIENTE_ID
    ticket_id = f"NUB-{_SIGUIENTE_ID}"
    _SIGUIENTE_ID += 1
    return {"resultado": "creado", "id": ticket_id, "prioridad": args["prioridad"]}

El detalle más fino está en el rechazo: se devuelve como tool_result normal (no is_error), porque no ha fallado nada — el humano ha ejercido su derecho de veto, y el modelo debe enterarse para reaccionar bien ("De acuerdo, no he creado el ticket. ¿Quieres que cambie algo antes de volver a intentarlo?"). El input() es la versión de juguete del patrón; en una aplicación real la confirmación sería un botón en la interfaz o un paso de aprobación asíncrono, pero la arquitectura — la herramienta de escritura no ejecuta sin un sí humano — es exactamente la misma. En 06-01 veremos por qué esta línea de defensa importa aún más de lo que hoy parece.

Trazas: ver lo que el agente hace paso a paso

Un pipeline fijo se depura leyendo el código: el camino es siempre el mismo. Un agente, no — el camino lo decide el modelo en cada ejecución, así que si no registras lo que hizo, no sabes lo que hizo. De ahí el registrar_paso que salpica el bucle:

# trazas.py — versión mínima (06-04 la profesionalizará)
import json, time

def registrar_paso(iteracion: int, accion: str, argumentos: dict | None):
    marca = time.strftime("%H:%M:%S")
    args = json.dumps(argumentos, ensure_ascii=False) if argumentos else ""
    print(f"[{marca}] paso {iteracion}: {accion} {args}")

Con esto, la pregunta que abría la lección deja de ser una caja negra. bucle_agente("¿Mi proyecto Atlas supera el límite de tareas de mi plan?") produce una traza como esta:

[10:42:01] paso 1: buscar_documentacion {"consulta": "límite de tareas por proyecto según plan"}
[10:42:04] paso 2: consultar_proyecto {"nombre": "Atlas"}
[10:42:07] paso 3: respuesta_final

Y la respuesta final combina ambas fuentes: "Tu plan Profesional permite hasta 200 tareas abiertas por proyecto (fuente: /docs/planes). Atlas tiene ahora 27 abiertas, así que estás muy lejos del límite." Léela otra vez despacio: la primera mitad viene del RAG, la segunda de la API, y la comparación la hizo el modelo en la vuelta 3 al observar ambos resultados en el historial. Ningún código nuestro sabía que esta pregunta necesitaba dos herramientas — eso es el agente.

Las trazas son, además, tu detector de patologías: una traza donde consultar_proyecto aparece cuatro veces con el mismo argumento delata un modelo atascado reintentando; una donde buscar_documentacion no aparece antes de una respuesta sobre planes delata que el sistema no está sujetando al modelo. Esta semilla — registrar cada decisión con su marca de tiempo — germinará en 06-04 como observabilidad de verdad (trazas estructuradas, tokens y coste por paso, paneles); por ahora, un print honesto vale oro.

Cuándo NO usar un agente: pipeline fijo vs bucle agéntico

La pregunta de cierre, y la más rentable de la lección. Ahora que DocuBot puede funcionar como agente, ¿debe hacerlo siempre? No — y la tabla explica por qué el responder_con_rag() de 04-04 no se jubila:

Criterio Pipeline fijo (p. ej. responder_con_rag) Agente (bucle_agente)
Coste Conocido y constante: 1 llamada LLM (+embeddings) Variable: N llamadas, historial creciente en cada una
Latencia Predecible (~una llamada) Impredecible: cada vuelta suma segundos
Previsibilidad Mismo camino siempre; testeable como función El camino cambia entre ejecuciones; testear cuesta más (06-03)
Depuración Leer el código Leer las trazas
Flexibilidad Solo resuelve el caso para el que se diseñó Combina herramientas para casos no previstos
Cuándo brilla Tarea conocida con pasos conocidos Tarea cuyo plan depende de la petición

La regla práctica que se deriva: usa la herramienta más simple que resuelva el problema. "¿Cómo configuro el SSO?" es una pregunta de documentación pura: responder_con_rag() la resuelve con una llamada, coste fijo y el contrato JSON validado de siempre — mandarla al agente es pagar flexibilidad que no se usa. "¿Atlas supera el límite de mi plan?" necesita plan dinámico: agente. Un buen DocuBot de producción probablemente combinaría ambos — un clasificador delante (¿te suena prompt_clasificacion() de 02-02?) que enruta lo simple al pipeline y lo compuesto al agente. Esa orquestación de flujos con ramas es justo el terreno de los frameworks, y es la siguiente lección.

Errores Comunes y Consejos

  • Bucle sin límite de iteraciones. "El modelo siempre termina" es verdad hasta que una herramienta rota lo mete en un ciclo de reintentos. max_iteraciones es como el timeout de una petición HTTP: no negociable.
  • Reventar el bucle cuando una herramienta falla. Una excepción no capturada mata al agente a mitad de tarea. Captúrala y devuélvela como tool_result con is_error — muchas veces el modelo se recupera solo (prueba otro argumento, u otra herramienta) y esa resiliencia es gratis.
  • Procesar solo el primer tool_use del turno. El modelo puede pedir varias herramientas a la vez (nuestro ejemplo de Atlas a veces pide documentación y proyecto en la misma vuelta). Cada tool_use_id exige su tool_result; si falta uno, la API rechaza la siguiente llamada.
  • Herramientas de escritura sin freno humano. Si el agente puede crear/modificar/borrar sin confirmación, has delegado una decisión irreversible a un proceso no determinista. Propone el agente, decide el humano — sobre todo mientras no tengas la evaluación (06-03) y las defensas (06-01) que justificarían relajar el freno en casos acotados.
  • Convertirlo todo en agente. El síntoma: latencias que se triplican y facturas que no cuadran con la proyección de 03-04, para preguntas que un pipeline fijo resolvía igual de bien. Revisa la tabla; el agente es un martillo excelente, no todo es un clavo.
  • Trazas que llegan tarde. Añadir registro después del primer comportamiento raro significa depurar a ciegas la primera vez. La traza se escribe con el bucle, no tras el incidente.

Ejercicios

  1. Trazas con coste. Amplía registrar_paso (y el bucle) para registrar también, en cada llamada al LLM, los tokens de entrada y salida (respuesta.usage.input_tokens / output_tokens) y el coste estimado con estimar_coste() de precios.py (03-04). Ejecuta la pregunta de Atlas y responde: ¿qué proporción del coste total se fue en la última vuelta, y por qué es la más cara?
  2. Salida de emergencia más digna. Cuando se agota max_iteraciones, MENSAJE_LIMITE no aprovecha nada de lo averiguado. Modifica el bucle para que, al agotarse el límite, haga una última llamada sin herramientas (pista: tool_choice: {"type": "none"}) pidiendo al modelo que resuma lo que ha encontrado hasta el momento y qué le ha faltado.
  3. ¿Pipeline o agente? Para cada petición, decide qué camino tomaría un DocuBot bien diseñado y justifica en una frase con los criterios de la tabla: (a) "¿Qué métodos de pago aceptáis?"; (b) "Compara las tareas abiertas de mis tres proyectos y dime cuál va peor"; (c) "¿Cómo creo un webhook?"; (d) "El webhook de Boreal ha dejado de funcionar; si es un problema conocido dímelo y si no, abre un ticket".

Soluciones

  1. Cambios mínimos:
# En el bucle, tras cada llamada:
coste = estimar_coste(respuesta.usage.input_tokens, respuesta.usage.output_tokens)
registrar_paso(iteracion, "llamada_llm", {
    "entrada": respuesta.usage.input_tokens,
    "salida": respuesta.usage.output_tokens,
    "coste": round(coste, 5),
})

La última vuelta es la más cara en tokens de entrada porque el historial acumula todos los tool_result anteriores (los fragmentos de documentación pesan especialmente); típicamente concentra el 40-60% del coste total. Es la razón económica del "usa la herramienta más simple": cada vuelta evitada ahorra la más cara de las llamadas que habrían venido después. (Y sí: el prefijo estable — sistema + definiciones de herramientas — es candidato ideal al prompt caching de 03-04.)

  1. Sustituir el return MENSAJE_LIMITE final por:
    messages.append({"role": "user", "content": (
        "Has alcanzado el número máximo de pasos. No uses más herramientas: "
        "resume qué has averiguado hasta ahora, qué te ha faltado por confirmar "
        "y qué podría hacer el usuario a continuación."
    )})
    final = llamar_con_reintentos(
        model=MODELO, max_tokens=1024,
        system=SYSTEM_DOCUBOT_AGENTE,
        tools=HERRAMIENTAS,
        tool_choice={"type": "none"},   # prohibido pedir más herramientas
        messages=messages,
    )
    return extraer_texto(final)

Degradación elegante, edición agéntica: la misma filosofía que RESPUESTA_FALLBACK en 03-02 — si no puedes dar todo, da lo mejor que tengas y sé honesto sobre lo que falta.

  1. (a) Pipeline (responder_con_rag): documentación pura, camino conocido, coste fijo. (b) Agente: número de pasos dinámico (listar proyectos, consultar cada uno, comparar) que ningún pipeline fijo prevé con elegancia. (c) Pipeline: otra pregunta de documentación con disfraz técnico. (d) Agente, y de manual: requiere buscar en documentación (¿problema conocido?), quizá consultar el proyecto, y condicionalmente una acción de escritura con confirmación humana — es literalmente el bucle razonar-actuar-observar con las tres herramientas de la lección.

Conclusión

Veinte líneas de bucle han cambiado la naturaleza de DocuBot. Repasemos lo construido: el ciclo razonar → actuar → observar implementado en agente.py::bucle_agente(), con la salida natural por stop_reason y el cinturón de seguridad de max_iteraciones; tres herramientas donde antes había una — buscar_documentacion (el RAG del módulo 4 reconvertido en herramienta: recuperar y aumentar, dejando la generación al bucle), consultar_proyecto (05-01) y crear_ticket_soporte, la primera acción de escritura del curso, estrenando el patrón human-in-the-loop: el agente propone con argumentos completos, el humano confirma, y el rechazo vuelve al modelo como información, no como error; trazas que convierten cada ejecución en una historia legible (la semilla de 06-04); y una tabla que nos vacuna contra el entusiasmo — el pipeline fijo de 04-04 sigue siendo la mejor respuesta para la pregunta simple, y el agente se reserva para cuando el plan depende de la petición. Ahora bien: todo esto lo hemos escrito a mano — el bucle, el registro de herramientas, la memoria en messages, las trazas, la confirmación. Ha sido deliberado (no hay mejor forma de entender qué hace un agente que escribir uno), pero seguramente te has preguntado si no existe ya una pieza de estantería que traiga todo esto resuelto. Existe — existen decenas, y ese es precisamente el problema. La siguiente lección pone orden: qué resuelven los frameworks de orquestación, qué cuestan en abstracción y dependencias, cómo se comparan LangChain/LangGraph, LlamaIndex, Haystack y los SDKs de agentes de los proveedores, qué es ese estándar llamado MCP del que todo el mundo habla — y por qué, sabiéndolo todo, DocuBot va a seguir llevando su bucle escrito a mano.

© Copyright 2026. Todos los derechos reservados