#
Unidad Extra 2: Observabilidad y Evaluación de Agentes

> Puedes seguir el código en este notebook que puedes ejecutar usando Google Colab.

En este notebook, aprenderemos cómo monitorear los pasos internos (trazos) de nuestro agente de IA y evaluar su rendimiento utilizando herramientas de observabilidad de código abierto.

La capacidad de observar y evaluar el comportamiento de un agente es esencial para:

- Depurar problemas cuando las tareas fallan o producen resultados subóptimos
- Monitorear costos y rendimiento en tiempo real
- Mejorar la fiabilidad y seguridad a través de retroalimentación continua

##
Requisitos del Ejercicio 🏗️

Antes de ejecutar este notebook, asegúrate de que has:

🔲 📚 Estudiado Introducción a los Agentes

🔲 📚 Estudiado El framework smolagents

##
Paso 0: Instalar las Librerías Necesarias

Necesitaremos algunas librerías que nos permitan ejecutar, monitorear y evaluar nuestros agentes:

    %pip install 'smolagents[telemetry]'
    %pip install opentelemetry-sdk opentelemetry-exporter-otlp openinference-instrumentation-smolagents
    %pip install langfuse datasets 'smolagents[gradio]'

##
Paso 1: Instrumentar tu Agente

En este notebook, utilizaremos Langfuse como nuestra herramienta de observabilidad, pero puedes usar cualquier otro servicio compatible con OpenTelemetry. El código a continuación muestra cómo configurar variables de entorno para Langfuse (o cualquier endpoint OTel) y cómo instrumentar tu smolagent.

Nota: Si estás utilizando LlamaIndex o LangGraph, puedes encontrar documentación sobre cómo instrumentarlos aquí y aquí.

Primero, vamos a configurar la variable de entorno correcta para establecer la conexión con el endpoint OpenTelemetry de Langfuse.

    import os
    import base64

    # Obtén tus propias claves desde https://cloud.langfuse.com
    LANGFUSE_PUBLIC_KEY = = "pk-lf-..."
    LANGFUSE_SECRET_KEY = "sk-lf-..."
    os.environ["LANGFUSE_PUBLIC_KEY"] = LANGFUSE_PUBLIC_KEY
    os.environ["LANGFUSE_SECRET_KEY"] = LANGFUSE_SECRET_KEY
    os.environ["LANGFUSE_HOST"] = "https://cloud.langfuse.com"  # 🇪🇺 ejemplo de región EU
    # os.environ["LANGFUSE_HOST"] = "https://us.cloud.langfuse.com"  # 🇺🇸 ejemplo de región US

    LANGFUSE_AUTH = base64.b64encode(
        f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode()
    ).decode()

    os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = os.environ.get("LANGFUSE_HOST") + "/api/public/otel"
    os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"

También necesitamos configurar nuestro token de Hugging Face para las llamadas de inferencia.

    # Configura tu token de Hugging Face y otros tokens/secretos como variables de entorno
    os.environ["HF_TOKEN"] = "hf_..."

A continuación, podemos configurar un proveedor de trazoss para nuestro OpenTelemetry configurado.

    from opentelemetry.sdk.trace import TracerProvider
    from openinference.instrumentation.smolagents import SmolagentsInstrumentor
    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
    from opentelemetry.sdk.trace.export import SimpleSpanProcessor

    # Crear un TracerProvider para OpenTelemetry
    trace_provider = TracerProvider()

    # Añadir un SimpleSpanProcessor con el OTLPSpanExporter para enviar trazoss
    trace_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))

    # Establecer el proveedor de trazas predeterminado global
    from opentelemetry import trace
    trace.set_tracer_provider(trace_provider)
    tracer = trace.get_tracer(__name__)

    # Instrumentar smolagents con el proveedor configurado
    SmolagentsInstrumentor().instrument(tracer_provider=trace_provider)

##
Paso 2: Probar tu Instrumentación

Aquí hay un simple CodeAgent de smolagents que calcula `1+1`. Lo ejecutamos para confirmar que la instrumentación está funcionando correctamente. Si todo está configurado correctamente, verás registros/spans en tu panel de observabilidad.

    from smolagents import InferenceClientModel, CodeAgent

    # Crear un agente simple para probar la instrumentación
    agent = CodeAgent(
        tools=[],
        model=InferenceClientModel()
    )

    agent.run("1+1=")

