This entry is part 3 of 25 in the series Introducción a Microsoft Semantic Kernel

Introducción

La clasificación de intenciones es fundamental en aplicaciones conversacionales. Permite determinar qué quiere hacer el usuario basándose en su mensaje. En este tutorial aprenderás a implementar un sistema robusto de clasificación de intenciones usando Semantic Kernel y Azure OpenAI.

¿Qué es la Clasificación de Intenciones?

La clasificación de intenciones determina la acción que el usuario desea realizar. Por ejemplo:

  • “Quiero comprar un libro” → Intención: comprar
  • “¿Cuál es el estado de mi pedido?” → Intención: consultar_estado
  • “Necesito ayuda” → Intención: solicitar_ayuda

Arquitectura del Sistema

Un clasificador de intenciones efectivo tiene estos componentes:

  1. Servicio de Clasificación: Procesa el mensaje y determina la intención
  2. Sistema de Prompts: Define las instrucciones para el LLM
  3. Modelo de Resultado: Estructura que contiene la intención y metadatos
  4. Guardrails: Validaciones para garantizar resultados confiables

Implementación del Servicio

Modelo de Resultado

public record IntentClassificationResult
{
    public required string Intent { get; init; }
    public double Confidence { get; init; }
    public Dictionary<string, object>? Entities { get; init; }
    public string? Category { get; init; }
}

// Constantes para intenciones
public static class Intents
{
    public const string BuyProduct = "buy_product";
    public const string CheckStatus = "check_status";
    public const string RequestHelp = "request_help";
    public const string CancelOrder = "cancel_order";
    public const string Unknown = "unknown";
}

Servicio de Clasificación

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.Extensions.Logging;
using System.Text.Json;

public class IntentClassificationService
{
    private readonly Kernel _kernel;
    private readonly ILogger<IntentClassificationService> _logger;
    
    private const double MinimumConfidenceThreshold = 0.65;
    
    public IntentClassificationService(
        Kernel kernel,
        ILogger<IntentClassificationService> logger)
    {
        _kernel = kernel;
        _logger = logger;
    }
    
    public async Task<IntentClassificationResult> ClassifyAsync(
        string messageText,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(messageText))
        {
            return CreateUnknownResult("Mensaje vacío");
        }
        
