К обычному разбору
Тренировка по собеседованиюТехническое собеседованиеToloka AI2025-09-24

Toloka AI: Алгоритмическая секция

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

Шагов
5
Вопросов
3
Задач
2
1ЗадачаEasy

Majority vote для разметки задач

Условие

Implement a function that receives annotation rows with task_id, worker_id and label.

Return a mapping from each task_id to the most frequent label for that task.

If several labels tie, return the first label encountered for this task. In the original interview the tie-break was allowed to be arbitrary; the trainer fixes it for deterministic tests.

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Сгруппируйте по task_id

    Внешний словарь: task_id -> словарь частот labels.

  • Tie-break

    Если labels равны по частоте, нужен стабильный выбор. Запомните индекс первого появления label.

Идея решения

Соберите вложенный словарь: для каждого task_id храните счетчик labels. В отдельном словаре можно запомнить порядок первого появления labels внутри каждой задачи, чтобы tie-break был стабильным.

После подсчета голосов для каждой задачи выбирается label с максимальной парой (count, -first_seen_index): больше голосов лучше, а при равенстве выигрывает label, который встретился раньше.

На собеседовании tie-break разрешили выбрать произвольно; в тренажере он зафиксирован, чтобы автотесты были детерминированными.

Эталонный код

def majority_gold(rows: list[dict[str, str]]) -> dict[str, str]:
    votes: dict[str, dict[str, int]] = {}
    first_seen: dict[str, dict[str, int]] = {}

    for index, row in enumerate(rows):
        task_id = row["task_id"]
        label = row["label"]

        if task_id not in votes:
            votes[task_id] = {}
            first_seen[task_id] = {}

        votes[task_id][label] = votes[task_id].get(label, 0) + 1
        if label not in first_seen[task_id]:
            first_seen[task_id][label] = index

    result: dict[str, str] = {}
    for task_id, counts in votes.items():
        result[task_id] = max(
            counts,
            key=lambda label: (counts[label], -first_seen[task_id][label]),
        )

    return result
Сложность
Время: O(n). Память: O(t * l).
n - число строк разметки, t - число задач, l - число разных labels на задачу. Один проход считает голоса, второй выбирает максимум.
2ЗадачаHard

ConversationHistory для LLM-агента

Условие

Implement a simplified ConversationHistory manager for an LLM agent.

The history stores mixed system, user, assistant and tool messages. On each new message, trim old messages so that the history respects these limits.

  • max_tokens: total token count, where token count is approximated by len(str(content).split());
  • max_tool_calls: maximum number of assistant tool-call messages.

Do not remove system or user messages because of token pressure. Remove tool calls together with their following tool result, and remove orphan tool results.

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Не удаляйте user/system по token pressure

    Они входят в общий token count, но остаются даже если лимит из-за них невыполним.

  • Tool call удаляется вместе с result

    Если вы удалили assistant tool_call, следующее role=tool сообщение больше не имеет смысла.

  • Сначала correctness, потом deque

    Удаление из середины делает list проще для интервьюерского решения; оптимизация структуры данных идет после фиксации invariants.

Идея решения

Главная сложность не в структуре данных, а в invariants:

  • system и user остаются в истории даже если из-за них token limit уже невозможно выполнить;
  • assistant text messages можно удалять по token pressure;
  • tool call и следующий за ним tool result должны удаляться как пара;
  • tool result без preceding tool call нельзя оставлять в истории.

В интервью кандидат обсуждал deque/list trade-off. Для публичной задачи проще и надежнее показать list-based решение: оно легче проверяет пары и удаление из середины. Если нужен production вариант, можно хранить linked list или индексы removable сообщений и отдельные счетчики токенов/tool calls.

Эталонный код

