Начало работы с семантическим ядром: оркестровка ИИ в C#

· 13 мин чтения

Если вы создавали .NET-приложения и наблюдали за развитием ИИ-среды, вы, вероятно, задавались вопросом: какой лучший способ интегрировать большие языковые модели в мои проекты на C#, не превращая мою кодовую базу в спагетти? Именно эту проблему решает семантическое ядро ​​Microsoft, и, потратив последний год на создание производственных приложений с его помощью, я могу вам сказать, что оно стало одним из самых важных инструментов в моем наборе инструментов разработчика.

В этом посте я расскажу вам обо всем, что вам нужно для начала работы с семантическим ядром — от понимания основных концепций до создания реального помощника ИИ. Если вы только начинаете заниматься разработкой ИИ или ищете структурированный способ организации вызовов LLM в существующих приложениях .NET, это руководство поможет вам.

Что такое семантическое ядро?

Семантическое ядро ​​(SK) — это пакет SDK с открытым исходным кодом от Microsoft, который действует как уровень оркестрации между кодом вашего приложения и большими языковыми моделями, такими как GPT-4o, Azure OpenAI или другими службами искусственного интеллекта. Думайте об этом как о легком промежуточном программном обеспечении, которое позволяет вам простым и компонуемым образом сочетать традиционный код C# с возможностями искусственного интеллекта.

Но почему бы просто не вызвать API OpenAI напрямую? Вы абсолютно можете — и для простых случаев использования это нормально. Но в тот момент, когда вам нужно:

  • Пусть ИИ решает какие функции вызывать на основе пользовательского ввода.
  • Объедините несколько вызовов ИИ с традиционным кодом в конвейере.
  • Добавьте память и контекст, чтобы ИИ запоминал предыдущие взаимодействия.
  • Создавайте многоэтапных агентов, которые рассуждают посредством сложных задач.

…вы изобретаете велосипед. Semantic Kernel предоставляет вам все это «из коробки» с первоклассной поддержкой .NET, интеграцией внедрения зависимостей и архитектурой плагинов, которая кажется естественной любому разработчику C#.

Проект находится на GitHub в репозитории microsoft/semantic-kernel и имеет SDK для C#, Python и Java. C# SDK является наиболее зрелым, и именно на нем мы здесь сосредоточимся.

Основные понятия

Прежде чем писать какой-либо код, давайте разберемся со строительными блоками.

Ядро

Kernel — это центральный объект в семантическом ядре. Это оркестратор — то, что связывает воедино ваши службы искусственного интеллекта, плагины и конфигурацию. Вы создаете его, регистрируете в нем свои службы и плагины, а затем используете его для выполнения подсказок или вызова функций. Если вы знакомы с внедрением зависимостей в ASP.NET Core, ядро ​​покажется вам очень знакомым — по сути, это сервисный контейнер со сверхспособностями искусственного интеллекта.

Плагины и функции

Плагин — это набор связанных функций, которые может вызывать ядро. Функции бывают двух видов:

  • Функции подсказок — определяются как шаблоны естественного языка, которые отправляются в LLM.
  • Встроенные функции — обычные методы C#, украшенные атрибутами, которые ядро может обнаружить и вызвать.

Например, у вас может быть WeatherPlugin со встроенной функцией GetCurrentWeather(string city) и функцией подсказки, которая в удобной форме суммирует данные о погоде.### AI-коннекторы

Коннекторы — это то, как семантическое ядро взаимодействует со службами ИИ. Наиболее распространенными из них являются:

  • AzureOpenAIChatCompletion — для службы Azure OpenAI.
  • OpenAIChatCompletion — напрямую для API OpenAI.
  • Встраивание коннекторов для векторного поиска и памяти

Вы регистрируете их в ядре при запуске, и все остальное просто работает.

Настройка вашего проекта

Давайте запачкаем руки. Начните с создания нового консольного приложения:

[[[ТОК_6]]]

Теперь добавьте пакеты NuGet семантического ядра:

[[[ТОК_7]]]

Если вы используете OpenAI напрямую вместо Azure OpenAI:

[[[ТОК_8]]]

Для поддержки памяти и встраивания (мы воспользуемся этим позже):

[[[ТОК_9]]]

Ваш .csproj должен быть ориентирован на .NET 8 или более позднюю версию. Последние версии Semantic Kernel в полной мере используют современные функции .NET.

