ДокументацияПрактические руководства (Cookbooks)Самосогласованность: Choices

Самосогласованность: Choices

Add an uncertain outcome to moderation decisions and compare label agreement with the share of automatic actions.

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

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

Рубрика состоит из 8 вопросов Choice, и каждый запуск представляет собой один вызов, отвечающий на все 8 вопросов. Мы делаем 15 повторений для каждого условия, где условие — это одна модель плюс одна настройка, и отображаем на графике каждую возвращенную метку.

Условия:

  • LLM без блока рассуждений (non-reasoning) claude-haiku-4-5 и gpt-5.4-mini при температуре 0 и значении API по умолчанию.
  • Модели с рассуждениями (reasoning) gpt-5.5 и claude-opus-4-8, у которых нет регулятора температуры.
  • TypeSafe: один вызов system_one для 8 вопросов Choice с новым полем uid (одноразовым уникальным значением) при каждом вызове, аналогично схеме из руководства по noul.

На что обратить внимание: выбранные метки могут меняться внутри одного и того же условия, включая TypeSafe, а условия расходятся во мнениях друг с другом.

В этом запуске режимы распределения вероятностей LLM повторяют свои мажоритарные метки в 87.5%–100% случаев по сравнению с 90.8% у TypeSafe. TypeSafe демонстрирует более низкую среднюю вариативность вероятностей, чем пять из шести вероятностных условий LLM; только Haiku при температуре 0 варьируется меньше. Близкие вероятности все же допускают изменение маршрутизации: TypeSafe меняет решение на 2 из 8 вопросов.

Для принятия решений на уровне приложения мы также требуем, чтобы максимальная вероятность составляла не менее 0.60; в противном случае результат считается uncertain (неопределенным) и отправляется на проверку человеку. Согласованность TypeSafe при этом возрастает до 99.2% при доле автоматических меток 74.2% ответов. Мы показываем исходные результаты и применяем тот же порог к вероятностным режимам LLM, сохраняя наглядность отказов от решения и изменений.

Настройка

BASH THEME={NULL} api.wedstack.ru/v1
pip install anthropic openai matplotlib ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

затем задайте TYPESAFE_API_KEY, ANTHROPIC_API_KEY и OPENAI_API_KEY.
В этом запуске используется jev-latest на продакшн-API, выборка получена 11.09.2026.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Состояние: пограничный пост пользователя в формате JSON

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

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

LLM получают json.dumps(POST) в промпте. TypeSafe получает словарь Python напрямую.

PYTHON THEME={NULL} api.wedstack.ru/v1
POST = {
    "post_id": "P-88213",
    "author": {
        "user_id": "u/4471",
        "account_age_days": 38,
        "prior_strikes": 1,
        "followers": 210,
    },
    "context": {
        "surface": "public reply",
        "in_reply_to": "another user defending a game patch",
        "community": "r/gamedebates",
    },
    "content": {
        "text": (
            "Are you seriously this dense? Anyone who defends that patch is a complete clown and "
            "should be embarrassed to even post here. People like you are what's ruining this "
            "community and honestly you need to be dealt with. Come say it to my face, invite's "
            "right here. Keep it up and I'll end your whole channel."
        ),
        "has_link": True,
        "link_domain": "discord.gg",
        "language": "en",
    },
    "reports": {"user_reports": 4, "report_reasons": ["harassment", "spam", "threat"]},
}

Рубрика: 8 вопросов Choice

У каждого вопроса есть ключ (key), строка инструкций и фиксированный набор меток. Метки внутри одного вопроса являются взаимоисключающими (применима ровно одна), и у каждой есть краткое описание. TypeSafe возвращает выбранный вариант (choice) вместе с распределением вероятностей (probabilities) по всем меткам. От LLM требуется использовать те же наборы меток, что обеспечивает сопоставимость всех строк.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Как отправляются запросы

Каждый вызов LLM представляет собой один промпт, содержащий json.dumps(POST), все 8 вопросов и все допустимые метки. Предусмотрено два формата ответов. В режиме распределения модель возвращает один JSON-объект на вопрос с вероятностью для каждой метки. В режиме единичного выбора она возвращает только одну метку на вопрос, и наш анализ переносит всю вероятностную массу на эту метку.

Вызов TypeSafe — это один запрос system_one по тому же посту и тем же 8 вопросам Choice, возвращающий одно распределение на вопрос.

