Saltar al contenido principal

09 · Agentes y AI Engineering

De la aplicación con LLM al agente

Aplicaciones con LLM, workflows y agentes se distinguen por quién decide el siguiente paso. El agent loop, y por qué es una superficie de seguridad.

12 minCaso de estudio: docket (se abre en una ventana nueva)

"", "asistente", "copiloto", " agentic": el mercado usa estas palabras con poca precisión, y esa imprecisión tiene costo. Un equipo que llama "agente" a un pipeline fijo sobreestima su riesgo. Un equipo que llama "pipeline" a un agente lo subestima, y eso es peor.

Separemos tres categorías con un criterio único: quién decide el siguiente paso. Después veremos el bucle que implementa a un agente, por qué su versión ingenua falla en producción y por qué ese bucle es una superficie de seguridad.

Tres categorías, un criterio

Aplicación con LLM: el modelo responde

Una application llama a un modelo en uno o más puntos fijos. Todo el flujo de control es código escrito por el equipo.

ticket --> [prompt: "summarize"] --> LLM --> summary --> save to DB

Si el modelo falla, el daño está acotado: guardamos un resumen malo.

Workflow: el modelo llena huecos en un grafo fijo

Un workflow es una secuencia explícita de pasos, algunos con LLM y otros con código o herramientas. Puede tener ramas, paralelismo y reintentos, pero todas las transiciones posibles existen en el código antes de ejecutar. Se puede dibujar el grafo completo leyendo el programa.

input -> LLM: classify -> "billing"
      -> tool: search_kb("billing", text)   <- chosen by code
      -> LLM: draft reply with 3 articles -> output

Agente: el modelo elige la acción

Para "arregla este test que falla en un repositorio desconocido" no sabemos de antemano qué archivos leer ni cuántos intentos harán falta. Un agente es un sistema en el que un modelo, dentro de un bucle, elige qué acción ejecutar a continuación a partir de un objetivo y de las observaciones acumuladas, hasta decidir que terminó o hasta que algo externo lo detenga.

Categoría Quién decide el siguiente paso Grafo conocido de antemano
Aplicación con LLM el código sí, lineal
Workflow el código sí, con ramas
Agente el modelo, dentro de límites no

Para CTOs

La primera decisión no es qué framework usar, sino si el problema necesita autonomía. Si puedes dibujar el grafo, construye un workflow: es más barato, más fácil de testear y su peor caso es predecible. Reserva el agente para tareas cuyo camino no se conoce de antemano, y presupuesta desde el principio el costo de gobernarlo.

El bucle ingenuo

El es el bucle concreto que implementa lo anterior. Su versión mínima cabe en pocas líneas:

# Illustrative only: do not ship this.
def agent_loop(model, tools, goal):
    messages = [system("You are an agent..."), user(goal)]
    while True:
        response = model.complete(messages, tools=tools.specs())
        messages.append(response.message)
        if not response.message.tool_calls:
            return response.message.content
        for call in response.message.tool_calls:
            result = tools.run(call)  # the real world happens here
            messages.append(tool_result(call, result))

Funciona en una demo. En producción tiene al menos siete problemas:

  1. while True sin límite: un modelo confundido itera y gasta indefinidamente.
  2. Sin límite de herramientas: una sola respuesta puede pedir doscientos tool calls.
  3. Sin límite de tiempo: una llamada colgada bloquea todo.
  4. Sin control sobre tools.run: cualquier herramienta, con cualquier argumento, se ejecuta.
  5. Sin manejo de salidas defectuosas: argumentos que no son JSON válido, o una respuesta cortada por longitud.
  6. Sin persistencia correcta: si el proceso muere a mitad se pierde todo, y si guardamos a mitad podemos dejar un tool call sin su resultado.
  7. Sin registro: después no hay forma de reconstruir qué pasó.

Un bucle con condiciones de parada explícitas

El núcleo es corto, tomado tal cual del archivo:

while True:
    outcome = state.run_iteration()
    if outcome is not None:
        return outcome

Cada run_iteration hace cuatro cosas en orden: comprobar límites, preparar un pedido que quepa en la , llamar al modelo y despachar herramientas. Lo instructivo es que cada salida está enumerada en un tipo cerrado, StopReason:

stop_reason Qué lo dispara Valor por defecto
final_message el modelo responde sin pedir herramientas; la única parada exitosa
max_iterations demasiadas idas y vueltas con el modelo 20
max_tool_calls demasiadas herramientas despachadas 40
tool_denials demasiadas denegaciones consecutivas 3
timeout tiempo total de reloj excedido 300 s
token_budget tokens medidos acumulados excedidos 100 000
truncated la respuesta se cortó por longitud
backend_error, context_fit, compaction_failed falla del endpoint o del ajuste de contexto
run_cancelled, approval_unavailable cancelación, o se necesitaba un humano y no hay

Los valores por defecto están en src/docket/config.py (constantes AGENT_LOOP_*, sobreescribibles por variable de entorno). Tres decisiones merecen atención:

  • El cupo de herramientas se comprueba antes del lote, no durante. Si el modelo pide cinco y quedan tres, no se ejecuta ninguna: ejecutar solo algunas dejaría un mensaje con tool calls respondidos y otros huérfanos, un estado que el historial no debe guardar.
  • Una respuesta truncada nunca se ejecuta. Puede contener un tool call a medio escribir; ChatResponse.truncated en src/docket/core/llm.py existe para detectarlo.
  • El timeout se comprueba entre iteraciones, y el docstring admite que no interrumpe una llamada HTTP en curso. Para eso hay un segundo límite por llamada (AGENT_LOOP_REQUEST_TIMEOUT_S, 120 s).

Además, run_agent_turn nunca lanza excepciones por fallas ordinarias: toda salida vuelve como un AgentLoopResult con ok, stop_reason, error y usage.

El bucle como superficie de seguridad

En software tradicional, la superficie de ataque son las entradas: validamos en la frontera y adentro confiamos en nuestro código. En un agente, lo que decide la próxima acción es un modelo que lee todo lo que entra a su contexto: el pedido del usuario, pero también archivos, páginas web, salidas de comandos y respuestas de otras herramientas. Cualquiera de esas fuentes puede contener texto que el modelo interprete como instrucción.

Traditional:  input -> [validation] -> trusted code -> action

Agent:        request --+
              file    --+
              web     --+--> model (reads all, decides) --> action
              tool    --+          ^
                                   +-- every iteration adds inputs

El bucle realimenta al modelo con datos que el propio modelo decidió buscar, y cada iteración es una oportunidad nueva para contenido hostil. De ahí tres consecuencias:

  • validar solo el pedido inicial no protege al agente;
  • no se puede confiar en que el modelo "decida bien" siempre;
  • el control tiene que estar donde una decisión del modelo se convierte en una acción real, en cada iteración.

Ese punto se llama policy enforcement point. En docket se llama dispatch_tool, y lo estudiamos en Tool calling y harness. Las amenazas concretas (, la ) están en Sandboxing y seguridad de agentes.

Para llevar

  • La diferencia entre aplicación con LLM, workflow y agente es quién decide el siguiente paso.
  • Si puedes dibujar el grafo antes de ejecutar, probablemente necesitas un workflow y no un agente.
  • Un agent loop útil en producción enumera sus condiciones de parada y las reporta; final_message es la única salida exitosa en docket.
  • Límites de lote, respuestas truncadas y persistencia atómica cambian la forma del bucle; no se agregan después.
  • El bucle es una superficie de seguridad porque cada iteración agrega entradas no confiables; el control va donde la decisión se vuelve acción.

Comprueba lo aprendido

Un sistema clasifica un ticket con un LLM y, según la categoría, llama a una de tres herramientas fijas. ¿Es un agente?

No. Todas las transiciones posibles están escritas en el código antes de ejecutar; el modelo solo llena un hueco. Es un workflow con ramas.

¿Por qué docket no ejecuta parte de un lote cuando el modelo pide más herramientas de las que permite el cupo?

Porque ejecutar solo algunas dejaría en el historial un mensaje con tool calls respondidos y otros sin resultado. El lote se despacha completo o no se despacha.

¿Por qué no basta con validar el pedido inicial del usuario?

Porque en cada iteración entran nuevos datos (archivos, web, resultados de herramientas) que el modelo lee y que pueden contener instrucciones hostiles. El control debe aplicarse a cada acción, no solo a la entrada.