Inicio / Artículos

NOVA: clasificador de tickets con Jev, reglas de Python y un agente LangChain

Un asistente de soporte de nivel 1 que separa el análisis de la solicitud con Jev, las reglas de negocio en Python y la redacción de un boceto por un agente de LangChain.

Editor: REACTAPP.TOP10 min de lectura2331 palabras

Fuentes: Trier les tickets avec Jev, appliquer ses règles en Python et laisser le LLM préparer la réponse : un tutoriel concret avec NOVA.

Un cliente escribió: «Mi pedido aún no ha llegado. Ya han pasado tres recordatorios, y se necesita para mañana». Antes de redactar una respuesta, un agente de soporte debe resolver varias cuestiones: ¿A qué equipo corresponde la solicitud? ¿Debe tratarse sin demora? ¿El tono revela una verdadera frustración? Y, sobre todo, ¿qué se puede verificar realmente sobre este pedido?

Un único prompt extenso entregado a un LLM podría manejar todo. Otro enfoque divide el trabajo en tres etapas: Jev lee el ticket y realiza un análisis, un código Python aplica las reglas de la empresa, y luego un agente de LangChain busca información con sus herramientas y elabora un borrador.

El proyecto descrito aquí se llama NOVA. Se trata de un asistente de primer nivel para el soporte de Atelier Nova, una marca ficticia que vende accesorios de oficina. El código completo y el notebook se encuentran en el repositorio GitHub Langchain-Jev, y los fragmentos que siguen explican únicamente las partes centrales. Para ejecutar todo, basta con abrir el notebook y ejecutar las celdas en orden.

Jev decide, otro modelo escribe

En esta configuración, Jev, desarrollado por TypeSafe, toma decisiones estructuradas a partir de una situación descrita en lenguaje natural. No se le pide que responda al cliente. Las preguntas que se le formulan son más bien de este tipo:

  • ¿Qué servicio debe encargarse del ticket?
  • ¿Los hechos relatados requieren atención inmediata?
  • ¿Qué grado de frustración expresa el cliente?

Las respuestas llegan en campos de tipo específico que el programa lee directamente. El LLM generativo interviene solo después, para consultar la información disponible y preparar un texto adecuado.

La ventaja de esta separación radica en la visibilidad de las reglas. Cambiar el umbral que desencadena una revisión humana equivale a modificar una condición en Python, sin tener que reescribir todo el prompt del agente. Por otro lado, nada demuestra por sí solo que el conjunto resultante sea más rápido o económico. Solo una medición del flujo de trabajo completo, incluyendo las llamadas a Jev y las llamadas al LLM, permitiría afirmarlo.

El camino de un ticket antes de escribir código

Aquí se muestra el recorrido que sigue un mensaje.

Message client + historique
            ↓
Jev : service, urgence, frustration
            ↓
Python : validation et règles métier
            ↓
Agent LangChain + LLM
            ↓
Consultation des commandes et procédures
            ↓
Dossier pour l’équipe support + brouillon de réponse

Jev se ejecuta antes del ciclo del agente, de modo que su ejecución nunca depende de una posible elección de herramienta realizada por el LLM. A continuación, el programa calcula una orientación compuesta por el servicio seleccionado, la prioridad del ticket y la necesidad de una revisión humana. Esta orientación se incluye en el contexto del agente, junto con dos herramientas de solo lectura.

No se le encomienda ninguna acción a NOVA: ni envío de mensajes, ni reembolsos, ni modificación de pedidos. Su función consiste en preparar el trabajo de los agentes humanos, y este alcance restringido es intencional.

Configuración del entorno

El notebook se basa principalmente en langchain, langchain-core, langchain-openai, langchain-typesafe y python-dotenv. La integración con TypeSafe está fijada en la versión 0.0.1a3, y es mejor conservar las versiones de la celda de instalación para reproducir el ejemplo.

Se necesitan dos claves API, que deben guardarse en un archivo .env:

