Inicio / Artículos

El Agente Desprotegido: Qué se rompe cuando nada protege al modelo.

Construye el mismo agente de viajes sin protección en el Claude Agent SDK y LangChain Deep Agents, y luego observa tres fallos para identificar qué controles de manejo faltan.

Editor: REACTAPP.TOP9 min de lectura2152 palabras

Fuentes: Harness Engineering: The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness — I

Un cliente pide a un agente que rereserve otro vuelo. El modelo suena seguro de sí mismo, la llamada a la herramienta tiene éxito y la respuesta indica que todo está resuelto. Luego alguien se da cuenta de que la fecha era el 32 de marzo.

En ese instante se rompe la ilusión. Lo que faltaba al modelo no era inteligencia; lo que falló fue todo lo que lo rodeaba, ya que ningún componente sabía cuándo intervenir. Lo que sigue comienza con un agente básico: sin validadores, sin memoria, sin capacidad de recuperación y sin red de seguridad. Verlo fallar revela dónde cometen errores los agentes en producción, y demuestra que el entorno de ejecución alrededor de un modelo de lenguaje es más importante que el modelo en sí. Ese mismo agente simplificado se construye dos veces, con Claude Agent SDK y con LangChain Deep Agents, y luego falla de tres maneras concretas, cada una señalando un mecanismo de control que un agente real debe ofrecer. El resultado es un método para distinguir entre un problema del modelo y un problema del entorno que lo rodea.

El incidente del 32 de marzo

Imagínese a un agente de reservas de viajes que recibe el mensaje “32 de marzo” de un usuario real. El modelo pasó esa cadena directamente a la API de vuelos. Después de unos cuarenta minutos y cincuenta dólares en llamadas al modelo, alguien tuvo que explicarle a un cliente que todo ya estaba resuelto una reserva que en realidad no existía.

El modelo no fabricó nada. Envió exactamente lo que se le proporcionó, ya que el sistema de control no tenía criterio para decidir si debía hacerlo. Eso resume el caso. La inteligencia proviene del modelo y la disciplina del sistema de control; por lo tanto, cuando un agente de producción se comporta mal, la culpa recae casi siempre en la capa de disciplina. Sustituir por un modelo más potente o perfeccionar la instrucción no proporcionará un control que no exista, y crear controles comienza con comprender cuánto cuesta su ausencia. Por eso el punto de partida es sencillo: un agente frágil, dos frameworks, sin sistema de control.

Evidencia de que el tiempo de ejecución domina

Los datos de referencia respaldan este punto. Los investigadores de Princeton congelaron el modelo en GPT-4 Turbo y modificaron únicamente el código de la interfaz que lo conectaba con la base de código. La proporción de problemas resueltos en SWE-bench aumentó del 3,8 % al 12,47 %, casi el triple, mientras que los pesos del modelo permanecieron sin cambios. El trabajo, titulado “SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering”, fue publicado en 2024 y aceptado en NeurIPS ese mismo año. Fue el diseño de la interfaz el que impulsó esta mejora, no la inteligencia del modelo.

La disciplina subyacente tiene un nombre más antiguo: simpatía mecánica, que se refiere a la creación de software que funcione en conjunto con el hardware o plataforma subyacente en lugar de en contra de ellos. Harness Engineering extiende este enfoque a un nuevo sustrato: el modelo de lenguaje, con su ventana de contexto y su presupuesto de atención. El modelo establece un límite, y Harness Engineering determina hasta qué punto el sistema puede acercarse consistentemente a ese límite.

El cambio en la forma de pensar modifica lo que se hace después de un fracaso:

  • Antes: tras una respuesta incorrecta, se ajusta la instrucción, se añaden muchas reservas, se prueba un modelo más grande y se espera
  • Después: tras una respuesta incorrecta, se identifica qué control falta y esa regla se elimina de la instrucción para aplicarse durante la ejecución