Каждый запрос также получает новый uid — одноразовое уникальное значение, которое изменяется при каждом запуске, оставляя пост и рубрику неизменными. Оно передается в промпт LLM и как дополнительное поле в состояние TypeSafe. Данная схема не разделяет чувствительность к нерелевантному полю и вариативность, которая возникла бы при идентичных запросах.

Каждая вспомогательная функция возвращает ответ, оценочную стоимость и задержку туда и обратно (round-trip latency).

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Экспериментальные условия

Сетка эксперимента

Группа моделей Модель Распределение (t=0) Распределение (по умолчанию) Единичный выбор (t=0)
Модели без рассуждений claude-haiku-4-5
Модели без рассуждений gpt-5.4-mini
Модели с рассуждениями gpt-5.5
Модели с рассуждениями claude-opus-4-8
TypeSafe jev-latest (typesafe_choice)
  • отмечает условие, протестированное с 15 повторениями; отмечает непротестированную комбинацию.
  • В столбце по умолчанию аргумент температуры не передается: модели без рассуждений используют значение API по умолчанию, а модели с рассуждениями и TypeSafe работают без настройки температуры.
  • Условия с единичным выбором возвращают одну метку на вопрос.
  • Температура 0 обычно рекомендуется для воспроизводимости, поэтому она сравнивается со значением API по умолчанию.

Мы делаем NUM_SAMPLES = 15 повторений на каждое условие. У каждого повторения свой ключ кэша, и оно считается отдельной выборкой; кэш (json_cache.json) поставляется вместе с руководством, поэтому повторный рендеринг использует его и не тратит вызовы API. Удалите кэш, чтобы заново собрать данные в реальном времени.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
TypeSafe requested model: jev-latest
TypeSafe returned models (calls): {'jev-1.13.0': 15}

Стоимость + скорость (на один запрос к рубрике)

Стоимость ниже рассчитана на основе исторических предположений о ценах из раздела «Настройка», включая тариф speed_latest для TypeSafe. Они не являются проверенными ценами jev-latest или текущими тарифами в биллинге.

Одна строка соответствует одному полному вызову рубрики из 8 вопросов. В столбцах time/call и cost/call усреднены показатели 15 вызовов, а столбцы vs ts_choice показывают отношение к показателям TypeSafe. LLM выполняются в пуле из 16 потоков.

PYTHON THEME={NULL} api.wedstack.ru/v1
typesafe_cost = mean([cost for cost, _latency in stats["typesafe_choice"]])
typesafe_latency = mean([latency for _cost, latency in stats["typesafe_choice"]])
name_w = max(len(name) for name in ALL_LABELS) + 2  # fit the longest condition label
# Stack comparison headers so the relative speed and cost columns can stay narrow.
print(
    f"{'':<{name_w + 31}}{'speed vs':>11}{'cost vs':>11}\n"
    f"{'condition':<{name_w}}{'calls':>7}{'time/call':>11}{'cost/call':>13}"
    f"{'ts_choice':>11}{'ts_choice':>11}"
)
for name in ALL_LABELS:
    costs, latencies = zip(*stats[name])
    cost = mean(costs)
    latency = mean(latencies)
    print(
        f"{name:<{name_w}}{len(costs):>7}{latency * 1000:>9.0f}ms"
        f"{'$' + format(cost, '.6f'):>13}"
        f"{format(latency / typesafe_latency, '.1f') + 'x':>11}"
        f"{format(cost / typesafe_cost, '.1f') + 'x':>11}"
    )
PLAINTEXT api.wedstack.ru/v1
speed vs    cost vs
condition                           calls  time/call    cost/call  ts_choice  ts_choice
claude-haiku-4-5 t=0                   15     3853ms    $0.003498      33.8x      76.1x
claude-haiku-4-5 t=default             15     3860ms    $0.003494      33.8x      76.0x
claude-haiku-4-5 single-pick t=0       15      992ms    $0.001527       8.7x      33.2x
gpt-5.4-mini t=0                       15     2293ms    $0.002299      20.1x      50.0x
gpt-5.4-mini t=default                 15     1986ms    $0.002164      17.4x      47.1x
gpt-5.4-mini single-pick t=0           15      826ms    $0.000936       7.2x      20.3x
gpt-5.5-reasoning                      15    12978ms    $0.041255     113.7x     897.4x
claude-opus-4-8-reasoning              15    10376ms    $0.028375      90.9x     617.2x
typesafe_choice                        15      114ms    $0.000046       1.0x       1.0x

