Подготовка к собеседованию по LLM и генеративному ИИ
Вопросы для собеседования инженера по LLM и генеративному ИИ
15 отобранных вопросов для собеседования по LLM и генеративному ИИ, сгруппированных по уровням подготовки. Используйте их для повторения основ, понимания практических компромиссов и рассуждений на уровне Senior-специалиста в производственной среде.
1Что такое субсловная токенизация и почему её используют в языковых моделях?
BPE, WordPiece или Unigram делят текст на единицы переменной длины. Частые слова могут быть отдельными токенами, редкие составляются из частей. Это уменьшает словарь относительно разбиения по словам и обычно сокращает последовательность относительно символов. Побайтовый резерв позволяет представить неизвестные слова и символы, если он предусмотрен. Без такого механизма отсутствие токенов всё ещё возможно.
2Чем статические эмбеддинги слов отличаются от контекстных представлений и скрытых состояний трансформера?
Статическое представление слова не меняется по контексту, поэтому два употребления одного слова получают один вектор. Контекстное представление зависит от окружения. Скрытые состояния трансформера — векторы позиций на каждом слое после преобразований внимания и других операций. Из выбранных состояний можно получить контекстные эмбеддинги токенов или агрегировать их для текста. Свойства слоёв зависят от модели и задачи.
import torch
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
model = AutoModel.from_pretrained('bert-base-uncased', output_hidden_states=True)
text1 = 'River bank'
text2 = 'Bank deposit'
inputs1 = tokenizer(text1, return_tensors='pt')
inputs2 = tokenizer(text2, return_tensors='pt')
with torch.no_grad():
out1 = model(**inputs1)
out2 = model(**inputs2)
# Layer 0 (Input token embeddings - static lookup before self-attention)
static_bank_1 = out1.hidden_states[0][0, 2] # 'bank'
# Layer 12 (Final contextual hidden state after all attention layers)
contextual_bank_1 = out1.hidden_states[12][0, 2]
contextual_bank_2 = out2.hidden_states[12][0, 1]
print('Cosine similarity of bank in different contexts (Layer 12):',
torch.cosine_similarity(contextual_bank_1, contextual_bank_2, dim=0).item())
3Что представляет пространство эмбеддингов и как понимать косинусное сходство текстов?
Модель отображает тексты в векторы так, чтобы геометрия помогала решать обучающую задачу. Косинусное сходство ненулевых векторов равно их скалярному произведению, делённому на произведение норм. Оно лежит от −1 до 1 и измеряет совпадение направлений, не учитывая длину. Большое значение часто указывает на близость по критериям модели. Ноль не доказывает независимость по смыслу, а отрицательное значение не означает антонимию. Пороги проверяйте на своих данных.
4Объясните различие токенных, позиционных и сегментных эмбеддингов входа transformer.
В BERT-подобных моделях представление токена обычно складывается поэлементно из трёх векторов. Токенный переводит ID словаря в плотный вектор лексической и смысловой идентичности. Позиционный добавляет порядок и индекс: немаскированное self-attention без позиции эквивариантно перестановке — перестановка входов переставляет выходы и сама порядка не кодирует. Другие архитектуры вместо суммы используют rotary/relative-механизмы. Segment/token-type отличает фрагменты или предложения одной последовательности, например A/B в парной классификации и QA. Сумма кодирует значение, позицию и группу в одном плотном тензоре до первого слоя.
import torch
import torch.nn as nn
vocab_size, max_seq_len, num_segments, d_model = 30522, 512, 2, 768
tok_embed = nn.Embedding(vocab_size, d_model)
pos_embed = nn.Embedding(max_seq_len, d_model)
seg_embed = nn.Embedding(num_segments, d_model)
input_ids = torch.tensor([[101, 7592, 102, 2023, 102]]) # Token IDs
type_ids = torch.tensor([[0, 0, 0, 1, 1 ]]) # Segment IDs (Sentence A vs B)
positions = torch.arange(input_ids.size(1)).unsqueeze(0) # Indices: [0, 1, 2, 3, 4]
# Final representation is the element-wise sum
input_rep = tok_embed(input_ids) + pos_embed(positions) + seg_embed(type_ids)
print(input_rep.shape)
5Как механизм внимания связывает позиции последовательности и для чего нужны запросы, ключи, значения и несколько голов?
Запрос Q позиции сравнивается с ключами K доступных позиций. После деления оценок на √dₖ и применения маски softmax превращает оценки в веса. Выход — взвешенная сумма значений V. Масштабирование сдерживает рост разброса оценок и насыщение softmax. Несколько голов используют разные обучаемые проекции, а их выходы объединяются и преобразуются. Головы могут учиться разным связям, но фиксированная специализация им не назначается.
6Чем каузальная маска внимания отличается от внимания двунаправленного энкодера?
Каузальная маска разрешает позиции смотреть на себя и предшествующие позиции, исключая будущий текст. Это позволяет обучать прогноз следующего токена параллельно по позициям без утечки ответа. Двунаправленный энкодер обычно использует контекст слева и справа, сохраняя маску заполнителей и другие ограничения задачи. Его представления рассчитаны на доступный вход, а для обычной авторегрессивной генерации нужен каузальный путь.
import torch
import torch.nn.functional as F
scores = torch.randn(3, 3)
# Create upper-triangular mask for future positions
causal_mask = torch.triu(torch.ones(3, 3, dtype=torch.bool), diagonal=1)
# Mask future positions with -inf before softmax
masked_scores = scores.masked_fill(causal_mask, float('-inf'))
atten_weights = F.softmax(masked_scores, dim=-1)
print(atten_weights)
7Чем отличаются трансформеры с энкодером, с декодером и с парой энкодер–декодер?
Энкодер обычно видит контекст с обеих сторон и строит представления для классификации или извлечения ответа. Каузальный декодер предсказывает продолжение, используя предыдущие позиции. В архитектуре энкодер–декодер энкодер обрабатывает вход, а декодер авторегрессивно строит выход и через перекрёстное внимание читает представления входа. Это удобно, например, для перевода и суммаризации.
8Как байтовая, Unicode-ориентированная и многоязычная токенизация влияют на стоимость и качество для разных языков?
Словарь, обученный на неравномерном корпусе, может делить одни языки на большее число токенов на сопоставимый текст. Это расходует окно, увеличивает работу и может влиять на качество. Байтовое покрытие позволяет представить незнакомые символы, но редкий текст может распасться на длинные последовательности. Нормализация Unicode и баланс корпуса меняют результат. Сравнивайте число токенов на сопоставимый объём и качество задач по языкам. Больший словарь сам по себе равенства не гарантирует.
9Какие проблемы токенизации возникают для чисел, кода и редких символов Unicode и как их уменьшить?
Цифры могут группироваться непоследовательно, что усложняет работу с разрядами. В коде неудачное разбиение отступов и операторов увеличивает длину. Составные символы могут разбиваться на несколько байтовых единиц. Специальные правила для цифр, пробелов и операторов и побайтовый резерв помогают управлять покрытием. Они меняют длину и обучающий сигнал, поэтому проверяйте арифметику, синтаксис и точное восстановление Unicode на целевых данных.
import tiktoken
# tiktoken cl100k_base (GPT-4 / ChatGPT) enforces digit and whitespace handling
enc = tiktoken.get_encoding('cl100k_base')
num1 = '12345'
num2 = '12346'
code_indent = ' def foo():'
print('Tokens num1:', [enc.decode([t]) for t in enc.encode(num1)])
print('Tokens num2:', [enc.decode([t]) for t in enc.encode(num2)])
print('Tokens code:', [enc.decode([t]) for t in enc.encode(code_indent)])
10Обсудите длинный контекст: квадратичное полное внимание, окна, sparse/global, стоимость KV-кэша и размывание внимания.
1. Dense attention квадратично по длине N по арифметике; явная матрица оценок также требует квадратичной памяти, но точный IO-aware FlashAttention её не материализует. Локальное окно W даёт O(N×W), но дальняя информация проходит несколько слоёв. Sparse может совместить фиксированные окна и ограниченное число global-токенов для линейного масштаба паттерна и дальних связей. 2. При autoregressive-generation K/V прошлых токенов кэшируются; память O(B×L×H_KV×D×N). На 32k–128k+ это десятки ГБ VRAM на batch, ограничение размера пакета и bandwidth. 3. Длинный контекст может добавлять отвлекающие токены и вызывать проблемы, связанные с позицией и обобщением. Lost-in-the-middle — наблюдаемая зависимость извлечения от позиции, а не универсальный результат роста энтропии softmax: внимание может оставаться сосредоточенным на немногих токенах. Измеряйте точность по длинам и позициям доказательств, не выводите её лишь из знаменателя.
11Чем MHA, MQA и GQA отличаются по кэшу ключей и значений и скорости декодирования?
В MHA каждой голове запроса соответствует своя пара голов ключей и значений. MQA делит одну пару между всеми запросами. GQA делит запросы на группы, у каждой своя пара. При прочих равных размер KV-кэша пропорционален числу таких пар. Меньший кэш может снизить трафик памяти и увеличить ёмкость пакета. Выигрыш и качество зависят от модели, реализации и нагрузки.
# Model with 32 Query Heads
num_q_heads = 32
mha_kv_heads = 32 # 1:1 ratio
gqa_kv_heads = 8 # 4:1 ratio (4 query heads per KV head)
mqa_kv_heads = 1 # 32:1 ratio (1 shared KV head)
print(f"KV Cache Size Relative to MHA:")
print(f"MHA: {mha_kv_heads / mha_kv_heads * 100:.1f}%")
print(f"GQA: {gqa_kv_heads / mha_kv_heads * 100:.1f}%")
print(f"MQA: {mqa_kv_heads / mha_kv_heads * 100:.1f}%")
Алгоритм обрабатывает Q, K и V блоками в быстрой памяти и ведёт текущий максимум и нормировку softmax. Он не записывает всю матрицу оценок в глобальную память GPU. При обратном проходе нужные промежуточные значения можно пересчитать. Это уменьшает обмен данными и объём промежуточного хранения. Операция математически эквивалентна полному вниманию, хотя округление может отличаться, а квадратичная арифметическая сложность сохраняется.
13Как сделать выполнение процесса с языковой моделью предсказуемым через граф или автомат, типизированное состояние и проверку результатов?
Опишите разрешённые шаги и переходы в графе или конечном автомате. Модель может предложить план из допустимых действий, который приложение проверит до выполнения. Храните состояние в типизированном формате и проверяйте результат каждого инструмента. Ограничьте сроки, повторы и права и предусмотрите конечные состояния и ручное рассмотрение. Управляющая логика может быть детерминированной, даже если вывод модели меняется.
from pydantic import BaseModel
from typing import Optional, Literal
class WorkflowState(BaseModel):
user_query: str
extracted_id: Optional[str] = None
verification_status: Literal["PENDING", "VERIFIED", "FAILED"] = "PENDING"
retry_count: int = 0
max_retries: int = 3
def execute_validation_node(state: WorkflowState) -> WorkflowState:
if state.retry_count >= state.max_retries:
state.verification_status = "FAILED"
return state
try:
result = call_verification_service(state.extracted_id)
state.verification_status = "VERIFIED" if result.is_valid else "FAILED"
except Exception:
state.retry_count += 1
return state
14Как задать политику уверенности и воздержания для помощника в регулируемой области?
Определите допустимые задачи и исходы вместе с ответственными специалистами. Проверяйте применимость и актуальность источников, а сигналы уверенности калибруйте на профильной выборке. При нехватке сведений уточняйте запрос, ограничивайте ответ либо передавайте человеку согласно уровню риска. Высокая вероятность или согласие нескольких генераций не заменяют проверку. Для аудита храните минимально необходимые сведения о версиях, источниках и решении.
15Как маршрутизировать запросы между моделями по сложности, цене, риску и качеству?
Сначала задайте допустимые модели и операции по ограничениям задачи. На представительных данных измерьте качество, задержку и цену каждого маршрута. Простые запросы можно отдавать меньшей модели, а неопределённые или не прошедшие проверку — более подходящей в пределах бюджета. Высокий риск может требовать внешней проверки, а не просто большей модели. Следите за долей повторных маршрутов, ошибками и стоимостью успешного ответа.
class DynamicModelRouter:
def __init__(self, small_client, medium_client, large_client, classifier, guardrail):
self.small = small_client
self.medium = medium_client
self.large = large_client
self.classifier = classifier
self.guardrail = guardrail
async def route_and_execute(self, request):
# 1. Deterministic Risk Gate
if request.risk_level == "high" or request.domain in ["legal", "medical_compliance"]:
return await self.large.generate(request.prompt)
# 2. Predictive Complexity Classifier
complexity = self.classifier.predict_complexity(request.prompt) # 0.0 to 1.0
if complexity < 0.35:
response = await self.small.generate(request.prompt)
if self.guardrail.is_acceptable(response):
return response
return await self.medium.generate(request.prompt) # Fallback
if complexity < 0.75:
response = await self.medium.generate(request.prompt)
if self.guardrail.is_acceptable(response):
return response
return await self.large.generate(request.prompt) # Fallback
# 3. High complexity frontier execution
return await self.large.generate(request.prompt)