Ваше первое ядро

Начнем с самого простого примера — создания ядра, подключения его к сервису ИИ и задания ему вопроса.

[[[ТОК_11]]]

Если вы используете OpenAI напрямую, поменяйте местами регистрацию сервиса:

[[[ТОК_12]]]

Вот и все. Запустите его, и вы получите краткое объяснение внедрения зависимостей. Но это лишь поверхностный взгляд.

Использование шаблонов подсказок

Шаблоны подсказок позволяют параметризовать подсказки переменными, используя синтаксис в стиле Handlebars:

var prompt = """
    You are a technical writer. Write a brief summary of {{$topic}} 
    aimed at developers with {{$experienceLevel}} experience.
    Keep it under 200 words.
    """;

var function = kernel.CreateFunctionFromPrompt(prompt);

var arguments = new KernelArguments
{
    ["topic"] = "gRPC in .NET",
    ["experienceLevel"] = "intermediate"
};

var result = await kernel.InvokeAsync(function, arguments);
Console.WriteLine(result);

Именно здесь семантическое ядро начинает проявлять себя — вы можете определять многократно используемые шаблоны подсказок, создавать их версии и объединять их в более крупные рабочие процессы.

Плагины и встроенные функции

Плагины — это место, где семантическое ядро устраняет разрыв между искусственным интеллектом и существующим кодом C#. Собственная функция — это обычный метод, который вы предоставляете ядру.

using Microsoft.SemanticKernel;
using System.ComponentModel;

public class TimePlugin
{
    [KernelFunction("get_current_time")]
    [Description("Gets the current date and time in UTC")]
    public string GetCurrentTime()
    {
        return DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC");
    }

    [KernelFunction("get_time_in_timezone")]
    [Description("Gets the current time in a specific timezone")]
    public string GetTimeInTimezone(
        [Description("The IANA timezone identifier, e.g. 'America/New_York'")] string timezone)
    {
        var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
        var time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
        return time.ToString("yyyy-MM-dd HH:mm:ss");
    }
}

Обратите внимание на атрибуты [KernelFunction] и [Description]. Это очень важно: описания — это то, что читает ИИ, чтобы понять, когда и как вызывать ваши функции. Хорошие описания определяют разницу между ИИ, который эффективно использует ваши инструменты, и тем, кто запутался.

Зарегистрируйте плагин в своем ядре:

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o",
    endpoint: "https://your-resource.openai.azure.com/",
    apiKey: "your-api-key"
);

builder.Plugins.AddFromType<TimePlugin>();

var kernel = builder.Build();

Вы также можете создавать более сложные плагины, внедряющие сервисы. Поскольку семантическое ядро интегрируется с Microsoft.Extensions.DependencyInjection, ваши плагины могут получать зависимости конструктора, как и любой другой сервис:

public class OrderPlugin
{
    private readonly IOrderRepository _repository;

    public OrderPlugin(IOrderRepository repository)
    {
        _repository = repository;
    }

    [KernelFunction("get_order_status")]
    [Description("Retrieves the status of an order by its ID")]
    public async Task<string> GetOrderStatus(
        [Description("The order ID to look up")] string orderId)
    {
        var order = await _repository.GetByIdAsync(orderId);
        return order is null
            ? $"No order found with ID {orderId}"
            : $"Order {orderId}: {order.Status}, placed on {order.CreatedAt:d}";
    }
}

Вызов функций и автовызов

Здесь все становится действительно интересно. С помощью вызова функции (также известного как вызов инструмента) вы позволяете модели ИИ решать, какую из ваших зарегистрированных функций вызывать в зависимости от контекста разговора. Модель не выполняет код — она возвращает структурированный запрос «Я хочу вызвать функцию X с этими аргументами», а фактический вызов обрабатывается ядром.

Вот как включить автоматический вызов функций:

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o",
    endpoint: "https://your-resource.openai.azure.com/",
    apiKey: "your-api-key"
);

builder.Plugins.AddFromType<TimePlugin>();
builder.Plugins.AddFromType<WeatherPlugin>();

var kernel = builder.Build();

// Enable automatic function calling
var settings = new OpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var result = await kernel.InvokePromptAsync(
    "What time is it in Tokyo and what's the weather like there?",
    new KernelArguments(settings)
);

Console.WriteLine(result);