        try
        {
            var chatService = _kernel.GetRequiredService<IChatCompletionService>();
            
            var systemPrompt = BuildSystemPrompt();
            var userPrompt = BuildUserPrompt(messageText);
            
            var chatHistory = new ChatHistory();
            chatHistory.AddSystemMessage(systemPrompt);
            chatHistory.AddUserMessage(userPrompt);
            
            // Configuración para respuesta JSON estructurada
            var settings = new OpenAIPromptExecutionSettings
            {
                Temperature = 0.1,  // Baja temperatura para clasificación consistente
                ResponseFormat = "json_object"
            };
            
            var response = await chatService.GetChatMessageContentAsync(
                chatHistory,
                settings,
                _kernel,
                cancellationToken);
            
            var jsonContent = response.Content ?? string.Empty;
            _logger.LogDebug("Respuesta de clasificación: {Response}", jsonContent);
            
            var result = ParseClassificationResponse(jsonContent, messageText);
            result = ApplyGuardrails(result, messageText);
            
            _logger.LogInformation(
                "Intención clasificada: {Intent} (confianza: {Confidence:F2})",
                result.Intent, result.Confidence);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error clasificando intención");
            return CreateUnknownResult("Error en clasificación");
        }
    }
    
    private string BuildSystemPrompt()
    {
        return """
        Eres un clasificador experto de intenciones para un sistema de e-commerce.
        
        Tu tarea es determinar QUÉ quiere hacer el usuario.
        
        Tipos de intención (en orden de prioridad):
        1. **buy_product**: Usuario quiere comprar algo (ej: "quiero comprar un libro", "añadir al carrito")
        2. **check_status**: Usuario consulta el estado de algo (ej: "¿dónde está mi pedido?", "estado de mi compra")
        3. **cancel_order**: Usuario quiere cancelar (ej: "cancelar mi pedido", "no quiero el producto")
        4. **request_help**: Usuario pide ayuda o información (ej: "ayuda", "¿cómo funciona?", "soporte")
        5. **unknown**: No está clara la intención
        
        REGLAS CRÍTICAS:
        - Analiza el CONTEXTO completo del mensaje
        - Si detectas producto mencionado en compra, extrae el nombre en "product_name"
        - Si hay número de pedido, extráelo en "order_id"
        - Confidence debe ser 0.0-1.0 basado en tu certeza
        - Si la intención es ambigua, usa "unknown"
        
        Responde SOLO con JSON válido:
        {
          "intent": "buy_product|check_status|cancel_order|request_help|unknown",
          "confidence": 0.0-1.0,
          "entities": {
            "product_name": "string|null",
            "order_id": "string|null",
            "quantity": number|null
          },
          "category": "string|null"
        }
        
        Ejemplos:
        - "Quiero comprar tres camisetas" → {"intent":"buy_product","confidence":0.95,"entities":{"product_name":"camisetas","quantity":3}}
        - "¿Dónde está mi pedido 12345?" → {"intent":"check_status","confidence":0.98,"entities":{"order_id":"12345"}}
        - "Ayuda con mi cuenta" → {"intent":"request_help","confidence":0.90,"entities":{}}
        """;
    }
    
    private string BuildUserPrompt(string messageText)
    {
        return $"""
        Clasifica la intención de este mensaje:
        
        Mensaje: "{messageText}"
        
        Responde con JSON válido siguiendo el esquema.
        """;
    }
    
    private IntentClassificationResult ParseClassificationResponse(
        string jsonContent,
        string originalText)
    {
        try
        {
            // Limpiar respuesta de markdown si existe
            jsonContent = jsonContent.Trim();
            if (jsonContent.StartsWith("```json"))
                jsonContent = jsonContent[7..];
            if (jsonContent.StartsWith("```"))
                jsonContent = jsonContent[3..];
            if (jsonContent.EndsWith("```"))
                jsonContent = jsonContent[..^3];
            jsonContent = jsonContent.Trim();
            
            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            
            var parsed = JsonSerializer.Deserialize<JsonClassificationResult>(
                jsonContent, options);
            
            if (parsed == null)
                throw new JsonException("Resultado nulo");
            
            return new IntentClassificationResult
            {
                Intent = NormalizeIntent(parsed.Intent),
                Confidence = parsed.Confidence,
                Entities = parsed.Entities,
                Category = parsed.Category
            };
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error parseando JSON: {Json}", jsonContent);
            return CreateUnknownResult("Error parsing JSON");
        }
    }
    
    private IntentClassificationResult ApplyGuardrails(
        IntentClassificationResult result,
        string originalText)
    {
        // Guardrail 1: Confianza baja → marcar como unknown
        if (result.Confidence < MinimumConfidenceThreshold)
        {
            _logger.LogInformation(
                "Confianza baja ({Confidence:F2}) para '{Text}', marcando como unknown",
                result.Confidence, originalText);
            
            return result with { Intent = Intents.Unknown };
        }
        
        // Guardrail 2: Validar entidades requeridas
        if (result.Intent == Intents.BuyProduct)
        {
            if (result.Entities == null || 
                !result.Entities.ContainsKey("product_name"))
            {
                _logger.LogWarning(
                    "buy_product sin product_name para '{Text}'", originalText);
                return result with { Intent = Intents.Unknown };
            }
        }
        
        return result;
    }
    
    private string NormalizeIntent(string? intent)
    {
        if (string.IsNullOrWhiteSpace(intent))
            return Intents.Unknown;
        
        return intent.ToLowerInvariant().Replace("_", "").Replace("-", "") switch
        {
            "buyproduct" or "buy" or "purchase" => Intents.BuyProduct,
            "checkstatus" or "status" => Intents.CheckStatus,
            "cancelorder" or "cancel" => Intents.CancelOrder,
            "requesthelp" or "help" or "ayuda" => Intents.RequestHelp,
            _ => Intents.Unknown
        };
    }
    
    private IntentClassificationResult CreateUnknownResult(string reason)
    {
        _logger.LogInformation("Creando resultado unknown: {Reason}", reason);
        
        return new IntentClassificationResult
        {
            Intent = Intents.Unknown,
            Confidence = 0.0,
            Entities = null,
            Category = null
        };
    }
    
    // Modelo interno para deserialización
    private class JsonClassificationResult
    {
        public string? Intent { get; set; }
        public double Confidence { get; set; }
        public Dictionary<string, object>? Entities { get; set; }
        public string? Category { get; set; }
    }
}

