- 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
Los servicios de Chat Completion permiten crear conversaciones naturales con modelos de IA. En este tutorial aprenderás a implementar servicios de chat robustos y escalables usando Azure OpenAI y Semantic Kernel.
Conceptos Fundamentales
Chat History
El historial de chat mantiene el contexto de la conversación:
using Microsoft.SemanticKernel.ChatCompletion;
var chatHistory = new ChatHistory();
// Mensaje del sistema: define el comportamiento del asistente
chatHistory.AddSystemMessage("Eres un experto en programación .NET.");
// Mensajes del usuario
chatHistory.AddUserMessage("¿Qué es async/await?");
// Respuestas del asistente
chatHistory.AddAssistantMessage("async/await es un patrón para programación asíncrona...");
Servicio de Chat Completion
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4",
endpoint: "https://tu-recurso.openai.azure.com/",
apiKey: "tu-api-key")
.Build();
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var response = await chatService.GetChatMessageContentAsync(
chatHistory,
kernel: kernel);
Console.WriteLine(response.Content);
Implementación de un Servicio de Chat Completo
Modelo de Conversación
public class ConversationMessage
{
public required string Role { get; init; } // "user", "assistant", "system"
public required string Content { get; init; }
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
public Dictionary<string, object>? Metadata { get; init; }
}
public class Conversation
{
public string Id { get; init; } = Guid.NewGuid().ToString();
public List<ConversationMessage> Messages { get; init; } = new();
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
public Dictionary<string, object>? Context { get; set; }
}
Servicio de Chat con Estado
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.Extensions.Logging;
public class ChatService
{
private readonly Kernel _kernel;
private readonly ILogger<ChatService> _logger;
private readonly Dictionary<string, Conversation> _conversations;
private readonly string _systemPrompt;
public ChatService(
Kernel kernel,
ILogger<ChatService> logger,
string systemPrompt = "Eres un asistente útil y amigable.")
{
_kernel = kernel;
_logger = logger;
_conversations = new Dictionary<string, Conversation>();
_systemPrompt = systemPrompt;
}
public string CreateConversation(Dictionary<string, object>? context = null)
{
var conversation = new Conversation
{
Context = context
};
_conversations[conversation.Id] = conversation;
_logger.LogInformation("Conversación creada: {ConversationId}", conversation.Id);
return conversation.Id;
}
public async Task<string> SendMessageAsync(
string conversationId,
string message,
CancellationToken cancellationToken = default)
{
if (!_conversations.TryGetValue(conversationId, out var conversation))
{
throw new InvalidOperationException($"Conversación {conversationId} no encontrada");
}
try
{
// Agregar mensaje del usuario
conversation.Messages.Add(new ConversationMessage
{
Role = "user",
Content = message
});
// Construir historial de chat
var chatHistory = BuildChatHistory(conversation);
// Obtener respuesta del modelo
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
var settings = new OpenAIPromptExecutionSettings
{
Temperature = 0.7,
MaxTokens = 800,
TopP = 0.9
};
var response = await chatService.GetChatMessageContentAsync(
chatHistory,
settings,
_kernel,
cancellationToken);
var assistantMessage = response.Content ?? string.Empty;
// Guardar respuesta del asistente
conversation.Messages.Add(new ConversationMessage
{
Role = "assistant",
Content = assistantMessage,
Metadata = new Dictionary<string, object>
{
["model"] = response.ModelId ?? "unknown",
["tokens"] = response.Metadata?.ContainsKey("Usage") == true
? response.Metadata["Usage"]
: null
}
});
conversation.LastUpdated = DateTime.UtcNow;
_logger.LogInformation(
"Mensaje procesado en conversación {ConversationId}. Tokens: {Tokens}",
conversationId,
response.Metadata?.ContainsKey("Usage") == true
? response.Metadata["Usage"]
: "N/A");
return assistantMessage;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error procesando mensaje en conversación {ConversationId}",
conversationId);
throw;
}
}
public Conversation? GetConversation(string conversationId)
{
return _conversations.TryGetValue(conversationId, out var conversation)
? conversation
: null;
}
public void ClearConversation(string conversationId)
{
if (_conversations.TryGetValue(conversationId, out var conversation))
{
conversation.Messages.Clear();
conversation.LastUpdated = DateTime.UtcNow;
_logger.LogInformation("Conversación {ConversationId} limpiada", conversationId);
}
}
public void DeleteConversation(string conversationId)
{
_conversations.Remove(conversationId);
_logger.LogInformation("Conversación {ConversationId} eliminada", conversationId);
}
private ChatHistory BuildChatHistory(Conversation conversation)
{
var chatHistory = new ChatHistory();
// Agregar mensaje del sistema
chatHistory.AddSystemMessage(_systemPrompt);
// Agregar contexto si existe
if (conversation.Context != null && conversation.Context.Count > 0)
{
var contextInfo = string.Join(", ",
conversation.Context.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
chatHistory.AddSystemMessage($"Contexto: {contextInfo}");
}
// Agregar mensajes de la conversación (últimos N mensajes para no exceder límite)
var recentMessages = conversation.Messages.TakeLast(10);
foreach (var msg in recentMessages)
{
switch (msg.Role.ToLower())
{
case "user":
chatHistory.AddUserMessage(msg.Content);
break;
case "assistant":
chatHistory.AddAssistantMessage(msg.Content);
break;
case "system":
chatHistory.AddSystemMessage(msg.Content);
break;
}
}
return chatHistory;
}
}
Streaming de Respuestas
Para respuestas en tiempo real:
public class StreamingChatService
{
private readonly Kernel _kernel;
public async IAsyncEnumerable<string> StreamMessageAsync(
string conversationId,
string message,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = BuildChatHistory(conversationId);
chatHistory.AddUserMessage(message);
var settings = new OpenAIPromptExecutionSettings
{
Temperature = 0.7,
MaxTokens = 800
};
var fullResponse = new StringBuilder();
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
chatHistory,
settings,
_kernel,
cancellationToken))
{
var content = chunk.Content ?? string.Empty;
fullResponse.Append(content);
yield return content;
}
// Guardar mensaje completo después del streaming
SaveAssistantMessage(conversationId, fullResponse.ToString());
}
private void SaveAssistantMessage(string conversationId, string message)
{
// Implementación para guardar el mensaje
}
}
Chat con Funciones (Function Calling)
public class FunctionCallingChatService
{
private readonly Kernel _kernel;
public FunctionCallingChatService(Kernel kernel)
{
_kernel = kernel;
// Registrar funciones disponibles
_kernel.Plugins.AddFromObject(new WeatherPlugin());
_kernel.Plugins.AddFromObject(new CalculatorPlugin());
}
public async Task<string> ChatWithFunctionsAsync(
string message,
CancellationToken cancellationToken = default)
{
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage("""
Eres un asistente que puede usar funciones para responder preguntas.
Tienes acceso a:
- WeatherPlugin: para obtener información del clima
- CalculatorPlugin: para realizar cálculos
Usa las funciones cuando sea necesario.
""");
chatHistory.AddUserMessage(message);
var settings = new OpenAIPromptExecutionSettings
{
Temperature = 0.7,
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
var response = await chatService.GetChatMessageContentAsync(
chatHistory,
settings,
_kernel,
cancellationToken);
return response.Content ?? "No pude generar una respuesta";
}
}
// Plugins de ejemplo
public class WeatherPlugin
{
[KernelFunction("get_weather")]
[Description("Obtiene el clima actual de una ciudad")]
public string GetWeather([Description("Nombre de la ciudad")] string city)
{
// Simular llamada a API de clima
return $"En {city}: 22°C, soleado";
}
}
public class CalculatorPlugin
{
[KernelFunction("calculate")]
[Description("Realiza un cálculo matemático")]
public double Calculate(
[Description("Primera operando")] double a,
[Description("Operador: +, -, *, /")] string op,
[Description("Segundo operando")] double b)
{
return op switch
{
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" => a / b,
_ => 0
};
}
}
Chat Multimodal (Texto e Imágenes)
using Microsoft.SemanticKernel.ChatCompletion;
public class MultimodalChatService
{
private readonly Kernel _kernel;
public async Task<string> AnalyzeImageAsync(
string imageUrl,
string question,
CancellationToken cancellationToken = default)
{
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage("Eres un experto en análisis de imágenes.");
// Agregar imagen y pregunta
var message = new ChatMessageContent(
AuthorRole.User,
new ChatMessageContentItemCollection
{
new TextContent(question),
new ImageContent(new Uri(imageUrl))
});
chatHistory.Add(message);
var response = await chatService.GetChatMessageContentAsync(
chatHistory,
kernel: _kernel,
cancellationToken: cancellationToken);
return response.Content ?? "No pude analizar la imagen";
}
}
Chat con Memoria Persistente
using System.Text.Json;
public class PersistentChatService
{
private readonly ChatService _chatService;
private readonly string _storageDirectory;
public PersistentChatService(ChatService chatService, string storageDirectory)
{
_chatService = chatService;
_storageDirectory = storageDirectory;
Directory.CreateDirectory(storageDirectory);
}
public async Task SaveConversationAsync(string conversationId)
{
var conversation = _chatService.GetConversation(conversationId);
if (conversation == null)
throw new InvalidOperationException("Conversación no encontrada");
var filePath = Path.Combine(_storageDirectory, $"{conversationId}.json");
var json = JsonSerializer.Serialize(conversation, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
}
public async Task<string> LoadConversationAsync(string conversationId)
{
var filePath = Path.Combine(_storageDirectory, $"{conversationId}.json");
if (!File.Exists(filePath))
throw new FileNotFoundException("Conversación no encontrada");
var json = await File.ReadAllTextAsync(filePath);
var conversation = JsonSerializer.Deserialize<Conversation>(json);
if (conversation == null)
throw new InvalidOperationException("Error deserializando conversación");
// Cargar en el servicio de chat
// Implementación específica según tu arquitectura
return conversationId;
}
public async Task<List<string>> ListConversationsAsync()
{
var files = Directory.GetFiles(_storageDirectory, "*.json");
return files.Select(Path.GetFileNameWithoutExtension).ToList()!;
}
}
Manejo de Rate Limits
public class RateLimitedChatService
{
private readonly ChatService _chatService;
private readonly SemaphoreSlim _semaphore;
private readonly int _maxConcurrent;
public RateLimitedChatService(ChatService chatService, int maxConcurrent = 5)
{
_chatService = chatService;
_maxConcurrent = maxConcurrent;
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
}
public async Task<string> SendMessageAsync(
string conversationId,
string message,
CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
return await _chatService.SendMessageAsync(
conversationId,
message,
cancellationToken);
}
finally
{
_semaphore.Release();
}
}
}
Retry con Exponential Backoff
using Polly;
using Polly.Retry;
public class ResilientChatService
{
private readonly ChatService _chatService;
private readonly AsyncRetryPolicy _retryPolicy;
public ResilientChatService(ChatService chatService)
{
_chatService = chatService;
_retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (exception, timeSpan, retryCount, context) =>
{
Console.WriteLine($"Intento {retryCount} después de {timeSpan.TotalSeconds}s");
});
}
public async Task<string> SendMessageWithRetryAsync(
string conversationId,
string message,
CancellationToken cancellationToken = default)
{
return await _retryPolicy.ExecuteAsync(async () =>
await _chatService.SendMessageAsync(conversationId, message, cancellationToken));
}
}
Testing de Servicios de Chat
using Xunit;
using Moq;
public class ChatServiceTests
{
[Fact]
public async Task SendMessageAsync_ReturnsResponse()
{
// Arrange
var kernel = CreateTestKernel();
var logger = Mock.Of<ILogger<ChatService>>();
var chatService = new ChatService(kernel, logger);
var conversationId = chatService.CreateConversation();
// Act
var response = await chatService.SendMessageAsync(
conversationId,
"Hola, ¿cómo estás?");
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response);
}
[Fact]
public void CreateConversation_GeneratesUniqueId()
{
// Arrange
var kernel = CreateTestKernel();
var logger = Mock.Of<ILogger<ChatService>>();
var chatService = new ChatService(kernel, logger);
// Act
var id1 = chatService.CreateConversation();
var id2 = chatService.CreateConversation();
// Assert
Assert.NotEqual(id1, id2);
}
private Kernel CreateTestKernel()
{
var builder = Kernel.CreateBuilder();
// Configurar kernel de prueba
return builder.Build();
}
}
Mejores Prácticas
1. Limitar Historial de Mensajes
var recentMessages = conversation.Messages
.TakeLast(10) // Solo últimos 10 mensajes
.ToList();
2. Validar Longitud de Mensajes
public async Task<string> SendMessageAsync(string conversationId, string message)
{
const int maxLength = 4000;
if (message.Length > maxLength)
{
throw new ArgumentException($"Mensaje excede {maxLength} caracteres");
}
// Procesar mensaje
}
3. Sanitizar Entrada del Usuario
private string SanitizeInput(string input)
{
// Eliminar caracteres peligrosos o no deseados
return input
.Replace("<script>", "")
.Replace("</script>", "")
.Trim();
}
4. Monitorear Uso de Tokens
private void LogTokenUsage(ChatMessageContent response)
{
if (response.Metadata?.ContainsKey("Usage") == true)
{
var usage = response.Metadata["Usage"];
_logger.LogInformation("Tokens usados: {Usage}", usage);
}
}
Conclusión
Los servicios de chat completion son la base de aplicaciones conversacionales modernas. Con Semantic Kernel puedes crear servicios robustos que manejan estado, funciones, streaming y más. Las prácticas de manejo de errores, rate limiting y persistencia aseguran un sistema confiable en producción.
Palabras clave: chat completion, Azure OpenAI, Semantic Kernel, conversational AI, chatbot, streaming, function calling, C#
Share this content: