Инженер LLM: от архитектуры Transformer до высоконагруженных RAG-систем

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

Фундамент: архитектура Transformer, механизмы Attention и токенизация

Фундамент: архитектура Transformer, механизмы Attention и токенизация

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

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

Токенизация: как текст становится числами

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

Исторически существовало два крайних подхода:

  1. Пословная токенизация. Каждое слово — это токен. Проблема: словарь становится гигантским (миллионы слов с учетом падежей и опечаток), а неизвестные слова модель не понимает вообще.
  2. Посимвольная токенизация. Каждый символ (буква) — токен. Словарь крошечный (около 100 символов), но последовательности становятся слишком длинными, а отдельная буква не несет смысловой нагрузки.

Индустриальным стандартом стал компромисс — сабворд-токенизация (Subword Tokenization), в частности алгоритм BPE (Byte Pair Encoding).

Алгоритм BPE начинает с отдельных символов и итеративно объединяет самые частые пары в новые токены. В результате частые слова (например, «мама», «the», «работа») становятся одним токеном, а редкие или новые слова разбиваются на смысловые части. Например, слово «нейробиология» может разбиться на токены нейро, биолог и ия.

После разбиения каждый токен заменяется на свой уникальный ID из словаря (например, нейро = 4501, биолог = 892, ия = 112).

Эмбеддинги: от ID к пространству смыслов

Сам по себе ID токена (например, 4501) ничего не значит для нейросети. Число 4501 не больше и не «лучше», чем 892. Чтобы модель могла работать со смыслами, каждый ID заменяется на эмбеддинг (Embedding) — плотный вектор из чисел с плавающей точкой (например, размерностью 4096).

В этом многомерном пространстве векторы похожих по смыслу токенов находятся рядом. Вектор токена «король» минус вектор токена «мужчина» плюс вектор токена «женщина» окажется очень близко к вектору токена «королева».

Эмбеддинг — это координаты смысла слова в многомерном математическом пространстве.

Но здесь возникает проблема. В базовом словаре эмбеддингов у токена «банк» всегда один и тот же вектор, независимо от контекста. Модель получает на вход набор изолированных смыслов. Чтобы понять предложение, токенам нужно «посмотреть» друг на друга и скорректировать свои значения.

Self-Attention: механизм внимания

Сердце архитектуры Transformer — механизм Self-Attention (внимание на самого себя). Он позволяет каждому токену в последовательности вычислить свою связь со всеми остальными токенами и обновить свой вектор с учетом этого контекста.

Представьте, что вы ищете видео на YouTube. Вы вводите поисковый запрос (Query). Платформа сравнивает его с тегами и названиями всех видео в базе (Keys). Если запрос и тег совпадают, платформа показывает вам само видео (Value).

Механизм Attention работает точно так же, но внутри одного предложения. Каждый токен одновременно выступает в трех ролях, для чего его исходный эмбеддинг умножается на три разные матрицы весов, создавая три новых вектора:

  • Query (Q) — запрос: «какой контекст мне нужен, чтобы уточнить свой смысл?»
  • Key (K) — ключ: «какой смысл я содержу, на что во мне можно обратить внимание?»
  • Value (V) — значение: «вот моя фактическая смысловая нагрузка, которую я передам, если на меня обратят внимание».

Математика Attention

Процесс вычисления внимания описывается одной из самых важных формул в современном машинном обучении:

Attention(Q,K,V)=softmax(QKTdk)VAttention(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V

Разберем каждый элемент:

  • Q,K,VQ, K, V — матрицы запросов, ключей и значений для всех токенов последовательности.
  • QKTQK^T — скалярное произведение (dot product) матриц запросов и ключей. Оно вычисляет «оценку совпадения» (score) между каждым словом-запросом и каждым словом-ключом. Чем больше число, тем сильнее связь.
  • dk\sqrt{d_k} — корень из размерности вектора ключа. Это масштабирующий фактор. При умножении больших векторов значения могут стать огромными, что сломает градиенты при обучении. Деление на этот корень стабилизирует процесс.
  • softmaxsoftmax — функция, которая превращает сырые оценки в вероятности (от 0 до 1), сумма которых равна 1. Это веса внимания.
  • Умножение на VV — финальный шаг. Мы берем значения всех токенов и смешиваем их пропорционально полученным весам внимания.

В результате токен «банк» в финансовом контексте получит 80% своего нового значения от слова «деньги», 15% от «снять» и лишь 5% оставит от своего исходного смысла. Его вектор изменится — теперь это контекстуализированный эмбеддинг.

Multi-Head Attention: многомерный взгляд

Слова связаны между собой по-разному. Одно слово может зависеть от другого грамматически (подлежащее и сказуемое), семантически (прилагательное описывает существительное) или логически (причина и следствие).

Если использовать только один набор матриц Q,K,VQ, K, V, модель усреднит все эти разные типы связей в одну кашу. Чтобы избежать этого, Transformer использует Multi-Head Attention (многоголовое внимание).

Вместо одного вычисления внимания, модель параллельно запускает несколько (например, 32 «головы» в модели Llama 2). Каждая голова имеет свои собственные матрицы весов и учится обращать внимание на разные аспекты языка. Одна голова может следить за местоимениями, другая — за рифмой, третья — за фактологическими связями. Результаты всех голов затем склеиваются (конкатенируются) и пропускаются через линейный слой.

Позиционное кодирование (Positional Encoding)

В отличие от старых рекуррентных сетей (RNN), которые читали текст слово за словом, Transformer обрабатывает все токены одновременно (параллельно). Это дает колоссальную скорость, но порождает критическую проблему: матричные умножения в Self-Attention не знают порядка слов. Для формулы фразы «собака кусает человека» и «человек кусает собаку» математически идентичны.

Чтобы вернуть информацию о порядке, к изначальным эмбеддингам токенов добавляется Positional Encoding — специальные векторы, которые содержат информацию о позиции токена в последовательности. Модель получает на вход сумму: смысл слова + его место в предложении.

Блок Трансформера: собираем воедино

Одного слоя внимания недостаточно для глубокого понимания текста. Архитектура LLM состоит из десятков слоев (блоков Трансформера), поставленных друг на друга.

Каждый блок включает в себя:

  1. Multi-Head Attention — токены обмениваются информацией и собирают контекст.
  2. Add & Norm — остаточное соединение (Residual Connection), которое прибавляет вход слоя к его выходу, и нормализация. Это помогает избежать затухания градиентов при обучении глубоких сетей.
  3. Feed-Forward Network (FFN) — классическая полносвязная нейросеть, которая применяется к каждому токену индивидуально. Если Attention — это сбор информации от соседей, то FFN — это осмысление этой информации внутри самого токена.
  4. Снова Add & Norm.

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

Эволюция LLM: от предобучения к SFT и специфике семейств Llama, Mistral, Qwen

Эволюция LLM: от предобучения к SFT и специфике семейств Llama, Mistral, Qwen

Если подать в базовую языковую модель запрос «Как написать скрипт на Python?», она с высокой вероятностью ответит: «Как написать скрипт на Java?». Модель не пытается помочь или ответить на вопрос — она лишь продолжает текст так, как видела это на миллионах страниц форумов, где пользователи перечисляют похожие темы. Чтобы превратить статистического «попугая» в полезного ассистента, способного писать код, извлекать данные и поддерживать диалог, архитектура и процесс обучения Трансформеров прошли серьезную эволюцию.

Два этапа жизни модели: Pre-training и SFT

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

Pre-training (Предобучение)

На этом этапе модель читает терабайты сырого текста (веб-страницы, книги, код) и учится фундаментальным законам языка, логике и фактам о мире. Задача модели сводится к авторегрессионному предсказанию следующего токена (Causal Language Modeling).

Математически цель предобучения — максимизировать вероятность P(X)P(X) для реальных текстовых последовательностей:

P(X)=i=1nP(xix1,x2,,xi1)P(X) = \prod_{i=1}^{n} P(x_i \mid x_1, x_2, \dots, x_{i-1})

где P(X)P(X) — вероятность всей последовательности, nn — общее количество токенов, xix_i — целевой токен на текущем шаге, а x1,,xi1x_1, \dots, x_{i-1} — весь предшествующий контекст.

На выходе получается Base Model (базовая модель). Она отлично знает грамматику и факты, но не умеет вести диалог.

SFT (Supervised Fine-Tuning)

Чтобы научить модель следовать инструкциям, применяется обучение с учителем на специально подготовленных парах «запрос-ответ». Этот процесс называется SFT (или Instruction Tuning).

Для SFT сырой текст больше не подходит. Данные структурируются в виде диалогов с четким разделением ролей: System (системный промпт), User (пользователь) и Assistant (модель).

{"messages": [
  {"role": "system", "content": "Ты полезный AI-ассистент."},
  {"role": "user", "content": "Что такое RAG?"},
  {"role": "assistant", "content": "RAG (Retrieval-Augmented Generation) — это..."}
]}

Модель не видит JSON напрямую. Перед подачей в нейросеть этот словарь преобразуется в плоский текст с помощью Chat Template (шаблона диалога), в который вшиваются специальные управляющие токены. Например, популярный формат ChatML выглядит так:

<|im_start|>system\nТы полезный AI-ассистент.<|im_end|>\n<|im_start|>user\nЧто такое RAG?<|im_end|>\n<|im_start|>assistant\n

Инженерный инсайт: Несовпадение Chat Template при инференсе (генерации) с тем, который использовался при SFT — самая частая причина «глупого» поведения модели, галлюцинаций и отказа отвечать на вопросы.

Архитектурная эволюция: как Трансформер стал быстрее

Классическая архитектура Трансформера оказалась слишком медленной и ресурсоемкой для работы с длинными контекстами (книгами, кодовой базой). Создатели современных открытых LLM внедрили несколько критических оптимизаций.

1. От абсолютных позиций к RoPE

Вместо того чтобы прибавлять статический вектор позиции к эмбеддингу токена, современные модели используют RoPE (Rotary Position Embedding).

RoPE применяет математическое вращение векторов Query и Key в многомерном пространстве. Угол поворота зависит от позиции токена в тексте. Это дает важнейшее преимущество: модель начинает понимать относительное расстояние между словами. Для внимания модели больше не важно, находятся ли слова «векторная» и «база» на 10-й и 11-й позициях или на 1000-й и 1001-й — угол между их векторами будет одинаковым. Это позволяет моделям экстраполировать контекст за пределы длины, на которой они обучались.

2. KV-cache и Grouped-Query Attention (GQA)

При генерации текста модель предсказывает по одному токену за раз. Чтобы не пересчитывать векторы Key (ключ) и Value (значение) для всех предыдущих токенов на каждом шаге, они сохраняются в оперативную память GPU. Этот механизм называется KV-cache.

В классическом Multi-Head Attention (MHA) для каждой «головы» внимания хранятся свои собственные матрицы K и V. При контексте в 32 000 токенов размер KV-cache для одного запроса может достигать нескольких гигабайт, что делает параллельную обработку запросов (batching) невозможной.

Для решения этой проблемы был внедрен GQA (Grouped-Query Attention):

Тип Attention Как работают Query (Q), Key (K) и Value (V) Потребление памяти (KV-cache) Качество генерации
MHA (Multi-Head) У каждой головы Q есть свои уникальные K и V. Максимальное Эталонное
MQA (Multi-Query) Все головы Q делят между собой одну пару K и V. Минимальное Сниженное (потеря нюансов)
GQA (Grouped-Query) Головы Q разбиты на группы (например, по 4). Каждая группа делит одну пару K и V. Умеренное (баланс) Почти равно MHA

GQA стал стандартом индустрии. Он позволяет радикально сжать KV-cache, высвобождая память для обработки десятков параллельных запросов пользователей без потери качества ответов.

3. RMSNorm и SwiGLU

Вместо классического LayerNorm современные модели используют RMSNorm (Root Mean Square Normalization). Он нормализует векторы без вычисления среднего значения (mean centering). Это экономит от 7% до 64% вычислительного времени, практически не влияя на стабильность обучения.

В качестве функции активации в полносвязных слоях (FFN) повсеместно применяется SwiGLU. Она позволяет модели улавливать более сложные нелинейные зависимости в данных, хотя и требует хранения трех матриц весов вместо двух.

Специфика современных семейств LLM

Три основных семейства открытых моделей (Llama, Mistral, Qwen) делят между собой лидерство в индустрии, каждое со своим фокусом.

Семейство Llama (Meta)

Llama задала стандарт для всей open-source экосистемы. Большинство библиотек (включая vLLM и Hugging Face) в первую очередь оптимизируются под архитектуру Llama.

  • Особенности Llama 3: Использует токенизатор с огромным словарем (128 256 токенов), что сделало модель гораздо эффективнее на языках, отличных от английского (текст сжимается в меньшее количество токенов). GQA применяется во всех версиях, даже в самых маленьких (8B).
  • Применение: Универсальный выбор для большинства RAG-систем и базовый фундамент для дообучения.

Семейство Mistral (Mistral AI)

Mistral сфокусировались на максимальной производительности и эффективности при небольшом количестве параметров.

  • Особенности: Внедрение SWA (Sliding Window Attention). Вместо того чтобы каждый токен смотрел на все предыдущие токены с самого начала текста, внимание ограничено «скользящим окном» (например, 4096 токенов). Информация передается дальше по цепочке через слои Трансформера. Это радикально снижает вычислительную сложность на длинных текстах.
  • Применение: Отличный выбор для задач с ограниченными ресурсами GPU и потоковой обработки длинных документов.

Семейство Qwen (Alibaba Cloud)

Qwen (особенно поколения 2 и 2.5) выделяется выдающимися результатами в бенчмарках, математике и программировании.

  • Особенности: Словарь содержит 151 643 токена. Модели Qwen изначально тренировались как мультиязычные и обладают мощной способностью строго следовать системным промптам (System Prompt adherence), что делает их менее склонными к «забыванию» своей роли в длинном диалоге.
  • Применение: Сложные агенты, написание кода, мультиязычные RAG-системы и задачи, где требуется строгий формат вывода (например, чистый JSON без лишних комментариев).

Понимание этих архитектурных особенностей — от структуры Chat Template до механики GQA — является критическим для LLM-инженера. Именно эти знания определяют, почему одна модель помещается на видеокарту и выдает 50 токенов в секунду, а другая падает с ошибкой Out Of Memory (OOM).

Эффективное дообучение: методы PEFT, LoRA и QLoRA в деталях

Эффективное дообучение: методы PEFT, LoRA и QLoRA в деталях

Чтобы провести Instruction Tuning (SFT) для модели Llama 3 с 70 миллиардами параметров, вам потребуется около 1.2 терабайта видеопамяти. Сами веса модели в 16-битном формате занимают 140 ГБ, но алгоритм оптимизации AdamW хранит моменты первого и второго порядка для каждого параметра, а также необходимо держать в памяти градиенты и активации для обратного прохода. Это требует кластера из 16 видеокарт A100 (по 80 ГБ каждая), что делает классическое дообучение недоступным для большинства команд. Как исследователям удается адаптировать такие модели под специфичные задачи на одной потребительской видеокарте?

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

Проблема полного дообучения (Full Fine-Tuning)

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

Математически обновление весов выглядит так:

Wnew=W0+ΔWW_{new} = W_0 + \Delta W

Где WnewW_{new} — обновленная матрица весов, W0W_0 — исходная матрица весов слоя, а ΔW\Delta W — матрица обновлений (изменений весов), которая имеет точно такую же размерность, как и W0W_0.

Если размерность скрытого состояния модели (hidden size) равна 4096, то только одна матрица проекции W0W_0 содержит 4096×409616.74096 \times 4096 \approx 16.7 млн параметров. При полном Fine-tuning система вынуждена вычислять и хранить в памяти матрицу ΔW\Delta W такого же размера для каждого слоя Трансформера, включая все проекции механизма внимания (Query, Key, Value) и полносвязные сети (SwiGLU).

Для решения этой проблемы было разработано семейство методов PEFT (Parameter-Efficient Fine-Tuning). Их суть заключается в заморозке оригинальных весов W0W_0 и добавлении небольшого количества новых, обучаемых параметров. Самым успешным методом из этого семейства стал LoRA.

LoRA: Низкоранговая адаптация

Гипотеза, лежащая в основе LoRA (Low-Rank Adaptation), гласит: несмотря на то, что матрицы весов LLM огромны, изменения этих весов при адаптации к новой задаче лежат в пространстве низкой размерности (имеют низкий внутренний ранг). Нам не нужно обновлять каждый параметр независимо, достаточно найти ключевые направления изменений.

Вместо того чтобы обучать огромную матрицу ΔW\Delta W, LoRA представляет ее в виде произведения двух матриц гораздо меньшего размера:

ΔW=B×A\Delta W = B \times A

Где ΔW\Delta W — итоговая матрица обновлений размерности d×dd \times d, матрица AA имеет размерность r×dr \times d, а матрица BB имеет размерность d×rd \times r. Переменная rr обозначает ранг (rank) — гиперпараметр, который всегда значительно меньше dd (rdr \ll d).

Посмотрим на математику прямого прохода (forward pass) слоя с примененной LoRA:

h=W0x+αrBAxh = W_0 x + \frac{\alpha}{r} B A x

Где:

  • hh — выходной вектор слоя.
  • W0W_0 — замороженная оригинальная матрица весов.
  • xx — входной вектор (эмбеддинг токена).
  • α\alpha (Alpha) — коэффициент масштабирования, определяющий силу влияния LoRA на оригинальные веса.
  • rr — ранг матриц.
  • BB и AA — обучаемые матрицы низкого ранга.

Магия сокращения параметров

Рассмотрим конкретный пример. Пусть размерность слоя d=4096d = 4096, а мы выбираем ранг r=8r = 8.

  • В классическом подходе матрица ΔW\Delta W содержит 4096×4096=16,777,2164096 \times 4096 = 16,777,216 параметров.
  • В подходе LoRA матрица AA содержит 8×4096=32,7688 \times 4096 = 32,768 параметров. Матрица BB содержит 4096×8=32,7684096 \times 8 = 32,768 параметров. Суммарно: 65,53665,536 параметров.

Мы сократили количество обучаемых параметров для этого слоя в 256 раз. Следовательно, пропорционально уменьшилась потребность в видеопамяти для хранения градиентов и состояний оптимизатора.

Инициализация и гиперпараметры

Чтобы в начале обучения модель не деградировала и выдавала ровно те же результаты, что и базовая версия, матрицы инициализируются особым образом:

  • Матрица AA заполняется случайными числами из нормального распределения.
  • Матрица BB заполняется строгими нулями.

В результате на первом шаге обучения произведение B×AB \times A равно нулю. Добавка никак не влияет на предсказания модели, и обучение начинается плавно.

Куда применять LoRA? Изначально авторы метода рекомендовали применять матрицы AA и BB только к проекциям Query и Value в механизме Self-Attention. Однако современная практика (особенно для моделей Llama и Mistral) показывает, что применение LoRA ко всем линейным слоям (включая Key, Output-проекцию и слои FFN) дает качество, практически неотличимое от полного Full Fine-tuning, при сохранении колоссальной экономии памяти.

QLoRA: Квантование для экстремальной экономии

LoRA решает проблему памяти для градиентов и оптимизатора, но сами базовые веса W0W_0 все еще хранятся в 16-битном формате (FP16 или BF16). Для модели на 70B параметров это 140 ГБ только для весов.

QLoRA (Quantized LoRA) делает следующий шаг: метод позволяет обучать адаптеры, пока базовая модель W0W_0 находится в сжатом (квантованном) до 4 бит состоянии.

QLoRA вводит три критических инновации:

  1. Тип данных 4-bit NormalFloat (NF4). Веса глубоких нейросетей обычно имеют нормальное распределение (колокол Гаусса) вокруг нуля. Классическое линейное квантование распределяет биты равномерно, что неэффективно. NF4 — это информационно-теоретически оптимальный тип данных, который распределяет уровни квантования так, чтобы в зонах наибольшего скопления весов (возле нуля) точность была максимальной.
  2. Двойное квантование (Double Quantization). При квантовании блоков весов создаются константы масштабирования (scale factors). В больших моделях эти константы сами по себе начинают занимать гигабайты. QLoRA квантует эти константы с 32 бит до 8 бит, экономя еще около 0.37 бит на каждый параметр (что дает около 3 ГБ для модели на 65B параметров).
  3. Paged Optimizers. При обработке длинных последовательностей (например, при SFT на длинных диалогах) возникают пики потребления VRAM. QLoRA интегрируется с механизмом Unified Memory от NVIDIA, автоматически выгружая состояния оптимизатора в оперативную память (CPU RAM) при нехватке видеопамяти, предотвращая ошибки Out-Of-Memory.

Важный нюанс QLoRA: Обучение не происходит в 4 битах. Во время прямого и обратного прохода нужные блоки базовых весов W0W_0 на лету деквантуются из 4-bit NF4 в 16-bit BF16, умножаются на входные данные xx, а затем снова отбрасываются. Обучаются по-прежнему только 16-битные матрицы AA и BB из LoRA.

Сравнение методов

Характеристика Full Fine-Tuning LoRA QLoRA
Состояние базовых весов W0W_0 Обновляются Заморожены (16-bit) Заморожены (4-bit NF4)
Обучаемые параметры ~100% 0.1% - 2% 0.1% - 2%
Потребление VRAM (Llama 3 8B) ~120 ГБ ~24 ГБ ~8 ГБ
Скорость обучения Медленно Быстро Средне (траты на деквантование)

Weight Merging: подготовка к инференсу

После успешного обучения LoRA или QLoRA мы получаем на диске два артефакта: огромную базовую модель W0W_0 и маленькую папку (обычно несколько сотен мегабайт) с весами адаптера (матрицы AA и BB).

Во время инференса в продакшене вычислять два пути (W0xW_0 x и BAxB A x) неэффективно — это увеличивает задержку (latency) генерации токенов. Поскольку математическая операция линейна, мы можем слить веса (Weight Merging) перед деплоем.

Формула слияния:

Wmerged=W0+αr(B×A)W_{merged} = W_0 + \frac{\alpha}{r} (B \times A)

Где WmergedW_{merged} — итоговая матрица весов, которая загружается в видеопамять для продакшена. W0W_0 — исходные веса, BB и AA — обученные матрицы адаптера, α\alpha и rr — гиперпараметры LoRA.

После перемножения матриц BB и AA мы получаем матрицу ΔW\Delta W точно такого же размера, как W0W_0. Мы просто складываем их. Результат — модель с архитектурой, полностью идентичной базовому Трансформеру. Она не требует дополнительных вычислений при генерации, и ее инференс стоит ровно столько же, сколько инференс оригинальной модели, но теперь она обладает нужными нам знаниями или стилем.

Понимание того, как отделить обучение матриц от итогового графа вычислений, открывает дорогу к высокопроизводительному инференсу, где счет идет на миллисекунды.

Архитектура RAG: пайплайн обработки запроса и стратегии индексации данных

Архитектура RAG: пайплайн обработки запроса и стратегии индексации данных

Представьте, что вы развернули Llama 3 с оптимизированным KV-cache и идеальным системным промптом, а затем спрашиваете её о финансовом отчете компании, подписанном сегодня утром. Модель уверенно сгенерирует ответ, который будет звучать профессионально, логично и... абсолютно неверно. Архитектура Трансформера и этапы SFT делают модель блестящим собеседником, но её знания навсегда заморожены в момент окончания предобучения.

Чтобы языковая модель могла оперировать актуальными или закрытыми корпоративными данными, нам нужно либо постоянно дообучать её, либо динамически подкладывать нужную информацию прямо в контекстное окно. Первый путь дорог и не решает проблему галлюцинаций. Второй путь привел к созданию стандарта де-факто в индустрии — архитектуры RAG (Retrieval-Augmented Generation).

Парадигма RAG: разделение памяти и логики

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

Характеристика Fine-tuning (Дообучение) RAG (Генерация с дополненной выборкой)
Обновление данных Требует повторного запуска цикла обучения. Мгновенно: достаточно добавить документ в базу.
Приватность Веса модели впитывают данные, есть риск утечки через промпты. Документы лежат в защищенной БД, доступ контролируется на этапе поиска.
Галлюцинации Модель может выдумывать факты, опираясь на внутренние паттерны. Снижаются, так как модель строго опирается на предоставленный контекст.
Прослеживаемость Невозможно точно сказать, откуда модель взяла факт. Всегда известна точная цитата и исходный документ (Source Tracking).

RAG — это архитектурный паттерн, который связывает внешнюю систему информационного поиска (Retrieval) с генеративной языковой моделью (Generation).

Пайплайн RAG строго делится на две независимые фазы: Offline (индексация данных) и Online (обработка пользовательского запроса).

Offline-фаза: Стратегии индексации и чанкинга

Прежде чем мы сможем что-то искать, сырые данные (PDF, базы знаний, логи) нужно подготовить. Языковые модели имеют лимит контекстного окна, а механизмы векторного поиска теряют точность, если пытаться сжать целую книгу в один векторный эмбеддинг.

Поэтому сырой текст проходит через процесс чанкинга (Chunking) — разбиения на смысловые фрагменты (чанки), каждый из которых будет векторизован отдельно.

Базовые стратегии чанкинга

  1. Фиксированный размер (Fixed-size chunking) Текст рубится на блоки заданного размера, например, по 500 токенов. Чтобы не разорвать мысль или предложение на полуслове, используется перекрытие (Chunk overlap) — например, 50 токенов. Конец первого чанка дублируется в начале второго. Это самый быстрый и дешевый метод.
  2. Рекурсивный чанкинг (Recursive character chunking) Алгоритм пытается разбить текст, уважая его структуру. Сначала текст делится по двойным переносам строк (абзацы). Если абзац больше лимита, он делится по одинарным переносам, затем по пробелам (слова), и в крайнем случае — по отдельным символам.
  3. Семантический чанкинг (Semantic chunking) Самый продвинутый метод. Алгоритм анализирует смысл предложений и объединяет их в один чанк до тех пор, пока тема не изменится. Это требует дополнительных вычислительных затрат на этапе индексации, но резко повышает релевантность поиска.

Каждый полученный чанк обогащается метаданными (дата, автор, теги документа) и пропускается через модель-энкодер, которая превращает текст в вектор. Эти векторы сохраняются в специализированную векторную базу данных.

Online-фаза: Пайплайн обработки запроса

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

1. Векторизация запроса

Вопрос пользователя передается в ту же самую модель-энкодер, которая использовалась на этапе индексации. На выходе получается вектор запроса в том же многомерном пространстве, где лежат векторы наших чанков.

2. Поиск (Retrieval)

Система должна найти KK наиболее релевантных чанков (Top-K). Для этого вычисляется расстояние между вектором запроса и векторами документов. Самая популярная метрика здесь — косинусное сходство (Cosine Similarity).

Формула косинусного сходства:

similarity=cos(θ)=ABAB\text{similarity} = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|}

Где:

  • A\mathbf{A} — вектор пользовательского запроса.
  • B\mathbf{B} — вектор проверяемого чанка из базы данных.
  • AB\mathbf{A} \cdot \mathbf{B} — скалярное произведение этих векторов.
  • A\|\mathbf{A}\| и B\|\mathbf{B}\| — длины (нормы) векторов.
  • θ\theta — угол между векторами в многомерном пространстве.

Если векторы указывают в одном направлении (смысл совпадает), косинус угла стремится к 11. Если они ортогональны (темы не связаны) — к 00. Векторная база находит топ самых близких чанков по этому показателю (на практике для этого используются алгоритмы приближенного поиска, а не полный перебор всех записей).

3. Аугментация промпта (Augmentation)

Найденные текстовые чанки извлекаются из базы и встраиваются в промпт. Здесь мы возвращаемся к форматам SFT и Chat Template, которые разбирали ранее. Система формирует итоговый запрос, где найденные факты помещаются в системную или пользовательскую роль.

Пример того, как RAG-система собирает итоговый промпт перед отправкой в LLM:

<|im_start|>system
Ты корпоративный ассистент. Отвечай на вопросы пользователя, опираясь ТОЛЬКО на предоставленный контекст. Если ответа в контексте нет, скажи "Я не знаю".

Контекст:
[Документ 1: Отчет_Q3.pdf] Выручка в третьем квартале составила 15 млн долл.
[Документ 2: Отчет_Q3.pdf] Основной драйвер роста — запуск нового продукта "Альфа".<|im_end|>
<|im_start|>user
За счет чего выросла выручка в Q3?<|im_end|>
<|im_start|>assistant

4. Генерация (Generation)

Сформированный промпт с внедренным контекстом отправляется в LLM (например, Qwen или Llama 3). Модель применяет механизмы Self-Attention: токены вопроса пользователя вычисляют свою связь (Query) с токенами предоставленного контекста (Key, Value). Благодаря этому модель генерирует ответ: «Выручка выросла за счет запуска нового продукта "Альфа"», не используя свои внутренние веса для вспоминания факта, а извлекая его прямо из контекстного окна.

Узкие места базового пайплайна

Описанный выше процесс — это "Naive RAG" (наивный RAG). На практике он сталкивается с рядом проблем:

  • Проблема поиска: Пользователь может спросить «Что с доходами?», а в документе написано «Выручка составила...». Векторы могут оказаться недостаточно близки.
  • Проблема контекста: Если мы извлечем 20 чанков, контекстное окно раздуется. Это приведет к огромному потреблению памяти под KV-cache и замедлению генерации (Latency).
  • Проблема потери середины (Lost in the Middle): LLM хуже обращают внимание на факты, расположенные в середине длинного контекста, лучше запоминая начало и конец.

Чтобы преодолеть эти барьеры, инженеры усложняют пайплайн: внедряют переписывание запросов, гибридный поиск и механизмы переранжирования (Reranking). Но прежде чем управлять поиском, необходимо детально разобраться в том, как именно векторные базы данных хранят миллионы эмбеддингов и почему они способны находить нужный косинус за миллисекунды.

Retrieval: векторные представления, поиск и специфика векторных БД (Qdrant, PGVector)

Retrieval: векторные представления, поиск и специфика векторных БД

Представьте корпоративную базу знаний на 10 миллионов текстовых фрагментов (чанков), каждый из которых переведен в вектор размерностью 1536 (стандарт модели text-embedding-3-small). Чтобы ответить на один запрос пользователя, точный поиск потребует вычислить расстояние от вектора запроса до каждого из 10 миллионов векторов базы. Это около 15 миллиардов операций с плавающей запятой на один вопрос. Тем не менее, современные RAG-системы находят релевантный контекст менее чем за 50 миллисекунд. Этот парадокс скорости и огромной размерности данных — фундамент этапа Retrieval.

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

Метрики векторной близости

Мы уже знаем, что эмбеддинги — это плотные числовые векторы. На этапе Retrieval наша задача — найти в многомерном пространстве векторы документов, которые расположены ближе всего к вектору запроса пользователя. Для измерения «близости» используются три основные метрические функции.

Косинусное сходство (Cosine Similarity)

Самая популярная метрика в NLP. Она измеряет угол между двумя векторами, игнорируя их длину. Это важно: длинный текст и короткий текст с одинаковым смыслом будут иметь разную длину вектора, но смотреть в одном направлении.

cos(θ)=ABAB\text{cos}(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|}

Где θ\theta — угол между векторами, A\mathbf{A} и B\mathbf{B} — векторы запроса и документа, \cdot — скалярное произведение, а A\|\mathbf{A}\| и B\|\mathbf{B}\| — длины (нормы) этих векторов. Значение варьируется от 11 (векторы сонаправлены, смысл идентичен) до 1-1 (противоположны).

Скалярное произведение (Dot Product)

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

AB=i=1dAiBi\mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^{d} A_i B_i

Где dd — размерность пространства, а AiA_i и BiB_i — компоненты векторов.

Ключевая оптимизация инференса: вычисление косинусного сходства требует деления на длины векторов (тяжелая операция). Если предварительно нормализовать все векторы в базе (привести их длину к 11), то знаменатель в формуле косинусного сходства становится равен единице: AB=1\|\mathbf{A}\| \|\mathbf{B}\| = 1. В этом случае скалярное произведение становится математически равно косинусному сходству, но вычисляется в разы быстрее. Большинство современных векторных БД требуют нормализации векторов именно для использования Dot Product под капотом.

Евклидово расстояние (L2L_2)

Измеряет физическое расстояние между концами векторов по прямой.

L2=i=1d(AiBi)2L_2 = \sqrt{\sum_{i=1}^{d} (A_i - B_i)^2}

Где dd — размерность, AiA_i и BiB_i — координаты. В отличие от предыдущих метрик, здесь чем значение меньше, тем векторы ближе. L2L_2 чувствительно к длине вектора, поэтому в RAG-системах применяется реже, за исключением специфических задач компьютерного зрения.

Проблема масштаба: от kNN к ANN

Поиск, при котором мы честно вычисляем расстояние от запроса до каждого документа в базе, называется kNN (k-Nearest Neighbors). Его алгоритмическая сложность составляет O(N×d)O(N \times d), где NN — количество документов, а dd — размерность вектора. Для продакшена это неприемлемо медленно.

Чтобы решить эту проблему, векторные базы данных используют ANN (Approximate Nearest Neighbor) — приближенный поиск.

ANN — класс алгоритмов, которые жертвуют абсолютной точностью (могут пропустить идеальный результат) ради радикального ускорения поиска (от O(N)O(N) до O(logN)O(\log N)).

Самым эффективным и распространенным алгоритмом ANN на сегодня является HNSW.

Алгоритм HNSW: граф навигации