TYPESAFE_API_KEY=ta_cle_typesafe
OPENAI_API_KEY=ta_cle_openai
JEV_MODEL=jev-latest
OPENAI_MODEL=gpt-4.1-mini

Este archivo nunca debe publicarse en GitHub, y las claves no deben aparecer en las salidas del notebook. Una vez cargadas las variables de entorno, los dos modelos se inicializan de la siguiente manera:

jev = TypeSafeClassifier(
    model=os.getenv("JEV_MODEL") or "jev-latest",
    timeout=30,
)
modele = ChatOpenAI(
    model=os.getenv("OPENAI_MODEL") or "gpt-4.1-mini",
    timeout=60,
    max_retries=1,
)

Estos ajustes corresponden al portátil, y no hay obligación de utilizar este LLM en otro lugar. El alias jev-latest también puede cambiar. Para comparar resultados a lo largo del tiempo, es mejor conservar la versión exacta del modelo cuando sea posible.

Tres primitivas: Choice, Noul y Score

La forma más sencilla de comprender a Jev es partir de las preguntas que se le hacen.

Choice: seleccionar un servicio

Choice selecciona una categoría entre las que se definen. Aquí hay cuatro: entrega, facturación, técnica y otra. Cada una cuenta con una descripción que especifica qué debe abarcar la clasificación. Un paquete retrasado se asigna a entrega, un pago impugnado a facturación, un dispositivo defectuoso al soporte técnico. La categoría “otra” incluye las solicitudes ambiguas o fuera del alcance, en lugar de forzarlas en un servicio que no es adecuado.

Noul: una pregunta de sí o no

Noul indica la probabilidad de una respuesta positiva, entre 0 y 1. La pregunta que se plantea es si los hechos descritos requieren atención inmediata en lugar de un tratamiento habitual. La instrucción especifica que no se debe confiar únicamente en el tono, ya que un cliente puede estar muy insatisfecho sin que su situación exija una intervención inmediata, y una solicitud tranquila puede ser, por el contrario, urgente. El resultado sigue siendo una estimación basada en el contexto proporcionado. No indica si un paquete llegará mañana mientras falten información fiables sobre la entrega.

Puntuación: situar la frustración en una escala

Con Puntuación, se definen niveles ordenados:

  • nivel bajo: sin signos de insatisfacción;
  • nivel intermedio: cierto fastidio, expresado con moderación;
  • nivel alto: ira marcada o reclamaciones que se acumulan.

La escala utilizada abarca el rango de 0 a 2, con valores decimales posibles: la respuesta no es simplemente una etiqueta de calma o enojo. Las tres preguntas se combinan en una sola definición.

from langchain_typesafe import Choice, Noul, Score
def questions_triage():
    return {
        "service": Choice(
            instructions=(
                "Quel service doit traiter en priorité la dernière demande du client ? "
                "Utilise le contexte seulement pour comprendre cette demande."
            ),
            criteria={
                "livraison": "Retard, suivi ou réception d'une commande.",
                "facturation": "Paiement, facture ou remboursement.",
                "technique": "Panne ou utilisation d'un produit.",
                "autre": "Demande ambiguë ou sans rapport avec les catégories précédentes.",
            },
        ),
        "urgence": Noul(
            instructions=(
                "Les faits décrits nécessitent-ils une prise en charge immédiate, "
                "plutôt qu'un traitement normal ? Ne te fonde pas seulement sur le ton."
            )
        ),
        "frustration": Score(
            instructions="Quel niveau de frustration le client exprime-t-il ?",
            criteria=[
                "Le client s'exprime calmement, sans insatisfaction.",
                "Le client exprime une insatisfaction tout en restant mesuré.",
                "Le client exprime une forte colère ou des réclamations répétées.",
            ],
        ),
    }

Cada clave (servicio, urgencia, frustración) permite buscar la respuesta correspondiente en el código. Lo que se evalúa se basa en las instrucciones y en los criterios asociados.

Un primer ticket para analizar

El mensaje utilizado en el cuaderno de notas es el siguiente: AN-1001 aún no ha sido entregado; esta es la tercera solicitud de seguimiento. Es necesario entregarlo para mañana. ¿Qué se puede hacer?

En cuanto a la preparación, la función requete_triage reúne el mensaje y su contexto, limitando su longitud. Solo los tres últimos mensajes del cliente en el historial se envían a Jev, cortándolos si es necesario. La llamada y la lectura de los resultados son similares a lo que sigue.

reponse_jev = jev.invoke(requete_triage(ticket))
print("Service :", reponse_jev.choices["service"].choice)
print("Urgence :", reponse_jev.nouls["urgence"].noul)
print("Frustration sur 2 :", reponse_jev.scores["frustration"].score)

La salida registrada en el cuaderno de notas muestra:

Service : livraison
Urgence : 0.76
Frustration sur 2 : 1.93

Estos valores provienen de una ejecución guardada y no garantizan nada para las llamadas posteriores.

Otro campo que merece atención es la confianza. En el primer ejemplo, la clasificación de entrega recibe una confianza de 1.0, lo cual no significa que el dispositivo sea infalible. En TypeSafe, este valor deriva de la distribución de probabilidades entre las respuestas posibles. Indica hasta qué punto el modelo favorece una respuesta, no si dicha respuesta se ha basado en los hechos reales.

Las reglas de negocio permanecen en el programa

Antes de utilizar los resultados, normalizar_analyse isola los campos necesarios y verifica, en particular, que el servicio figure efectivamente en las proceduras y que el nivel de frustración cumpla con la escala prevista. El resultado es un diccionario de Python fácil de leer. La puntuación de frustración no se convierte a otra escala, y nada aquí demuestra que el análisis sea correcto.

Luego se aplican las reglas:

def orienter_ticket(analyse, seuil_urgence=0.8, seuil_confiance=0.6):
    raisons = []
    if analyse["urgence"] >= seuil_urgence:
        raisons.append("urgence élevée")
    if analyse["frustration"] >= 1.5:
        raisons.append("forte frustration")
    if analyse["confiance_service"] < seuil_confiance:
        raisons.append("service incertain")
    if analyse["service"] == "autre":
        raisons.append("demande à clarifier")    return {
        "service": analyse["service"],
        "priorite": "haute" if analyse["urgence"] >= seuil_urgence else "normale",
        "revue_humaine": bool(raisons),
        "raisons": raisons or ["traitement courant"],
    }

Los umbrales son de carácter pedagógico. Un equipo real los calibraría con una muestra representativa de tickets, identificando los errores en la derivación y las urgencias que pasaron desapercibidas. La orientación obtenida durante la prueba completa registrada es la siguiente:

{
  "service": "livraison",
  "priorite": "normale",
  "revue_humaine": true,
  "raisons": ["forte frustration"]
}

Por lo tanto, una prioridad normal puede coexistir con una revisión humana. La prioridad depende aquí de la señal de emergencia, mientras que la frustración puede desencadenar una nueva revisión. Estas dos dimensiones se tratan por separado por elección.

Información de referencia

La demostración se basa en un simple diccionario de pedidos erróneos. AN-1001 designa allí una lámpara de escritorio que aún está en tránsito y que presenta un retraso de tres días. El agente dispone de dos herramientas:

  • consulter_commande(numero) permite encontrar un pedido a partir de su referencia;
  • consulter_procedure(service) muestra la procedura relacionada con el servicio seleccionado.

El procedimiento de entrega exige verificar el estado, proponer una verificación por parte del equipo logístico en caso de retraso y nunca inventar una fecha de entrega. En cuanto a la facturación, todo reembolso debe pasar por una validación humana. Desde el punto de vista técnico, el agente debe anotar el producto afectado, el problema detectado y las pruebas ya realizadas.