В этом запуске средняя задержка (round-trip) typesafe_choice составила 114 мс. Условия LLM варьируются от 826 мс до 13.0 секунд на вызов при указанных выше настройках параллелизма.

График: решение каждой выборки в виде тепловой карты

Как читать график:

  • Внешняя группа строк: вопрос.
  • Внутренняя строка: условие.
  • Столбец: один полный вызов рубрики.
  • Текст ячейки: решение приложения плюс вероятность наилучшей метки.
  • Цвет ячейки: позиция метки внутри этого вопроса, поэтому одинаковый цвет по всей строке означает одинаковое решение каждый раз.
  • Серый uncertain: максимальная вероятность ниже 0.60, поэтому случай направляется человеку на проверку.
  • Штриховка n/a: ответ не удалось распарсить в пригодные метки (ошибка парсинга).
  • Пустые строки — разделители.

Условия с единичным выбором сохраняют возвращенные метки: они не дают оценки неопределенности.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
output

Более однозначные вопросы показывают стабильные результаты: target единогласно определяется как Person, а severity — как High. Пограничные вопросы вызывают расхождения между условиями: category, primary_risk, action, review_path и link_handling. Некоторые условия также меняют решение внутри собственных 15 повторений. До применения правила отказа TypeSafe меняет лидирующую метку в вопросах primary_risk (Harassment 11 раз, Violence 4 раза) и link_handling (RmLink 8 раз, Brigade 7 раз). Теперь в обеих строках везде отображается uncertain, так как их наивысшая вероятность ниже 0.60.

Стандартное отклонение вероятностей

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

В таблице сравнивается каждое вероятностное условие LLM с TypeSafe. Строки с единичным выбором исключены, так как они возвращают жесткие метки, а не распределения вероятностей.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
condition                           mean prob std  max prob std  parse fail  x TypeSafe
claude-haiku-4-5 t=0                       0.0012        0.0221         0%        0.12x
claude-haiku-4-5 t=default                 0.0516        0.3150         1%        5.29x
gpt-5.4-mini t=0                           0.0312        0.0905         0%        3.20x
gpt-5.4-mini t=default                     0.0543        0.2303         0%        5.56x
gpt-5.5-reasoning                          0.0305        0.1047         0%        3.12x
claude-opus-4-8-reasoning                  0.0245        0.0693         0%        2.52x
typesafe_choice                            0.0098        0.0515         0%        1.00x

В этом запуске TypeSafe имеет среднее стандартное отклонение вероятностей 0.0098 и максимальное стандартное отклонение для одной метки 0.0515. У Haiku при температуре 0 среднее стандартное отклонение ниже — 0.0012. Остальные пять вероятностных условий LLM находятся в диапазоне от 0.0245 до 0.0543, что примерно в 2.5x5.6x превышает среднее значение TypeSafe. Небольшие колебания все же могут менять лидирующую метку, если две метки близки по вероятности.

График: согласованность решений при наличии статуса неопределенности

Возвращайте uncertain, когда наивысшая вероятность ниже 0.60. Для каждого вероятностного условия и вопроса подсчитайте наиболее частое решение приложения, включая uncertain, и разделите на все 15 запусков. Ошибки парсинга снижают согласованность. Каждый столбец отображает средний балл по всем 8 вопросам; условия отсортированы по убыванию согласованности.

Условия LLM с единичным выбором исключены, поскольку они не предоставляют оценки неопределенности.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
output

При том же правиле 0.60 Haiku при температуре 0 набрала 100%. TypeSafe показал 99.2%, а другие условия LLM расположились в интервале от 84.2% до 94.2%. TypeSafe вернул uncertain в 25.8% ответов и принял автоматическое решение в остальных 74.2%; Haiku при температуре 0 ни разу не отказалась от выбора. Эти проценты отражают исключительно воспроизводимость. В таблице ниже исходная согласованность и доли отказов сопоставлены с согласованностью политики из этого графика.

Допуск неоднозначных вероятностей к формированию статуса неопределенности