HNSW (Hierarchical Navigable Small World) — графовый алгоритм приближенного поиска, строящий многоуровневую структуру связей между векторами, подобно сети автомобильных дорог разного значения.

Представьте, что вам нужно доехать из спального района Москвы в конкретный двор в Санкт-Петербурге. Вы не едете дворами через всю страну. Вы выезжаете на скоростную трассу (верхний уровень), быстро преодолеваете основное расстояние, съезжаете на региональную дорогу (средний уровень), а затем петляете по улицам (нижний уровень). HNSW работает точно так же.

Структура HNSW состоит из нескольких слоев:

  1. Нижний слой (Layer 0): Содержит абсолютно все векторы базы. Каждый вектор соединен короткими связями со своими ближайшими соседями.
  2. Верхние слои (Layer 1, 2... L): Содержат экспоненциально меньше векторов. Связи здесь длинные, соединяющие отдаленные кластеры.

Как происходит поиск запроса:

  1. Поиск начинается с заранее определенной точки входа на самом верхнем слое.
  2. Алгоритм вычисляет расстояние от запроса до соседей текущей точки.
  3. Происходит «жадный переход» (Greedy Routing): алгоритм шагает в ту соседнюю точку, которая ближе всего к запросу.
  4. Если ни один сосед не находится ближе к запросу, чем текущая точка (достигнут локальный минимум), алгоритм «проваливается» на слой ниже.
  5. Процесс повторяется, пока алгоритм не найдет ближайших соседей на самом нижнем слое (Layer 0).

HNSW обеспечивает невероятную скорость, но требует значительного объема оперативной памяти для хранения графа связей.

Специфика векторных баз данных

HNSW можно запустить локально с помощью библиотеки FAISS от Meta. Но в реальном продукте нам нужен не просто поиск по массиву чисел. Нам нужны CRUD-операции (создание, чтение, обновление, удаление), персистентность (сохранение на диск) и, самое главное, гибридный поиск с фильтрацией по метаданным.

Представьте запрос: «Найди инструкции по настройке VPN, написанные после 2023 года для отдела продаж». Векторный поиск найдет инструкции по VPN, но как учесть дату и отдел?

Существует три стратегии фильтрации, которые определяют архитектуру векторной БД:

  • Post-filtering (Пост-фильтрация): Сначала находим топ-100 векторов через HNSW, затем отбрасываем те, что не подходят по дате и отделу. Проблема: если из топ-100 только 2 документа подходят под фильтр, мы вернем пользователю пустой ответ, хотя в базе (на 101-м месте) мог быть идеальный документ.
  • Pre-filtering (Пре-фильтрация): Сначала фильтруем базу SQL-запросом по дате и отделу, а по оставшимся документам запускаем точный kNN-поиск. Проблема: разрушает структуру HNSW-графа, работает медленно на больших объемах.
  • Single-stage / In-filtering (Встроенная фильтрация): Фильтры применяются прямо во время обхода графа HNSW. Алгоритм просто игнорирует узлы, не подходящие по метаданным, продолжая поиск соседей. Это самый передовой метод.

Рассмотрим два популярных решения, по-разному подходящих к этой архитектуре.

Qdrant

Qdrant — специализированная векторная база данных, написанная на Rust. Она изначально проектировалась для высоконагруженных ML-систем.

Ключевые особенности:

  • Payload-based In-filtering: Qdrant хранит метаданные (payload) в формате JSON вместе с векторами и блестяще реализует In-filtering. При обходе графа HNSW движок на лету оценивает условия фильтрации.
  • Оптимизация памяти: Поддерживает квантование векторов (Scalar и Product Quantization), сжимая векторы типа float32 в int8 или int4, что позволяет держать граф в RAM, а сами векторы на диске.
  • Архитектура: Распределенная, поддерживает шардирование и репликацию из коробки.

PGVector

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

Ключевые особенности:

  • Единая экосистема: Идеально, если ваши данные уже лежат в Postgres. Не нужно поднимать отдельную инфраструктуру и синхронизировать базы.
  • ACID-транзакции: Полная поддержка транзакционности, свойственная реляционным БД.
  • Гибридные SQL-запросы: Позволяет элегантно объединять строгие реляционные JOIN'ы с векторным поиском.

Пример запроса в PGVector, использующего оператор <=> для косинусного расстояния:

SELECT chunk_text
FROM documents
WHERE department = 'Sales' AND created_at >= '2023-01-01'
ORDER BY embedding <=> '[0.12, -0.45, 0.88, ...]'
LIMIT 5;

PGVector поддерживает два типа индексов:

  1. IVFFlat (Inverted File with Flat compression): Разбивает пространство на кластеры (ячейки Вороного). Быстрее строится, но менее точный.
  2. HNSW: Добавлен в последних версиях, обеспечивает state-of-the-art скорость поиска, но дольше индексируется. PGVector использует гибридный подход к фильтрации, опираясь на планировщик запросов Postgres.

Что выбрать?

Критерий Qdrant PGVector
Основной юзкейс Микросервисная RAG-архитектура, миллиарды векторов Монолиты, стартапы, данные уже лежат в PostgreSQL
Производительность ANN Максимальная (Rust, кастомный HNSW) Высокая (но зависит от тюнинга Postgres)
Фильтрация метаданных Нативная In-filtering во время обхода графа Опирается на планировщик SQL (Pre/Post-filtering)
Сложность инфраструктуры Требует отдельного кластера и синхронизации Устанавливается как расширение CREATE EXTENSION

Векторный поиск и базы данных позволяют нам мгновенно извлекать семантически близкие фрагменты текста. Однако «близкий по смыслу» не всегда означает «отвечающий на вопрос». Часто векторный поиск возвращает документы, в которых используются те же термины, но контекст совершенно иной. Чтобы RAG-система стала по-настоящему интеллектуальной, найденные результаты необходимо просеивать и переупорядочивать, а сами запросы пользователя — адаптировать под логику векторного пространства.

Оптимизация поиска: Query Rewriting, HyDE и механизмы Reranking

Оптимизация поиска: Query Rewriting, HyDE и механизмы Reranking

Пользователь пишет в корпоративного бота: «Почему вчера упало приложение?». База знаний содержит точный ответ в тикете с заголовком: «NullPointerException в AuthModule из-за протухшего JWT-токена 2026-06-23». Наивный RAG сработает здесь отвратительно. Векторное представление пользовательского вопроса, описывающего симптом бытовым языком, окажется бесконечно далеко от технического описания корневой причины в документе. Чтобы преодолеть эту семантическую пропасть, систему необходимо научить переводить намерения пользователя на язык базы данных до начала поиска, а затем безжалостно фильтровать найденное.

Проблема семантического разрыва

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

Возникает фундаментальный конфликт: мы пытаемся найти документ, используя вектор вопроса, хотя вектор вопроса и вектор ответа лежат в разных областях многомерного пространства. Чтобы решить эту проблему, применяются техники трансформации запроса на стороне LLM.

Query Rewriting: подготовка запроса

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

Multi-Query (Множественная генерация)

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

LLM получает системный промпт: «Сгенерируй 3 альтернативные формулировки для вопроса пользователя, сохраняя исходный смысл». Каждая из трех новых фраз векторизуется, для каждой выполняется поиск (например, топ-5 результатов). Затем полученные 15 чанков объединяются, дубликаты удаляются, и итоговый набор передается генеративной модели.

Step-back Prompting (Шаг назад)

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

Step-back Prompting — техника, при которой модель генерирует более абстрактный, высокоуровневый вопрос на основе исходного.

Если пользователь спрашивает: «Как настроить PagedAttention во vLLM для модели Llama-3 8B на одной видеокарте?», абстрактный шаг назад будет звучать как: «Каковы базовые принципы управления памятью и настройки PagedAttention во vLLM?». Поиск по обоим запросам позволит извлечь как специфичные сниппеты кода, так и фундаментальную документацию, давая модели полный контекст для ответа.

HyDE: поиск по галлюцинации

Что если вообще отказаться от попыток улучшить вопрос, а вместо этого искать по ответу? Этот элегантный подход называется HyDE (Hypothetical Document Embeddings).

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

Алгоритм работы HyDE:

  1. Пользователь задает вопрос.
  2. Базовая LLM (без доступа к RAG) генерирует гипотетический ответ, опираясь только на свои внутренние веса.
  3. Этот выдуманный текст превращается в вектор vHv_{H}, где vHv_{H} — эмбеддинг гипотетического документа.
  4. Векторная база данных ищет реальные документы, ближайшие к вектору vHv_{H}.

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

Проблема точности: Bi-encoder против Cross-encoder

Применив Multi-Query или HyDE, мы значительно расширили воронку поиска. Теперь база данных стабильно находит нужные документы, но вместе с ними вытаскивает десятки нерелевантных чанков. Мы не можем передать их все в финальную LLM из-за ограничений контекстного окна и проблемы снижения внимания к середине текста. Нам нужен механизм точной сортировки.

Векторные базы данных используют модели архитектуры Bi-encoder. В них запрос и каждый документ векторизуются независимо друг от друга. Оценка их релевантности — это просто вычисление близости двух готовых векторов: S=cos(u,v)S = \cos(u, v), где SS — итоговая оценка сходства, cos\cos — функция косинусного расстояния, а uu и vv — эмбеддинги запроса и документа соответственно. Это невероятно быстро, что позволяет искать по миллионам записей за миллисекунды, но грубо. Bi-encoder не видит, как конкретные слова из запроса взаимодействуют со словами в документе.

Для финальной фильтрации применяется Cross-encoder (механизм Reranking). Это Трансформер, который принимает на вход запрос и документ одновременно, склеенные через специальный токен сепарации.

Математически это выглядит так: S=Model(QD)S = \text{Model}(Q \oplus D), где SS — оценка релевантности, Model\text{Model} — нейросеть (Cross-encoder), QQ — токены запроса, DD — токены документа, а \oplus означает их конкатенацию (склеивание).

В Cross-encoder механизм Self-Attention работает сквозным образом: каждый токен запроса может «смотреть» на каждый токен документа на всех слоях нейросети. Это позволяет улавливать тончайшие смысловые связи, сарказм, отрицания и сложный контекст. Модель выдает одно число — точную оценку релевантности документа конкретному запросу от 0 до 1.

Характеристика Bi-encoder (Retrieval) Cross-encoder (Reranking)
Архитектура Два независимых потока Один совместный поток
Скорость Миллисекунды (векторы предвычислены) Сотни миллисекунд (вычисляется на лету)
Точность Средняя (семантическая близость) Высокая (глубокий анализ связей)
Масштабируемость Поиск по миллионам документов Оценка максимум десятков/сотен документов

Двухэтапный пайплайн (Advanced RAG)

Объединение этих подходов формирует современный стандарт высоконагруженных RAG-систем — пайплайн Retrieve & Rerank.

  1. Трансформация (Query Rewriting / HyDE): Исходный запрос «разворачивается» для максимального охвата.
  2. Грубый поиск (Retrieval): Векторная база данных с помощью Bi-encoder извлекает широкую выборку кандидатов — например, топ-50 или топ-100 чанков. На этом этапе важна полнота (Recall): мы должны гарантировать, что правильный ответ есть где-то среди этих 100 фрагментов.
  3. Точное ранжирование (Reranking): Тяжеловесная модель Cross-encoder попарно оценивает исходный запрос и каждый из 100 найденных чанков. Результаты сортируются по убыванию оценки, и система отсекает всё, оставляя только топ-5 самых релевантных фрагментов.
  4. Генерация: Идеально отфильтрованный, компактный контекст передается в LLM для формирования итогового ответа.

