Decisões orientadas por esquema
Transforme um esquema JSON, ou um modelo pydantic, em perguntas do Laya, e receba de volta valores tipados com confiança calibrada. Esta é a ponte que faz do Laya um motor de saída estruturada: você descreve o formato que quer, e o Laya o responde em uma passada direta.
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}
Com pydantic (instale 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)
O subconjunto suportado
O nível de topo deve ser um objeto com properties. Cada propriedade se torna uma pergunta.
Toda linha abaixo é um esquema real: tests/test_structured_docs.py compila a primeira coluna e
verifica a pergunta que o compilador de fato produz, então esta tabela não consegue divergir do
código. Uma célula é ou um esquema de propriedade sozinho, ou uma chamada a um ponto de entrada.
| Esquema JSON | Pergunta do Laya | Valor retornado |
|---|---|---|
{"enum": ["billing", "support"]} |
choice |
o valor escolhido, com seu tipo original |
{"const": "billing"} |
choice |
aquele único valor |
{"type": "boolean"} |
noul |
true / false |
{"type": "integer", "minimum": 0, "maximum": 5} |
score |
o nível de maior probabilidade, como inteiro |
{"type": "number", "minimum": 0, "maximum": 5} |
score |
o nível, como inteiro |
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} |
choice |
How urgent? é o texto da pergunta |
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} |
choice |
como a linha de enum simples; uma resposta ausente deixa a chave de fora |
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} |
noul |
como a linha de boolean simples |
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} |
score |
como a linha de inteiro limitado simples |
Literal[...] e Optional[...] são as grafias pydantic das linhas enum e anyOf:
questions_from_pydantic as renderiza nesses formatos e as mesmas linhas se aplicam.
title não é lido. O pydantic coloca um em todo campo de model_json_schema(), você tendo
pedido ou não, e um nome por propriedade não consegue rotular as escolhas por opção das quais uma
pergunta é construída, então a alavanca de redação é description — veja Como ele mapeia
internamente abaixo.
A projeção é exata: um enum: [1, 2, 3] retorna 2, não "2"; um inteiro limitado retorna um nível
entre minimum e maximum; um booleano é noul >= 0.5.
Rejeições
Um esquema que não pode ser respondido a partir de um conjunto fixo de opções lança
laya.structured.SchemaError (um ValueError) nomeando o caminho exato. Cada linha também é
executada, com o campo chamado name:
| esquema de propriedade | mensagem |
|---|---|
{"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 |
Os próprios pontos de entrada rejeitam isto:
| chamada | mensagem |
|---|---|
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 |
Limites: MAX_PROPERTIES = 32, MAX_OPTIONS = 32, MAX_SCORE_LEVELS = 10.
A API
| função | propósito |
|---|---|
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) |
a função livre, funciona para Agent e Router |
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) |
métodos de conveniência |
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) |
o mesmo sobre muitos estados, uma chamada em lote |
questions_from_json_schema(schema) |
esquema para perguntas do Laya |
questions_from_pydantic(model) |
modelo pydantic para perguntas (exige pydantic) |
answers_to_json(answers, schema) |
projeta respostas cruas sobre os valores do esquema |
answer_to_pydantic(model, answers) |
projeta respostas cruas em uma instância pydantic |
plan_from_json_schema(schema) |
o plano de campo validado (avançado) |
Passe exatamente um entre schema ou questions. Com questions, decide retorna as respostas cruas
em vez de projetar. Argumentos de palavra-chave extras são repassados para predict, então hooks,
model=, task= e o orçamento de tokens todos funcionam:
router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])
Pontuar muitos estados
decide_batch é a forma de throughput: o esquema é planejado uma vez e suas perguntas rodam sobre
todo estado através de predict_batch, então os estados compartilham passadas diretas em vez de uma
por chamada. Os resultados voltam na ordem de entrada, projetados exatamente como decide projeta, e
return_details=True dá um 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)
Em um Router cada estado ainda é roteado por conta própria, então uma chamada pode abranger
checkpoints. Argumentos de palavra-chave chegam a predict_batch, então batch_size=, model= e
hooks funcionam como funcionam para decide. Agent, ONNXAgent e Router todos têm isso; um runner
sem predict_batch lança TypeError em vez de cair silenciosamente para um laço — chame decide por
estado ali. O batching pode deslocar argmaxes limítrofes da mesma forma que predict_batch; o README
registra as acelerações medidas para os dois dispositivos.
Confiança e probabilidades
Por padrão decide retorna apenas os valores. Passe return_details=True para obter um
DecisionResult com confiança por campo, probabilidades, as respostas cruas, e o uso e o roteamento da
chamada:
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 é o bloco que predict() montou, repassado inteiro: input_tokens soma o estado ao longo de uma linha por pergunta e output_tokens é sempre 0 porque nada é gerado, enquanto as outras quatro chaves são o relatório de truncamento (#174) — state_tokens é o que todo o estado serializado precisa, state_tokens_dropped a maior parte que o head de qualquer pergunta individual abriu mão, e truncated / truncated_questions dizem qual. Uma sétima chave, options, só está presente quando o orçamento do head deixou as opções de alguma pergunta compartilhando um trecho de tokens (#538); docs/http-api.md documenta tanto esse bloco quanto esse campo como chaves de resposta, e tests/test_structured_docs.py prende a lista desta página ao código que a monta.
confidence e answer_confidence são quantidades diferentes, e os nomes seguem as definições.
answer_confidence é max(p), a massa de probabilidade na resposta que está sendo reportada. É o que
o escalonamento por temperatura ajusta, sobre o que todo número de calibração deste repositório é
calculado, e contra o que min_confidence é comparado — que é a razão para aplicar gating sobre ele em
vez de sobre confidence. confidence é a entropia normalizada, que depende de quantas opções a
pergunta tinha: tests/test_confidence.py fixa que uma distribuição de duas opções volta como 0.90 em
um noul e 0.53 em um choice equivalente, então ela não se compara a um limiar. Um campo que não
reportou nenhuma answer_confidence utilizável mapeia para None, que não é o mesmo que um 0.0
reportado.
Aplique gating sobre a mesma quantidade que o gate usa:
if result.answer_confidence["department"] < 0.6:
result.values["department"] = "human-review"
answer_confidence ser o número certo para filtrar não é o mesmo que ele ser uma probabilidade
confiável. Lê-lo como “cerca de c das respostas retornadas em c estão corretas” só vale depois que as
temperaturas foram ajustadas e validadas em dados reservados para aquele checkpoint e formato de
pergunta. Os checkpoints distribuídos são excessivamente confiantes como distribuídos e o
laya-multilingual vem sem nenhuma temperatura ajustada — veja as seções
Calibration e
Honest limits do README, e o
notebook de ajuste fino
para o ciclo de ajuste. Ajuste antes de confiar no nível; reporte-o porque é a quantidade que tanto o
gate quanto o harness de avaliação usam.
Como ele mapeia internamente
- Enum e
Literalviram perguntaschoicecom os valores como rótulos string; o rótulo é mapeado de volta ao valor original na saída, então inteiros continuam inteiros. - Um inteiro limitado vira uma pergunta
scorecom um nível por valor; o valor retornado éminimum + argmax. - Um booleano vira uma pergunta
noul; o valor énoul >= 0.5. - Uma
descriptionvira as instruções da pergunta, então uma boa descrição é o que torna a decisão precisa. Isso segue a mesma regra do guia de hooks: seja explícito sobre o que cada opção significa. - Um branch
nullé descartado antes de o campo ser planejado, entãoOptional[X]faz exatamente a pergunta queXfaz. A chave do campo fica simplesmente ausente dos valores quando não há resposta para ela, que é o que torna seguro declarar um campo opcional sem mudar o que o modelo vê.
Veja também
- Hooks de predição: observe, molde, cacheie ou aplique gating às decisões que isto produz.
- Primitivas de decisão:
choice,scoreenoulem profundidade. - LangChain e LangGraph:
LayaDecisioné esta chamada como um runnable em uma chain.