С помощью FunctionChoiceBehavior.Auto() ядро будет:

  1. Отправьте ИИ запрос вместе с описанием всех доступных функций.
  2. ИИ решает, что ему необходимо вызвать get_time_in_timezone и get_weather
  3. Ядро автоматически выполняет эти функции.
  4. Результаты отправляются обратно в ИИ.
  5. ИИ формирует ответ на естественном языке, используя результаты функции.Этот цикл может происходить несколько раз за один вызов — ИИ может последовательно вызывать несколько функций, чтобы собрать всю необходимую информацию. Вы также можете использовать FunctionChoiceBehavior.Required(), чтобы заставить ИИ вызвать хотя бы одну функцию или предоставить определенный список функций, которые ему разрешено использовать.

Завершение чата с историей

Для диалоговых приложений вы захотите использовать ChatCompletionService напрямую с объектом ChatHistory:

using Microsoft.SemanticKernel.ChatCompletion;

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();

history.AddSystemMessage("""
    You are a helpful developer assistant. You have access to tools 
    for checking the time and weather. Be concise and friendly.
    """);

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(input)) break;

    history.AddUserMessage(input);

    var response = await chatService.GetChatMessageContentAsync(
        history,
        executionSettings: settings,
        kernel: kernel
    );

    history.AddAssistantMessage(response.Content ?? "");
    Console.WriteLine($"Assistant: {response.Content}");
}

Это дает вам полностью интерактивный чат-бот, который сохраняет историю разговоров и может вызывать ваши плагины по мере необходимости.

Память и встраивания

Одним из самых мощных шаблонов в приложениях ИИ является Расширенная генерация данных (RAG) — предоставление ИИ доступа к вашим собственным данным путем их внедрения в векторное пространство и извлечения соответствующих фрагментов во время запроса.

Семантическое ядро ​​предоставляет абстракции для работы с векторными хранилищами и внедрениями. Вот как настроить векторное хранилище в памяти для разработки:

using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Embeddings;

#pragma warning disable SKEXP0010

// Create an embedding generation service
var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAITextEmbeddingGeneration(
    deploymentName: "text-embedding-ada-002",
    endpoint: "https://your-resource.openai.azure.com/",
    apiKey: "your-api-key"
);

var kernel = builder.Build();
var embeddingService = kernel.GetRequiredService<ITextEmbeddingGenerationService>();

// Generate embeddings for your documents
var documents = new[]
{
    "Semantic Kernel is an open-source SDK for AI orchestration.",
    "Azure OpenAI provides enterprise-grade AI models.",
    "Plugins in SK allow you to expose C# methods to AI models."
};

var embeddings = await embeddingService.GenerateEmbeddingsAsync(documents);

В производственных сценариях эти внедрения следует хранить в выделенной векторной базе данных, например Azure AI Search, Qdrant или Pinecone. В семантическом ядре есть соединители для всего этого через абстракции Microsoft.Extensions.VectorData.

Типичный процесс RAG выглядит следующим образом:

  1. Всасывание: разбивайте документы на части, создавайте вложения и сохраняйте их в векторной базе данных.
  2. Извлечь: когда пользователь задает вопрос, встройте запрос и найдите наиболее похожие документы.
  3. Создать: передать полученные документы в качестве контекста в LLM вместе с вопросом пользователя.
[KernelFunction("search_knowledge_base")]
[Description("Searches the internal knowledge base for relevant information")]
public async Task<string> SearchKnowledgeBase(
    [Description("The search query")] string query)
{
    var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query);

    var results = await _vectorStore.SearchAsync(queryEmbedding, limit: 3);

    return string.Join("\n\n", results.Select(r => r.Text));
}

Предоставляя ваш конвейер RAG как функцию ядра, ИИ может автоматически решать, когда ему нужно выполнить поиск в вашей базе знаний, сохраняя чистоту оркестрации и позволяя модели делать то, что она делает лучше всего.

Планировщики и агенты

По мере усложнения ваших приложений ИИ вам понадобится ИИ для планирования и выполнения многоэтапных задач. Именно здесь на помощь приходит структура агента Semantic Kernel.

Основы: агент завершения чата

Самый простой тип агента включает в себя модель завершения чата с инструкциями и плагинами:

using Microsoft.SemanticKernel.Agents;

#pragma warning disable SKEXP0110