Uso del Clasificador

Ejemplo Básico

var classifier = new IntentClassificationService(kernel, logger);

var result = await classifier.ClassifyAsync("Quiero comprar un libro de programación");

Console.WriteLine($"Intención: {result.Intent}");
Console.WriteLine($"Confianza: {result.Confidence:F2}");

if (result.Entities != null && result.Entities.ContainsKey("product_name"))
{
    Console.WriteLine($"Producto: {result.Entities["product_name"]}");
}

Integración con Router

public class MessageRouter
{
    private readonly IntentClassificationService _classifier;
    private readonly Dictionary<string, Func<IntentClassificationResult, Task<string>>> _handlers;
    
    public MessageRouter(IntentClassificationService classifier)
    {
        _classifier = classifier;
        _handlers = new()
        {
            [Intents.BuyProduct] = HandleBuyProduct,
            [Intents.CheckStatus] = HandleCheckStatus,
            [Intents.CancelOrder] = HandleCancelOrder,
            [Intents.RequestHelp] = HandleRequestHelp,
            [Intents.Unknown] = HandleUnknown
        };
    }
    
    public async Task<string> RouteMessageAsync(string message)
    {
        var classification = await _classifier.ClassifyAsync(message);
        
        if (_handlers.TryGetValue(classification.Intent, out var handler))
        {
            return await handler(classification);
        }
        
        return "No pude procesar tu mensaje. ¿Puedes reformularlo?";
    }
    
    private async Task<string> HandleBuyProduct(IntentClassificationResult result)
    {
        if (result.Entities?.ContainsKey("product_name") == true)
        {
            var productName = result.Entities["product_name"].ToString();
            return $"Perfecto, te ayudo a comprar {productName}. ¿Cuántas unidades necesitas?";
        }
        
        return "¿Qué producto te gustaría comprar?";
    }
    
    private async Task<string> HandleCheckStatus(IntentClassificationResult result)
    {
        if (result.Entities?.ContainsKey("order_id") == true)
        {
            var orderId = result.Entities["order_id"].ToString();
            return $"Buscando información del pedido {orderId}...";
        }
        
        return "Por favor, proporciona tu número de pedido para consultar el estado.";
    }
    
    private async Task<string> HandleCancelOrder(IntentClassificationResult result)
    {
        return "Entiendo que quieres cancelar un pedido. ¿Puedes darme el número de pedido?";
    }
    
    private async Task<string> HandleRequestHelp(IntentClassificationResult result)
    {
        return "¡Estoy aquí para ayudarte! ¿Qué necesitas saber?";
    }
    
    private async Task<string> HandleUnknown(IntentClassificationResult result)
    {
        return "No estoy seguro de entender. ¿Puedes explicar qué necesitas?";
    }
}

Clasificación Multi-Intención

Para casos más complejos donde un mensaje puede tener múltiples intenciones:

public class MultiIntentClassificationService
{
    private readonly Kernel _kernel;
    