Такая архитектура элегантно решает компромисс между скоростью и качеством. Мы используем быстрые векторы, чтобы сузить пространство поиска с миллионов до сотни, а затем применяем ресурсоемкий Cross-encoder там, где объем данных уже достаточно мал для вычислений в реальном времени.

Промпт-инжиниринг: паттерны проектирования для извлечения знаний и классификации

Промпт-инжиниринг: паттерны проектирования для извлечения знаний и классификации

Даже если архитектура RAG безупречно отфильтровала миллионы документов и Cross-encoder выдал идеальный топ-3 релевантных абзацев, система рухнет на последнем этапе, если языковая модель ответит: «Конечно, вот ваш ответ в формате JSON...» вместо самого JSON. В продуктовой разработке промпты перестают быть творческим диалогом и превращаются в строгий инженерный интерфейс, управляющий вероятностным распределением токенов. Наша задача — заставить недетерминированную модель работать как надежный парсер, классификатор и экстрактор данных.

Анатомия продакшен-промпта

В основе управления поведением LLM лежит понимание того, как модель обрабатывает инструкции после этапа SFT (Supervised Fine-Tuning). Продакшен-промпт — это не просто текст, а структурированный шаблон, который минимизирует энтропию на этапе генерации.

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

  1. System Directive (Системная директива): Устанавливает базовую роль и абсолютные ограничения. Здесь задается формат вывода и запрет на галлюцинации.
  2. Context (Контекст): Блок, куда инжектируются данные, полученные на этапе Retrieval.
  3. Task / Instruction (Задача): Конкретное действие, которое нужно выполнить над контекстом.
  4. Formatting Constraints (Ограничения формата): Схема, по которой модель обязана структурировать ответ.

Чтобы модель не отвлекалась на знания, заложенные в нее на этапе Pre-training, системная директива должна явно смещать фокус внимания (Attention) на блок контекста.

Ты — строгий парсер данных. Твоя задача — извлекать сущности только из предоставленного текста. Если информации нет в тексте, верни null. Запрещено использовать внешние знания.

In-Context Learning и Few-Shot Prompting

Когда базовых инструкций недостаточно для сложного форматирования, применяется Few-Shot Prompting — предоставление модели нескольких примеров «вход-выход» непосредственно в тексте запроса.

В отличие от Fine-tuning (например, LoRA), где мы физически обновляем веса матриц, Few-Shot использует механизм In-Context Learning. Модель на лету выявляет паттерн в предоставленных примерах через механизм Self-Attention, не меняя своих внутренних параметров.

Рассмотрим задачу классификации пользовательских обращений в службу поддержки.

Классифицируй обращение пользователя по одной из категорий: [BILLING, TECH_SUPPORT, SALES].

Пример 1:
User: У меня дважды списали деньги за подписку в этом месяце.
Class: BILLING

Пример 2:
User: Как интегрировать ваше API с моим Python-приложением?
Class: TECH_SUPPORT

Пример 3:
User: Хочу купить корпоративную лицензию на 50 человек, дадите скидку?
Class:

Примеры служат «якорями» для вероятностного распределения. Авторегрессионная генерация описывается формулой:

P(yx)=i=1NP(yix,y<i)P(y|x) = \prod_{i=1}^{N} P(y_i|x, y_{<i})

Где P(yx)P(y|x) — вероятность всей сгенерированной последовательности, xx — входной промпт (включая примеры), yiy_i — текущий генерируемый токен, а y<iy_{<i} — все ранее сгенерированные токены. Наличие четких примеров в условии xx резко повышает вероятность того, что следующим токеном yiy_i станет именно метка класса, а не пространное рассуждение.

Управление рассуждением: Chain of Thought (CoT)

Традиционные языковые модели (до появления reasoning-моделей вроде OpenAI o1) не имеют скрытого «внутреннего монолога» перед выдачей ответа. Вычислительная мощность, затрачиваемая на один токен, фиксирована архитектурой Трансформера. Если задача требует сложной логики (например, агрегации цифр из финансового отчета), такая модель не может «подумать» в фоне — она должна выводить промежуточные шаги в виде токенов.

Паттерн Chain of Thought (CoT) заставляет модель разбивать задачу на последовательные этапы логического вывода. Генерируя текст шага, модель помещает его в свой контекст, и механизм внимания использует этот промежуточный вывод для генерации следующего правильного токена.

Сравним два подхода при извлечении данных из сложного юридического договора:

Без CoT (высокий риск ошибки): Вопрос: Какова итоговая сумма штрафа при расторжении договора на 3 месяце? Ответ модели: 1500 USD. (Модель пытается угадать ответ сразу).

С использованием CoT: Инструкция: Сначала выпиши базовую ставку, затем найди условия досрочного расторжения, рассчитай штраф и только потом выдай итоговую сумму. Ответ модели: Базовая ставка составляет 500 USD в месяц. В пункте 4.2 указано, что при расторжении до 6 месяцев штраф равен двум базовым ставкам. 500 умножить на 2 равно 1000. Итоговая сумма штрафа: 1000 USD.

Добавляя CoT, мы фактически выделяем модели больше вычислительного времени (через генерацию дополнительных токенов) на решение задачи.

Извлечение структурированных данных (Structured Extraction)

В RAG-системах результат работы LLM обычно передается дальше по коду: в API, базу данных или UI. Это требует строгого формата, чаще всего JSON.

Чтобы гарантировать валидность JSON, используется комбинация промпт-инжиниринга и архитектурных особенностей (например, Function Calling, если модель этому обучена). Паттерн включает в себя передачу точной схемы данных.

Извлеки информацию о пациенте из медицинского заключения.
Ответ должен быть СТРОГО в формате JSON по следующей схеме:
{
  "patient_name": "string",
  "symptoms": ["string", "string"],
  "duration_days": "integer",
  "is_chronic": "boolean"
}
Не пиши никаких пояснений до или после JSON.

Для повышения надежности этот паттерн часто комбинируют с CoT, создавая гибридную структуру: модель сначала рассуждает в специальном текстовом поле, а затем заполняет целевые поля.

Подход Преимущества Недостатки
Прямой JSON Экономия токенов, быстрый ответ Выше шанс ошибки в логике извлечения
JSON с полем reasoning Высокая точность, легко отлаживать (видно логику) Больший расход токенов, увеличение задержки (latency)

В гибридном подходе схема JSON модифицируется: первым ключом добавляется "step_by_step_analysis": "string", куда модель выгружает свои рассуждения, и только потом идут ключи с самими данными.

Классификация и маршрутизация (Routing)

Промпт-инжиниринг применяется не только в конце RAG-пайплайна, но и в самом его начале — для маршрутизации (Query Routing). Когда пользователь задает вопрос, система должна решить, в какую базу данных идти: делать семантический поиск по документации в векторной БД или генерировать SQL-запрос к таблице с транзакциями.

Для этого LLM выступает в роли классификатора. Паттерн маршрутизации требует жесткого ограничения выходного словаря (Output Space).

Проанализируй запрос пользователя. Если запрос касается технических характеристик продукта, верни ровно одно слово: VECTOR_DB. Если запрос касается баланса счета или истории оплат, верни ровно одно слово: SQL_DB. Запрос: "Сколько я заплатил за сервер в прошлом месяце?"

На уровне инференса (который будет детально разобран при изучении серверных движков) такие промпты часто сопровождаются параметром max_tokens=1 и модификацией логитов (Logit Bias), чтобы физически запретить модели генерировать токены, не относящиеся к именам маршрутов.

Сведение всех этих паттернов воедино позволяет строить детерминированные конвейеры поверх вероятностных моделей. Однако использование сложных промптов с длинным контекстом (Few-Shot) и объемной генерацией (Chain of Thought) создает серьезную нагрузку на инфраструктуру. Вычисление внимания для тысяч токенов контекста и пошаговая генерация длинных JSON-ответов требуют оптимизации работы с GPU-памятью при обработке множества параллельных пользовательских запросов.

Высокопроизводительный инференс: архитектура vLLM, PagedAttention и Continuous Batching

Высокопроизводительный инференс: архитектура vLLM, PagedAttention и Continuous Batching

Представьте, что вы развернули модель Llama 3 70B в продакшене. В 16-битном формате ее веса занимают около 140 ГБ видеопамяти, и вы арендовали сервер с двумя GPU по 80 ГБ. Модель успешно загрузилась, вы отправляете первый тестовый запрос — все работает безупречно. Но как только система получает 50 параллельных пользовательских запросов, сервер падает с ошибкой Out Of Memory (OOM). Проблема кроется не в размере самой модели, а в механизме кэширования контекста, который при высоких нагрузках начинает потреблять память абсолютно непредсказуемо.

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

Анатомия инференса: Prefill и Decode

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

  1. Prefill (фаза предзаполнения). Модель получает весь промпт пользователя целиком. Благодаря архитектуре Трансформера, токены промпта обрабатываются параллельно. На этом этапе вычисляются и сохраняются матрицы Key и Value для каждого токена промпта. Эта фаза сильно нагружает вычислительные ядра GPU (Compute-bound).
  2. Decode (фаза декодирования). Модель начинает генерировать ответ по одному токену за раз. Каждый новый токен требует загрузки весов модели из памяти GPU в вычислительные ядра, а также чтения всего накопленного контекста. Эта фаза упирается в пропускную способность памяти (Memory-bandwidth bound).

Именно на этапе Decode в игру вступает KV-cache, с которым мы познакомились ранее. Он избавляет нас от необходимости пересчитывать контекст, но создает колоссальную нагрузку на видеопамять.

Рассчитаем объем памяти, необходимый для хранения KV-cache одного токена. Формула выглядит так:

S=2×Nlayers×Nkv×Dhead×BbytesS = 2 \times N_{layers} \times N_{kv} \times D_{head} \times B_{bytes}

Где:

  • SS — размер памяти для одного токена.
  • 22 — множитель, так как мы храним два вектора: Key и Value.
  • NlayersN_{layers} — количество слоев в модели.
  • NkvN_{kv} — количество KV-голов внимания (Key/Value Heads). Для современных моделей с Grouped Query Attention (как Llama 3) это число меньше общего числа голов внимания.
  • DheadD_{head} — размерность одной головы внимания.
  • BbytesB_{bytes} — количество байт на один параметр (например, 22 для формата float16).

Для модели класса 70B один токен может занимать около 320 КБ. Если у нас 50 пользователей, каждый из которых отправил промпт на 1000 токенов и ожидает ответ на 1000 токенов, суммарный объем KV-cache превысит 30 ГБ. И это динамически растущая величина.

Проблема фрагментации памяти

До появления специализированных фреймворков инференс-движки выделяли память под KV-cache непрерывными блоками. Поскольку длина ответа модели неизвестна заранее (генерация останавливается только при выпадении токена EOS), система резервировала память по максимально возможной длине последовательности, например, на 2048 токенов.