def manage_conversation_history(
    max_tokens: int,
    max_tool_calls: int,
    messages: list[dict],
) -> list[dict]:
    history: list[dict] = []

    def token_count(message: dict) -> int:
        return len(str(message.get("content", "")).split())

    def is_protected(message: dict) -> bool:
        return message.get("role") in {"system", "user"}

    def is_tool_call(message: dict) -> bool:
        content = message.get("content")
        return (
            message.get("role") == "assistant"
            and isinstance(content, dict)
            and content.get("type") == "tool_call"
        )

    def total_tokens() -> int:
        return sum(token_count(message) for message in history)

    def tool_call_count() -> int:
        return sum(1 for message in history if is_tool_call(message))

    def remove_at(index: int) -> None:
        removed = history.pop(index)
        if is_tool_call(removed) and index < len(history) and history[index].get("role") == "tool":
            history.pop(index)

    def remove_orphan_tool_results() -> None:
        index = 0
        while index < len(history):
            if history[index].get("role") == "tool":
                if index == 0 or not is_tool_call(history[index - 1]):
                    history.pop(index)
                    continue
            index += 1

    def oldest_removable_message_index() -> int | None:
        for index, message in enumerate(history):
            if not is_protected(message):
                return index
        return None

    def oldest_tool_call_index() -> int | None:
        for index, message in enumerate(history):
            if is_tool_call(message):
                return index
        return None

    for message in messages:
        history.append(dict(message))
        remove_orphan_tool_results()

        while tool_call_count() > max_tool_calls:
            index = oldest_tool_call_index()
            if index is None:
                break
            remove_at(index)
            remove_orphan_tool_results()

        while total_tokens() > max_tokens:
            index = oldest_removable_message_index()
            if index is None:
                break
            remove_at(index)
            remove_orphan_tool_results()

    return history
Сложность
Время: O(n^2). Память: O(n).
Простая реализация пересчитывает token count и tool call count после удалений. В production можно поддерживать счетчики инкрементально и хранить индексы removable сообщений.
3Вопрос15 мин

Современный training pipeline LLM: pretrain, SFT, alignment

Расскажите про современную архитектуру LLM и процесс обучения: какие основные этапы, данные, objective и loss используются?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Современные LLM чаще всего строятся как decoder-only Transformers и обучаются по этапам: next-token pretraining на больших корпусах, затем SFT/instruction tuning, затем preference/alignment оптимизация вроде RLHF, DPO или близких методов.

Подробный разбор

Базовая современная LLM - это decoder-only Transformer: token embeddings проходят через блоки causal self-attention, feed-forward/MLP layers, residual connections, normalization и language-modeling head. Основная pretraining objective - next-token prediction с cross-entropy по большим корпусам текста и кода.

Второй этап - supervised fine-tuning или instruction tuning. Модель обучают на prompt-response pairs, чтобы сырой pretrained model начал хорошо отвечать в диалоге, следовать инструкциям, использовать tool-use formats и решать доменные задачи. Полный fine-tuning возможен, но при ограничениях по стоимости и данным часто используют parameter-efficient методы вроде LoRA/adapters.

Третий этап - alignment/preference optimization. Классический RLHF обучает reward model по human preference comparisons, затем оптимизирует policy PPO-подобными методами. Более прямые preference methods вроде DPO и новых вариантов обходятся без отдельного online RL loop и оптимизируются по preference pairs напрямую. Цель этого этапа - сделать ответы более полезными, безопасными и согласованными с требованиями продукта, а не заново научить модель базовым знаниям.

В современных системах также часто добавляют reasoning-style training/inference, tool-use или agent scaffolding, RAG для внешних знаний и MoE-варианты, чтобы увеличить общее число параметров при меньшем active compute.

Типичные ошибки

  • Описать только архитектуру Transformer и пропустить этапы обучения.
  • Сказать, что RLHF учит базовые языковые знания.
  • Считать LoRA отдельной архитектурой модели, а не parameter-efficient fine-tuning методом.
  • Забыть реальный pretraining loss: causal next-token cross-entropy.