Revisa tu Panel de rastros de Langfuse (o tu herramienta de observabilidad elegida) para confirmar que los spans y registros han sido grabados.

Captura de pantalla de ejemplo de Langfuse:

Enlace a los rastros

##
Paso 3: Observar y Evaluar un Agente Más Complejo

Ahora que has confirmado que tu instrumentación funciona, probemos una consulta más compleja para ver cómo se rastrean las métricas avanzadas (uso de tokens, latencia, costos, etc.).

    from smolagents import (CodeAgent, DuckDuckGoSearchTool, InferenceClientModel)

    search_tool = DuckDuckGoSearchTool()
    agent = CodeAgent(tools=[search_tool], model=InferenceClientModel())

    agent.run("¿Cuántos cubos de Rubik podrías meter dentro de la Catedral de Notre Dame?")

###
Estructura de Rastros

La mayoría de las herramientas de observabilidad registran una rastro que contiene spans, que representan cada paso de la lógica de tu agente. Aquí, el rastro contiene la ejecución general del agente y sub-spans para:

- Las llamadas a herramientas (DuckDuckGoSearchTool)
- Las llamadas al LLM (InferenceClientModel)

Puedes inspeccionarlos para ver precisamente dónde se gasta el tiempo, cuántos tokens se utilizan, etc.:

Enlace a los rastros

##
Evaluación en Línea

En la sección anterior, aprendimos sobre la diferencia entre evaluación en línea y fuera de línea. Ahora, veremos cómo monitorear tu agente en producción y evaluarlo en vivo.

###
Métricas Comunes para Seguir en Producción

1. Costos — La instrumentación de smolagents captura el uso de tokens, que puedes transformar en costos aproximados asignando un precio por token.
2. Latencia — Observa el tiempo que toma completar cada paso, o la ejecución completa.
3. Retroalimentación del Usuario — Los usuarios pueden proporcionar retroalimentación directa (pulgar arriba/abajo) para ayudar a refinar o corregir el agente.
4. LLM-como-Juez — Utiliza un LLM separado para evaluar la salida de tu agente en tiempo casi real (por ejemplo, verificando toxicidad o corrección).

A continuación, mostramos ejemplos de estas métricas.

####
1. Costos

A continuación se muestra una captura de pantalla que muestra el uso para llamadas a `Qwen2.5-Coder-32B-Instruct`. Esto es útil para ver pasos costosos y optimizar tu agente.

Enlace a los rastros

####
2. Latencia

También podemos ver cuánto tiempo tomó completar cada paso. En el ejemplo a continuación, toda la conversación tomó 32 segundos, que puedes desglosar por paso. Esto te ayuda a identificar cuellos de botella y optimizar tu agente.

Enlace a los rastros

####
3. Atributos Adicionales

También puedes pasar atributos adicionales, como IDs de usuario, IDs de sesión o etiquetas, configurándolos en los spans. Por ejemplo, la instrumentación de smolagents utiliza OpenTelemetry para adjuntar atributos como `langfuse.user.id` o etiquetas personalizadas.

    from smolagents import (CodeAgent, DuckDuckGoSearchTool, InferenceClientModel)
    from opentelemetry import trace

    search_tool = DuckDuckGoSearchTool()
    agent = CodeAgent(
        tools=[search_tool],
        model=InferenceClientModel()
    )

    with tracer.start_as_current_span("Smolagent-Trace") as span:
        span.set_attribute("langfuse.user.id", "smolagent-user-123")
        span.set_attribute("langfuse.session.id", "smolagent-session-123456789")
        span.set_attribute("langfuse.tags", ["city-question", "testing-agents"])

        agent.run("¿Cuál es la capital de Alemania?")

####
4. Retroalimentación del Usuario

Si tu agente está integrado en una interfaz de usuario, puedes registrar la retroalimentación directa del usuario (como un pulgar arriba/abajo en una interfaz de chat). A continuación se muestra un ejemplo utilizando Gradio para integrar un chat con un mecanismo de retroalimentación simple.