Это приводило к двум типам фрагментации:

  • Внутренняя фрагментация: система зарезервировала память на 2048 токенов, но модель ответила коротко, сгенерировав всего 15 токенов. Оставшаяся память заблокирована и простаивает.
  • Внешняя фрагментация: запросы приходят и уходят, оставляя в памяти «дыры». Даже если суммарно свободной памяти достаточно для нового запроса, движок не может его запустить, потому что нет единого непрерывного куска нужного размера.

В традиционных системах инференса до 80% зарезервированной под KV-cache памяти тратилось впустую из-за фрагментации и избыточного резервирования.

vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention

PagedAttention: виртуальная память для Трансформеров

Решение этой проблемы пришло из классических операционных систем. Инженеры, создавшие фреймворк vLLM, перенесли концепцию страничной организации памяти (Virtual Memory Paging) на GPU, назвав этот механизм PagedAttention.

Вместо того чтобы хранить KV-cache запроса в виде одного непрерывного тензора, PagedAttention разбивает его на блоки фиксированного размера (например, по 16 токенов). Эти блоки могут располагаться в физической памяти GPU абсолютно хаотично.

Связь между логической последовательностью токенов и физическим расположением блоков обеспечивает Block Table (таблица блоков).

Как это работает на практике

  1. Приходит промпт на 35 токенов. При размере блока в 16 токенов, vLLM выделяет ровно 3 физических блока (два заполнены полностью, а в третьем занято 3 слота из 16).
  2. Начинается фаза Decode. Модель генерирует токены. Они записываются в третий блок, пока он не заполнится до 16.
  3. Как только генерируется 49-й токен (когда 3 блока по 16 полностью заполнены), vLLM выделяет новый, 4-й блок памяти в любом свободном месте GPU и обновляет Block Table.

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

Совместное использование памяти (Memory Sharing)

PagedAttention открывает еще одну мощную возможность — переиспользование контекста. Если вы используете паттерны вроде параллельного сэмплинга (когда на один промпт генерируется несколько вариантов ответа для последующего выбора лучшего), vLLM не копирует KV-cache промпта.

Обе ветки генерации ссылаются через свои Block Tables на одни и те же физические блоки промпта. Разделение памяти происходит только в момент генерации первого различающегося токена (механизм Copy-on-Write). Это снижает потребление памяти на десятки процентов в сложных RAG-системах с маршрутизацией или оценкой ответов.

Continuous Batching: максимизация пропускной способности

Решив проблему памяти, мы сталкиваемся с проблемой утилизации вычислительных мощностей. Чтобы GPU не простаивал, запросы объединяются в батчи (пакеты).

В классическом Static Batching запросы группируются до начала инференса. Если в батче 4 запроса, и три из них сгенерировали ответ за 20 токенов, а четвертый требует 100 токенов, весь батч будет ждать завершения самого длинного запроса. Вычислительные ресурсы GPU в это время простаивают.

Характеристика Static Batching Continuous Batching
Формирование батча До начала обработки Динамически на каждом шаге
Ожидание завершения Ждет самый длинный запрос Независимое завершение
Пропускная способность Низкая (много простоев) Высокая (GPU всегда загружен)
Сложность реализации Низкая Высокая (требует PagedAttention)

Continuous Batching (или Iteration-level scheduling) меняет парадигму. Планировщик работает на уровне отдельных итераций генерации (на уровне токенов), а не на уровне целых запросов.

Как только один из запросов в батче генерирует токен EOS и завершается, планировщик немедленно удаляет его из батча и на следующей же итерации (для генерации следующего токена) вставляет на его место новый запрос из очереди.

Это означает, что батч постоянно находится в движении: на одном такте GPU одновременно может происходить фаза Prefill для нового запроса и фаза Decode для трех старых. Именно комбинация Continuous Batching и PagedAttention делает vLLM индустриальным стандартом для высоконагруженных систем, позволяя увеличить пропускную способность (Throughput) до 24 раз по сравнению с базовой реализацией Hugging Face Transformers (и в 2–3.5 раза по сравнению с Text Generation Inference).

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

Оптимизация пропускной способности: квантование, спекулятивный стриминг и настройка vLLM

Оптимизация пропускной способности: квантование, спекулятивный стриминг и настройка vLLM

Представьте, что вы успешно развернули Llama 3 70B. На этапе тестирования модель отвечала мгновенно. Но как только RAG-система вышла в продакшен и получила 100 одновременных запросов, генерация замедлилась до 2 токенов в секунду, а затем сервер упал с ошибкой Out of Memory (OOM). Проблема в том, что инференс больших языковых моделей редко упирается в вычислительную мощность ядер GPU (Compute-bound). Почти всегда узким местом становится пропускная способность памяти (Memory-bandwidth bound): для генерации каждого нового токена видеокарте нужно прочитать все веса модели из VRAM в вычислительные блоки.

Чтобы система выдерживала высокую нагрузку, нам нужно разорвать эту зависимость. Для этого мы сожмем веса (квантование), научим модель генерировать несколько токенов за один такт (спекулятивное декодирование) и тонко настроим движок vLLM под профиль нашей нагрузки.

Анатомия задержки: TTFT и TPOT

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

  1. TTFT (Time To First Token) — время до появления первого токена. Это фаза Prefill, когда модель параллельно обрабатывает весь входящий промпт (включая объемные куски контекста из векторной базы) и вычисляет для них KV-cache. Эта фаза сильно нагружает вычислительные ядра (Compute-bound).
  2. TPOT (Time Per Output Token) — время генерации каждого последующего токена. Это фаза Decode, авторегрессионный процесс. Вычисления здесь минимальны, но модель вынуждена постоянно гонять гигабайты весов через шину памяти (Memory-bound).

Оптимизация пропускной способности (Throughput) — это всегда компромисс. Увеличивая размер батча для обработки большего числа запросов одновременно, мы повышаем общую пропускную способность системы, но рискуем увеличить TTFT (запросы ждут в очереди) и TPOT (память не успевает обслуживать все активные генерации).

Квантование для инференса: почему NF4 не подходит для продакшена

В главе о дообучении мы разбирали формат NF4 (QLoRA), который отлично экономит память при тренировке. Однако для высоконагруженного инференса он не годится: распаковка (декватование) весов из NF4 в FP16 на лету происходит слишком медленно, так как для этого формата нет оптимизированных аппаратных ядер (CUDA kernels).

Для продакшена используется PTQ (Post-Training Quantization) в форматы INT8 или INT4, которые поддерживаются на уровне «железа». Существуют два главных подхода к PTQ:

GPTQ (Generative Pre-trained Transformer Quantization)

Алгоритм послойно сжимает веса, используя матрицу Гессе (вторых производных) для оценки того, как изменение одного веса повлияет на общую ошибку. GPTQ пытается компенсировать ошибку квантования одних весов за счет корректировки других. Это работает хорошо, но процесс квантования занимает много времени и иногда приводит к деградации качества на специфичных задачах.

AWQ (Activation-aware Weight Quantization)

AWQ совершил революцию в инференсе, сместив фокус с самих весов на данные, которые через них проходят. Исследователи заметили: не все веса одинаково важны. Примерно 1% весов критически влияет на качество генерации.

Как найти этот 1%? AWQ прогоняет через модель небольшой калибровочный датасет и смотрит на активации (выходы нейронов).

Рассмотрим базовую линейную операцию в нейросети: Y=WXY = W X

Где YY — матрица выходных активаций, WW — матрица весов, а XX — матрица входных активаций. Если определенный признак во входных активациях XX стабильно имеет большое значение (выброс), то соответствующий ему столбец в матрице весов WW становится критически важным.

AWQ умножает эти «важные» веса на масштабный коэффициент перед квантованием (чтобы они заняли больший диапазон в сетке INT4 и меньше пострадали от округления), а затем делит на тот же коэффициент при деквантовании. Остальные 99% весов квантуются агрессивно.

Результат: Llama 3 8B в формате FP16 требует около 16 ГБ VRAM. В формате AWQ INT4 она занимает всего ~5 ГБ. Но главное — объем данных, которые нужно переносить из памяти в ядра для каждого токена, снижается в 3-4 раза. Это радикально уменьшает метрику TPOT.

Спекулятивное декодирование (Speculative Decoding)

Даже с AWQ-квантованием мы остаемся заложниками авторегрессии: генерация идет строго по одному токену. Спекулятивное декодирование ломает это ограничение, позволяя генерировать несколько токенов за один проход памяти.