    public async Task<List<IntentClassificationResult>> ClassifyMultipleAsync(
        string messageText,
        CancellationToken cancellationToken = default)
    {
        var systemPrompt = """
        Analiza el mensaje y extrae TODAS las intenciones presentes.
        Un mensaje puede tener múltiples intenciones.
        
        Por ejemplo: "Quiero comprar un libro y cancelar mi pedido anterior"
        Tiene dos intenciones: buy_product y cancel_order
        
        Responde con JSON array:
        [
          {"intent": "buy_product", "confidence": 0.95, "entities": {"product_name": "libro"}},
          {"intent": "cancel_order", "confidence": 0.90, "entities": {}}
        ]
        """;
        
        var chatService = _kernel.GetRequiredService<IChatCompletionService>();
        var chatHistory = new ChatHistory();
        chatHistory.AddSystemMessage(systemPrompt);
        chatHistory.AddUserMessage($"Mensaje: {messageText}");
        
        var settings = new OpenAIPromptExecutionSettings
        {
            Temperature = 0.1,
            ResponseFormat = "json_object"
        };
        
        var response = await chatService.GetChatMessageContentAsync(
            chatHistory, settings, _kernel, cancellationToken);
        
        // Parsear array de intenciones
        var results = JsonSerializer.Deserialize<List<JsonClassificationResult>>(
            response.Content ?? "[]");
        
        return results?.Select(r => new IntentClassificationResult
        {
            Intent = r.Intent ?? Intents.Unknown,
            Confidence = r.Confidence,
            Entities = r.Entities,
            Category = r.Category
        }).ToList() ?? new List<IntentClassificationResult>();
    }
}

Clasificación con Contexto

public class ContextAwareIntentClassifier
{
    private readonly Kernel _kernel;
    
    public async Task<IntentClassificationResult> ClassifyWithContextAsync(
        string messageText,
        List<string> conversationHistory,
        CancellationToken cancellationToken = default)
    {
        var chatHistory = new ChatHistory();
        chatHistory.AddSystemMessage(BuildSystemPrompt());
        
        // Agregar historial de conversación
        foreach (var previousMessage in conversationHistory.TakeLast(5))
        {
            chatHistory.AddUserMessage(previousMessage);
        }
        
        // Agregar mensaje actual
        chatHistory.AddUserMessage($"Clasifica esta intención: {messageText}");
        
        var chatService = _kernel.GetRequiredService<IChatCompletionService>();
        var response = await chatService.GetChatMessageContentAsync(
            chatHistory,
            kernel: _kernel,
            cancellationToken: cancellationToken);
        
        return ParseResponse(response.Content ?? "");
    }
    
    private string BuildSystemPrompt()
    {
        return """
        Clasifica la intención considerando el CONTEXTO de la conversación.
        
        Si el usuario dice "sí" o "no", determina a qué se refiere basándote
        en los mensajes anteriores.
        
        Responde con JSON válido.
        """;
    }
    
    private IntentClassificationResult ParseResponse(string content)
    {
        // Implementación de parsing
        return new IntentClassificationResult
        {
            Intent = Intents.Unknown,
            Confidence = 0.0
        };
    }
}

Testing del Clasificador

using Xunit;
using Microsoft.Extensions.Logging.Abstractions;

public class IntentClassificationServiceTests
{
    [Theory]
    [InlineData("Quiero comprar un libro", Intents.BuyProduct)]
    [InlineData("¿Dónde está mi pedido?", Intents.CheckStatus)]
    [InlineData("Cancelar mi orden", Intents.CancelOrder)]
    [InlineData("Necesito ayuda", Intents.RequestHelp)]
    public async Task ClassifyAsync_IdentifiesCorrectIntent(
        string message,
        string expectedIntent)
    {
        // Arrange
        var kernel = CreateTestKernel();
        var logger = NullLogger<IntentClassificationService>.Instance;
        var classifier = new IntentClassificationService(kernel, logger);
        
        // Act
        var result = await classifier.ClassifyAsync(message);
        
        // Assert
        Assert.Equal(expectedIntent, result.Intent);
        Assert.True(result.Confidence >= 0.65);
    }
    