Как сказать на собеседовании

  • Начни с decoder-only Transformer и next-token loss, затем пройди pretrain -> SFT -> preference/alignment.
  • Для каждого этапа назови форму данных: raw corpus, instruction pairs, preference pairs или rewards.
4Вопрос8 мин

Где в Transformer применяется Mixture of Experts

В MoE LLM где обычно находится Mixture of Experts: в каком слое Transformer и зачем это делают?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

В типичных Transformer MoE-архитектурах experts заменяют или дополняют feed-forward/MLP sublayer, а router выбирает top-k экспертов для каждого токена. Attention обычно остается dense.

Подробный разбор

В Transformer block обычно есть self-attention sublayer и feed-forward/MLP sublayer. Во многих MoE LLM плотный MLP заменяется несколькими expert MLP и router/gating network. Для каждого токена router выбирает одного или несколько экспертов, и только они вычисляются для этого токена.

Так модель получает большую суммарную емкость параметров, но active compute per token остается ближе к меньшей dense model. Это особенно полезно, потому что FFN/MLP часть занимает большую долю параметров и вычислений Transformer. Эксперты могут специализироваться, а router учится выбирать полезные маршруты для токена и контекста.

Компромиссы: routing balance, communication overhead между устройствами, expert collapse, сложность batching и serving latency. Обычно нужны load-balancing losses и capacity constraints, чтобы router не отправлял все токены одному эксперту.

Типичные ошибки

  • По умолчанию помещать MoE внутрь attention heads.
  • Говорить, что каждый expert запускается для каждого токена.
  • Игнорировать routing/load-balancing проблемы.

Как сказать на собеседовании

  • Сразу скажи "обычно FFN/MLP sublayer" - это напрямую отвечает на уточнение интервьюера.
  • Затем объясни зачем: большая parameter capacity без пропорционального роста active FLOPs.
5Вопрос12 мин

Long context в LLM: проблемы и способы решения

Какие проблемы возникают при использовании длинного контекста в LLM и какими подходами их адресуют?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Long context увеличивает стоимость attention/KV cache, может ухудшать поиск нужных фактов и выходить за режим, на котором модель обучалась. Помогают RAG, chunking/summarization, sparse/sliding-window attention, positional scaling и optimized kernels вроде FlashAttention.

Подробный разбор

Первая проблема вычислительная. Dense self-attention на обучении имеет квадратичную стоимость по длине последовательности, а inference сильно платит за prefill attention и растущий KV cache. Очень длинные prompts увеличивают latency, memory pressure и serving cost.

Вторая проблема - качество. Модель могла обучаться или дообучаться на ограниченном context range; за его пределами positional generalization и attention behavior могут деградировать. Даже внутри заявленного context window модель может пропускать информацию в середине, переиспользовать начало/конец или не выбрать релевантное evidence среди большого числа distractors.

Mitigations работают на нескольких уровнях. На product/system уровне обычно не стоит складывать все в prompt: используют RAG, metadata filters, reranking, chunking, compression и summaries, чтобы в prompt попадало только релевантное evidence. На model level: longer-context training, RoPE/positional scaling variants, sparse attention, sliding-window/block attention, landmark/global tokens и memory-подходы. На systems level: KV-cache management, paged attention и FlashAttention-style kernels.

FlashAttention улучшает IO/memory efficiency exact attention; сам по себе он не заставляет модель семантически понимать произвольно длинный контекст. RAG и summarization уменьшают нужный объем контекста, а sparse/block attention меняет паттерн attention, чтобы длинные последовательности были дешевле.

Типичные ошибки

  • Сказать только "use FlashAttention" и проигнорировать retrieval quality.
  • Смешать размер context window с гарантированной способностью использовать все токены.
  • Забыть KV cache memory и prefill latency во время inference.
  • Использовать RAG как buzzword без retrieval, reranking или chunking.

Как сказать на собеседовании

  • Раздели ответ на compute, memory, quality и mitigation layers.
  • Явно отличи exact-kernel optimizations от подходов, которые уменьшают или перестраивают context.