Документация

Решения на основе схемы

Превратите JSON-схему или модель pydantic в вопросы Laya и получите обратно типизированные значения с калиброванной уверенностью. Это мост, который делает Laya движком структурированного вывода: вы описываете нужную форму, а Laya отвечает на неё за один прямой проход.

import laya

schema = {
    "type": "object",
    "properties": {
        "department": {"type": "string", "enum": ["billing", "support", "sales"],
                       "description": "Which team should handle this?"},
        "urgency": {"type": "integer", "minimum": 0, "maximum": 2},
        "needs_human": {"type": "boolean"},
    },
}

agent = laya.load("convaiinnovations/laya")
values = agent.decide("I was charged twice, refund me.", schema=schema)
# {"department": "billing", "urgency": 2, "needs_human": True}

С pydantic (установите laya[structured]):

from typing import Literal
from pydantic import BaseModel

class Ticket(BaseModel):
    department: Literal["billing", "support", "sales"]
    urgency: Literal[0, 1, 2]
    needs_human: bool

ticket = agent.decide("I was charged twice, refund me.", schema=Ticket)

Поддерживаемое подмножество

Верхний уровень должен быть объектом с properties. Каждое свойство становится одним вопросом.

Каждая строка ниже — настоящая схема: tests/test_structured_docs.py компилирует первый столбец и проверяет вопрос, который компилятор действительно производит, поэтому эта таблица не может разойтись с кодом. Ячейка — это либо схема свойства сама по себе, либо вызов точки входа.

JSON-схема Вопрос Laya Возвращаемое значение
{"enum": ["billing", "support"]} choice выбранное значение с его исходным типом
{"const": "billing"} choice это единственное значение
{"type": "boolean"} noul true / false
{"type": "integer", "minimum": 0, "maximum": 5} score уровень с наибольшей вероятностью, как целое число
{"type": "number", "minimum": 0, "maximum": 5} score уровень, как целое число
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} choice How urgent? — это формулировка вопроса
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} choice как в строке с простым enum; при отсутствии ответа ключ опускается
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} noul как в строке с простым boolean
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} score как в строке с простым ограниченным целым

Literal[...] и Optional[...] — это то, как pydantic записывает строки enum и anyOf: questions_from_pydantic приводит их к этим формам, и применяются те же строки.

title не читается. pydantic ставит его на каждое поле model_json_schema(), хотите вы того или нет, а имя свойства не может разметить варианты выбора, из которых строится вопрос, поэтому рычаг для формулировки — description; см. Как это устроено внутри ниже.

Проекция точна: enum: [1, 2, 3] возвращает 2, а не "2"; ограниченное целое возвращает уровень между minimum и maximum; булево значение — это noul >= 0.5.

Отклонения

Схема, на которую нельзя ответить из фиксированного набора вариантов, вызывает laya.structured.SchemaError (это ValueError) с указанием точного пути. Каждая строка тоже исполняется, с полем с именем name:

схема свойства сообщение
{"type": "string"} properties.name: a free string cannot be a fixed option set; use 'enum' or a boolean
{"type": "array", "items": {"type": "string"}} properties.name: arrays are not supported; ask one field per element
{"type": "object", "properties": {"inner": {"type": "boolean"}}} properties.name: nested objects are not supported; flatten the schema
{"$ref": "#/definitions/node"} properties.name: $ref/recursion is not supported; flatten the schema
{"enum": [1, "1"]} properties.name: enum values produce duplicate choice labels
{"enum": []} properties.name: 'enum' must not be empty
{"type": "number"} properties.name: a numeric field needs integer 'minimum' and 'maximum' to become a score
{"type": "integer", "minimum": 5, "maximum": 2} properties.name: 'maximum' 2 is below 'minimum' 5
{"type": "integer", "minimum": 0, "maximum": 10} properties.name: 11 levels exceeds MAX_SCORE_LEVELS=10; narrow the range or use an enum
{"anyOf": [{"type": "string"}, {"type": "integer"}]} properties.name: only 'Optional[...]' unions (one non-null branch) are supported, got 2
{"type": ["string", "integer"]} properties.name: 'type' has multiple non-null types; unions are not supported
{"format": "date"} properties.name: unsupported schema {'format': 'date'}
"boolean" properties.name: property must be an object, got str

Сами точки входа отклоняют следующее:

вызов сообщение
plan_from_json_schema("not a schema") expected a JSON schema object, got str
plan_from_json_schema({"type": "object"}) the top level must be an object with 'properties'
plan_from_json_schema({"type": "object", "properties": {}}) 'properties' must be a non-empty object
plan_from_json_schema({"type": "object", "properties": {"p%d" % i: {"type": "boolean"} for i in range(33)}}) 33 properties exceeds MAX_PROPERTIES=32
plan_from_json_schema({"type": "object", "properties": {"name": {"enum": ["v%d" % i for i in range(33)]}}}) properties.name: 33 options exceeds MAX_OPTIONS=32
decide(None, "I was charged twice.", schema=42) expected a JSON schema dict or a pydantic model, got int

Ограничения: MAX_PROPERTIES = 32, MAX_OPTIONS = 32, MAX_SCORE_LEVELS = 10.

API

функция назначение
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) свободная функция, работает для Agent и Router
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) удобные методы
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) то же самое для множества состояний, один пакетный вызов
questions_from_json_schema(schema) из схемы в вопросы Laya
questions_from_pydantic(model) из модели pydantic в вопросы (требует pydantic)
answers_to_json(answers, schema) проецирует сырые ответы на значения схемы
answer_to_pydantic(model, answers) проецирует сырые ответы в экземпляр pydantic
plan_from_json_schema(schema) проверенный план полей (продвинутый)