    [Fact]
    public async Task ClassifyAsync_ExtractsEntities()
    {
        // Arrange
        var kernel = CreateTestKernel();
        var logger = NullLogger<IntentClassificationService>.Instance;
        var classifier = new IntentClassificationService(kernel, logger);
        
        // Act
        var result = await classifier.ClassifyAsync("Quiero comprar 3 libros");
        
        // Assert
        Assert.Equal(Intents.BuyProduct, result.Intent);
        Assert.NotNull(result.Entities);
        Assert.True(result.Entities.ContainsKey("product_name"));
        Assert.Equal("libros", result.Entities["product_name"]);
    }
    
    private Kernel CreateTestKernel()
    {
        // Configuración de kernel para tests
        var builder = Kernel.CreateBuilder();
        // Configurar con mock o test deployment
        return builder.Build();
    }
}

Mejores Prácticas

1. Temperatura Baja para Consistencia

var settings = new OpenAIPromptExecutionSettings
{
    Temperature = 0.1  // Muy baja para clasificación consistente
};

2. Validación de Resultados

private bool IsValidResult(IntentClassificationResult result)
{
    if (result.Confidence < 0.0 || result.Confidence > 1.0)
        return false;
        
    if (string.IsNullOrWhiteSpace(result.Intent))
        return false;
        
    return true;
}

3. Logging Detallado

_logger.LogInformation(
    "Clasificación: Intent={Intent}, Confidence={Confidence:F2}, Entities={Entities}",
    result.Intent,
    result.Confidence,
    JsonSerializer.Serialize(result.Entities));

4. Caché de Clasificaciones

public class CachedIntentClassifier
{
    private readonly IMemoryCache _cache;
    private readonly IntentClassificationService _classifier;
    
    public async Task<IntentClassificationResult> ClassifyAsync(string message)
    {
        var cacheKey = $"intent_{message.GetHashCode()}";
        
        if (_cache.TryGetValue<IntentClassificationResult>(cacheKey, out var cached))
        {
            return cached;
        }
        
        var result = await _classifier.ClassifyAsync(message);
        
        _cache.Set(cacheKey, result, TimeSpan.FromMinutes(30));
        
        return result;
    }
}

Conclusión

La clasificación de intenciones es crucial para aplicaciones conversacionales efectivas. Con Semantic Kernel y Azure OpenAI puedes crear clasificadores robustos que entienden el contexto y manejan casos complejos. Los guardrails y validaciones aseguran resultados confiables en producción.


Palabras clave: intent classification, NLU, Semantic Kernel, Azure OpenAI, chatbot, conversational AI, C#

Share this content:

Introducción a Microsoft Semantic Kernel

.Creando Plugins Personalizados en Semantic Kernel . Servicios de Chat Completion con Azure OpenAI y Semantic Kernel

por David Cantón Nadales

David Cantón Nadales, ingeniero de software de Sevilla, España, es autor del bestseller Build Your own Metaverse with Unity. Reconocido como Microsoft MVP y Top Voices en Aplicaciones Móviles de LinkedIn. Con más de 20 años de experiencia, David ha liderado cientos proyectos a lo largo de su carrera, incluyendo videojuegos y aplicaciones de realidad virtual y aumentada con Oculus, Hololens, HTC Vive, DayDream y LeapMotion. Ha trabajado como Tech Lead en importantes multinacionales como Grupo Viajes El Corte Inglés y actualmente en SCRM Lidl del Grupo Schwarz. Fue embajador de la comunidad Samsung Dev Spain y organizador del Google Developers Group Sevilla. Durante el confinamiento por COVID-19, destacó como emprendedor social con la creación de Grita, una red social que facilitaba el apoyo psicológico entre personas. En 2022, ganó los Samsung Top Developers Awards.