3. Clasificación de Intenciones con LLMs en .NET

This entry is parte 3 de 25 in the series Introducción a Microsoft Semantic Kernel

Introducción a Microsoft Semantic Kernel

1. Introducción a Semantic Kernel con C#: Construyendo tu Primera Aplicación de IA

2.Creando Plugins Personalizados en Semantic Kernel

3. Clasificación de Intenciones con LLMs en .NET

4. Servicios de Chat Completion con Azure OpenAI y Semantic Kernel

5. Vector Embeddings y Búsqueda Semántica con .NET

6. Prompt Engineering: Mejores Prácticas para LLMs

7. Integración de Azure OpenAI en Aplicaciones .NET

8. Configuración de Temperatura y Tokens en Modelos LLM

9. Estrategias de Caché para Servicios de IA

10. Manejo de Errores en Aplicaciones de IA con .NET

11. Salida JSON Estructurada con LLMs

12. Testing de Servicios de IA en .NET

13. Inyección de Dependencias con Semantic Kernel

14. Comprensión de Consultas con Lenguaje Natural

15. Sistemas de Búsqueda Semántica en Producción

16. Configuración de HttpClient para Servicios de IA

17. Guardrails y Validación en Sistemas de IA

18. Workflows Multi-Paso con IA

19. Optimización de Costos en Aplicaciones de IA

20. Monitoreo y Observabilidad de Servicios de IA

21. Arquitectura de Microservicios con IA

22. Seguridad en Aplicaciones de IA

23. Implementación de Routers Conversacionales Inteligentes

24. Filtrado por Relevancia Semántica en Búsquedas

25. Normalización y Preprocesamiento de Datos para IA

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

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