Передайте ровно одно из schema или questions. С questions decide возвращает сырые ответы вместо проецирования. Дополнительные именованные аргументы перенаправляются в predict, поэтому hooks, model=, task= и бюджет токенов работают:

router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])

Оценка множества состояний

decide_batch — форма для пропускной способности: схема планируется один раз, и её вопросы прогоняются по каждому состоянию через predict_batch, поэтому состояния разделяют прямые проходы, а не по одному на вызов. Результаты возвращаются в порядке входных данных, спроецированные точно так же, как проецирует decide, а return_details=True даёт по одному DecisionResult на состояние:

values = agent.decide_batch(ticket_texts, schema=Ticket)          # values[i] matches ticket_texts[i]
results = router.decide_batch(states, schema=Ticket, return_details=True, batch_size=64)

На Router каждое состояние по-прежнему маршрутизируется отдельно, поэтому один вызов может охватывать несколько чекпойнтов. Именованные аргументы доходят до predict_batch, поэтому batch_size=, model= и hooks работают так же, как для decide. Он есть у Agent, ONNXAgent и Router; runner без predict_batch вызывает TypeError, а не молча откатывается к циклу, — в этом случае вызывайте decide для каждого состояния. Пакетная обработка может смещать пограничные argmax так же, как это делает predict_batch; README фиксирует измеренные ускорения для обоих устройств.

Уверенность и вероятности

По умолчанию decide возвращает только значения. Передайте return_details=True, чтобы получить DecisionResult с уверенностью по каждому полю, вероятностями, сырыми ответами, а также использованием и маршрутизацией вызова:

result = agent.decide(state, schema=Ticket, return_details=True)
result.values["department"]            # "billing"
result.answer_confidence["department"] # 0.94  max(p): the quantity min_confidence gates on
result.confidence["department"]        # 0.71  normalized entropy, which depends on label count
result.probabilities["department"]     # {"billing": 0.94, "support": 0.06, "sales": 0.0}
result.usage                           # {"input_tokens": ..., "output_tokens": 0,
                                       #  "state_tokens": ..., "state_tokens_dropped": ...,
                                       #  "truncated": ..., "truncated_questions": [...]}
result.routing                         # the Router decision, when a Router answered

usage — это блок, который построил predict(), переданный целиком: input_tokens суммирует состояние по одной строке на вопрос, а output_tokens всегда равен 0, потому что ничего не генерируется, тогда как остальные четыре ключа — это отчёт об усечении (#174): state_tokens — это то, что нужно всему сериализованному состоянию, state_tokens_dropped — наибольшая часть, от которой отказался head любого отдельного вопроса, а truncated / truncated_questions говорят, какого именно. Седьмой ключ, options, присутствует только тогда, когда бюджет head позволил вариантам какого-либо вопроса разделить один диапазон токенов (#538); docs/http-api.md документирует и этот блок, и это поле как ключи ответа, а tests/test_structured_docs.py привязывает список этой страницы к коду, который его строит.

confidence и answer_confidence — разные величины, и имена следуют определениям. answer_confidence — это max(p), масса вероятности на сообщаемом ответе. Именно её подгоняет температурное масштабирование, именно на ней вычисляется каждая цифра калибровки в этом репозитории, и именно с ней сравнивается min_confidence — что и есть причина применять gating по ней, а не по confidence. confidence — это нормализованная энтропия, которая зависит от того, сколько вариантов было у вопроса: tests/test_confidence.py фиксирует, что распределение из двух вариантов возвращается как 0.90 на noul и 0.53 на эквивалентном choice, поэтому его не сравнивают с порогом. Поле, которое не сообщило пригодную answer_confidence, отображается в None, и это не то же самое, что сообщённый 0.0.

Опирайтесь на ту же величину, что и сам порог:

if result.answer_confidence["department"] < 0.6:
    result.values["department"] = "human-review"

То, что answer_confidence — правильное число, по которому стоит фильтровать, не значит, что это заслуживающая доверия вероятность. Читать её как «примерно c из ответов, возвращённых при c, верны» можно только после того, как температуры подогнаны и проверены на отложенных данных для этого чекпойнта и формы вопроса. Поставляемые чекпойнты в исходном виде слишком самоуверенны, а laya-multilingual вообще поставляется без подогнанных температур — см. разделы README Калибровка и Честные ограничения, а также блокнот дообучения для цикла подгонки. Подгоняйте, прежде чем полагаться на уровень; сообщайте его, потому что именно эту величину используют и порог, и harness оценки.

Как это устроено внутри

  • Значения Enum и Literal становятся вопросами choice со значениями в роли строковых меток; при выводе метка отображается обратно в исходное значение, поэтому целые числа остаются целыми.
  • Ограниченное целое становится вопросом score с одним уровнем на каждое значение; возвращаемое значение — minimum + argmax.
  • Булево значение становится вопросом noul; значение — noul >= 0.5.
  • description становится инструкциями вопроса, поэтому именно хорошее описание делает решение точным. Это следует тому же правилу, что и руководство по hooks: будьте явны в том, что означает каждый вариант.
  • Ветвь null отбрасывается до планирования поля, поэтому Optional[X] задаёт ровно тот вопрос, что и X. Ключ поля просто отсутствует в значениях, когда для него нет ответа, — именно это делает безопасным объявление поля необязательным без изменения того, что видит модель.

См. также