var agent = new ChatCompletionAgent
{
    Name = "DevAssistant",
    Instructions = """
        You are a senior .NET developer assistant. Help users with code 
        reviews, architecture decisions, and debugging. Always provide 
        code examples when relevant. Use your available tools to look up 
        current information when needed.
        """,
    Kernel = kernel,
    Arguments = new KernelArguments(settings)
};

var history = new ChatHistory();

history.AddUserMessage("How should I structure a clean architecture project in .NET 8?");

await foreach (var message in agent.InvokeAsync(history))
{
    Console.WriteLine(message.Content);
    history.Add(message);
}

Многоагентное сотрудничество

По-настоящему эффектно получается, когда у вас несколько агентов, работающих вместе. Семантическое ядро поддерживает шаблоны групповых чатов, в которых сотрудничают агенты с разными специализациями:

#pragma warning disable SKEXP0110

var codeReviewer = new ChatCompletionAgent
{
    Name = "CodeReviewer",
    Instructions = """
        You review C# code for bugs, performance issues, and best practices.
        Be specific about what you find and suggest concrete fixes.
        """,
    Kernel = kernel
};

var securityAuditor = new ChatCompletionAgent
{
    Name = "SecurityAuditor",
    Instructions = """
        You focus exclusively on security vulnerabilities in code.
        Look for injection attacks, authentication issues, data exposure,
        and OWASP Top 10 violations.
        """,
    Kernel = kernel
};

var groupChat = new AgentGroupChat(codeReviewer, securityAuditor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = new MaximumIterationTerminationStrategy(4)
    }
};

groupChat.AddChatMessage(
    new ChatMessageContent(AuthorRole.User, "Review this code: ...")
);

await foreach (var message in groupChat.InvokeAsync())
{
    Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}

Этот шаблон невероятно полезен для сложных задач, где необходимо учитывать различные точки зрения или области знаний. Каждый агент работает со своей собственной системной подсказкой и может иметь свой собственный набор плагинов.

Пример из реальной жизни: создание помощника по проектной документации

Давайте свяжем все вместе на практическом примере — ИИ-помощнике, который помогает разработчикам понимать кодовую базу, читая файлы и отвечая на вопросы.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

// Define our plugins
public class FileSystemPlugin
{
    private readonly string _rootPath;

    public FileSystemPlugin(string rootPath)
    {
        _rootPath = rootPath;
    }

    [KernelFunction("read_file")]
    [Description("Reads the contents of a file from the project directory")]
    public async Task<string> ReadFile(
        [Description("Relative path to the file")] string path)
    {
        var fullPath = Path.Combine(_rootPath, path);

        if (!File.Exists(fullPath))
            return $"File not found: {path}";

        var content = await File.ReadAllTextAsync(fullPath);

        // Truncate very large files
        if (content.Length > 8000)
            content = content[..8000] + "\n... [truncated]";

        return content;
    }

    [KernelFunction("list_files")]
    [Description("Lists files in a directory, optionally filtered by extension")]
    public string ListFiles(
        [Description("Relative directory path")] string directory,
        [Description("File extension filter like '.cs' or '.json'")] string? extension = null)
    {
        var fullPath = Path.Combine(_rootPath, directory);

        if (!Directory.Exists(fullPath))
            return $"Directory not found: {directory}";

        var files = Directory.GetFiles(fullPath, "*.*", SearchOption.AllDirectories)
            .Select(f => Path.GetRelativePath(_rootPath, f))
            .Where(f => extension is null || f.EndsWith(extension))
            .Take(50);

        return string.Join("\n", files);
    }
}

public class DocumentationPlugin
{
    [KernelFunction("generate_summary")]
    [Description("Generates a structured markdown summary for documentation")]
    public string GenerateSummaryTemplate(
        [Description("Name of the component")] string componentName,
        [Description("Brief description")] string description,
        [Description("Key responsibilities as comma-separated values")] string responsibilities)
    {
        var items = responsibilities.Split(',', StringSplitOptions.TrimEntries);
        var bullets = string.Join("\n", items.Select(r => $"- {r}"));

        return $"""
            ## {componentName}

            {description}

            ### Responsibilities
            {bullets}

            ---
            *Generated documentation — review and expand as needed.*
            """;
    }
}

// Wire it all up
var builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o",
    endpoint: "https://your-resource.openai.azure.com/",
    apiKey: "your-api-key"
);

builder.Plugins.AddFromObject(new FileSystemPlugin("./src"));
builder.Plugins.AddFromType<DocumentationPlugin>();