En el fragmento de código a continuación, cuando un usuario envía un mensaje de chat, capturamos el ID de traza de OpenTelemetry. Si al usuario le gusta/no le gusta la última respuesta, adjuntamos una puntuación a la traza.

    import gradio as gr
    from opentelemetry.trace import format_trace_id
    from smolagents import (CodeAgent, InferenceClientModel)
    from langfuse import Langfuse

    langfuse = Langfuse()
    model = InferenceClientModel()
    agent = CodeAgent(tools=[], model=model, add_base_tools=True)

    formatted_trace_id = None  # Almacenaremos el trace_id actual globalmente para demostración

    def respond(prompt, history):
        with trace.get_tracer(__name__).start_as_current_span("Smolagent-Trace") as span:
            output = agent.run(prompt)

            current_span = trace.get_current_span()
            span_context = current_span.get_span_context()
            trace_id = span_context.trace_id
            global formatted_trace_id
            formatted_trace_id = str(format_trace_id(trace_id))
            langfuse.trace(id=formatted_trace_id, input=prompt, output=output)

        history.append({"role": "assistant", "content": str(output)})
        return history

    def handle_like(data: gr.LikeData):
        # Para demostración, mapeamos la retroalimentación del usuario a un 1 (me gusta) o 0 (no me gusta)
        if data.liked:
            langfuse.score(
                value=1,
                name="user-feedback",
                trace_id=formatted_trace_id
            )
        else:
            langfuse.score(
                value=0,
                name="user-feedback",
                trace_id=formatted_trace_id
            )

    with gr.Blocks() as demo:
        chatbot = gr.Chatbot(label="Chat", type="messages")
        prompt_box = gr.Textbox(placeholder="Escribe tu mensaje...", label="Tu mensaje")

        # Cuando el usuario presiona 'Enter' en el prompt, ejecutamos 'respond'
        prompt_box.submit(
            fn=respond,
            inputs=[prompt_box, chatbot],
            outputs=chatbot
        )

        # Cuando el usuario hace clic en un botón de 'me gusta' en un mensaje, ejecutamos 'handle_like'
        chatbot.like(handle_like, None, None)

    demo.launch()

La retroalimentación del usuario se captura entonces en tu herramienta de observabilidad:

####
5. LLM-como-Juez

LLM-como-Juez es otra forma de evaluar automáticamente la salida de tu agente. Puedes configurar una llamada LLM separada para medir la corrección, toxicidad, estilo o cualquier otro criterio que te importe.

Flujo de trabajo:

1. Defines una Plantilla de Evaluación, por ejemplo, “Verifica si el texto es tóxico”.
2. Cada vez que tu agente genera una salida, pasas esa salida a tu LLM “juez” con la plantilla.
3. El LLM juez responde con una calificación o etiqueta que registras en tu herramienta de observabilidad.

Ejemplo de Langfuse:

    # Ejemplo: Verificar si la salida del agente es tóxica o no.
    from smolagents import (CodeAgent, DuckDuckGoSearchTool, InferenceClientModel)

    search_tool = DuckDuckGoSearchTool()
    agent = CodeAgent(tools=[search_tool], model=InferenceClientModel())

    agent.run("¿Puede comer zanahorias mejorar tu visión?")

Puedes ver que la respuesta de este ejemplo se juzga como “no tóxica”.

####
6. Resumen de Métricas de Observabilidad

Todas estas métricas pueden visualizarse juntas en paneles. Esto te permite ver rápidamente cómo se desempeña tu agente a través de muchas sesiones y te ayuda a seguir las métricas de calidad a lo largo del tiempo.

##
Evaluación fuera de línea

La evaluación en línea es esencial para la retroalimentación en vivo, pero también necesitas evaluación fuera de línea—verificaciones sistemáticas antes o durante el desarrollo. Esto ayuda a mantener la calidad y fiabilidad antes de implementar cambios en producción.

###
Evaluación con Conjuntos de Datos

En la evaluación fuera de línea, típicamente:

1. Tienes un conjunto de datos de referencia (con pares de prompt y salida esperada)
2. Ejecutas tu agente en ese conjunto de datos
3. Comparas las salidas con los resultados esperados o utilizas un mecanismo de puntuación adicional

A continuación, demostramos este enfoque con el conjunto de datos GSM8K, que contiene preguntas matemáticas y soluciones.

    import pandas as pd
    from datasets import load_dataset

    # Obtener GSM8K desde Hugging Face
    dataset = load_dataset("openai/gsm8k", 'main', split='train')
    df = pd.DataFrame(dataset)
    print("Primeras filas del conjunto de datos GSM8K:")
    print(df.head())

