Создание системы RAG на C# с семантическим ядром

· 4 мин чтения

Введение

Если вы пытались использовать LLM для ответа на вопросы о ваших собственных данных — документах компании, спецификациях продуктов, внутренних базах знаний — вы, вероятно, заметили, что это либо галлюцинирует, либо просто говорит: «У меня нет информации об этом». Это потому, что модель знает только то, чему она обучалась.

RAG (Расширенная генерация извлечения) исправляет это. Вместо точной настройки модели на основе ваших данных вы извлекаете соответствующие фрагменты своих документов во время запроса и передаете их в LLM в качестве контекста. Затем модель генерирует ответы, основанные на ваших фактических данных.

В этом посте я расскажу вам, как создать полный конвейер RAG на C# с использованием семантического ядра.

Как работает RAG

Порядок действий прост:

  1. Всасывание: разбивайте документы на фрагменты, создавайте вложения для каждого фрагмента и сохраняйте их в векторной базе данных.
  2. Запрос: когда пользователь задает вопрос, сгенерируйте вложение для запроса, выполните поиск в базе данных векторов похожих фрагментов.
  3. Создать: передать полученные фрагменты в качестве контекста в LLM вместе с вопросом пользователя.

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

Предварительные условия

dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
dotnet add package Microsoft.Extensions.VectorData.Abstractions
dotnet add package Microsoft.SemanticKernel.Connectors.InMemory

В рабочей среде вы можете заменить хранилище в памяти на Azure AI Search, Qdrant, Pinecone или любую другую поддерживаемую базу данных векторов. Но in-memory идеально подходит для обучения и прототипирования.

Настройка ядра

[[[ТОК_1]]]

Нам нужны две модели: одна для завершения чата (ответы на вопросы) и одна для генерации вложений (преобразование текста в векторы).

Определение модели данных

Нам нужен класс для представления фрагментов нашего документа в векторном хранилище:

[[[ТОК_2]]]

Атрибут VectorStoreRecordVector(1536) сообщает векторному хранилищу размерность наших вложений. Модель text-embedding-3-small создает 1536-мерные векторы.

Разбиение документов на части

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

public static class TextChunker
{
    public static List<string> SplitText(string text, int maxChunkSize = 500, int overlap = 50)
    {
        var chunks = new List<string>();
        var paragraphs = text.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
        var currentChunk = new System.Text.StringBuilder();

        foreach (var paragraph in paragraphs)
        {
            if (currentChunk.Length + paragraph.Length > maxChunkSize && currentChunk.Length > 0)
            {
                chunks.Add(currentChunk.ToString().Trim());

                // Keep overlap from the end of the previous chunk
                var overlapText = currentChunk.ToString();
                currentChunk.Clear();
                if (overlapText.Length > overlap)
                {
                    currentChunk.Append(overlapText[^overlap..]);
                    currentChunk.Append(' ');
                }
            }

            currentChunk.Append(paragraph);
            currentChunk.Append("\n\n");
        }

        if (currentChunk.Length > 0)
        {
            chunks.Add(currentChunk.ToString().Trim());
        }

        return chunks;
    }
}

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

Загрузка документов

Теперь давайте соберем все это вместе, чтобы загрузить документы в наше векторное хранилище:

[[[ТОК_6]]]

Поиск соответствующих фрагментов

Когда пользователь задает вопрос, мы генерируем вложение для его запроса и ищем похожие фрагменты:

[[[ТОК_7]]]

Генерация ответов с учетом контекста

Теперь часть RAG — мы берем полученные фрагменты и включаем их в качестве контекста в нашу подсказку:

[[[ТОК_8]]]

Использование этого

[[[ТОК_9]]]

Переезд в производство

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

[[[ТОК_10]]]

Заменить местами очень просто, поскольку все они реализуют один и тот же интерфейс IVectorStore:

[[[ТОК_12]]]Все остальное остается прежним. В этом красота абстракции.

Советы по созданию RAG-систем

Несколько вещей, которые я усвоил на собственном горьком опыте:

  • Размер блока имеет большое значение. Если он слишком мал, вы теряете контекст. Слишком большой размер — и вы потратите токены на нерелевантный контент. Начните с 500-800 токенов и корректируйте их в зависимости от ваших данных.
  • Перекрытие предотвращает проблемы с границами. Обычно достаточно перекрытия в 50–100 токенов между фрагментами.
  • Получите больше, чем вы думаете. Начните с topK = 5 и уменьшите, если вы слышите слишком много шума. Лучше иметь дополнительный контекст, чем пропустить соответствующий фрагмент. – Системные подсказки имеют решающее значение. Будьте предельно откровенны, используя только предоставленный контекст. Без этой инструкции модель будет с радостью галлюцинировать «на основе своих обучающих данных».
  • Отслеживайте источники. Всегда сохраняйте метаданные вместе с фрагментами, чтобы вы могли указать, откуда пришел ответ. Пользователи больше доверяют ответам, когда могут проверить источник.
  • При необходимости измените ранжирование. Сходство векторов не является идеальным. Для критически важных приложений добавьте этап переранжирования с использованием модели перекрестного кодирования для повышения точности.

Заключение

RAG на данный момент является одним из самых практичных шаблонов в области искусственного интеллекта. Он позволяет создавать системы вопросов и ответов на основе искусственного интеллекта на основе ваших собственных данных без тонкой настройки, а семантическое ядро ​​делает их на C# удивительно чистыми. Начните с хранилища в памяти, правильно разбейте фрагменты и подсказки, а затем замените реальную векторную базу данных, когда будете готовы к работе.

Приятного кодирования!

Ресурсы