VKВлад КуликовОткрытые лекции
RL для LLM11 / 13

Модуль 11. RL для LLM-агентов: инструменты, среда и длинные траектории

Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.

Скачать файлы курса (.zip) 382,1 КБ

Архив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.

Четыре слоя информации

Агент работает с интернет-магазином. В базе данных хранится фактический статус заказа; программный интерфейс (application programming interface, API) возвращает только последние два события; в контексте модели находится история диалога; отдельно агент ведёт краткую таблицу фактов.

Сопоставьте объекты с понятиями:

  1. состояние среды;

  2. наблюдение;

  3. история;

  4. внешняя память.

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

Показать ответ и решение
  • Фактический статус заказа в базе — состояние среды. Он существует независимо от того, показан ли агенту.
  • Два последних события, возвращённые программным интерфейсом (application programming interface, API), — наблюдение.
  • Диалог, доступный модели на текущем ходу, — история.
  • Отдельная таблица фактов — внешняя память.

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

Два масштаба действия

Модель токен за токеном генерирует вызов run_tests, после чего среда выполняет тесты 40 секунд и возвращает отчёт.

  1. Что является микродействием?

  2. Что является макродействием?

  3. Почему для макродействия может понадобиться полумарковская модель?

  4. Какая величина должна отражать различие между вызовом на 0,1 секунды и вызовом на 40 секунд, если задержка важна пользователю?

Показать ответ и решение
  1. Микродействие — очередной токен строки run_tests или структурированного вызова.

  2. Макродействие — завершённый вызов тестового инструмента вместе с переходом среды к новому состоянию и появлением отчёта.

  3. Время выполнения макродействий различается. В полумарковском процессе принятия решений (semi-Markov decision process, semi-MDP) переход после действия длительности τt\tau_t дисконтируется через γτt\gamma^{\tau_t}, а не через одну фиксированную степень на каждый вызов.

  4. Различие должно отражаться продолжительностью τt\tau_t, явной стоимостью задержки в награде либо отдельным ограничением и метрикой стоимости.

Происхождение токенов

Транскрипт содержит system, user, assistant(tool_call), tool, assistant(final). Какие сегменты обычно входят в функцию потерь политики и почему? Что изменится, если вызов в формате обмена данными (JavaScript Object Notation, JSON) был создан не моделью, а детерминированным контроллером?

Показать ответ и решение

Обычно в функцию потерь входят assistant(tool_call) и assistant(final), поскольку эти токены были сгенерированы политикой и для них можно сохранить логарифмы вероятностей поведения. system, user и tool служат условием для будущих действий, но не являются действиями политики.

Если объект в формате обмена данными (JavaScript Object Notation, JSON) создал детерминированный контроллер, его токены не были сгенерированы обучаемой моделью. Тогда действием модели является вход контроллера: например, скрытый идентификатор функции и аргументы. Преобразование контроллера нужно записывать отдельно; приписывать модели логарифм вероятности готового JSON без соответствующего распределения нельзя.

Четыре причины остановки

Для каждого случая выберите: success, task_failure, truncated или infra_error.

  1. Агент выполнил задачу и проверка состояния прошла.

  2. Правила задачи явно разрешают не более 10 вызовов; сделан 11-й.

  3. Сборщик остановил эпизод через 60 секунд, хотя лимит не входил в постановку.

  4. Контейнер не запустился из-за ошибки образа.

Объясните, почему одинаковая награда 0 для случаев 2–4 может испортить обучение.

Показать ответ и решение
  1. success.

  2. task_failure, потому что ограничение входит в правила задачи.

  3. truncated, потому что эпизод остановил внешний сборщик.

  4. infra_error.

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

Одна награда на всю траекторию

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

Показать ответ и решение

Утверждение неверно. Оцениватель

R(τ)∑t∇θlog⁡πθ(at∣ht)R(\tau)\sum_t\nabla_\theta\log\pi_\theta(a_t\mid h_t)

может быть несмещённым оценивателем градиента ожидаемой траекторной награды. Две реальные проблемы:

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

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

Безопасное формирование награды