Идея опирается на то, что многие последовательности слов легко предсказуемы. Если мы пишем код, после def __init__( с вероятностью 99% последует self. Зачем тратить драгоценное время 70-миллиардной модели на поочередный вывод этих очевидных токенов?

Для спекулятивного декодирования нужны две модели:

  1. Draft Model (Черновик) — маленькая и быстрая модель (например, Llama 3 1B).
  2. Target Model (Целевая) — большая и точная модель (например, Llama 3 70B).

Алгоритм работы:

  1. Маленькая модель быстро генерирует γ\gamma токенов подряд (например, γ=4\gamma = 4). Это происходит почти мгновенно.
  2. Большая модель берет эти 4 токена и прогоняет их через себя за один параллельный шаг (точно так же, как она читает промпт в фазе Prefill).
  3. Большая модель вычисляет свои вероятности для этих токенов. Если вероятности большой модели совпадают с предсказаниями маленькой — токены принимаются (Accept).
  4. Если на 3-м токене предсказания разошлись, большая модель отвергает его, оставляет первые два, а вместо 3-го подставляет свой правильный токен. 4-й токен отбрасывается.

Математика эффективности: если время генерации одного токена большой моделью равно tt, то генерация 4 токенов обычным способом займет 4t4t. При спекулятивном декодировании проверка 4 токенов занимает примерно то же время tt (благодаря параллелизму). Если маленькая модель угадала все токены, мы получаем ускорение почти в 4 раза без потери качества (ответ математически идентичен тому, что выдала бы большая модель).

Тонкая настройка vLLM в продакшене

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

Пример запуска оптимизированного сервера:

python -m vllm.entrypoints.openai.api_server \
  --model neuralmagic/Meta-Llama-3-8B-Instruct-AWQ \
  --quantization awq \
  --gpu-memory-utilization 0.85 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 4096

Разберем рычаги управления, которые спасают систему при разных симптомах деградации:

1. Управление памятью: --gpu-memory-utilization

По умолчанию vLLM захватывает 90% (0.9) доступной VRAM. Эта память делится на две части: статические веса модели и динамический KV-cache для контекста пользователей.

  • Симптом: Ошибки OOM при пиковых нагрузках или при использовании внешних библиотек (например, если рядом крутится эмбеддинг-модель для RAG).
  • Решение: Снизить значение до 0.8 или 0.85. Это уменьшит пул для KV-cache. Система сможет держать меньше одновременных пользователей, но перестанет падать.

2. Контроль параллелизма: --max-num-seqs

Определяет максимальное количество запросов (последовательностей), которые движок обрабатывает одновременно (Continuous Batching).

  • Симптом: Высокий TPOT (генерация идет рывками, токены появляются медленно). Это значит, что GPU пытается обслуживать слишком много контекстов одновременно, и вычисления размазываются.
  • Решение: Снизить --max-num-seqs (например, с 256 до 64). Новые запросы будут ставиться в очередь (вырастет TTFT), зато те, что уже обрабатываются, будут генерироваться быстро.

3. Балансировка Prefill и Decode: --max-num-batched-tokens

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

  • Симптом: Резкие зависания генерации у текущих пользователей, когда в систему приходит новый пользователь с огромным промптом (например, 10 страниц из векторной БД). Обработка этого промпта (Prefill) блокирует ресурсы, и авторегрессия (Decode) у остальных замирает.
  • Решение: Ограничить --max-num-batched-tokens (например, до 4096). vLLM начнет применять Chunked Prefill — разбивать длинный промпт нового пользователя на куски. Это немного замедлит старт для новичка, но сохранит плавную генерацию (низкий TPOT) для всех остальных.

Связка AWQ-квантования, спекулятивного декодирования и грамотной настройки лимитов vLLM позволяет обслуживать в 5–10 раз больше пользователей на том же оборудовании. Когда система работает стабильно и быстро, следующим шагом становится оценка того, что именно она генерирует — метрики качества RAG-систем, детекция галлюцинаций и оценка релевантности.

Оценка качества (Evaluation): метрики RAG Triad, детекция галлюцинаций и фреймворки (RAGAS)

Оценка качества (Evaluation): метрики RAG Triad, детекция галлюцинаций и фреймворки (RAGAS)

Представьте enterprise-систему для банка. Пользователь спрашивает: «Какой штраф за досрочное погашение кредита?». Система находит документ о кэшбэке по кредитным картам и уверенно генерирует: «Штрафов нет, наоборот, вы получите бонус 5%!». Ответ написан идеальным языком, грамматически корректен и звучит максимально правдоподобно. Традиционные NLP-метрики вроде BLEU или ROUGE, оценивающие n-граммное перекрытие с эталоном, здесь бесполезны: они не понимают смысла текста. Автоматически отловить такую катастрофическую ошибку в CI/CD пайплайне без участия человека — главная задача процесса Evaluation.

Оценка качества генеративных систем требует перехода от лексического сравнения к семантическому анализу. Для этого используется подход LLM-as-a-Judge, где сильная языковая модель (например, GPT-4 или специализированная Prometheus) выступает в роли асессора, анализируя работу RAG-пайплайна по заданным критериям.

RAG Triad: локализация ошибки

Классический пайплайн состоит из извлечения данных (Retrieval) и генерации ответа (Generation). Если итоговый ответ неверен, нам нужно точно знать, где произошел сбой: система не нашла нужный документ, или модель проигнорировала найденный контекст и выдумала факты?

Для изоляции проблем используется концепция RAG Triad (Триада RAG) — три независимые оси оценки, каждая из которых проверяет конкретный участок конвейера.

Метрика Что проверяет Входные данные для оценки Суть проверки
Context Relevance Этап Retrieval Запрос (Query) + Контекст (Context) Принес ли поиск релевантные чанки, очищенные от информационного шума?
Faithfulness Этап Generation Контекст (Context) + Ответ (Answer) Основан ли ответ исключительно на предоставленном контексте?
Answer Relevance End-to-End Запрос (Query) + Ответ (Answer) Отвечает ли сгенерированный текст на изначальный вопрос пользователя?

Если Context Relevance низкий — мы идем тюнить параметры HNSW-индекса, менять стратегию чанкинга или внедрять гибридный поиск. Если падает Faithfulness — проблема в System Directive генератора или слишком высокой температуре (temperature).

Детекция галлюцинаций: метрика Faithfulness (Groundedness)

Галлюцинация в контексте RAG — это не просто «неверный факт», это факт, которого нет в извлеченном контексте. Даже если модель утверждает, что Земля круглая, но в контексте об этом не сказано — для RAG-системы это галлюцинация, так как нарушается принцип доверия к источнику.

В основе вычисления Faithfulness лежит задача NLI (Natural Language Inference) — определение логического следования. Фреймворки оценки, такие как RAGAS, разбивают этот процесс на два шага с использованием Chain of Thought (CoT).

Шаг 1: Экстракция утверждений. Модель-судья получает сгенерированный ответ и разбивает его на атомарные утверждения (claims). Пусть ответ AA содержит множество утверждений C={c1,c2,...,cn}C = \{c_1, c_2, ..., c_n\}.

Шаг 2: Проверка следования (Entailment). Для каждого cic_i судья проверяет, можно ли логически вывести это утверждение из контекста CtxCtx. Результат — бинарная оценка vi{0,1}v_i \in \{0, 1\}.

Итоговая формула метрики:

Faithfulness=i=1nviCFaithfulness = \frac{\sum_{i=1}^{n} v_i}{|C|}

Где C|C| — общее количество выделенных утверждений, а viv_i — индикатор подтверждения ii-го утверждения контекстом.

Пример расчета: Контекст: «Выручка компании в третьем квартале выросла на 20%». Ответ RAG: «Выручка выросла на 20%, а чистая прибыль удвоилась».

Утверждения: c1c_1: Выручка выросла на 20%. (v1=1v_1 = 1) c2c_2: Чистая прибыль удвоилась. (v2=0v_2 = 0)

Faithfulness=1+02=0.5Faithfulness = \frac{1 + 0}{2} = 0.5. Ответ наполовину галлюцинирован.

Answer Relevance: реверс-инжиниринг запроса

Метрика Answer Relevance проверяет, не ушла ли модель от темы. Если пользователь спросил про штрафы, а модель подробно рассказала про историю создания кредитных карт (даже если опиралась на контекст) — ответ бесполезен.

Вместо прямой оценки «насколько ответ подходит к вопросу», RAGAS использует элегантный математический трюк с генерацией гипотетических вопросов (похожий на логику HyDE, но в обратную сторону).

  1. LLM-судья получает итоговый ответ и генерирует из него kk возможных вопросов, на которые этот ответ мог бы быть дан: q1,q2,...,qkq'_1, q'_2, ..., q'_k.
  2. Вычисляются векторные эмбеддинги оригинального запроса E(q)E(q) и сгенерированных вопросов E(qi)E(q'_i).
  3. Вычисляется среднее косинусное сходство между ними.

Answer Relevance=1ki=1kcos(E(q),E(qi))Answer\ Relevance = \frac{1}{k} \sum_{i=1}^{k} \cos(E(q), E(q'_i))

Где E(x)E(x) — векторное представление текста xx, а cos\cos — функция косинусного сходства.

Если ответ содержит много воды или уходит в сторону, сгенерированные из него вопросы qiq'_i будут семантически далеки от оригинального запроса qq, и метрика упадет.

Оценка Retrieval: Precision и Recall

Оценка качества поиска в RAGAS опирается на классические метрики информационного поиска, адаптированные под LLM.

Context Precision (Точность контекста)

Оценивает способность системы ранжировать релевантные чанки выше нерелевантных. Это метрика качества работы Reranker-моделей (Cross-encoders). Context Precision штрафует систему, если релевантный документ оказался на 5-м месте, а не на 1-м. Для вычисления используется метрика на основе Average Precision (AP@K), где релевантность каждого чанка определяет LLM-судья.

Context Recall (Полнота контекста)

Для этой метрики требуется эталонный ответ (Ground Truth). Context Recall проверяет, все ли факты из эталонного ответа присутствуют в извлеченном контексте.

  1. Эталонный ответ разбивается на утверждения.
  2. LLM-судья ищет подтверждение каждому утверждению в извлеченном контексте.
  3. Считается доля найденных утверждений.

Если Context Recall низкий — значит, в векторной базе либо вообще нет нужной информации, либо Top-K алгоритма ANN (например, HNSW) настроен слишком агрессивно и отсекает важные данные.

Фреймворк RAGAS на практике

RAGAS (Retrieval Augmented Generation Assessment) стандартизирует описанные выше подходы. Для запуска полноценного пайплайна оценки фреймворку требуется датасет (Evaluation Dataset), состоящий из четырех колонок:

{
  "user_input": "Какой штраф за досрочное погашение?",
  "retrieved_contexts": ["Чанк 1: Кредитные карты...", "Чанк 2: Штрафы за погашение..."],
  "response": "Штраф составляет 2% от суммы.",
  "reference": "При досрочном погашении взимается комиссия 2%."
}
  • Reference-free метрики (Faithfulness, Answer Relevance) требуют только user_input, retrieved_contexts и response. Их можно считать прямо в продакшене на реальных логах пользователей (Shadow Evaluation).
  • Reference-based метрики (Context Recall) требуют наличия reference. Они используются на этапе разработки и CI/CD для регрессионного тестирования при смене эмбеддинг-модели или промптов.

Ключевой вызов при использовании RAGAS — стоимость и скорость. LLM-as-a-Judge требует множества дополнительных вызовов тяжелых моделей. Проверка одного ответа может потребовать 3-4 промптов с Chain of Thought. Поэтому в высоконагруженных системах оценку проводят асинхронно на выборке (семплировании) из 1-5% пользовательских сессий, формируя дашборды качества, которые позволяют инженерам вовремя заметить деградацию системы.

Проектирование агентов: использование LangChain, LlamaIndex и LangGraph в продукте

Проектирование агентов: использование LangChain, LlamaIndex и LangGraph в продукте

Более 80% корпоративных RAG-систем застревают на стадии прототипа, когда сталкиваются с реальными бизнес-задачами. Причина проста: классический пайплайн поиска отлично отвечает на вопрос «Каковы правила возврата товара?», но абсолютно бесполезен при запросе «Отмени мой заказ №404 и верни деньги на карту». Базовый RAG — это наблюдатель, который умеет только читать базу знаний. Бизнесу же нужен исполнитель, способный взаимодействовать с внешним миром.

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

Паттерн ReAct: от генерации к действию

В основе большинства современных агентов лежит фреймворк ReAct (Reasoning and Acting). В отличие от стандартного промптинга, где модель сразу генерирует финальный ответ, ReAct заставляет LLM работать в цикле: подумать, выбрать инструмент, получить результат, снова подумать.

ReAct — это парадигма промптинга, объединяющая цепочку рассуждений (Chain of Thought) с возможностью вызова внешних API (Tool Use) для итеративного решения задачи.

ReAct: Synergizing Reasoning and Acting in Language Models

Математически шаг работы агента можно описать как функцию перехода:

At=LLM(St,T)A_t = \text{LLM}(S_t, T)

Где AtA_t — выбранное действие (вызов функции или финальный ответ), StS_t — текущее состояние контекста (системный промпт, история диалога и результаты предыдущих вызовов), а TT — множество доступных инструментов с их описаниями.

Когда пользователь просит отменить заказ №404, агент не пытается угадать статус из весов модели. Он генерирует внутреннюю мысль: «Мне нужно узнать текущий статус заказа №404», после чего формирует JSON для вызова инструмента check_order_status(order_id="404"). Получив ответ от API, контекст StS_t обновляется, и на следующем шаге модель принимает решение об отмене.

Разделение зон ответственности: LlamaIndex и LangChain

Для реализации агентных систем в продакшене используются специализированные фреймворки. Исторически на рынке доминируют LlamaIndex и LangChain. Несмотря на пересечение функционала, их архитектурные фокусы принципиально различаются.

Характеристика LlamaIndex LangChain
Главный фокус Data-centric: подключение, индексация и умный поиск по данным. Action-centric: цепочки вызовов, инструменты и логика агентов.
Сильные стороны Продвинутые стратегии RAG, иерархические индексы, парсинг сложных PDF. Интеграция с сотнями внешних API, управление памятью, маршрутизация.
Абстракции Document, Node, QueryEngine, VectorStoreIndex. Chain, Tool, AgentExecutor, Memory.
Идеальный юзкейс Корпоративный поисковик по терабайтам разрозненной документации. Автономный ассистент, который бронирует билеты и пишет email-ы.

На практике эти фреймворки часто комбинируют. LlamaIndex выступает как мощный инструмент извлечения знаний (Retrieval), который упаковывается в виде одного из Tool для агента, написанного на LangChain.

Проблема AgentExecutor и переход к графам

Стандартный подход к созданию агентов в LangChain долгое время опирался на класс AgentExecutor. Это цикл while True, который крутится до тех пор, пока модель не выдаст токен завершения задачи.

Для простых сценариев это работает, но в высоконагруженных продуктовых решениях AgentExecutor превращается в неконтролируемый «черный ящик»:

  • Если API возвращает ошибку, агент может застрять в бесконечном цикле, сжигая токены.
  • Невозможно жестко задать бизнес-логику (например, «если сумма возврата >1000> 1000 руб., обязательно передать диалог оператору»).
  • Сложно внедрить механизм Human-in-the-loop (одобрение действия человеком перед выполнением).

Для решения этих проблем был создан LangGraph — расширение экосистемы LangChain, которое моделирует агентов как циклические конечные автоматы (графы).

LangGraph: агенты как конечные автоматы

Вместо того чтобы отдавать весь контроль LLM, LangGraph позволяет разработчику жестко спроектировать возможные пути выполнения задачи. Архитектура строится на трех китах:

  1. State (Состояние) — типизированный словарь, который передается между узлами графа. Он хранит всю историю и промежуточные переменные.
  2. Nodes (Узлы) — Python-функции или LLM-вызовы, которые принимают текущее состояние, выполняют работу и возвращают обновления для этого состояния.
  3. Edges (Ребра) — правила маршрутизации. Могут быть прямыми (от узла А к узлу Б) или условными (выбор следующего узла с помощью LLM-классификатора).

Рассмотрим проектирование агента технической поддержки. Нам нужно, чтобы бот сначала классифицировал запрос (используя Query Routing), затем либо искал ответ в базе знаний, либо обращался к API биллинга.

Сначала мы задаем структуру состояния:

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class SupportState(TypedDict):
    # add_messages гарантирует, что новые сообщения добавляются в список, а не перезаписывают его
    messages: Annotated[list, add_messages]
    intent: str
    escalate_to_human: bool

Затем мы создаем узлы. Узел — это обычная функция. Например, узел маршрутизации анализирует запрос и обновляет поле intent:

def intent_classifier_node(state: SupportState):
    user_message = state["messages"][-1].content
    # Вызов LLM для классификации (код скрыт для краткости)
    intent = llm_classify(user_message)
    return {"intent": intent}

Ключевая магия LangGraph происходит при настройке условных ребер (Conditional Edges). Мы можем написать строгую логику перехода на основе обновленного состояния:

def route_next_step(state: SupportState):
    if state["escalate_to_human"]:
        return "human_operator"
    if state["intent"] == "billing":
        return "billing_api_node"
    return "rag_search_node"

В результате мы получаем предсказуемую систему. LLM используется только там, где нужна эвристика (классификация, генерация ответа, извлечение параметров для API). Вся критичная бизнес-логика (переходы, проверки доступов, лимиты циклов) контролируется детерминированным графом на Python. Это позволяет легко тестировать отдельные узлы и возобновлять работу агента с любого состояния, если произошел сбой сети.

LLMOps: мониторинг, логирование и поддержка систем в продакшене

LLMOps: мониторинг, логирование и поддержка систем в продакшене

Вы успешно прошли все этапы разработки. Ваш LangGraph-агент из прошлой главы идеально маршрутизирует запросы, векторная база мгновенно находит нужные чанки, а метрики RAGAS на тестовом датасете показывают зеленые значения. Вы выкатываете систему в продакшен. В первый день всё отлично. На третий день счет за API вырастает на 400%, задержка ответа улетает за 15 секунд, а пользователи жалуются, что бот начал советовать перезагрузить сервер при ошибках в CSS.

Добро пожаловать в реальный мир, где поведение пользователей непредсказуемо, а языковые модели подвержены дрейфу. Чтобы система не развалилась при столкновении с реальностью, инженерный фокус смещается с разработки на LLMOps (Large Language Model Operations).

Парадигма LLMOps: почему классический мониторинг не работает

В традиционном машинном обучении (MLOps) мы работаем с детерминированными метриками. Модель предсказывает отток клиента (да/нет) или цену квартиры (число). Если распределение входных данных меняется (Data Drift), мы видим это по статистическим тестам и переобучаем модель.

С LLM всё иначе. Входом и выходом является неструктурированный текст. У нас нет объективной «истины» (Ground Truth) в реальном времени, с которой можно сравнить ответ.

Характеристика Классический MLOps LLMOps
Оценка качества Точные метрики (Accuracy, F1-score, MSE) Эвристики, LLM-as-a-Judge, пользовательский фидбек
Мониторинг входа Сдвиг распределения числовых фичей Изменение семантики промптов, инъекции (Prompt Injection)
Стоимость ошибки Неверная классификация одного объекта Галлюцинация, способная нанести репутационный или юридический ущерб
Производительность Задержка измеряется миллисекундами Задержка зависит от длины ответа и измеряется секундами

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

Трассировка (Tracing): рентгеновский снимок пайплайна

Когда пользователь пишет: «Почему у меня списали деньги дважды?», запрос проходит через множество узлов. Если бот отвечает: «Я не знаю», обычный текстовый лог не скажет вам, где произошел сбой. Не сработал классификатор намерений? Векторная база не нашла документ? Или LLM проигнорировала контекст?

Для решения этой проблемы используется трассировка, заимствованная из микросервисной архитектуры. Выполнение одного пользовательского запроса оборачивается в единый Trace (след), который состоит из иерархии Spans (пролетов).

  • Trace — весь жизненный цикл запроса от получения до отправки ответа.
  • Span — отдельная логическая операция внутри трейса (вызов БД, генерация промпта, обращение к LLM).

Каждый Span содержит критически важные метаданные:

  1. Входы и выходы: точный текст, который вошел в узел, и то, что из него вышло.
  2. Длительность: сколько миллисекунд занял конкретно этот шаг.
  3. Токены: сколько токенов было потрачено (если это вызов LLM).

Трассировка позволяет визуализировать граф выполнения. Вы можете увидеть, что из 4 секунд общего времени ответа 3.5 секунды занял этап переписывания запроса (Query Rewriting), а сам поиск отработал за 50 мс. Это сразу указывает точку приложения усилий для оптимизации.

Экономика инференса: мониторинг токенов и затрат

В LLMOps технические метрики напрямую связаны с деньгами. Если вы используете проприетарные модели (OpenAI, Anthropic) или облачные GPU для инференса vLLM, бесконтрольный рост контекста убьет юнит-экономику продукта.

Расчет стоимости одного вызова описывается простой формулой:

Cost=(Tin×Rin)+(Tout×Rout)Cost = (T_{in} \times R_{in}) + (T_{out} \times R_{out})

Где TinT_{in} и ToutT_{out} — количество входных и выходных токенов соответственно, а RinR_{in} и RoutR_{out} — их стоимость (Rate) за единицу.

Что необходимо логировать на уровне каждого вызова LLM:

  • prompt_tokens — размер контекста. Если он резко растет, возможно, ваша RAG-система извлекает слишком много мусорных чанков.
  • completion_tokens — размер ответа.
  • generation_model — точная версия модели.

Пример из практики: В агенте техподдержки разработчик забыл ограничить историю диалога в State (состоянии графа). С каждым новым сообщением пользователя в модель отправлялась вся предыдущая переписка. К 15-му сообщению TinT_{in} достигал 30 000 токенов, вызывая экспоненциальный рост стоимости и задержки (TTFT). Мониторинг метрики prompt_tokens по session_id позволяет отловить такие утечки в первые часы.

Непрерывная оценка (Continuous Evaluation)

В десятой главе мы разбирали метрики RAG Triad (Faithfulness, Answer Relevance) и паттерн LLM-as-a-Judge. В продакшене возникает проблема: мы не можем запускать тяжелую модель-судью для каждого пользовательского запроса. Это удвоит задержку и затраты.

Решение — асинхронное семплирование.

Пайплайн непрерывной оценки строится так:

  1. Система обслуживает пользователей максимально быстро, сохраняя все трейсы в базу логов (например, ClickHouse).
  2. Фоновый процесс (Cron-джоба) раз в час случайным образом выбирает 5-10% логов.
  3. На этой выборке запускается пайплайн LLM-as-a-Judge, который вычисляет метрики качества.
  4. Результаты агрегируются на дашборде.

Если средняя метрика Faithfulness (опора на факты) падает ниже 0.85, система отправляет алерт в мессенджер команды: «Внимание, обнаружен всплеск галлюцинаций».

Сигналы от пользователей: явные и неявные

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

Явный фидбек (Explicit Feedback): Кнопки 👍 (Like) и 👎 (Dislike) под ответом. Это самый чистый сигнал. Все трейсы, получившие дизлайк, должны автоматически попадать в датасет для ручного анализа инженером. На их основе в дальнейшем формируются Few-Shot примеры для улучшения промптов или данные для DPO (Direct Preference Optimization).

Неявный фидбек (Implicit Feedback): Пользователи редко нажимают кнопки. Поэтому LLMOps-инженеры отслеживают поведенческие паттерны:

  • Копирование в буфер обмена: если пользователь скопировал сгенерированный код или текст, ответ с высокой вероятностью релевантен.
  • Регенерация (Regenerate): запрос нового ответа без изменения промпта — признак неудовлетворенности.
  • Модификация запроса: если после ответа пользователь пишет «нет, я имел в виду для версии 2.0», значит первоначальный ответ не решил задачу.

Guardrails: защитные механизмы в продакшене

Мониторинг показывает проблему постфактум. Чтобы предотвратить выдачу критически неверной информации, внедряются Guardrails (ограждения) — легковесные проверки на входе и выходе.

Проверка на входе (Input Guardrails)

До того как запрос попадет в тяжелую LLM, он проходит через быстрые фильтры:

  • Детекция PII (Personally Identifiable Information): регулярные выражения скрывают номера кредитных карт и паспортов, заменяя их на [CARD_NUMBER].
  • Семантический роутинг: легковесная модель (например, Bi-encoder) проверяет близость запроса к списку запрещенных тем (конкуренты, политика). Если сходство выше порога, запрос блокируется до генерации.

Проверка на выходе (Output Guardrails)

  • Валидация структуры: если система ожидает JSON, а модель выдала текст с извинениями, парсер выбрасывает ошибку и запускает механизм Fallback (резервный сценарий).
  • Проверка тональности: легковесный классификатор убеждается, что бот не использует токсичную лексику.

Semantic Caching (Семантическое кэширование)

Многие пользователи задают одни и те же вопросы («как сбросить пароль?», «какие тарифы?»). Прогонять их каждый раз через LLM — расточительство. Семантический кэш векторизует входящий запрос и ищет в базе ранее успешно сгенерированных ответов. Если косинусное сходство между новым и старым запросом высоко (например, >0.95> 0.95), система моментально возвращает закэшированный ответ. Это снижает стоимость (CostCost) до нуля и уменьшает задержку до миллисекунд.

Резюме

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