Введение
C# разработчики все чаще сталкиваются с необходимостью интеграции искусственного интеллекта в свои приложения, будь то чат-боты, анализ текста, генерация изображений или умные помощники. В то время как Python долгое время был доминирующим языком в сфере AI, экосистема .NET активно развивается, предлагая современные решения для работы с ИИ. Сегодня C# разработчики имеют два основных пути: использование облачных API (OpenAI, Azure AI) или развертывание локальных моделей с помощью библиотек вроде ML.NET и ONNX Runtime. Каждый подход имеет свои преимущества: облачные решения предлагают мощные предобученные модели и простоту интеграции, в то время как локальные модели обеспечивают конфиденциальность данных, отсутствие зависимости от интернета и предсказуемую стоимость.
Интеграция с OpenAI API
1.1. Работа с GPT-4 и ChatGPT
1.1.1. Настройка и базовое использование
// Установка: dotnet add package OpenAI using OpenAI; using OpenAI.Chat; using OpenAI.Embeddings; public class OpenAIService { private readonly OpenAIClient _client; private readonly ILogger<OpenAIService> _logger; private readonly ChatHistory _chatHistory; public OpenAIService(string apiKey, ILogger<OpenAIService> logger) { _client = new OpenAIClient(apiKey); _logger = logger; _chatHistory = new ChatHistory(); // Инициализация системного промпта _chatHistory.AddSystemMessage("Ты полезный ассистент, который помогает разработчикам на C#."); } // Базовый запрос к GPT-4 public async Task<string> GetCompletionAsync(string prompt, string model = "gpt-4") { try { var options = new ChatCompletionOptions { Model = model, Temperature = 0.7f, MaxTokens = 2000, FrequencyPenalty = 0.5f, PresencePenalty = 0.5f }; _chatHistory.AddUserMessage(prompt); var response = await _client.ChatEndpoint.GetCompletionAsync( _chatHistory, options); var assistantMessage = response.FirstChoice.Message; _chatHistory.AddAssistantMessage(assistantMessage.Content); _logger.LogInformation($"Used tokens: {response.Usage.TotalTokens}"); return assistantMessage.Content; } catch (Exception ex) { _logger.LogError(ex, "OpenAI API error"); throw; } } // Потоковая передача ответов (streaming) public async IAsyncEnumerable<string> StreamCompletionAsync(string prompt) { _chatHistory.AddUserMessage(prompt); var options = new ChatCompletionOptions { Model = "gpt-4", Temperature = 0.7f, MaxTokens = 2000, Stream = true // Включаем streaming }; var completion = _client.ChatEndpoint.StreamCompletionAsync( _chatHistory, options); await foreach (var result in completion) { var content = result.FirstChoice.Message.Content; if (!string.IsNullOrEmpty(content)) { yield return content; } } // Сохраняем финальное сообщение в историю var finalMessage = await GetFinalMessageAsync(completion); _chatHistory.AddAssistantMessage(finalMessage); } // Функции (functions) для структурированных ответов public async Task<StructuredResponse> GetStructuredResponseAsync(string query) { var functions = new List<Function> { new Function { Name = "generate_code", Description = "Генерирует код на C#", Parameters = new JsonSchema { Type = JsonSchemaType.Object, Properties = new Dictionary<string, JsonSchema> { ["code"] = new JsonSchema { Type = JsonSchemaType.String, Description = "Сгенерированный код" }, ["language"] = new JsonSchema { Type = JsonSchemaType.String, Enum = new[] { "csharp", "python", "javascript" } }, ["complexity"] = new JsonSchema { Type = JsonSchemaType.String, Enum = new[] { "beginner", "intermediate", "advanced" } } }, Required = new[] { "code", "language" } } }, new Function { Name = "analyze_code", Description = "Анализирует код и предлагает улучшения", Parameters = new JsonSchema { Type = JsonSchemaType.Object, Properties = new Dictionary<string, JsonSchema> { ["analysis"] = new JsonSchema { Type = JsonSchemaType.String, Description = "Анализ кода" }, ["suggestions"] = new JsonSchema { Type = JsonSchemaType.Array, Items = new JsonSchema { Type = JsonSchemaType.String, Description = "Предложения по улучшению" } }, ["score"] = new JsonSchema { Type = JsonSchemaType.Integer, Minimum = 0, Maximum = 100 } } } } }; var options = new ChatCompletionOptions { Model = "gpt-4", Functions = functions, FunctionCall = "auto" }; _chatHistory.AddUserMessage(query); var response = await _client.ChatEndpoint.GetCompletionAsync( _chatHistory, options); var message = response.FirstChoice.Message; if (message.FunctionCall != null) { return await ProcessFunctionCallAsync(message.FunctionCall); } return new StructuredResponse { Text = message.Content }; } private async Task<StructuredResponse> ProcessFunctionCallAsync(FunctionCall functionCall) { switch (functionCall.Name) { case "generate_code": var codeArgs = JsonSerializer.Deserialize<CodeGenerationArgs>(functionCall.Arguments); return await GenerateCodeAsync(codeArgs); case "analyze_code": var analysisArgs = JsonSerializer.Deserialize<CodeAnalysisArgs>(functionCall.Arguments); return await AnalyzeCodeAsync(analysisArgs); default: throw new NotSupportedException($"Function {functionCall.Name} not supported"); } } // Работа с изображениями через DALL-E public async Task<ImageResult> GenerateImageAsync(string prompt, ImageSize size = ImageSize.Large) { var request = new ImageGenerationRequest { Prompt = prompt, Size = size switch { ImageSize.Small => "256x256", ImageSize.Medium => "512x512", ImageSize.Large => "1024x1024", _ => "1024x1024" }, Quality = "standard", ResponseFormat = "url", Style = "vivid" }; var response = await _client.ImagesEndpoint.GenerateImageAsync(request); return new ImageResult { Url = response.First().Url, RevisedPrompt = response.First().RevisedPrompt, Created = response.First().Created }; } // Пакетная обработка запросов public async Task<List<string>> ProcessBatchAsync(List<string> prompts) { var tasks = prompts.Select(async prompt => { try { return await GetCompletionAsync(prompt); } catch (Exception ex) { _logger.LogError(ex, $"Error processing prompt: {prompt}"); return $"Error: {ex.Message}"; } }); var results = await Task.WhenAll(tasks); return results.ToList(); } }
1.2. Embeddings и семантический поиск
1.2.1. Создание и использование векторных представлений
public class EmbeddingService { private readonly OpenAIClient _client; private readonly Dictionary<string, float[]> _embeddingsCache; private readonly ISemanticCache _semanticCache; public EmbeddingService(string apiKey) { _client = new OpenAIClient(apiKey); _embeddingsCache = new Dictionary<string, float[]>(); _semanticCache = new MemorySemanticCache(); } // Генерация эмбеддингов для текста public async Task<float[]> GetEmbeddingAsync(string text, string model = "text-embedding-3-small") { // Проверка кэша if (_embeddingsCache.TryGetValue(text, out var cachedEmbedding)) { return cachedEmbedding; } var request = new EmbeddingRequest { Input = text, Model = model, Dimensions = 1536 // Можно уменьшить для экономии }; var response = await _client.EmbeddingsEndpoint.CreateEmbeddingAsync(request); var embedding = response.First().Embedding.ToArray(); // Кэширование _embeddingsCache[text] = embedding; return embedding; } // Косинусное сходство public float CosineSimilarity(float[] vector1, float[] vector2) { if (vector1.Length != vector2.Length) throw new ArgumentException("Vectors must have the same length"); float dotProduct = 0; float magnitude1 = 0; float magnitude2 = 0; for (int i = 0; i < vector1.Length; i++) { dotProduct += vector1[i] * vector2[i]; magnitude1 += vector1[i] * vector1[i]; magnitude2 += vector2[i] * vector2[i]; } magnitude1 = (float)Math.Sqrt(magnitude1); magnitude2 = (float)Math.Sqrt(magnitude2); if (magnitude1 == 0 || magnitude2 == 0) return 0; return dotProduct / (magnitude1 * magnitude2); } // Семантический поиск public async Task<List<SearchResult>> SemanticSearchAsync( string query, List<Document> documents, int topK = 5) { var queryEmbedding = await GetEmbeddingAsync(query); var results = new List<SearchResult>(); foreach (var doc in documents) { var docEmbedding = await GetEmbeddingAsync(doc.Content); var similarity = CosineSimilarity(queryEmbedding, docEmbedding); results.Add(new SearchResult { Document = doc, Similarity = similarity }); } return results .OrderByDescending(r => r.Similarity) .Take(topK) .ToList(); } // Кластеризация документов public async Task<List<DocumentCluster>> ClusterDocumentsAsync( List<Document> documents, int numberOfClusters = 5) { // Генерация эмбеддингов для всех документов var embeddings = new List<float[]>(); foreach (var doc in documents) { var embedding = await GetEmbeddingAsync(doc.Content); embeddings.Add(embedding); } // K-means кластеризация var clusters = await KMeansClusteringAsync(embeddings, numberOfClusters); // Группировка документов по кластерам var documentClusters = new List<DocumentCluster>(); for (int i = 0; i < clusters.Count; i++) { var clusterDocs = new List<Document>(); for (int j = 0; j < clusters[i].Count; j++) { clusterDocs.Add(documents[clusters[i][j]]); } documentClusters.Add(new DocumentCluster { Id = i, Documents = clusterDocs, Center = CalculateCentroid(embeddings.Where((_, idx) => clusters[i].Contains(idx))) }); } return documentClusters; } private async Task<List<List<int>>> KMeansClusteringAsync( List<float[]> embeddings, int k, int maxIterations = 100) { // Инициализация центроидов случайными точками var random = new Random(); var centroids = new List<float[]>(); for (int i = 0; i < k; i++) { centroids.Add(embeddings[random.Next(embeddings.Count)]); } var clusters = new List<List<int>>(); for (int iteration = 0; iteration < maxIterations; iteration++) { // Сброс кластеров clusters = Enumerable.Range(0, k) .Select(_ => new List<int>()) .ToList(); // Назначение точек ближайшим центроидам for (int i = 0; i < embeddings.Count; i++) { var distances = centroids .Select((c, idx) => new { Index = idx, Distance = EuclideanDistance(embeddings[i], c) }) .OrderBy(d => d.Distance) .First(); clusters[distances.Index].Add(i); } // Пересчет центроидов var newCentroids = new List<float[]>(); for (int i = 0; i < k; i++) { if (clusters[i].Count > 0) { var clusterEmbeddings = clusters[i] .Select(idx => embeddings[idx]) .ToList(); newCentroids.Add(CalculateCentroid(clusterEmbeddings)); } else { // Если кластер пуст, инициализируем случайно newCentroids.Add(embeddings[random.Next(embeddings.Count)]); } } // Проверка сходимости if (CentroidsConverged(centroids, newCentroids, threshold: 0.001f)) { break; } centroids = newCentroids; } return clusters; } private float[] CalculateCentroid(IEnumerable<float[]> vectors) { var first = vectors.First(); var centroid = new float[first.Length]; foreach (var vector in vectors) { for (int i = 0; i < vector.Length; i++) { centroid[i] += vector[i]; } } int count = vectors.Count(); for (int i = 0; i < centroid.Length; i++) { centroid[i] /= count; } return centroid; } private float EuclideanDistance(float[] a, float[] b) { float sum = 0; for (int i = 0; i < a.Length; i++) { float diff = a[i] - b[i]; sum += diff * diff; } return (float)Math.Sqrt(sum); } private bool CentroidsConverged( List<float[]> oldCentroids, List<float[]> newCentroids, float threshold) { for (int i = 0; i < oldCentroids.Count; i++) { if (EuclideanDistance(oldCentroids[i], newCentroids[i]) > threshold) { return false; } } return true; } }
Локальные модели ИИ на C#
2.1. Использование ONNX Runtime
2.1.1. Загрузка и выполнение ONNX моделей
// Установка: dotnet add package Microsoft.ML.OnnxRuntime // dotnet add package Microsoft.ML.OnnxRuntime.Managed using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class OnnxModelService : IDisposable { private readonly InferenceSession _session; private readonly ILogger<OnnxModelService> _logger; private readonly Tokenizer _tokenizer; public OnnxModelService(string modelPath, ILogger<OnnxModelService> logger) { var options = new SessionOptions { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING, GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL, ExecutionMode = ExecutionMode.ORT_PARALLEL, InterOpNumThreads = Environment.ProcessorCount, IntraOpNumThreads = Environment.ProcessorCount }; // Использование CUDA если доступно if (HasCuda()) { options.AppendExecutionProvider_CUDA(); } else { options.AppendExecutionProvider_CPU(); } _session = new InferenceSession(modelPath, options); _logger = logger; _tokenizer = new Tokenizer(); LogModelInfo(); } private bool HasCuda() { try { var cudaSessionOptions = SessionOptions.MakeSessionOptionWithCudaProvider(); return true; } catch { return false; } } private void LogModelInfo() { _logger.LogInformation($"Model inputs: {_session.InputMetadata.Count}"); _logger.LogInformation($"Model outputs: {_session.OutputMetadata.Count}"); foreach (var input in _session.InputMetadata) { _logger.LogInformation($"Input: {input.Key}, Type: {input.Value.ElementType}, " + $"Shape: {string.Join(",", input.Value.Dimensions)}"); } } // Текстовая генерация с локальной моделью public async Task<string> GenerateTextAsync(string prompt, int maxLength = 100) { var tokens = _tokenizer.Encode(prompt); var inputTensor = new DenseTensor<long>( new[] { 1, tokens.Length }, new[] { tokens.Select(t => (long)t).ToArray() }); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputTensor) }; var generatedText = prompt; for (int i = 0; i < maxLength; i++) { using var results = _session.Run(inputs); var logits = results.First().AsTensor<float>(); var nextToken = SampleToken(logits); if (nextToken == _tokenizer.EosTokenId) break; generatedText += _tokenizer.Decode(new[] { nextToken }); // Обновление inputs для следующей итерации tokens = tokens.Append(nextToken).ToArray(); inputTensor = new DenseTensor<long>( new[] { 1, tokens.Length }, new[] { tokens.Select(t => (long)t).ToArray() }); inputs[0] = NamedOnnxValue.CreateFromTensor("input_ids", inputTensor); } return generatedText; } private int SampleToken(Tensor<float> logits) { // Реализация sampling с temperature var temperature = 0.7f; var probabilities = new float[logits.Length]; for (int i = 0; i < logits.Length; i++) { probabilities[i] = (float)Math.Exp(logits[i] / temperature); } // Softmax var sum = probabilities.Sum(); for (int i = 0; i < probabilities.Length; i++) { probabilities[i] /= sum; } // Выбор токена на основе вероятностей var random = new Random(); var randomValue = random.NextDouble(); var cumulative = 0.0; for (int i = 0; i < probabilities.Length; i++) { cumulative += probabilities[i]; if (randomValue <= cumulative) { return i; } } return probabilities.Length - 1; } // Классификация текста public async Task<ClassificationResult> ClassifyTextAsync(string text) { var tokens = _tokenizer.Encode(text); var inputIds = new DenseTensor<long>( new[] { 1, tokens.Length }, new[] { tokens.Select(t => (long)t).ToArray() }); var attentionMask = new DenseTensor<long>( new[] { 1, tokens.Length }, Enumerable.Repeat(1L, tokens.Length).ToArray()); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputIds), NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask) }; using var results = _session.Run(inputs); var logits = results.First().AsTensor<float>(); var probabilities = Softmax(logits.ToArray()); var predictedClass = Array.IndexOf(probabilities, probabilities.Max()); return new ClassificationResult { ClassId = predictedClass, Confidence = probabilities[predictedClass], Probabilities = probabilities }; } private float[] Softmax(float[] logits) { var max = logits.Max(); var exp = logits.Select(x => (float)Math.Exp(x - max)).ToArray(); var sum = exp.Sum(); return exp.Select(x => x / sum).ToArray(); } // Пакетная обработка public async Task<List<string>> ProcessBatchAsync(List<string> texts) { var batchSize = 8; var results = new List<string>(); for (int i = 0; i < texts.Count; i += batchSize) { var batch = texts.Skip(i).Take(batchSize).ToList(); var batchResults = await ProcessSingleBatchAsync(batch); results.AddRange(batchResults); _logger.LogInformation($"Processed batch {i / batchSize + 1}/" + $"{Math.Ceiling((double)texts.Count / batchSize)}"); } return results; } private async Task<List<string>> ProcessSingleBatchAsync(List<string> batch) { // Подготовка batch inputs var maxLength = batch.Max(t => _tokenizer.Encode(t).Length); var batchSize = batch.Count; var inputIds = new DenseTensor<long>(new[] { batchSize, maxLength }); var attentionMask = new DenseTensor<long>(new[] { batchSize, maxLength }); for (int i = 0; i < batch.Count; i++) { var tokens = _tokenizer.Encode(batch[i]); for (int j = 0; j < tokens.Length; j++) { inputIds[i, j] = tokens[j]; attentionMask[i, j] = 1; } // Padding for (int j = tokens.Length; j < maxLength; j++) { inputIds[i, j] = _tokenizer.PadTokenId; attentionMask[i, j] = 0; } } var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputIds), NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask) }; using var results = _session.Run(inputs); var logits = results.First().AsTensor<float>(); // Обработка результатов batch var batchResults = new List<string>(); for (int i = 0; i < batchSize; i++) { var slice = logits.GetSubTensor(i); var generated = ProcessSingleResult(slice); batchResults.Add(generated); } return batchResults; } public void Dispose() { _session?.Dispose(); } }
2.2. Локальные эмбеддинг-модели
2.2.1. Использование Sentence Transformers в C#
public class LocalEmbeddingService { private readonly OnnxModelService _modelService; private readonly Tokenizer _tokenizer; private readonly int _embeddingDimension; public LocalEmbeddingService(string modelPath) { _modelService = new OnnxModelService(modelPath, logger); _tokenizer = new Tokenizer(); // Определение размерности эмбеддингов из модели _embeddingDimension = GetEmbeddingDimension(modelPath); } public async Task<float[]> GenerateEmbeddingAsync(string text) { var tokens = _tokenizer.Encode(text); var inputIds = new DenseTensor<long>( new[] { 1, tokens.Length }, new[] { tokens.Select(t => (long)t).ToArray() }); var attentionMask = new DenseTensor<long>( new[] { 1, tokens.Length }, Enumerable.Repeat(1L, tokens.Length).ToArray()); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputIds), NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask) }; using var results = _modelService.RunModel(inputs); var lastHiddenState = results.First().AsTensor<float>(); // Mean pooling для получения sentence embedding return MeanPooling(lastHiddenState, attentionMask); } private float[] MeanPooling(Tensor<float> lastHiddenState, Tensor<long> attentionMask) { var batchSize = lastHiddenState.Dimensions[0]; var seqLength = lastHiddenState.Dimensions[1]; var hiddenSize = lastHiddenState.Dimensions[2]; var embeddings = new float[batchSize * hiddenSize]; for (int b = 0; b < batchSize; b++) { for (int h = 0; h < hiddenSize; h++) { float sum = 0; int count = 0; for (int s = 0; s < seqLength; s++) { if (attentionMask[b, s] == 1) { sum += lastHiddenState[b, s, h]; count++; } } embeddings[b * hiddenSize + h] = count > 0 ? sum / count : 0; } } return embeddings; } public async Task<float[][]> GenerateBatchEmbeddingsAsync(List<string> texts) { var batchSize = texts.Count; var maxLength = texts.Max(t => _tokenizer.Encode(t).Length); var inputIds = new DenseTensor<long>(new[] { batchSize, maxLength }); var attentionMask = new DenseTensor<long>(new[] { batchSize, maxLength }); for (int i = 0; i < texts.Count; i++) { var tokens = _tokenizer.Encode(texts[i]); for (int j = 0; j < tokens.Length; j++) { inputIds[i, j] = tokens[j]; attentionMask[i, j] = 1; } for (int j = tokens.Length; j < maxLength; j++) { inputIds[i, j] = _tokenizer.PadTokenId; attentionMask[i, j] = 0; } } var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputIds), NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask) }; using var results = _modelService.RunModel(inputs); var lastHiddenState = results.First().AsTensor<float>(); var batchEmbeddings = new List<float[]>(); for (int b = 0; b < batchSize; b++) { var embedding = ExtractSingleEmbedding(lastHiddenState, attentionMask, b); batchEmbeddings.Add(embedding); } return batchEmbeddings.ToArray(); } // Семантический поиск с локальными эмбеддингами public async Task<SearchResult> LocalSemanticSearchAsync( string query, List<Document> documents, int topK = 5) { var queryEmbedding = await GenerateEmbeddingAsync(query); // Генерация эмбеддингов для всех документов var documentEmbeddings = await GenerateBatchEmbeddingsAsync( documents.Select(d => d.Content).ToList()); var results = new List<(Document Doc, float Score)>(); for (int i = 0; i < documents.Count; i++) { var similarity = CosineSimilarity( queryEmbedding, documentEmbeddings[i]); results.Add((documents[i], similarity)); } return new SearchResult { TopDocuments = results .OrderByDescending(r => r.Score) .Take(topK) .Select(r => new RankedDocument { Document = r.Doc, Score = r.Score }) .ToList(), QueryEmbedding = queryEmbedding }; } // Сохранение и загрузка эмбеддингов public void SaveEmbeddingsToFile(Dictionary<string, float[]> embeddings, string filePath) { using var writer = new BinaryWriter(File.OpenWrite(filePath)); writer.Write(embeddings.Count); writer.Write(_embeddingDimension); foreach (var (text, embedding) in embeddings) { writer.Write(text); foreach (var value in embedding) { writer.Write(value); } } } public Dictionary<string, float[]> LoadEmbeddingsFromFile(string filePath) { var embeddings = new Dictionary<string, float[]>(); using var reader = new BinaryReader(File.OpenRead(filePath)); var count = reader.ReadInt32(); var dimension = reader.ReadInt32(); for (int i = 0; i < count; i++) { var text = reader.ReadString(); var embedding = new float[dimension]; for (int j = 0; j < dimension; j++) { embedding[j] = reader.ReadSingle(); } embeddings[text] = embedding; } return embeddings; } // Инкрементальное обновление эмбеддингов public async Task UpdateEmbeddingsAsync( Dictionary<string, float[]> existingEmbeddings, List<string> newTexts) { var newEmbeddings = await GenerateBatchEmbeddingsAsync(newTexts); for (int i = 0; i < newTexts.Count; i++) { existingEmbeddings[newTexts[i]] = newEmbeddings[i]; } } }
Гибридный подход: кэширование и оптимизация
3.1. Интеллектуальное кэширование ответов ИИ
3.1.1. Семантическое кэширование
public class SemanticCache { private readonly LocalEmbeddingService _embeddingService; private readonly Dictionary<string, CacheEntry> _cache; private readonly int _maxCacheSize; private readonly float _similarityThreshold; public SemanticCache( LocalEmbeddingService embeddingService, int maxCacheSize = 1000, float similarityThreshold = 0.85f) { _embeddingService = embeddingService; _cache = new Dictionary<string, CacheEntry>(); _maxCacheSize = maxCacheSize; _similarityThreshold = similarityThreshold; } public async Task<CacheResult> GetOrAddAsync( string query, Func<Task<string>> valueFactory) { // Поиск семантически похожих запросов в кэше var similarEntry = await FindSimilarAsync(query); if (similarEntry != null) { return new CacheResult { Value = similarEntry.Value, Source = CacheSource.SemanticCache, Similarity = similarEntry.Similarity }; } // Если не найдено, вычисляем новое значение var value = await valueFactory(); var embedding = await _embeddingService.GenerateEmbeddingAsync(query); var entry = new CacheEntry { Key = query, Value = value, Embedding = embedding, CreatedAt = DateTime.UtcNow, AccessCount = 0 }; // Добавление в кэш с учетом размера AddToCache(query, entry); return new CacheResult { Value = value, Source = CacheSource.Fresh, Similarity = 1.0f }; } private async Task<CacheEntry?> FindSimilarAsync(string query) { if (_cache.Count == 0) return null; var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query); CacheEntry? bestMatch = null; float bestSimilarity = 0; foreach (var entry in _cache.Values) { var similarity = CosineSimilarity(queryEmbedding, entry.Embedding); if (similarity > _similarityThreshold && similarity > bestSimilarity) { bestSimilarity = similarity; bestMatch = entry; } } if (bestMatch != null) { // Обновление статистики использования bestMatch.AccessCount++; bestMatch.LastAccessed = DateTime.UtcNow; } return bestMatch; } private void AddToCache(string key, CacheEntry entry) { // Проверка размера кэша if (_cache.Count >= _maxCacheSize) { // Удаление наименее используемых записей var toRemove = _cache.OrderBy(e => e.Value.AccessCount) .ThenBy(e => e.Value.LastAccessed) .Take(_cache.Count - _maxCacheSize + 1) .ToList(); foreach (var item in toRemove) { _cache.Remove(item.Key); } } _cache[key] = entry; } public async Task PrewarmCacheAsync(List<string> commonQueries, Func<string, Task<string>> valueFactory) { var tasks = commonQueries.Select(async query => { var value = await valueFactory(query); var embedding = await _embeddingService.GenerateEmbeddingAsync(query); return new CacheEntry { Key = query, Value = value, Embedding = embedding, CreatedAt = DateTime.UtcNow, AccessCount = 0 }; }); var entries = await Task.WhenAll(tasks); foreach (var entry in entries) { AddToCache(entry.Key, entry); } } public CacheStatistics GetStatistics() { return new CacheStatistics { TotalEntries = _cache.Count, HitRate = CalculateHitRate(), AverageSimilarity = CalculateAverageSimilarity(), MemoryUsage = CalculateMemoryUsage() }; } private float CalculateHitRate() { // Здесь должна быть логика отслеживания hits/misses return 0.85f; // Заглушка } }
3.2. Динамический выбор модели
public class AdaptiveAIService { private readonly OpenAIService _openAIService; private readonly LocalModelService _localModelService; private readonly ILogger<AdaptiveAIService> _logger; private readonly AdaptiveRoutingStrategy _routingStrategy; public AdaptiveAIService( OpenAIService openAIService, LocalModelService localModelService, ILogger<AdaptiveAIService> logger) { _openAIService = openAIService; _localModelService = localModelService; _logger = logger; _routingStrategy = new AdaptiveRoutingStrategy(); } public async Task<string> ProcessAdaptiveAsync( string prompt, UserContext context, CancellationToken cancellationToken = default) { // Анализ запроса для выбора оптимальной стратегии var analysis = await AnalyzeRequestAsync(prompt, context); // Выбор модели на основе анализа var selectedModel = await SelectModelAsync(analysis, context); string result; switch (selectedModel.Type) { case ModelType.OpenAI_GPT4: result = await _openAIService.GetCompletionAsync( prompt, "gpt-4", cancellationToken); break; case ModelType.OpenAI_GPT35: result = await _openAIService.GetCompletionAsync( prompt, "gpt-3.5-turbo", cancellationToken); break; case ModelType.Local_Large: result = await _localModelService.GenerateWithLargeModelAsync( prompt, cancellationToken); break; case ModelType.Local_Fast: result = await _localModelService.GenerateWithFastModelAsync( prompt, cancellationToken); break; default: throw new InvalidOperationException("Unknown model type"); } // Логирование и обновление стратегии await LogAndUpdateStrategyAsync(analysis, selectedModel, result.Length); return result; } private async Task<RequestAnalysis> AnalyzeRequestAsync(string prompt, UserContext context) { var complexity = await EstimateComplexityAsync(prompt); var requiredQuality = EstimateRequiredQuality(context); var urgency = EstimateUrgency(context); var costSensitivity = context.CostSensitivity; return new RequestAnalysis { Complexity = complexity, RequiredQuality = requiredQuality, Urgency = urgency, CostSensitivity = costSensitivity, TokenEstimate = await EstimateTokensAsync(prompt), ContainsSensitiveData = await ContainsSensitiveDataAsync(prompt) }; } private async Task<ModelSelection> SelectModelAsync( RequestAnalysis analysis, UserContext context) { var candidates = new List<ModelCandidate>(); // Кандидаты от OpenAI if (!analysis.ContainsSensitiveData && context.Budget > 0) { candidates.Add(new ModelCandidate { Type = ModelType.OpenAI_GPT4, EstimatedCost = analysis.TokenEstimate * 0.06f / 1000, // примерная стоимость EstimatedLatency = TimeSpan.FromSeconds(2), QualityScore = 0.95f }); candidates.Add(new ModelCandidate { Type = ModelType.OpenAI_GPT35, EstimatedCost = analysis.TokenEstimate * 0.002f / 1000, EstimatedLatency = TimeSpan.FromSeconds(1), QualityScore = 0.85f }); } // Локальные кандидаты if (analysis.Urgency == UrgencyLevel.Low || analysis.CostSensitivity == CostSensitivity.High) { candidates.Add(new ModelCandidate { Type = ModelType.Local_Large, EstimatedCost = 0, EstimatedLatency = TimeSpan.FromSeconds(5), QualityScore = 0.75f }); candidates.Add(new ModelCandidate { Type = ModelType.Local_Fast, EstimatedCost = 0, EstimatedLatency = TimeSpan.FromSeconds(1), QualityScore = 0.65f }); } // Выбор лучшего кандидата на основе взвешенной оценки var bestCandidate = candidates .OrderByDescending(c => CalculateScore(c, analysis, context)) .First(); return new ModelSelection { Type = bestCandidate.Type, Reason = GenerateSelectionReason(bestCandidate, analysis) }; } private float CalculateScore( ModelCandidate candidate, RequestAnalysis analysis, UserContext context) { var weights = new ScoringWeights { QualityWeight = analysis.RequiredQuality == QualityRequirement.High ? 0.5f : 0.3f, CostWeight = analysis.CostSensitivity == CostSensitivity.High ? 0.4f : 0.2f, LatencyWeight = analysis.Urgency == UrgencyLevel.High ? 0.3f : 0.1f }; // Нормализация оценок var qualityScore = candidate.QualityScore; var costScore = 1.0f - Math.Min(candidate.EstimatedCost / context.Budget, 1.0f); var latencyScore = 1.0f - Math.Min( (float)candidate.EstimatedLatency.TotalSeconds / 10, 1.0f); return qualityScore * weights.QualityWeight + costScore * weights.CostWeight + latencyScore * weights.LatencyWeight; } // Пакетная обработка с адаптивным роутингом public async Task<List<ProcessResult>> ProcessBatchAdaptiveAsync( List<ProcessRequest> requests, CancellationToken cancellationToken = default) { var batches = new Dictionary<ModelType, List<ProcessRequest>>(); // Группировка запросов по выбранной модели foreach (var request in requests) { var analysis = await AnalyzeRequestAsync(request.Prompt, request.Context); var modelSelection = await SelectModelAsync(analysis, request.Context); if (!batches.ContainsKey(modelSelection.Type)) { batches[modelSelection.Type] = new List<ProcessRequest>(); } batches[modelSelection.Type].Add(request with { SelectedModel = modelSelection }); } // Параллельная обработка каждой группы var tasks = batches.Select(async batch => { var results = await ProcessModelBatchAsync( batch.Key, batch.Value, cancellationToken); return results; }); var allResults = await Task.WhenAll(tasks); return allResults.SelectMany(r => r).ToList(); } private async Task<List<ProcessResult>> ProcessModelBatchAsync( ModelType modelType, List<ProcessRequest> requests, CancellationToken cancellationToken) { switch (modelType) { case ModelType.OpenAI_GPT4: case ModelType.OpenAI_GPT35: var prompts = requests.Select(r => r.Prompt).ToList(); var openAiResults = await _openAIService.ProcessBatchAsync(prompts); return requests.Zip(openAiResults, (req, res) => new ProcessResult { RequestId = req.Id, ModelUsed = modelType, Result = res, Cost = CalculateOpenAICost(res, modelType), Latency = TimeSpan.Zero // Должно измеряться }).ToList(); case ModelType.Local_Large: case ModelType.Local_Fast: var localResults = await _localModelService.ProcessBatchAsync( requests.Select(r => r.Prompt).ToList(), modelType == ModelType.Local_Large ? LocalModelSize.Large : LocalModelSize.Fast); return requests.Zip(localResults, (req, res) => new ProcessResult { RequestId = req.Id, ModelUsed = modelType, Result = res, Cost = 0, Latency = TimeSpan.Zero // Должно измеряться }).ToList(); default: throw new InvalidOperationException($"Unsupported model type: {modelType}"); } } }
Безопасность и мониторинг
4.1. Защита данных и конфиденциальность
public class SecureAIService { private readonly IDataProtector _dataProtector; private readonly IEncryptionService _encryptionService; private readonly IAnonymizationService _anonymizationService; private readonly IAuditLogger _auditLogger; public SecureAIService( IDataProtectionProvider dataProtectionProvider, IEncryptionService encryptionService, IAnonymizationService anonymizationService, IAuditLogger auditLogger) { _dataProtector = dataProtectionProvider.CreateProtector("AI.Data"); _encryptionService = encryptionService; _anonymizationService = anonymizationService; _auditLogger = auditLogger; } public async Task<string> ProcessSecureAsync(string input, UserContext context) { // 1. Аудит входа await _auditLogger.LogInputAsync(input, context); // 2. Проверка на PII (Personally Identifiable Information) var piiDetection = await DetectPIIAsync(input); if (piiDetection.HasPII) { // 3. Анонимизация чувствительных данных input = await _anonymizationService.AnonymizeAsync(input, piiDetection.Entities); await _auditLogger.LogPIIEventAsync(piiDetection, context); } // 4. Проверка на вредоносный контент var safetyCheck = await CheckContentSafetyAsync(input); if (!safetyCheck.IsSafe) { throw new SecurityException($"Unsafe content detected: {safetyCheck.Reason}"); } // 5. Шифрование перед отправкой в облако (если нужно) string processedResult; if (context.UseCloudAI && !context.AllowUnencrypted) { var encryptedInput = await _encryptionService.EncryptAsync(input); var encryptedResult = await CallExternalAIAsync(encryptedInput); processedResult = await _encryptionService.DecryptAsync(encryptedResult); } else { processedResult = await CallLocalAIAsync(input); } // 6. Проверка выходных данных var outputSafetyCheck = await CheckContentSafetyAsync(processedResult); if (!outputSafetyCheck.IsSafe) { processedResult = "[CONTENT BLOCKED]"; await _auditLogger.LogBlockedOutputAsync(outputSafetyCheck, context); } // 7. Аудит выхода await _auditLogger.LogOutputAsync(processedResult, context); return processedResult; } private async Task<PIIDetectionResult> DetectPIIAsync(string text) { // Использование локальной модели для обнаружения PII var entities = new List<PIEntity>(); // Пример: обнаружение email, телефонов, имен var emailPattern = @"[\w\.-]+@[\w\.-]+\.\w+"; var phonePattern = @"\+?[\d\s\-\(\)]{10,}"; var emailMatches = Regex.Matches(text, emailPattern); var phoneMatches = Regex.Matches(text, phonePattern); foreach (Match match in emailMatches) { entities.Add(new PIEntity { Type = PIEntityType.Email, Value = match.Value, StartIndex = match.Index, EndIndex = match.Index + match.Length }); } foreach (Match match in phoneMatches) { entities.Add(new PIEntity { Type = PIEntityType.Phone, Value = match.Value, StartIndex = match.Index, EndIndex = match.Index + match.Length }); } return new PIIDetectionResult { HasPII = entities.Count > 0, Entities = entities }; } private async Task<SafetyCheckResult> CheckContentSafetyAsync(string text) { // Использование локальной модели классификации контента var categories = new[] { "hate", "hate/threatening", "self-harm", "sexual", "sexual/minors", "violence", "violence/graphic" }; var scores = await ClassifyContentAsync(text, categories); var maxScore = scores.Max(); var maxIndex = Array.IndexOf(scores, maxScore); return new SafetyCheckResult { IsSafe = maxScore < 0.5, // Порог безопасности Reason = maxScore >= 0.5 ? categories[maxIndex] : null, Scores = scores }; } // Регулярная проверка моделей на смещение (bias) public async Task<BiasAuditResult> AuditModelForBiasAsync( string modelId, List<BiasTestSet> testSets) { var results = new List<BiasTestResult>(); foreach (var testSet in testSets) { var groupResults = new Dictionary<string, List<float>>(); foreach (var example in testSet.Examples) { var prediction = await GetModelPredictionAsync(modelId, example.Input); var score = EvaluatePrediction(prediction, example.ExpectedOutput); if (!groupResults.ContainsKey(example.DemographicGroup)) { groupResults[example.DemographicGroup] = new List<float>(); } groupResults[example.DemographicGroup].Add(score); } // Статистический анализ различий между группами var biasMetrics = CalculateBiasMetrics(groupResults); results.Add(new BiasTestResult { TestSetName = testSet.Name, Metrics = biasMetrics, HasSignificantBias = biasMetrics.PValue < 0.05 }); } return new BiasAuditResult { ModelId = modelId, AuditDate = DateTime.UtcNow, TestResults = results, OverallBiasScore = results.Average(r => r.Metrics.EffectSize) }; } }
4.2. Мониторинг и метрики
public class AIMonitoringService { private readonly IMetricsCollector _metricsCollector; private readonly IAlertService _alertService; private readonly ILogger<AIMonitoringService> _logger; private readonly Dictionary<string, ModelMetrics> _modelMetrics; public AIMonitoringService( IMetricsCollector metricsCollector, IAlertService alertService, ILogger<AIMonitoringService> logger) { _metricsCollector = metricsCollector; _alertService = alertService; _logger = logger; _modelMetrics = new Dictionary<string, ModelMetrics>(); } public async Task TrackRequestAsync( AIRequest request, AIResponse response, TimeSpan processingTime) { var modelKey = $"{request.ModelType}:{request.ModelName}"; if (!_modelMetrics.ContainsKey(modelKey)) { _modelMetrics[modelKey] = new ModelMetrics(); } var metrics = _modelMetrics[modelKey]; lock (metrics) { metrics.TotalRequests++; metrics.TotalProcessingTime += processingTime; metrics.AverageProcessingTime = metrics.TotalProcessingTime / metrics.TotalRequests; if (response.IsSuccess) { metrics.SuccessfulRequests++; } else { metrics.FailedRequests++; metrics.LastError = response.Error; metrics.LastErrorTime = DateTime.UtcNow; } // Токены и стоимость metrics.TotalInputTokens += request.InputTokens; metrics.TotalOutputTokens += response.OutputTokens; metrics.TotalCost += CalculateCost(request, response); // Качество (если есть ground truth) if (request.ExpectedOutput != null) { var qualityScore = CalculateQualityScore(response.Output, request.ExpectedOutput); metrics.QualityScores.Add(qualityScore); metrics.AverageQuality = metrics.QualityScores.Average(); } } // Публикация метрик await PublishMetricsAsync(modelKey, metrics); // Проверка алертов await CheckAlertsAsync(modelKey, metrics); } private async Task CheckAlertsAsync(string modelKey, ModelMetrics metrics) { var alerts = new List<Alert>(); // Алерт на высокую частоту ошибок var errorRate = (double)metrics.FailedRequests / metrics.TotalRequests; if (errorRate > 0.05) // 5% ошибок { alerts.Add(new Alert { Type = AlertType.HighErrorRate, Model = modelKey, Value = errorRate, Threshold = 0.05, Message = $"High error rate detected: {errorRate:P2}" }); } // Алерт на увеличение времени обработки if (metrics.AverageProcessingTime > TimeSpan.FromSeconds(10)) { alerts.Add(new Alert { Type = AlertType.HighLatency, Model = modelKey, Value = metrics.AverageProcessingTime.TotalSeconds, Threshold = 10, Message = $"High latency detected: {metrics.AverageProcessingTime.TotalSeconds:F1}s" }); } // Алерт на увеличение стоимости var avgCostPerRequest = metrics.TotalCost / metrics.TotalRequests; if (avgCostPerRequest > 0.1m) // $0.10 per request { alerts.Add(new Alert { Type = AlertType.HighCost, Model = modelKey, Value = (double)avgCostPerRequest, Threshold = 0.1, Message = $"High cost per request: ${avgCostPerRequest:F4}" }); } // Алерт на снижение качества if (metrics.QualityScores.Count >= 100) { var recentQuality = metrics.QualityScores.TakeLast(100).Average(); var historicalQuality = metrics.QualityScores.SkipLast(100).Average(); if (recentQuality < historicalQuality * 0.9) // 10% снижение { alerts.Add(new Alert { Type = AlertType.QualityDegradation, Model = modelKey, Value = recentQuality, HistoricalValue = historicalQuality, Message = $"Quality degradation detected: {recentQuality:F3} vs {historicalQuality:F3}" }); } } // Отправка алертов foreach (var alert in alerts) { await _alertService.SendAlertAsync(alert); _logger.LogWarning($"Alert triggered: {alert.Message}"); } } public async Task<AIDashboard> GetDashboardAsync() { var dashboard = new AIDashboard { Timestamp = DateTime.UtcNow, Models = _modelMetrics.Select(kvp => new ModelDashboard { ModelKey = kvp.Key, Metrics = kvp.Value, HealthStatus = CalculateHealthStatus(kvp.Value) }).ToList(), Summary = new SummaryMetrics { TotalRequests = _modelMetrics.Sum(m => m.Value.TotalRequests), TotalCost = _modelMetrics.Sum(m => m.Value.TotalCost), AverageLatency = TimeSpan.FromSeconds( _modelMetrics.Average(m => m.Value.AverageProcessingTime.TotalSeconds)), OverallHealth = CalculateOverallHealth() } }; // Добавление трендов dashboard.Trends = await CalculateTrendsAsync(); return dashboard; } private async Task<List<MetricTrend>> CalculateTrendsAsync() { var trends = new List<MetricTrend>(); var now = DateTime.UtcNow; // Запрос исторических данных (например, из базы данных) var historicalData = await _metricsCollector.GetHistoricalMetricsAsync( now.AddHours(-24), now); // Расчет трендов для каждой модели foreach (var modelData in historicalData.GroupBy(d => d.ModelKey)) { var requestTrend = CalculateLinearTrend( modelData.Select(d => (double)d.RequestCount).ToArray()); var latencyTrend = CalculateLinearTrend( modelData.Select(d => d.AverageLatency.TotalSeconds).ToArray()); var costTrend = CalculateLinearTrend( modelData.Select(d => (double)d.TotalCost).ToArray()); trends.Add(new MetricTrend { ModelKey = modelData.Key, RequestTrend = requestTrend, LatencyTrend = latencyTrend, CostTrend = costTrend, IsIncreasing = requestTrend.Slope > 0 || costTrend.Slope > 0 }); } return trends; } private LinearTrend CalculateLinearTrend(double[] values) { if (values.Length < 2) return new LinearTrend { Slope = 0, R2 = 0 }; var x = Enumerable.Range(0, values.Length).Select(i => (double)i).ToArray(); var n = values.Length; var sumX = x.Sum(); var sumY = values.Sum(); var sumXY = x.Zip(values, (a, b) => a * b).Sum(); var sumX2 = x.Sum(a => a * a); var sumY2 = values.Sum(a => a * a); var slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); var intercept = (sumY - slope * sumX) / n; var r2 = Math.Pow( (n * sumXY - sumX * sumY) / Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)), 2); return new LinearTrend { Slope = slope, Intercept = intercept, R2 = r2 }; } }
Заключение
Интеграция искусственного интеллекта в C# приложения больше не является экзотической задачей, а становится стандартной практикой для современных разработчиков. Рассмотренные подходы — от облачных API до локальных моделей — предоставляют гибкие инструменты для решения различных бизнес-задач.