Агент получает +0.1+0.1 за каждый вызов inspect и +1+1 за завершение задачи. Он научился многократно вызывать inspect.

  1. Как называется этот режим отказа?

  2. Запишите потенциальную формирующую добавку.

  3. Перечислите не менее трёх условий или оговорок, необходимых для вывода об инвариантности оптимальной политики.

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

  2. Потенциальная добавка:

F(st,st+1)=γΦ(st+1)−Φ(st).F(s_t,s_{t+1})=\gamma\Phi(s_{t+1})-\Phi(s_t).
  1. Для вывода об инвариантности нужны, например:
  • та же величина γ\gamma, что и в исходной задаче;
  • марковское состояние, от которого зависит Φ\Phi;
  • корректная обработка терминального потенциала и граничного члена;
  • неизменные переходы, исходная награда и правила остановки;
  • точный потенциальный вид, а не произвольный бонус;
  • отдельная осторожность при частичной наблюдаемости.

Даже при этих условиях теорема сохраняет оптимум, но не гарантирует ускорения или стабильности обучения.

Вырожденная группа

При бинарной награде текущая политика решает задачу с вероятностью p=0.005p=0.005. Почему увеличение скорости обучения не исправляет отсутствие сигнала в группе из G=8G=8 роллаутов? Назовите не менее четырёх более прямых рычагов.

Показать ответ и решение

При p=0.005p=0.005 и G=8G=8 почти каждая группа состоит только из провалов. Групповое центрирование даёт одинаковые или нулевые преимущества. Скорость обучения меняет масштаб имеющегося градиента, но не создаёт отсутствующее сравнение.

Прямые рычаги:

  • увеличить GG;
  • выбирать задачи промежуточной сложности;
  • улучшить исходную политику с помощью SFT;
  • изменить параметры генерации для полезного разнообразия;
  • добавить проверяемый частичный сигнал;
  • использовать динамический отбор задач;
  • повторно использовать редкие успешные траектории с явным контролем смещения из-за данных другой политики;
  • построить ветвление в информативных состояниях.

Методы с похожими названиями

Объясните различия между:

  • Agentic Reinforced Policy Optimization;
  • Agentic Replay Policy Optimization;
  • Dynamic-filter Sequence-level Policy Optimization (DSPO);
  • Agentic-DPO.

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

Показать ответ и решение
МетодДанныеЕдиница сигналаГлавный риск
Agentic Reinforced Policy Optimizationонлайн, свежие траектории с инструментами и ветвлениемшаг после наблюдения и итог траекториисмещение отбора точек ветвления, стоимость и сложность атрибуции
Agentic Replay Policy Optimizationсвежие траектории плюс повторное использование успешного опытацелая траектория агента с графическим интерфейсомсмещение из-за данных другой политики и чрезмерное повторение редких удачных эпизодов
Dynamic-filter Sequence-level Policy Optimization (DSPO)преимущественно свежие траектории средыпоследовательностьдинамический фильтр меняет распределение задач и может скрыть слабые режимы
Agentic-DPOофлайн-экспертные траектории и одношаговые отрицательные действия текущей политикипредпочтение действия в конкретном состоянииограниченное покрытие эксперта и слабые или искусственные отрицательные примеры

Сокращение ARPO неоднозначно: две первые работы раскрывают его по-разному. Без полного названия читатель не знает, идёт ли речь об адаптивном ветвлении или о буфере повторного использования.

Маска и разбавление

Траектория состоит из сегментов:

РольЧисло токенов
assistant18
tool160
assistant12
tool70
assistant9

Считайте обучаемыми только assistant-токены.

  1. Найдите полную длину и число обучаемых токенов.

  2. Найдите долю маскированных токенов.

  3. Во сколько раз изменится масштаб, если делить на полную длину вместо числа токенов политики?

  4. При A^=1.5\widehat A=1.5 найдите коэффициент одного токена при обеих нормировках.

Округлите доли до четырёх, коэффициенты до шести, фактор до трёх знаков.

Показать ответ и решение

Полная длина:

18+160+12+70+9=269.18+160+12+70+9=269.

Число токенов политики:

18+12+9=39.18+12+9=39.

Доля маскированных:

1−39269=230269≈0.8550.1-\frac{39}{269}=\frac{230}{269}\approx0.8550.

Фактор разбавления:

26939≈6.897.\frac{269}{39}\approx6.897.

При A^=1.5\widehat A=1.5 коэффициент токена:

1.539≈0.038462\frac{1.5}{39}\approx0.038462

при нормировке по токенам политики и

1.5269≈0.005576\frac{1.5}{269}\approx0.005576

при делении на полную длину.

Вероятность информативной группы

Для бинарной награды вычислите

Pinfo=1−pG−(1−p)GP_{\mathrm{info}}=1-p^G-(1-p)^G

и ожидаемое число групп 1/Pinfo1/P_{\mathrm{info}} для:

  1. p=0.01p=0.01, G=8G=8;

  2. p=0.05p=0.05, G=4G=4;

  3. p=0.05p=0.05, G=16G=16.

Сравните два последних режима при равной стоимости одного роллаута.

Показать ответ и решение

Используем

Pinfo=1−pG−(1−p)G.P_{\mathrm{info}}=1-p^G-(1-p)^G.
ppGGPinfoP_{\mathrm{info}}1/Pinfo1/P_{\mathrm{info}}
0.0180.07725512.9441
0.0540.1854885.3912
0.05160.5598731.7861

При p=0.05p=0.05 группа из 16 роллаутов значительно чаще информативна, но стоит в четыре раза дороже группы из 4. Для одной информативной группы ожидаемая стоимость в роллаутах составляет примерно

4⋅5.3912≈21.564\cdot5.3912\approx21.56

и

16⋅1.7861≈28.58.16\cdot1.7861\approx28.58.

Следовательно, увеличение GG улучшает вероятность сигнала на группу, но не обязательно уменьшает стоимость одного информативного сравнения.

TD-остатки и телескоп

Даны

r=(0,0,1),V=(0.20,0.35,0.60,0),γ=0.9.r=(0,0,1), \quad V=(0.20,0.35,0.60,0), \quad \gamma=0.9.
  1. Вычислите δ0,δ1,δ2\delta_0,\delta_1,\delta_2.

  2. Найдите ∑t=02γtδt\sum_{t=0}^{2}\gamma^t\delta_t.

  3. Найдите G0−V0+γ3V3G_0-V_0+\gamma^3V_3 и подтвердите равенство.

  4. Объясните, почему наименьший δt\delta_t не обязан указывать на «самый плохой ход».

Показать ответ и решение
δ0=0+0.9⋅0.35−0.20=0.115,\delta_0=0+0.9\cdot0.35-0.20=0.115, δ1=0+0.9⋅0.60−0.35=0.190,\delta_1=0+0.9\cdot0.60-0.35=0.190, δ2=1+0−0.60=0.400.\delta_2=1+0-0.60=0.400.

Взвешенная сумма:

0.115+0.9⋅0.190+0.92⋅0.400=0.610.0.115+0.9\cdot0.190+0.9^2\cdot0.400 =0.610.

Исходная накопленная награда:

G0=0+0.9⋅0+0.92⋅1=0.81.G_0=0+0.9\cdot0+0.9^2\cdot1=0.81.

Поэтому

G0−V0+γ3V3=0.81−0.20+0=0.61.G_0-V_0+\gamma^3V_3=0.81-0.20+0=0.61.

Наименьший остаток не обязательно соответствует худшему действию: он зависит от ошибок обеих оценок ценности, наблюдения среды и момента появления пользы действия.

Потенциальное формирование числом

Пусть γ=0.9\gamma=0.9, базовые награды равны

r=(0,0,1),r=(0,0,1),

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

Φ=(0,0.3,0.6,0).\Phi=(0,0.3,0.6,0).
  1. Найдите Ft=γΦt+1−ΦtF_t=\gamma\Phi_{t+1}-\Phi_t.

  2. Найдите сформированные награды rt′=rt+Ftr'_t=r_t+F_t.

  3. Сравните дисконтированные накопленные награды исходной и сформированной траекторий.

  4. Как изменится ответ, если Φ(s0)=0.2\Phi(s_0)=0.2, а остальная траектория та же?

Показать ответ и решение

Добавки:

F0=0.9⋅0.3−0=0.27,F_0=0.9\cdot0.3-0=0.27, F1=0.9⋅0.6−0.3=0.24,F_1=0.9\cdot0.6-0.3=0.24, F2=0.9⋅0−0.6=−0.60.F_2=0.9\cdot0-0.6=-0.60.

Сформированные награды:

r′=(0.27,0.24,0.40).r'=(0.27,0.24,0.40).

Исходная накопленная награда:

G0=0.92=0.81.G_0=0.9^2=0.81.

Сформированный:

0.27+0.9⋅0.24+0.92⋅0.40=0.81.0.27+0.9\cdot0.24+0.9^2\cdot0.40 =0.81.

При Φ(s0)=0.2\Phi(s_0)=0.2 первая добавка уменьшается на 0.2, поэтому сформированная накопленная награда равна

0.81−0.2=0.61.0.81-0.2=0.61.

Это тот самый граничный член −Φ(s0)+γTΦ(sT)-\Phi(s_0)+\gamma^T\Phi(s_T).

Разные редукции

Есть две траектории. В первой два токена политики с членами функции потерь (0.2,0.2)(0.2,0.2); во второй восемь токенов, каждый с членом 0.10.1.

  1. Вычислите токеновое среднее.

  2. Вычислите траекторное среднее, если сначала усреднять внутри траектории.

  3. Какой режим сильнее взвешивает вторую, более длинную траекторию?

  4. Почему нельзя назвать одно из двух чисел «правильным» без определения целевой функции?

Показать ответ и решение

Сумма всех токеновых членов:

2⋅0.2+8⋅0.1=1.2.2\cdot0.2+8\cdot0.1=1.2.

Токеновое среднее:

1.210=0.12.\frac{1.2}{10}=0.12.

Средние внутри траекторий равны 0.2 и 0.1. Траекторное среднее:

0.2+0.12=0.15.\frac{0.2+0.1}{2}=0.15.

Токеновая редукция даёт второй траектории вес 8/108/10, а траекторная — 1/21/2. Выбор зависит от того, является ли статистической единицей токен или эпизод и какой стимул к длине допустим.

Полумарковское дисконтирование

Агент выполняет два макродействия. Первое длится τ0=2\tau_0=2 базовые единицы времени и даёт награду 0. Второе начинается после него, длится τ1=3\tau_1=3 единицы и выплачивает терминальную награду 1 в момент времени 5. В этой задаче награда, полученная через dd базовых единиц, умножается на γd\gamma^d, где γ=0.9\gamma=0.9.

  1. Какова накопленная награда из начального состояния?

  2. Чему равен дисконтирующий множитель перед терминальной наградой?

  3. Что бы получилось при ошибочном правиле «одна степень дисконтирования на один вызов», если первый вызов имеет степень 0, а второй — степень 1?

  4. Какую инженерную ошибку скрывает такое упрощение?

Показать ответ и решение

Терминальная награда выплачивается через

τ0+τ1=5\tau_0+\tau_1=5

базовых единиц. Множитель:

γ5=0.95=0.59049.\gamma^5=0.9^5=0.59049.

Накопленная награда равна 0.590490.59049.

При ошибочном правиле «одна степень на один вызов» награда второго вызова получила бы множитель 0.91=0.90.9^1=0.9. Такой расчёт делает вызов длительностью 40 секунд эквивалентным вызову на 0.1 секунды и скрывает задержку, стоимость и разный реальный горизонт макродействий.

Маска по ролям

Реализуйте функцию:

Python
def build_policy_mask(roles: list[str], lengths: list[int]) -> list[int]:
    """Верните токеновую маску: 1 только для роли assistant."""

Требования:

  • длины списков совпадают;
  • все длины — неотрицательные целые;
  • допустимы роли system, user, assistant, tool;
  • неизвестная роль вызывает ValueError;
  • результат имеет длину sum(lengths).

Пример:

Python
build_policy_mask(["user", "assistant", "tool", "assistant"], [2, 3, 4, 1])
# [0, 0, 1, 1, 1, 0, 0, 0, 0, 1]

Открытые проверки:

Python
mask = build_policy_mask(["user", "assistant", "tool", "assistant"], [2, 3, 4, 1])
assert mask == [0, 0, 1, 1, 1, 0, 0, 0, 0, 1]
Показать ответ и решение
Python
def build_policy_mask(roles: list[str], lengths: list[int]) -> list[int]:
    allowed = {"system", "user", "assistant", "tool"}
    if len(roles) != len(lengths):
        raise ValueError("roles and lengths must have equal lengths")

    result: list[int] = []
    for role, length in zip(roles, lengths, strict=True):
        if role not in allowed:
            raise ValueError(f"unknown role: {role}")
        if isinstance(length, bool) or not isinstance(length, int) or length < 0:
            raise ValueError("lengths must be non-negative integers")
        result.extend([1 if role == "assistant" else 0] * length)
    return result

Проверка bool нужна потому, что в Python bool является подклассом int.

TD-остатки

Реализуйте:

Python
def td_residuals(rewards: list[float], values: list[float], gamma: float) -> list[float]:
    """values содержит T+1 элементов, rewards — T."""

Проверьте размеры, конечность чисел и 0≤γ≤10\le\gamma\le1. Для примера B3 функция должна вернуть [0.115, 0.19, 0.4] с обычной погрешностью float.

Открытые проверки:

Python
rewards = np.array([0.0, 0.0, 1.0])
values = np.array([0.2, 0.35, 0.6, 0.0])
deltas = td_residuals(rewards, values, 0.9)
assert np.allclose(deltas, [0.115, 0.19, 0.4])
assert len(deltas) == len(rewards)
Показать ответ и решение
Python
import math


def td_residuals(
    rewards: list[float],
    values: list[float],
    gamma: float,
) -> list[float]:
    if len(values) != len(rewards) + 1:
        raise ValueError("values must have length len(rewards)+1")
    if not math.isfinite(gamma) or not 0.0 <= gamma <= 1.0:
        raise ValueError("gamma must lie in [0, 1]")
    numbers = [*rewards, *values]
    if not all(math.isfinite(float(x)) for x in numbers):
        raise ValueError("inputs must be finite")

    return [
        float(rewards[t]) + gamma * float(values[t + 1]) - float(values[t])
        for t in range(len(rewards))
    ]

Для терминального перехода в общем коде нужна отдельная маска бутстрэппинга. В задаче последняя ценность уже задана нулём.

Информативность группы

Реализуйте:

Python
def informative_group_probability(p: float, group_size: int) -> float:
    """Вероятность увидеть хотя бы один успех и один провал."""

Функция должна быть численно устойчивой при малых pp, проверять p∈[0,1]p\in[0,1] и положительность целого group_size.

Открытые проверки:

Python
assert math.isclose(informative_group_probability(0.01, 8), 0.07725530557207982, rel_tol=0.0, abs_tol=1e-15)
Показать ответ и решение
Python
import math


def informative_group_probability(p: float, group_size: int) -> float:
    p = float(p)
    if not math.isfinite(p) or not 0.0 <= p <= 1.0:
        raise ValueError("p must lie in [0, 1]")
    if isinstance(group_size, bool) or not isinstance(group_size, int) or group_size < 1:
        raise ValueError("group_size must be a positive integer")
    if p in (0.0, 1.0):
        return 0.0

    at_least_one_success = -math.expm1(group_size * math.log1p(-p))
    all_success = math.exp(group_size * math.log(p))
    return max(0.0, min(1.0, at_least_one_success - all_success))

expm1 и log1p уменьшают потерю точности при малом pp.

Потенциальное формирование

Реализуйте:

Python
def potential_shaping(
    rewards: list[float],
    potentials: list[float],
    gamma: float,
) -> list[float]:
    """Верните r[t] + gamma * Phi[t+1] - Phi[t]."""

potentials содержит на один элемент больше, чем rewards. Добавьте проверки размеров и конечности.

Открытые проверки:

Python
shaped = potential_shaping([0.0, 0.0, 1.0], [0.0, 0.3, 0.6, 0.0], 0.9)
assert all(abs(a - b) < 1e-12 for a, b in zip(shaped, [0.27, 0.24, 0.4]))
Показать ответ и решение
Python
import math


def potential_shaping(
    rewards: list[float],
    potentials: list[float],
    gamma: float,
) -> list[float]:
    if len(potentials) != len(rewards) + 1:
        raise ValueError("potentials must have length len(rewards)+1")
    if not math.isfinite(gamma) or not 0.0 <= gamma <= 1.0:
        raise ValueError("gamma must lie in [0, 1]")
    if not all(math.isfinite(float(x)) for x in [*rewards, *potentials]):
        raise ValueError("inputs must be finite")

    return [
        float(rewards[t])
        + gamma * float(potentials[t + 1])
        - float(potentials[t])
        for t in range(len(rewards))
    ]

Проверка полной записи роллаута

Реализуйте:

Python
def validate_rollout_record(record: dict) -> list[str]:
    """Верните список найденных ошибок; пустой список означает валидную запись."""

Обязательные поля:

text
task_id, environment_id, environment_version,
token_ids, policy_mask, behavior_logprobs,
policy_versions, turn_ids,
action_ids, observations, rewards, stop_reason,
terminated, truncated, infra_error

Инварианты:

  • token_ids, policy_mask, turn_ids имеют одинаковую длину;
  • маска содержит только целые 0 и 1;
  • число единиц маски равно длине behavior_logprobs;
  • policy_versions содержит версию для каждого хода, в котором есть токены политики;
  • observations содержит начальное наблюдение и ещё по одному наблюдению после каждого действия, поэтому его длина на единицу больше длины action_ids и rewards;
  • не более одного из terminated, truncated, infra_error истинно;
  • остановившийся роллаут имеет строковую stop_reason, а у незавершённой записи причина остановки равна None.

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

Открытые проверки:

Python
record = {
    "task_id": "t1",
    "environment_id": "keydoor",
    "environment_version": "1.0",
    "token_ids": [1, 2, 3],
    "policy_mask": [0, 1, 1],
    "behavior_logprobs": [-0.2, -0.3],  # по одному значению на токен политики
    "policy_versions": {"0": "v1", "1": "v1"},
    "turn_ids": [0, 0, 1],
    "action_ids": [2, 4],
    "observations": ["start", "carrying_red", "opened_red"],
    "rewards": [0.0, 1.0],
    "stop_reason": "success",
    "terminated": True,
    "truncated": False,
    "infra_error": False,
}
assert validate_rollout_record(record) == []
assert any("missing" in e for e in validate_rollout_record({}))
broken = dict(record, observations=["start", "opened_red"])
assert validate_rollout_record(broken) != []
Показать ответ и решение
Python
import math
from collections.abc import Mapping


def validate_rollout_record(record: dict) -> list[str]:
    required = {
        "task_id", "environment_id", "environment_version",
        "token_ids", "policy_mask", "behavior_logprobs",
        "policy_versions", "turn_ids",
        "action_ids", "observations", "rewards", "stop_reason",
        "terminated", "truncated", "infra_error",
    }
    errors: list[str] = []
    missing = sorted(required - set(record))
    if missing:
        return ["missing fields: " + ", ".join(missing)]

    token_ids = record["token_ids"]
    mask = record["policy_mask"]
    turns = record["turn_ids"]
    logprobs = record["behavior_logprobs"]

    if not all(isinstance(item, list) for item in (token_ids, mask, turns, logprobs)):
        return ["token_ids, policy_mask, turn_ids and behavior_logprobs must be lists"]
    if not (len(token_ids) == len(mask) == len(turns)):
        errors.append("token_ids, policy_mask and turn_ids must have equal lengths")
    if not all(x in (0, 1) and not isinstance(x, bool) for x in mask):
        errors.append("policy_mask must contain integer zeros and ones")
    if sum(x == 1 for x in mask) != len(logprobs):
        errors.append("logprob count must equal the number of policy tokens")
    try:
        valid_logprobs = all(math.isfinite(float(x)) and float(x) <= 0.0 for x in logprobs)
    except (TypeError, ValueError):
        valid_logprobs = False
    if not valid_logprobs:
        errors.append("logprobs must be finite and non-positive")

    versions = record["policy_versions"]
    if not isinstance(versions, Mapping):
        errors.append("policy_versions must be a mapping")
    elif len(token_ids) == len(mask) == len(turns):
        policy_turns = {str(t) for t, m in zip(turns, mask, strict=True) if m == 1}
        missing_turns = policy_turns - {str(t) for t in versions}
        if missing_turns:
            errors.append("missing versions for policy turns")

    actions = record["action_ids"]
    observations = record["observations"]
    rewards = record["rewards"]
    if not all(isinstance(item, list) for item in (actions, observations, rewards)):
        errors.append("action_ids, observations and rewards must be lists")
    elif not (len(actions) == len(rewards) and len(observations) == len(actions) + 1):
        errors.append("observations must have one more item than actions and rewards")

    status_count = sum(bool(record[name]) for name in ("terminated", "truncated", "infra_error"))
    if status_count > 1:
        errors.append("at most one terminal status may be true")
    if status_count == 0 and record["stop_reason"] is not None:
        errors.append("stop_reason requires a stopped rollout")
    if status_count == 1 and not isinstance(record["stop_reason"], str):
        errors.append("a stopped rollout must have a string stop_reason")
    return errors

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