La instrucción de NOVA describe su misión y establece un formato de salida: resumen de la dificultad, información verificada o faltante, próxima acción recomendada y boceto de respuesta. Sin embargo, indicar en una instrucción que está prohibido reembolsar no es suficiente para proteger una herramienta de pago. Por lo tanto, la restricción también es física: no se muestra ninguna herramienta de reembolso al agente.

Montaje del agente LangChain

La función create_agent recibe el LLM, las herramientas y un prompt enriquecido mediante el análisis de Jev y la orientación calculada.

def creer_agent(modele, analyse, orientation):
    contexte = json.dumps(
        {"analyse_jev": analyse, "orientation": orientation},
        ensure_ascii=False,
    )
    return create_agent(
        model=modele,
        tools=[consulter_commande, consulter_procedure],
        system_prompt=(
            ROLE_NOVA
            + "\nContexte de traitement fourni par le programme :\n"
            + contexte
        ),
    )

Por lo tanto, el análisis y la orientación no son parámetros propios de create_agent, ya que la función interna los incluye en las instrucciones básicas. La función traiter_ticket coordina todo: llama a Jev, normaliza el análisis, calcula la orientación, construye el agente y inicia su bucle con un límite de recursión.

El código establece el orden de las etapas, pero la utilización de las herramientas de consulta durante el bucle queda a discreción del LLM. Si una consulta debe volverse obligatoria en producción, es mejor incluirla en el flujo de trabajo en lugar de depender únicamente de una instrucción simple.

Lo que demuestran las pruebas

El registro conservado para la primera solicitud muestra que el agente utilizó primero “consultar_commande” y luego “consultar_procedure”. Identificó la lámpara, comprobó que se encontraba en tránsito y anotó los tres días de retraso. Recomendó una verificación por parte del departamento de logística sin establecer una fecha de entrega ficticia, y luego redactó un borrador para el equipo de soporte.

Este borrador requiere ser revisado. Frases como “vamos a transmitir su expediente” comprometen al equipo a realizar una acción que el programa no ha llevado a cabo, y una recomendación no equivale a una transferencia efectiva.

El segundo intento se refiere a una solicitud de seguimiento, en la que el cliente pregunta qué artículo contiene ese pedido. Como no repite la referencia, se transmite el historial del primer tratamiento.

suivi = traiter_ticket(
    "Quel article contient cette commande ?",
    modele,
    jev,
    historique=dossier["messages"],
)
print(suivi["reponse"])

En la salida registrada, se encuentra la referencia AN-1001 sin que el cliente la repita, y la respuesta indica una lámpara de escritorio. No apareció ninguna memoria persistente por sí sola: el programa simplemente volvió a mostrar los mensajes anteriores. Para otro cliente, es necesario partir de un historial distinto, ya que mezclar las conversaciones expondría la información de una persona a otra.

Antes de un despliegue real

La demostración funciona con datos ficticios y se limita a bocetos. Antes de conectarla a un soporte real, son necesarias varias etapas:

  • evaluar el filtrado en tickets anotados, incluyendo en francés, y luego decidir el comportamiento a adoptar si Jev o el LLM no responden;
  • verificar quién tiene permiso para leer qué comandos, y separar las conversaciones;
  • Reducir al mínimo necesario los datos personales que se envían a los modelos;
  • Conservar registros útiles sin exponer las claves API.

La bandera revue_humaine con valor true aquí sirve solo como información para el agente. Si el dispositivo realmente envía mensajes o realiza operaciones sensibles, esta información debe activar un proceso de aprobación efectivo.

El principio que debe recordarse desde ahora es la división de tareas. El análisis estructurado proviene de Jev, la decisión sobre su uso corresponde a Python, y el LLM se encarga de redactar la respuesta basándose en las herramientas. Para ir más allá, basta con añadir casos variados: tickets de facturación, una solicitud ambigua, un mensaje cortés pero urgente. Se observa la clasificación y se verifica que las reglas conduzcan al tratamiento esperado, lo que revela tanto las capacidades del agente como lo que aún queda por corregir.