Decisiones basadas en el esquema
Convierte un esquema JSON, o un modelo pydantic, en preguntas de Laya, y recupera valores tipados con confianza calibrada. Este es el puente que convierte a Laya en un motor de salida estructurada: describes la forma que quieres, y Laya la responde en una sola pasada hacia adelante.
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}
Con pydantic (instala 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)
El subconjunto admitido
El nivel superior debe ser un objeto con properties. Cada propiedad se convierte en una pregunta.
Cada fila de abajo es un esquema real: tests/test_structured_docs.py compila la primera columna y
verifica la pregunta que el compilador produce realmente, así que esta tabla no puede desviarse del
código. Una celda es o bien un esquema de propiedad por sí solo, o bien una llamada a un punto de entrada.
| Esquema JSON | Pregunta de Laya | Valor devuelto |
|---|---|---|
{"enum": ["billing", "support"]} |
choice |
el valor elegido, con su tipo original |
{"const": "billing"} |
choice |
ese único valor |
{"type": "boolean"} |
noul |
true / false |
{"type": "integer", "minimum": 0, "maximum": 5} |
score |
el nivel de mayor probabilidad, como entero |
{"type": "number", "minimum": 0, "maximum": 5} |
score |
el nivel, como entero |
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} |
choice |
How urgent? es el enunciado de la pregunta |
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} |
choice |
igual que la fila de enum simple; si no hay respuesta, la clave se omite |
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} |
noul |
igual que la fila de boolean simple |
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} |
score |
igual que la fila de entero acotado |
Literal[...] y Optional[...] son las grafías de pydantic para las filas de enum y anyOf:
questions_from_pydantic las convierte a esas formas y se aplican las mismas filas.
title no se lee. pydantic pone uno en cada campo de model_json_schema() lo hayas pedido o no, y
un nombre por propiedad no puede etiquetar las opciones por alternativa a partir de las que se construye
una pregunta, así que la palanca del enunciado es description — consulta Cómo se asigna internamente
más abajo.
La proyección es exacta: un enum: [1, 2, 3] devuelve 2, no "2"; un entero acotado devuelve un
nivel entre minimum y maximum; un booleano es noul >= 0.5.
Rechazos
Un esquema que no se puede responder desde un conjunto fijo de opciones lanza laya.structured.SchemaError
(un ValueError) indicando la ruta exacta. Cada fila también se ejecuta, con el campo llamado name:
| esquema de propiedad | mensaje |
|---|---|
{"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 |
Los propios puntos de entrada rechazan esto:
| llamada | mensaje |
|---|---|
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 |
Límites: MAX_PROPERTIES = 32, MAX_OPTIONS = 32, MAX_SCORE_LEVELS = 10.
La API
| función | propósito |
|---|---|
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) |
la función libre, sirve para Agent y Router |
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) |
métodos de conveniencia |
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) |
lo mismo sobre muchos estados, en una sola llamada por lotes |
questions_from_json_schema(schema) |
de esquema a preguntas de Laya |
questions_from_pydantic(model) |
de modelo pydantic a preguntas (requiere pydantic) |
answers_to_json(answers, schema) |
proyecta las respuestas en bruto sobre los valores del esquema |
answer_to_pydantic(model, answers) |
proyecta las respuestas en bruto en una instancia de pydantic |
plan_from_json_schema(schema) |
el plan de campos validado (avanzado) |
Pasa exactamente uno de schema o questions. Con questions, decide devuelve las respuestas en bruto
en lugar de proyectarlas. Los argumentos de palabra clave extra se reenvían a predict, así que los hooks,
model=, task= y el presupuesto de tokens funcionan:
router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])
Puntuar muchos estados
decide_batch es la forma de mayor rendimiento: el esquema se planifica una vez y sus preguntas se
ejecutan sobre cada estado a través de predict_batch, de modo que los estados comparten pasadas hacia
adelante en lugar de una por llamada. Los resultados vuelven en el orden de entrada, proyectados
exactamente como los proyecta decide, y return_details=True da un DecisionResult por estado:
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)
En un Router cada estado sigue enrutándose por su cuenta, así que una llamada puede abarcar varios
checkpoints. Los argumentos de palabra clave llegan a predict_batch, así que batch_size=, model= y
los hooks funcionan como con decide. Lo tienen Agent, ONNXAgent y Router; un runner sin
predict_batch lanza TypeError en lugar de recaer silenciosamente en un bucle — llama a decide por
estado en ese caso. El procesamiento por lotes puede desplazar argmax limítrofes igual que lo hace
predict_batch; el README registra las aceleraciones medidas para ambas unidades.
Confianza y probabilidades
Por defecto, decide devuelve solo los valores. Pasa return_details=True para obtener un DecisionResult
con la confianza por campo, las probabilidades, las respuestas en bruto y el uso y el enrutamiento de la llamada:
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 es el bloque que construyó predict(), reenviado entero: input_tokens suma el estado a lo largo de una fila por pregunta y output_tokens es siempre 0 porque no se genera nada, mientras que las otras cuatro claves son el informe de truncamiento (#174): state_tokens es lo que necesita todo el estado serializado, state_tokens_dropped lo máximo que cedió el head de cualquier pregunta individual, y truncated / truncated_questions dicen cuál. Una séptima clave, options, está presente solo cuando el presupuesto del head dejó que las opciones de alguna pregunta compartieran un tramo de tokens (#538); docs/http-api.md documenta tanto este bloque como ese campo como claves de respuesta, y tests/test_structured_docs.py ata la lista de esta página al código que la construye.
confidence y answer_confidence son cantidades distintas, y los nombres siguen las definiciones.
answer_confidence es max(p), la masa de probabilidad sobre la respuesta que se informa. Es lo que
ajusta el escalado por temperatura, sobre lo que se calcula cada cifra de calibración de este repositorio,
y contra lo que se compara min_confidence — que es la razón para aplicar gating sobre ella en lugar de
sobre confidence. confidence es la entropía normalizada, que depende de cuántas opciones tenía la
pregunta: tests/test_confidence.py fija que una distribución de dos opciones vuelve como 0.90 en un
noul y 0.53 en un choice equivalente, así que no se compara contra un umbral. Un campo que no informó
ningún answer_confidence utilizable se asigna a None, que no es lo mismo que un 0.0 informado.
Aplica el gating sobre la misma cantidad que usa la puerta:
if result.answer_confidence["department"] < 0.6:
result.values["department"] = "human-review"
Que answer_confidence sea el número correcto sobre el que filtrar no es lo mismo que sea una
probabilidad fiable. Leerlo como “aproximadamente c de las respuestas devueltas con c son correctas”
solo se sostiene después de que las temperaturas se hayan ajustado y validado sobre datos reservados
para ese checkpoint y esa forma de pregunta. Los checkpoints publicados son demasiado confiados tal
como se distribuyen y laya-multilingual no trae ninguna temperatura ajustada — consulta las secciones
Calibración y
Límites honestos del README, y el
notebook de ajuste fino
para el bucle de ajuste. Ajusta antes de confiar en el nivel; infórmalo porque es la cantidad que usan
tanto la puerta como el harness de evaluación.
Cómo se asigna internamente
- Los valores Enum y
Literalse convierten en preguntaschoicecon los valores como etiquetas de texto; la etiqueta se reasigna al valor original a la salida, así que los enteros siguen siendo enteros. - Un entero acotado se convierte en una pregunta
scorecon un nivel por valor; el valor devuelto esminimum + argmax. - Un booleano se convierte en una pregunta
noul; el valor esnoul >= 0.5. - Una
descriptionse convierte en las instrucciones de la pregunta, así que una buena descripción es lo que hace precisa la decisión. Esto sigue la misma regla que la guía de hooks: sé explícito sobre lo que significa cada opción. - Una rama
nullse descarta antes de planificar el campo, así queOptional[X]formula exactamente la pregunta que formulaX. La clave del campo simplemente está ausente de los valores cuando no hay respuesta para él, que es lo que hace seguro declarar un campo opcional sin cambiar lo que ve el modelo.
Véase también
- Hooks de predicción: observa, da forma, cachea o condiciona las decisiones que esto produce.
- Primitivas de decisión:
choice,scoreynoulen profundidad. - LangChain y LangGraph:
LayaDecisiones esta llamada como un ejecutable en una cadena.