Небольшое изменение вероятности может поменять местами две близкие метки. Приложению не обязательно принимать действие на основе победителя: возвращайте uncertain, если максимальная вероятность ниже 0.60, и отправляйте такой случай человеку. Ровно при 0.60 выбирайте лидирующую метку. В этом подходе используются возвращенные вероятности, а не отдельное поле API confidence, и не требуется дополнительных вызовов модели.

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

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

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
condition                            raw agree  policy agree   uncertain   automatic  conflicts
claude-haiku-4-5 t=0                   100.0%       100.0%       0.0%     100.0%          0
claude-haiku-4-5 t=default              87.5%        86.7%       0.8%      98.3%          2
gpt-5.4-mini t=0                        99.2%        87.5%      12.5%      87.5%          0
gpt-5.4-mini t=default                  90.8%        84.2%      22.5%      77.5%          2
gpt-5.5-reasoning                       90.0%        93.3%      30.8%      69.2%          1
claude-opus-4-8-reasoning               92.5%        94.2%      33.3%      66.7%          0
typesafe_choice                         90.8%        99.2%      25.8%      74.2%          0

Метрика policy agree учитывает uncertain как решение; ошибки парсинга снижают показатель согласованности. automatic — это доля всех ответов, в которых выбрана конкретная метка. conflicts подсчитывает количество вопросов, получивших более одной конкретной метки по всем повторениям, без учета отказов от решения. Эти показатели описывают воспроизводимость и частоту действий приложения, а не правильность его действий.

Согласованность TypeSafe выросла с 90.8% до 99.2%. Из всех ответов 25.8% составили отказы (uncertain), а 74.2% — автоматические решения. Вопросы primary_risk и link_handling возвращали неопределенность при каждом повторении; category чередовалась между Violence и uncertain, пересекая порог принятия решения в одних повторениях и не пересекая в других. Ни один вопрос не дал двух разных конкретных меток TypeSafe. Все это не доказывает точность или превосходство: Haiku при температуре 0 продемонстрировала здесь 100% согласованность без единого отказа от выбора.

PYTHON THEME={NULL} api.wedstack.ru/v1
# Show every TypeSafe decision while retaining the top probability behind it.
policy_decisions = decisions_by_condition[TYPESAFE_LABEL]
policy_values = []
for row, (_key, (_instructions, choices)) in zip(policy_decisions, QUESTIONS.items()):
    labels = list(choices)
    policy_values.append([
        10 if value == "uncertain" else labels.index(value) if value is not None else np.nan
        for value in row
    ])
policy_cmap = ListedColormap([*plt.get_cmap("tab10").colors, "#dddddd"])
policy_cmap.set_bad("white")
fig_policy, ax_policy = plt.subplots(figsize=(13, 4))
ax_policy.imshow(policy_values, cmap=policy_cmap, vmin=0, vmax=10, aspect="auto")
for row_index, key in enumerate(QUESTIONS):
    for sample_index in range(NUM_SAMPLES):
        decision = policy_decisions[row_index][sample_index]
        probability = max(typesafe_runs[sample_index][key])
        ax_policy.text(sample_index, row_index, f"{decision or 'n/a'}\n{probability:.2f}",
                       ha="center", va="center", fontsize=6)
ax_policy.set_yticks(range(len(QUESTIONS)), list(QUESTIONS))
ax_policy.set_xticks(range(NUM_SAMPLES), range(1, NUM_SAMPLES + 1))
ax_policy.set_xlabel("rubric query")
ax_policy.set_title(
    "TypeSafe application decisions: gray means uncertain "
    f"(top probability < {MIN_CHOICE_PROBABILITY:.2f})"
)
fig_policy.tight_layout()
display(fig_policy)
output

Эта политика не делает модель детерминированной. Отказ от выбора может заменить конкурирующие метки одним и тем же исходом — отправкой человеку на проверку, однако вероятность в районе 0.60 все еще может переходить между конкретной меткой и uncertain. Статистика вероятностей и столбец raw agree в таблице по-прежнему отражают исходные результаты модели.

Открыть в песочнице TypeSafe

Ссылка ниже открывает тот же пост и рубрику в песочнице: один пост, те же 8 вопросов Choice и модель TypeSafe jev-latest.

PYTHON THEME={NULL} api.wedstack.ru/v1
playground_link = make_playground_link(
    {"post": POST},
    {
        key: Choice(instructions=instructions, criteria=choices)
        for key, (instructions, choices) in QUESTIONS.items()
    },
    models=[TYPESAFE_MODEL],
)
display(
    Markdown(
        f"🔗 [Open this post + rubric in the TypeSafe playground]({playground_link})"
    )
)

Открыть этот пост + рубрику в песочнице TypeSafe →