Inicio / Artículos

Aplicación de reglas de agente en el código: PreToolUse, PostToolUse y Stop Hooks

Aprenda por qué la autorización para los agentes LLM debe estar en los ganchos de llamada a herramientas deterministas, cómo denegar llamadas de forma segura y cómo envolver un despachador sin recursión.

Editor: REACTAPP.TOP12 min de lectura2721 palabras

Fuentes: LLM Agents, The Way Nobody Told You — Part 8: Hooks

Un prompt del sistema que indica “solo los usuarios premium pueden solicitar reembolsos” parece ser una regla, pero para el modelo se trata de una fuerte sugerencia. Se respetará en casi todas las solicitudes, pero se ignorará en aquellas que estén formuladas de manera lo suficientemente extraña. Este artículo muestra cómo trasladar las reglas realmente importantes fuera del prompt y a los hooks: pequeños fragmentos de código determinista que se ejecutan con cada llamada a una herramienta, acceden al contexto confiable del lado del servidor y pueden detener una acción antes de que ocurra. Al final, sabrá qué tipo de hook utilizar para cada tarea, cómo rechazar una solicitud sin interrumpir la conversación y cómo adjuntar hooks de forma segura a un distribuidor de herramientas existente.

Cómo fallan las reglas de los prompts en la práctica

Imagínese a un agente de soporte al cliente junto con un especialista en reembolsos. La instrucción indica que solo los clientes de nivel premium pueden abrir casos de reembolso y que a los usuarios del plan gratuito se les debe informar amablemente de que los reembolsos no están incluidos en su plan. Las pruebas parecen ir bien: los usuarios gratuitos son rechazados, mientras que los usuarios de nivel premium reciben un archivo del caso.

Luego, un usuario del plan gratuito envía una queja redactada de manera inusual. Parece ser una solicitud de reembolso, aunque en realidad se trata de otro tipo de crédito en la cuenta. El modelo, diseñado para ser útil pero ahora incierto respecto a la forma en que se plantea el caso, decide que la situación es lo suficientemente similar y crea de todos modos el archivo del caso. Nada en las reglas resultaba ambiguo para un ser humano, pero un cliente a quien nunca se le permitió abrir casos ahora cuenta con uno.

Esa es la categoría de ganchos de errores que existen para eliminar. El modelo no es malo siguiendo instrucciones; es probabilístico. Una regla que funciona para el 99% de las entradas fallará en una parte del restante 1%, y en entornos de producción esa proporción se manifiesta. Cuando romper una regla tiene un costo real, dicha regla debe ser aplicada por código que se ejecute cada vez y con el cual el modelo no puede discutir.

Tres puntos de ganchos en el ciclo de vida de las llamadas a herramientas