var kernel = builder.Build();

var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();

history.AddSystemMessage("""
    You are a codebase documentation assistant. You help developers understand 
    projects by reading source files and explaining architecture, patterns, 
    and design decisions. 
    
    When asked about code, use your tools to read the actual files rather 
    than guessing. Be specific and reference actual code when possible.
    Generate documentation artifacts when asked.
    """);

var settings = new OpenAIPromptExecutionSettings
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

Console.WriteLine("Documentation Assistant ready. Ask me about your codebase!");
Console.WriteLine("Type 'exit' to quit.\n");

while (true)
{
    Console.Write("You: ");
    var input = Console.ReadLine();

    if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
        break;

    history.AddUserMessage(input);

    var response = await chatService.GetChatMessageContentAsync(
        history,
        executionSettings: settings,
        kernel: kernel
    );

    Console.WriteLine($"\nAssistant: {response.Content}\n");
    history.AddAssistantMessage(response.Content ?? "");
}

Этот помощник может:- Список и чтение файлов из каталога вашего проекта.

  • Отвечайте на вопросы о кодовой базе, читая реальные исходные файлы.
  • Создать документацию в формате уценки. – Сохраняйте контекст разговора, чтобы дополнительные вопросы работали естественно.

ИИ автоматически решает, когда вызывать read_file, list_files или generate_summary на основании вашего запроса. Спросите его: «Что делает OrderService?» и он прочитает файл, проанализирует его и объяснит. Попросите его «Создать документацию для модуля аутентификации», и он изучит файлы, поймет структуру и выдаст отформатированное резюме.

Советы по производству

Прежде чем вы выпустите свое приложение семантического ядра, я усвоил несколько вещей:

Правильно используйте внедрение зависимостей. В приложениях ASP.NET Core регистрируйте ядро и службы в контейнере DI, а не создавайте их в режиме реального времени:

builder.Services.AddKernel()
    .AddAzureOpenAIChatCompletion(
        deploymentName: "gpt-4o",
        endpoint: configuration["AzureOpenAI:Endpoint"]!,
        apiKey: configuration["AzureOpenAI:ApiKey"]!
    )
    .Plugins.AddFromType<TimePlugin>()
    .AddFromType<OrderPlugin>();

Обрабатывайте ошибки корректно. Вызовы LLM могут завершаться с ошибкой, истекать по истечении времени или возвращать неожиданные результаты. Оберните свои вызовы в блоки try-catch и реализуйте политики повторных попыток с помощью Polly или встроенных функций устойчивости.

Отслеживайте использование токенов. Каждое приглашение, каждое описание функции и каждая часть истории чата потребляют токены. Используйте фильтры для регистрации и отслеживания использования:

kernel.FunctionInvocationFilters.Add(new LoggingFilter());

Сохраняйте точность описаний функций. Расплывчатые описания приводят к неправильному вызову функций ИИ. Проверьте свои описания, задав вопрос: «Если бы я только прочитал описание, знал бы я точно, когда и как использовать эту функцию?»

Заключение

Семантическое ядро — одна из тех библиотек, которая фундаментально меняет ваше представление о создании приложений. Это не просто оболочка API — это среда оркестровки, которая позволяет объединять возможности ИИ с традиционным кодом таким образом, чтобы его можно было поддерживать, тестировать и готовить к использованию.

Что мне больше всего в нем нравится, так это то, что он уважает экосистему .NET. Он использует уже знакомые вам шаблоны — внедрение зависимостей, атрибуты, async/await, интерфейсы — и распространяет их на мир ИИ. Вам не нужно изучать совершенно новую парадигму; вы просто добавляете ИИ в качестве еще одной возможности в свой набор инструментов.

Если вы создаете .NET-приложения и еще не изучили семантическое ядро, сейчас самое время. SDK стабилен, сообщество активно, а шаблоны, которые он обеспечивает — от простой быстрой оркестровки до многоагентной совместной работы — становятся важными навыками для современных разработчиков.

Начните с малого. Создайте ядро, зарегистрируйте плагин и наблюдайте, как ИИ вызывает ваш код. Как только это произойдет, вы начнете видеть возможности для добавления интеллекта повсюду в ваших приложениях.

Официальная документация и репозиторий GitHub — отличные ресурсы для продолжения вашего путешествия. Приятного строительства!