A continuación, creamos una entidad de conjunto de datos en Langfuse para rastrear las ejecuciones. Luego, agregamos cada elemento del conjunto de datos al sistema. (Si no estás utilizando Langfuse, podrías simplemente almacenar estos en tu propia base de datos o archivo local para análisis).

    from langfuse import Langfuse
    langfuse = Langfuse()

    langfuse_dataset_name = "gsm8k_dataset_huggingface"

    # Crear un conjunto de datos en Langfuse
    langfuse.create_dataset(
        name=langfuse_dataset_name,
        description="Conjunto de datos de referencia GSM8K cargado desde Huggingface",
        metadata={
            "date": "2025-03-10",
            "type": "benchmark"
        }
    )

    for idx, row in df.iterrows():
        langfuse.create_dataset_item(
            dataset_name=langfuse_dataset_name,
            input={"text": row["question"]},
            expected_output={"text": row["answer"]},
            metadata={"source_index": idx}
        )
        if idx >= 9: # Cargar solo los primeros 10 elementos para demostración
            break

####
Ejecutando el Agente en el Conjunto de Datos

Definimos una función auxiliar `run_smolagent()` que:

1. Inicia un span de OpenTelemetry
2. Ejecuta nuestro agente en el prompt
3. Registra el ID de traza en Langfuse

Luego, recorremos cada elemento del conjunto de datos, ejecutamos el agente y vinculamos el rastro al elemento del conjunto de datos. También podemos adjuntar una puntuación de evaluación rápida si lo deseamos.

    from opentelemetry.trace import format_trace_id
    from smolagents import (CodeAgent, InferenceClientModel, LiteLLMModel)

    # Ejemplo: usando InferenceClientModel o LiteLLMModel para acceder a modelos de openai, anthropic, gemini, etc.:
    model = InferenceClientModel()

    agent = CodeAgent(
        tools=[],
        model=model,
        add_base_tools=True
    )

    def run_smolagent(question):
        with tracer.start_as_current_span("Smolagent-Trace") as span:
            span.set_attribute("langfuse.tag", "dataset-run")
            output = agent.run(question)

            current_span = trace.get_current_span()
            span_context = current_span.get_span_context()
            trace_id = span_context.trace_id
            formatted_trace_id = format_trace_id(trace_id)

            langfuse_trace = langfuse.trace(
                id=formatted_trace_id,
                input=question,
                output=output
            )
        return langfuse_trace, output

    dataset = langfuse.get_dataset(langfuse_dataset_name)

    # Ejecutar nuestro agente contra cada elemento del conjunto de datos (limitado a los primeros 10 arriba)
    for item in dataset.items:
        langfuse_trace, output = run_smolagent(item.input["text"])

        # Vincular la traza al elemento del conjunto de datos para análisis
        item.link(
            langfuse_trace,
            run_name="smolagent-notebook-run-01",
            run_metadata={ "model": model.model_id }
        )

        # Opcionalmente, almacenar una puntuación de evaluación rápida para demostración
        langfuse_trace.score(
            name="<example_eval>",
            value=1,
            comment="Este es un comentario"
        )

    # Vaciar datos para asegurar que toda la telemetría sea enviada
    langfuse.flush()

Puedes repetir este proceso con diferentes:

- Modelos (OpenAI GPT, LLM local, etc.)
- Herramientas (búsqueda vs. sin búsqueda)
- Prompts (diferentes mensajes de sistema)

Luego compararlos lado a lado en tu herramienta de observabilidad:

##
Consideraciones Finales

En este notebook, cubrimos cómo:

1. Configurar la Observabilidad usando smolagents + exportadores OpenTelemetry
2. Verificar la Instrumentación ejecutando un agente simple
3. Capturar Métricas Detalladas (costo, latencia, etc.) a través de herramientas de observabilidad
4. Recopilar Retroalimentación del Usuario a través de una interfaz Gradio
5. Usar LLM-como-Juez para evaluar automáticamente las salidas
6. Realizar Evaluación Fuera de Línea con un conjunto de datos de referencia

🤗 ¡Feliz programación!

Update on GitHub