Las instrucciones persuaden, mientras que los mecanismos de control obligan a seguir las reglas.

El bucle presente en cada framework

Todos los frameworks para agentes se basan en un mismo bucle de cinco pasos: se proporcionan las herramientas al modelo, se recibe la respuesta de uso de la herramienta, se ejecuta, se devuelve el resultado y se recibe el siguiente paso a seguir. La documentación sobre uso de herramientas de Anthropic, la API Responses de OpenAI y el entorno de ejecución de agentes de LangChain se reducen a lo mismo. Los SDKs van y vienen, pero la estructura general permanece constante.

La versión básica utiliza un SDK de proveedor sin procesamiento adicional, dos herramientas de navegación y no cuenta con ninguna medida de seguridad:

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY
TOOLS = [
    {"name": "search_flights",
     "description": "Search flights between two cities for a date.",
     "input_schema": {"type": "object", "properties": {
         "origin": {"type": "string"}, "destination": {"type": "string"},
         "date": {"type": "string", "description": "YYYY-MM-DD"}},
         "required": ["origin", "destination", "date"]}},
    {"name": "book_flight",
     "description": "Book a specific flight.",
     "input_schema": {"type": "object", "properties": {
         "flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
         "required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
    messages = [{"role": "user", "content": user_msg}]
    while True:                                   # ① no iteration cap
        resp = client.messages.create(
            model="claude-sonnet-4-6", max_tokens=1024,
            tools=TOOLS, messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return resp.content[0].text
        call = next(b for b in resp.content if b.type == "tool_use")
        result = dispatch(call.name, call.input)
                                             # ② direct side effect, no check
        messages.extend([
                                             # ③ whole history, every turn
            {"role": "assistant", "content": resp.content},
            {"role": "user", "content": [{"type": "tool_result",
                "tool_use_id": call.id, "content": result}]},
        ])

Lee esto teniendo en cuenta tres posibles fallos:

  1. El bucle while True: no tiene límite. Un plan defectuoso sigue invocando al modelo hasta agotar el presupuesto, ya que no se cuentan las iteraciones.
  2. dispatch pasa directamente los argumentos proporcionados por el modelo a un efecto secundario sin realizar ninguna verificación previa, por lo que un valor que se interpreta correctamente pero que no debería existir llega a la API en funcionamiento.
  3. Todo el historial de mensajes se extiende y se vuelve a enviar en cada turno, inflando silenciosamente el contexto hasta que disminuye la calidad.

Ninguna de las propiedades de seguridad que uno desearía se encuentra dentro de este bucle. Están en código que aún no se ha escrito. Adoptar un framework tampoco resuelve estos problemas. El Claude Agent SDK y Deep Agents ofrecen un bucle mejor estructurado, con un protocolo de herramientas adecuado y un camino de ejecución ordenado, pero por defecto no incluyen validadores, ni memoria sobre la cual razonar, ni mecanismos de recuperación. El bucle es lo que llega, y gestionarlo sigue siendo tarea del desarrollador.

Aquí está el mismo agente sin adornos en cada framework.

La versión del Claude Agent SDK

El SDK se encarga de gestionar el bucle por usted. Las herramientas se declaran con el decorador @tool, se exponen como un servidor MCP en el mismo proceso y se ejecutan a través de query(). Lo que falta es cualquier tipo de control: los argumentos solo se verifican contra el esquema, no se recuerda nada entre llamadas y no existe mecanismo de recuperación.

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions, tool,
    create_sdk_mcp_server, AssistantMessage, ResultMessage,
)

@tool("search_flights", "Search flights between two cities for a date.",
      {"origin": str, "destination": str, "date": str})
async def search_flights(args):
                                              # ① no check that date exists
    hits = flights_api.search(**args)
    return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
      {"flight_id": str, "passenger_name": str})
async def book_flight(args):
                                               # ② destructive, ungated
    confirmation = flights_api.book(**args)
    return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"travel": server},
        allowed_tools=["mcp__travel__search_flights",
                       "mcp__travel__book_flight"],
    )
    async for msg in query(prompt="Rebook this customer for March 32nd.",
                           options=options):
        if isinstance(msg, AssistantMessage):
            for b in msg.content:
                if hasattr(b, "text"):
                    print(b.text)
        elif isinstance(msg, ResultMessage):
            print("done:", msg.subtype)
                                              # ③ no state survives this run

Tres cosas destacan:

  1. search_flights acepta cualquier valor que el modelo proponga y que se ajuste al esquema, por lo que una fecha que no pueda existir llega al interior de la herramienta.
  2. book_flight es destructivo y no existe ninguna protección para él. El decorador lo integra en el bucle sin ningún mecanismo de aprobación ni validador.
  3. Después de que llegue ResultMessage y query() devuelva el resultado, la sesión finaliza y nada de esa ejecución sobrevive para la siguiente llamada.

El SDK proporcionó un bucle limpio. No tiene control alguno sobre fechas inválidas, cargos duplicados ni sobre lo que merece ser recordado.

La versión Deep Agents

Deep Agents estructura el bucle de manera diferente. Las herramientas habituales de LangChain se pasan a create_deep_agent(), la cual devuelve un grafo compilado para ser invocado. Las similitudes son las mismas: no existe validador, y como no se proporciona ningún checkpointer, el grafo olvida todo lo ocurrido entre llamadas.

from langchain.tools import tool
from deepagents import create_deep_agent

@tool
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search flights between two cities for a date (YYYY-MM-DD)."""
    return str(flights_api.search(origin, destination, date))
                                                # ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
    """Book a specific flight."""
    return flights_api.book(flight_id, passenger_name)
                                                 # ② ungated side effect
agent = create_deep_agent(
                                                 # ③ the loop, no controls
    model="anthropic:claude-sonnet-4-6",
    tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
    "content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke, and it starts from zero: no thread,
# no memory.

Otra vez tres puntos:

  1. El cuerpo de search_flights llama a la API con la cadena de texto date en bruto, y nada confirma que la fecha sea real antes de que se envíe la solicitud.
  2. book_flight realiza la reserva de forma destructiva en el momento en que el modelo la elige, sin ningún mecanismo que se interponga entre la propuesta y el efecto secundario.
  3. create_deep_agent devuelve un grafo que ejecuta el bucle y poco más. Al no haber un checkpointer, no existe hilo ni memoria entre las invocaciones.

Tres maneras en que falla un agente sin protecciones

Dos frameworks ofrecieron dos bucles simples pero sin ninguna disciplina, que es así como se presenta un agente sin protecciones en 2026. Tampoco se trata de un ejemplo simplificado. Es la forma que adoptan las decenas de demostraciones de agentes publicadas semanalmente en GitHub. Bajo tráfico real, falla de tres maneras, y cada fallo corresponde a un control que añade un sistema de gestión adecuado.

Entrada imposible. Al introducir “32 de marzo”, el modelo hace lo que hace habitualmente y llena el campo date con una cadena bien formada pero sin sentido. El esquema solo confirma que date es una cadena, y no puede determinar si ese día existe. Por lo tanto, book_flight se ejecuta con un itinerario ficticio, y el modelo indica que la operación tuvo éxito.

La llamada es a book_flight con un ID de vuelo inventado y un nombre de pasajero, y devuelve “Reservado”. La acción se ejecutó, y nada en el bucle preguntó si realmente era necesario hacerlo.

book_flight(flight_id=”AC-PHANTOM”, passenger_name=”J. Moffatt”)
# -> “Booked.” The action fired. Nothing in the loop asked whether it should.

La falla se debió a la falta de un límite de acción, ya que no había un validador que interviniera entre lo que proponía el modelo y lo que realmente ocurría. Cubrir esa brecha es tarea de los contratos de herramienta y los validadores.

Contexto excesivo. El bucle sin restricciones vuelve a enviar toda la historia en cada turno. Un intercambio largo de reprogramaciones, lleno de resultados de búsqueda, texto de políticas y intentos de repetición, puede superar los cincuenta mil tokens, y nada lo reduce. Se produce lo que la literatura denomina “putrefacción del contexto”, y el agente descarta pasos que había realizado correctamente anteriormente en la misma ejecución. No genera ningún error; simplemente empeora, de forma silenciosa.

Aquí, las piezas faltantes eran la memoria de trabajo y el ensamblaje de contexto, los cuales corresponden a la memoria y al estado duradero, así como al ensamblaje de contexto.

Errores ocultados. Una herramienta devuelve un mensaje de error, y el modelo, con más frecuencia de lo que cualquiera esperaría, escribe un resumen cortés que pasa por alto el fallo. Un análisis de incidentes en producción describió a los agentes como mentirosos seguros de sí mismos: una base de datos que devuelve cero filas se considera una respuesta factual en lugar de un error. Elegir la herramienta correcta nunca fue la parte difícil; lo complicado es distinguir entre una herramienta que realmente funciona y una que imita convincentemente el funcionamiento. Aquí no se observó ningún bucle de retroalimentación, y su implementación forma parte de la capacidad de observabilidad y detección de desviaciones.

Trátelos como un patrón de diagnóstico en lugar de una lista de situaciones vergonzosas. En los tres casos, el modelo hizo lo que hacen los modelos, y el sistema circundante no logró detectarlo. Ninguno involucra a un modelo que no fuera lo suficientemente inteligente.

Un bucle funcional para la ingeniería de aprovechamiento

Un procedimiento de cuatro pasos se repite al crear cualquier control:

  1. Introduce la función de soporte y el fallo que controla.
  2. Implementa esta función en ambos frameworks como un fragmento específico.
  3. Desencadena el fallo para demostrar que el control puede soportar carga.
  4. Inspecciona cómo responde el sistema de soporte.

Las secciones anteriores aplicaron este procedimiento tres veces sin etiquetarlo: identificar una unión sin protección, desencadenar el fallo que genera y nombrar el control al que se refiere. Luego, se procede a cerrar las uniones una por una.

Pruébalo tú mismo

Se puede percibir el problema sin seguir leyendo. En un editor, unos pocos experimentos rápidos lo demuestran:

  • Dirige al agente sin protección en cualquiera de los frameworks hacia una instrucción para reprogramar al cliente para el 32 de marzo, y observa cómo la fecha imposible llega al método book_flight
  • Limita el bucle sin condición a cinco iteraciones. Ese es el primer control de harness que realmente se escribe, y pertenece al tiempo de ejecución y no a la interfaz de entrada
  • Haz que book_flight devuelva {"is_error": true, ...} y verifica si el resumen final refleja el fallo o lo ignora
  • Arregla una versión de claude-agent-sdk y una versión de deepagents o langchain antes de comenzar, ya que ambas APIs cambian de versión, y considera los nombres de los modelos (claude-sonnet-4-6, anthropic:claude-sonnet-4-6) como representantes de lo que está actualmente en uso

Cada framework ofrece un bucle ordenado y bien estructurado, pero ninguno proporciona un sistema de control. Eso es una característica de la situación y no un defecto de las herramientas; ahí radica el verdadero trabajo. Claude Code, OpenCode, Codex y Deep Agents se consideran sistemas de control para agentes, y un desarrollador que tiene dificultades con ellos difiere de aquel que obtiene resultados fiables al diseñar los controles que el bucle omite. El modelo ha sido la parte fácil desde hace tiempo. La fiabilidad surge de la disciplina que se construye alrededor de él, a través de los fracasos, y el primer elemento esencial es el límite de acción, la primera regla para salir del prompt y operar en tiempo de ejecución.

Leer más