Мини-среда «ключ и дверь»

Реализуйте детерминированный исполнитель:

Python
def run_keydoor_episode(
    actions: list[str],
    *,
    key_color: str,
    door_color: str,
    max_steps: int = 6,
) -> dict:
    """Выполните действия и верните итоговый отчёт."""

Допустимые действия:

text
inspect
pickup_red
pickup_blue
open_red
open_blue
finish

Правила:

  • ключ можно подобрать только после inspect;
  • правильный ключ имеет цвет key_color;
  • дверь открывается только соответствующим переносимым ключом;
  • успех фиксируется только после finish, если открыта дверь цвета door_color;
  • некорректное действие увеличивает invalid_actions, но эпизод продолжается;
  • после max_steps без завершения выставляется truncated=True;
  • успешный эпизод получает reward=1.0, задачный провал после finish — 0.0, усечение — 0.0 с отдельным флагом.

Возвращаемый словарь должен содержать:

text
reward, success, terminated, truncated,
steps, invalid_actions, carrying, opened

Проверьте как минимум успешную последовательность, ранний finish, неверный ключ и усечение.

Открытые проверки:

Python
good = run_keydoor_episode(["inspect", "pickup_red", "open_red", "finish"], key_color="red", door_color="red")
assert good["success"] is True and good["reward"] == 1.0
assert good["invalid_actions"] == 0 and good["terminated"] is True
bad = run_keydoor_episode(["pickup_red", "open_red"], key_color="red", door_color="red")
assert bad["success"] is False and bad["invalid_actions"] == 2
assert bad["carrying"] is None
Показать ответ и решение
Python
def run_keydoor_episode(
    actions: list[str],
    *,
    key_color: str,
    door_color: str,
    max_steps: int = 6,
) -> dict:
    colors = {"red", "blue"}
    if key_color not in colors or door_color not in colors:
        raise ValueError("unknown color")
    if isinstance(max_steps, bool) or not isinstance(max_steps, int) or max_steps < 1:
        raise ValueError("max_steps must be positive")

    inspected = False
    carrying = None
    opened: set[str] = set()
    invalid = 0
    terminated = False
    success = False
    steps = 0

    for action in actions[:max_steps]:
        steps += 1
        valid = True
        if action == "inspect":
            inspected = True
        elif action in {"pickup_red", "pickup_blue"}:
            color = action.removeprefix("pickup_")
            if not inspected or color != key_color:
                valid = False
            else:
                carrying = color
        elif action in {"open_red", "open_blue"}:
            color = action.removeprefix("open_")
            if carrying != color:
                valid = False
            else:
                opened.add(color)
        elif action == "finish":
            terminated = True
            success = door_color in opened
        else:
            valid = False

        if not valid:
            invalid += 1
        if terminated:
            break

    truncated = not terminated and steps >= max_steps
    return {
        "reward": 1.0 if success else 0.0,
        "success": success,
        "terminated": terminated,
        "truncated": truncated,
        "steps": steps,
        "invalid_actions": invalid,
        "carrying": carrying,
        "opened": sorted(opened),
    }

Минимальные тесты:

Python
assert run_keydoor_episode(
    ["inspect", "pickup_red", "open_red", "finish"],
    key_color="red", door_color="red",
)["success"]

assert run_keydoor_episode(
    ["finish"], key_color="red", door_color="red",
)["terminated"]

assert run_keydoor_episode(
    ["inspect", "pickup_blue", "finish"],
    key_color="red", door_color="red",
)["invalid_actions"] == 1

assert run_keydoor_episode(
    ["inspect"] * 3,
    key_color="blue", door_color="blue", max_steps=3,
)["truncated"]