Los ganchos se conectan en momentos específicos del ciclo de vida de una llamada a herramientas. Hay tres de ellos, y cada uno tiene una función distinta:

  • PreToolUse se ejecuta antes de que la herramienta funcione. Puede denegar la llamada, reescribir sus datos de entrada o simplemente observarla.
  • PostToolUse se ejecuta después de que la herramienta haya finalizado su tarea. Solo puede observar: registrar, auditar o notificar. No puede revertir lo que ya ha ocurrido.
  • Stop se ejecuta cuando finaliza la sesión y se encarga de la limpieza, los resúmenes y la persistencia.
  • Una forma compacta de recordarlos es que PreToolUse pregunta si algo debe ocurrir, PostToolUse registra que efectivamente ocurrió, y Stop marca el final de la sesión.

    Estos roles no son intercambiables. La autorización pertenece a PreToolUse, porque la verificación en PostToolUse se realiza después de que la acción ya ha surtido efecto. El registro de auditoría pertenece a PostToolUse, porque una entrada escrita en PreToolUse solo registra una intención; la solicitud aún podría ser denegada o fallar, por lo que el registro describiría cosas que nunca ocurrieron. Asigne cada tarea al hook cuyo momento sea adecuado para ella.

    La capacidad, la autorización y las políticas son verificaciones diferentes

    Supongamos que desea agregar tres reglas nuevas a la herramienta de archivo de casos:

    • Regla A: un archivo de caso necesita una lista no vacía de pruebas.
    • Regla B: a los clientes de la versión gratuita no se les permite abrir archivos de caso.
    • Regla C: la creación de casos está bloqueada desde las 2 a.m. hasta las 4 a.m., mientras se ejecuta el despliegue semanal.

    Cada una de estas reglas rechaza algunas llamadas a herramientas, pero pertenecen a tres categorías diferentes. Es útil clasificar cada regla en una de cuatro categorías:

    • Capacidad, que se refiere a si las entradas son coherentes internamente, está relacionada con la implementación de la herramienta. “La lista de pruebas debe ser no vacía” es una verificación de capacidad.
    • Autorización, que se refiere a si el usuario que realiza la llamada puede hacerlo en este momento, está relacionada con un hook de PreToolUse. “Los usuarios gratuitos no pueden crear archivos de caso” es un ejemplo de autorización.
  • Política, que se refiere a una regla aplicable en toda la organización, también se encuentra en un gancho PreToolUse. “No realizar escrituras durante el período de despliegue” es una política.
  • Observación, que significa un registro de lo que ocurrió, se encuentra en un gancho PostToolUse. “Registrar cada escritura en el registro de auditoría” es una observación.
  • Un atajo útil: la herramienta pregunta si puede realizar la operación con los argumentos que recibió, mientras que el gancho pregunta si al llamante se le permite realizarla en este momento.

    Por qué la división surge de lo que cada capa puede ver

    La distinción no es estilística; proviene de los datos de entrada que tiene cada capa. Una herramienta solo recibe los argumentos generados por el modelo. Puede comparar esos argumentos entre sí, confirmando que una lista no esté vacía o que una cantidad sea positiva, pero no tiene nada más con lo que compararlos.

    Un gancho recibe los mismos argumentos más el contexto confiable generado por el servidor: el ID del usuario autenticado, su nivel real, la hora actual y el ID de sesión. Ese contexto se almacena en un objeto HookContext creado al inicio de la sesión y posteriormente congelado.

    Con ambos elementos en mano, un gancho puede razonar de la siguiente manera: el modelo desea llamar a lookup_user con user_id='u_999', pero la sesión pertenece a u_004, por lo que se deniega la llamada. La herramienta nunca podría tomar esa decisión, ya que nunca sabe a quién pertenece la sesión. La autorización implica siempre comparar la salida no confiable del modelo con el estado confiable del servidor, y solo los ganchos poseen ambos.

    Separar el contexto confiable de la entrada no confiable

    HookContext es un estado del lado del servidor. Tu propio código lo crea al inicio de la sesión, a partir de fuentes que controlas como la capa de autenticación, el gestor de sesiones y la base de datos. Es inmutable durante toda la duración de la sesión, y ni el usuario ni el modelo pueden influir en él. La clase dataclass congelada que se muestra a continuación captura lo mínimo necesario:

    @dataclass(frozen=True)
    class HookContext:
        user_id: str         # set from your auth layer
        tier: Literal["free", "premium"]
        session_id: str
    

    frozen=True realiza una función importante aquí: una vez creado, el objeto no puede ser modificado por ningún camino de código, incluyendo un gancho con errores.

    tool_input, en cambio, es el diccionario que el modelo coloca en su bloque tool_use. Trátalo exactamente como el cuerpo de una solicitud HTTP entrante. Un usuario puede hacer que el modelo genere casi cualquier cosa allí, ya sea mediante inyección de comandos o simplemente a través de la persistencia.

    Una verificación de autorización compara ambos valores. ¿Es igual el user_id proporcionado por el modelo a HookContext.user_id? ¿Excluye la regla HookContext.tier == "free" a esta herramienta de escritura? El principio que mantiene seguro al sistema es sencillo: la decisión de seguridad debe basarse en HookContext, nunca en algo proporcionado por el modelo.

    Denegar una llamada sin interrumpir la conversación

    La forma obvia de denegar una llamada es omitirla. Con las API de llamada a herramientas que siguen el formato de mensaje de Anthropic, esto no funciona. Cada bloque tool_use que emita el asistente debe ser respondido por un bloque tool_result que incluya el mismo tool_use_id en el siguiente mensaje del usuario. Si se deja uno sin respuesta, la solicitud API posterior fallará con un error 400.

    Por lo tanto, una negación de PreToolUse debe generar un resultado sintético que sustituya a la salida de la herramienta. El ejemplo a continuación es un diccionario de Python (fíjese en True) marcado como error, con un mensaje que explica por qué se rechazó la solicitud y qué hacer en su lugar:

    {
      "type": "tool_result",
      "tool_use_id": "toolu_01ABC",
      "is_error": True,
      "content": "BLOCKED: free-tier users cannot call prepare_case_file. Suggest upgrade or route to general FAQ."
    }
    

    Esto cumple con el contrato de la API, y el modelo lo trata como una observación más. Detecta que la solicitud fue rechazada por motivos de nivel de acceso, se disculpa y ofrece otra alternativa, permitiendo que la conversación continúe normalmente.

    Dos detalles hacen que esto funcione bien.

    Un prefijo consistente convierte las negaciones en una señal

    El prefijo BLOCKED: no es algo que exija la API. Se trata de una convención. Un marcador consistente ayuda al modelo a reconocer una negativa intencionada en lugar de un fallo temporal, de modo que cambie de estrategia en vez de volver a intentar la misma llamada. El mismo marcador permite posteriormente que el sistema de evaluación distinga entre negativas intencionadas y errores reales del herramienta. Una convención de texto plano se convierte así en una señal legible por máquinas.

    El mensaje de negación es una instrucción

    El modelo elaborará su próxima respuesta a partir del texto que usted proporcione, por lo que escríbalo con la misma atención que si fuera una instrucción. Un simple “Denied” no le da nada con qué trabajar. Un mensaje que indique el motivo (la versión gratuita no puede usar esta herramienta) y proponga alternativas (actualización o una ruta de Preguntas Frecuentes) le indica tanto por qué falló la llamada como qué ofrecer en su lugar.

    Adjuntar complementos al envolver al distribuidor

    La mayoría de los agentes ya cuentan con un despachador que posee una función call_tool capaz de aceptar un bloque tool_use e invocar la función Python correspondiente. La forma menos intrusiva de agregar ganchos es envolver dicha función en lugar de reescribir el despachador. El primer paso es conservar una referencia al original antes de modificar cualquier cosa:

    original_call_tool = dispatcher.call_tool   # save the reference FIRST
    

    Luego se define un envoltorio que ejecuta la cadena de ganchos alrededor del original guardado e instala este en lugar de la función antigua. En el fragmento publicado, las instrucciones return result y la reasignación dispatcher.call_tool = hooked_call_tool se han fusionado en una sola línea; en realidad son dos declaraciones separadas, siendo la reasignación parte del exterior del cuerpo de la función.

    def hooked_call_tool(name, args):
        event = build_event(name, args)
        if denial := registry.run_pre(event):
            return synthetic_error_result(denial)   # PreToolUse denied
        result = original_call_tool(name, args)     # call the saved original
        registry.run_post(event, result)
        return resultdispatcher.call_tool = hooked_call_tool   # NOW replace it
    

    El envoltorio realiza cinco pasos:

    1. Convierte la llamada a la herramienta en un evento de gancho.
    2. Ejecuta cada gancho de tipo PreToolUse.
  • Si alguno de ellos lo niega, devuelva un resultado de error sintético; la herramienta real nunca se invoca.
  • De lo contrario, llame a la versión original guardada y capture su resultado.
  • Ejecute cada hook de PostToolUse para fines de auditoría, notificación y registro.
  • Guardar antes de reemplazar

    El patrón a seguir es guardar la versión original antes de reemplazarla, y esto aplica igualmente a los decoradores, middleware e interceptores. Considere qué sucede si omite la referencia guardada y escribe dispatcher.call_tool = lambda *a, **kw: dispatcher.call_tool(*a, **kw). La función lambda busca dispatcher.call_tool solo cuando se ejecuta, y para entonces ese atributo apunta a la propia lambda. El wrapper se llama a sí mismo, lo que provoca que se llame nuevamente, hasta que la primera llamada a la herramienta termina en una recursión ilimitada y un desbordamiento de pila.

    Asignar primero el original a una variable significa que el cierre mantiene una referencia al verdadero objeto de función en lugar de a un nombre que será reutilizado más tarde. Por lo tanto, el wrapper siempre accede a la implementación real.

    Por qué los ganchos superan a las reglas de prompt y a las listas estáticas de herramientas

    Existen tres formas comunes de impedir que un agente haga algo, y cada una falla de manera diferente:

    • Una instrucción de prompt como “no hagas X” puede simplemente ser ignorada cuando el modelo está incierto.
    • El filtrado de herramientas, es decir, excluir la herramienta por completo de la lista, es estático y no puede reaccionar al estado en tiempo de ejecución.
    • Los ganchos son deterministas y tienen conocimiento del estado en tiempo de ejecución.

    Tool filtering is still valuable. If the free-tier specialist never needs create_jira_ticket, do not give it that tool. A model cannot emit a tool_use block for a tool it has never been told about, and an ability the model lacks is more reliable than an instruction it might disregard.

    Filtering falls short for rules that depend on the session. Whether the user is free-tier depends on who is logged in: the agent code is identical, but different sessions are allowed different actions. A static tool list cannot express that; a hook can.

    The robust arrangement layers all three:

    1. Tool filtering at the specialist level, for tools a specialist never needs.
    2. Prompt guidance for soft preferences and general nudges.
    3. Hooks for runtime rules that must hold without exception.

    Para un enfoque similar dentro de un framework, vea cómo los límites y intermedios de LangChain abordan la misma problemática.

    Una sola interfaz de herramientas implica una única capa de aplicación

    Los ganchos son más eficaces cuando cada acción que puede realizar el agente se modela como una llamada a herramienta: delegación de subagentes, solicitudes a APIs externas, consultas a bases de datos y búsquedas en bases de conocimiento. El wrapper hooked_call_tool luego recibe todas ellas, incluidas las herramientas añadidas posteriormente, y la autorización, las políticas y la auditoría se aplican de manera uniforme en toda la interfaz de acciones.

    Si la delegación a un subagente se implementara como una llamada de función directa, la capa de ganchos nunca la detectaría. Sería necesario un segundo mecanismo de autorización solo para el enrutamiento, lo que implica dos sistemas por mantener y dos lugares donde pueden ocultarse los errores. Diseñar desde el principio una interfaz de herramienta uniforme hace que los ganchos sean un añadido menor; implementarlos posteriormente los convierte en una gran refactorización.

    Cómo se comporta una cadena de ganchos PreToolUse

    Los sistemas reales acumulan varios ganchos PreToolUse, como por ejemplo una verificación de nivel, una comprobación del período de despliegue, una coincidencia con el ID de usuario y un límite de gasto. Se ejecutan en secuencia según estas semánticas:

    • Gana la primera denegación. Interrumpe la cadena, por lo que los ganchos posteriores no se ejecutan ni tampoco la herramienta.
    • Una autorización no interrumpe el proceso. Cada gancho debe superarse para que la llamada continúe.
  • Las modificaciones se componen. El gancho B recibe la entrada tal como la reescribió el gancho A.
  • Ordene la cadena por costo y criticidad, colocando primero las verificaciones económicas y decisivas. Verificar si el usuario está en la versión gratuita implica leer un campo; verificar el límite de gasto semanal de la organización puede requerir una consulta a la base de datos. Ejecutar primero la verificación económica evita trabajo innecesario en llamadas que de todos modos serán rechazadas, al igual que colocar primero las condiciones más selectivas en una cláusula WHERE de SQL.

    Qué debe ocurrir cuando un gancho falla

    Cada tipo de gancho necesita una política de fallo diferente, y equivocarse en ella genera errores reales en lugar de problemas de estilo:

    • Los errores de PreToolUse deben propagarse. Un gancho de política que falla es un defecto. Pretender que la verificación se ejecutó cuando en realidad no lo hizo es peor que detenerla, por lo que se debe interrumpir al agente hasta que la política se corrija.
    • Los errores de PostToolUse deben ser ignorados. Un registrador de auditoría que fallece no debe interrumpir la solicitud del usuario, lo cual es más importante en ese momento que la entrada del registro. Registre el fallo en un canal separado y continúe.
    • Los errores de detención también deben ser ignorados. De todos modos la sesión está terminando, por lo que la mejor opción es registrar el fallo en la limpieza y salir.

    Un complemento de PreToolUse que captura silenciosamente sus propias excepciones representa una brecha de seguridad: la verificación no se ejecutó, la llamada se realizó y el registro de auditoría no ofrece ninguna explicación. Un complemento de PostToolUse que permite que sus excepciones se escapen convierte cada interrupción en el registro en una interrupción del agente.

    Dónde se encuentra el registro de auditoría

    Una configuración sencilla crea dos archivos de solo escritura:

    • data/audit_log.jsonl, generado por PostToolUse para cada llamada a una herramienta, e incluye el nombre de la herramienta, los datos de entrada, el resultado, una marca de error y una marca de tiempo.
    • data/session_summaries.jsonl, creado una vez por sesión mediante el hook Stop, y contiene una fila con estadísticas de alto nivel para cada conversación.

    El registro de auditoría se convierte rápidamente en el archivo más valioso del proyecto. Al estar integrado desde el principio, proporciona información para cumplir con normativas, herramientas de evaluación, análisis de costos, depuración y paneles de control a partir de una única fuente fiable. En entornos de producción, normalmente se envían los mismos registros a un almacenamiento duradero, pero la estructura de los datos permanece igual.

    Puntos clave

    • Las instrucciones de los prompts son orientativas; cualquier regla cuya violación tenga consecuencias reales debe ser implementada en el código.
  • Incluir verificaciones de consistencia de entrada en la herramienta, autorización y políticas en PreToolUse, y auditoría en PostToolUse.
  • Basar cada decisión de seguridad en el contexto congelado del lado del servidor, y tratar la entrada de herramienta del modelo como datos de solicitud no confiables.
  • Denegar las llamadas devolviendo un tool_result sintético con un prefijo claro, para que la conversación siga siendo válida y el modelo pueda recuperarse.
  • Rodear al distribuidor guardando primero la función original, y enrutar cada acción del agente a través de esa única interfaz de herramienta.
  • Permitir que los fallos en PreToolUse detengan al agente, y que los fallos en PostToolUse y Stop se registren e ignoren.
  • Lecturas relacionadas

  • Acciones de los agentes por efecto, no por verbo: lecciones de un enjambre de cinco agentes — Cómo una pequeña configuración multiagente gestionada a través de un sistema de aprobación basado en palabras clave, lo que los agentes desarrollaron por su cuenta y por qué los permisos deben describir efectos, no palabras.