Решения на основе схемы
Превратите 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. Ключ поля просто отсутствует в значениях, когда для него нет ответа, — именно это делает безопасным объявление поля необязательным без изменения того, что видит модель.
См. также
- Hooks предсказания: наблюдайте, формируйте, кэшируйте или ограничивайте (gate) решения, которые это производит.
- Примитивы решений:
choice,scoreиnoulподробно. - LangChain и LangGraph:
LayaDecision— это данный вызов как runnable в цепочке.