Decisões a partir do esquema
Transforma um esquema JSON, ou um modelo pydantic, em perguntas do Laya, e recebe de volta valores tipados com confiança calibrada. Esta é a ponte que faz do Laya um motor de saída estruturada: descreves a forma que queres e o Laya responde-lhe numa única passagem 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 (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)
O subconjunto suportado
O nível de topo tem de ser um objeto com properties. Cada propriedade torna-se uma pergunta.
Cada linha abaixo é um esquema real: tests/test_structured_docs.py compila a primeira coluna e
verifica a pergunta que o compilador produz de facto, por isso esta tabela não pode divergir do
código. Uma célula é ou um esquema de propriedade isolado, ou uma chamada a um ponto de entrada.
| Esquema JSON | Pergunta do Laya | Valor devolvido |
|---|---|---|
{"enum": ["billing", "support"]} |
choice |
o valor escolhido, com o seu tipo original |
{"const": "billing"} |
choice |
esse ú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 enunciado da pergunta |
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} |
choice |
como a linha enum simples; uma não-resposta deixa a chave de fora |
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} |
noul |
como a linha 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 renderiza-as para essas formas e aplicam-se as mesmas linhas.
O title não é lido. O pydantic coloca um em cada campo de model_json_schema(), quer o peças
quer não, e um nome por propriedade não consegue etiquetar as escolhas por opção de que uma pergunta
é construída, por isso a alavanca do enunciado é a description — vê Como mapeia internamente
abaixo.
A projeção é exata: um enum: [1, 2, 3] devolve 2, e não "2"; um inteiro limitado devolve 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 levanta
laya.structured.SchemaError (um ValueError) que nomeia o caminho exato. Cada linha também é
executada, com o campo chamado name:
| esquema da 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 estes:
| 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 | finalidade |
|---|---|
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, numa única chamada em lote |
questions_from_json_schema(schema) |
esquema para perguntas do Laya |
questions_from_pydantic(model) |
modelo pydantic para perguntas (requer pydantic) |
answers_to_json(answers, schema) |
projeta respostas em bruto sobre valores do esquema |
answer_to_pydantic(model, answers) |
projeta respostas em bruto numa instância pydantic |
plan_from_json_schema(schema) |
o plano de campos validado (avançado) |
Passa exatamente um de schema ou questions. Com questions, o decide devolve as respostas em
bruto em vez de as projetar. Os argumentos de palavra-chave extra são encaminhados para predict,
por isso os hooks, model=, task= e o orçamento de tokens funcionam todos:
router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])
Pontuar muitos estados
decide_batch é a forma de débito: o esquema é planeado uma vez e as suas perguntas correm sobre
cada estado através de predict_batch, por isso os estados partilham passagens diretas em vez de
uma por chamada. Os resultados voltam na ordem de entrada, projetados exatamente como o 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)
Num Router, cada estado continua a ser encaminhado por si, por isso uma chamada pode abranger
checkpoints. Os argumentos de palavra-chave chegam ao predict_batch, por isso batch_size=,
model= e os hooks funcionam como em decide. O Agent, o ONNXAgent e o Router têm todos
isto; um runner sem predict_batch levanta TypeError em vez de cair silenciosamente num ciclo —
chama decide por estado aí. O processamento em lote pode deslocar argmaxes limítrofes da mesma
forma que o predict_batch; o README regista as acelerações medidas para ambos os dispositivos.
Confiança e probabilidades
Por predefinição, o decide devolve apenas os valores. Passa return_details=True para obter um
DecisionResult com a confiança por campo, as probabilidades, as respostas em bruto, e o usage e o
routing 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 o predict() construiu, reencaminhado inteiro: input_tokens soma o estado ao longo de uma linha por pergunta e output_tokens é sempre 0 porque nada é gerado, ao passo que as outras quatro chaves são o relatório de truncagem (#174) — state_tokens é o que todo o estado serializado precisa, state_tokens_dropped a maior parte que o head de qualquer pergunta individual cedeu, 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 partilharem um trecho de tokens (#538); docs/http-api.md documenta tanto este bloco como esse campo como chaves de resposta, e tests/test_structured_docs.py prende a lista desta página ao código que a constrói.
confidence e answer_confidence são grandezas diferentes, e os nomes seguem as definições.
answer_confidence é max(p), a massa de probabilidade sobre a resposta que está a ser reportada.
É o que a escala de temperatura ajusta, aquilo sobre que é calculada cada cifra de calibração neste
repositório, e aquilo com que min_confidence é comparado — e é essa a razão para fazer 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 num noul e 0.53 num choice equivalente, por isso não se compara com um limiar.
Um campo que não reportou nenhum answer_confidence utilizável mapeia para None, que não é o
mesmo que um 0.0 reportado.
Faz gating sobre a mesma grandeza que o gate usa:
if result.answer_confidence["department"] < 0.6:
result.values["department"] = "human-review"
Que answer_confidence seja o número certo para filtrar não é o mesmo que ser uma probabilidade
fiável. Lê-lo como «cerca de c das respostas devolvidas a c estão corretas» só é válido depois de as
temperaturas terem sido ajustadas e validadas em dados reservados para esse checkpoint e essa forma
de pergunta. Os checkpoints distribuídos são demasiado confiantes tal como são, e o
laya-multilingual é distribuído sem quaisquer temperaturas ajustadas — vê as secções
Calibration e
Honest limits do README, e o
notebook de ajuste fino
para o ciclo de ajuste. Ajusta antes de confiar no nível; reporta-o porque é a grandeza que tanto o
gate como o harness de avaliação usam.
Como mapeia internamente
- Enum e
Literaltornam-se perguntaschoicecom os valores como etiquetas de string; a etiqueta é mapeada de volta para o valor original à saída, por isso os inteiros continuam inteiros. - Um inteiro limitado torna-se uma pergunta
scorecom um nível por valor; o valor devolvido éminimum + argmax. - Um booleano torna-se uma pergunta
noul; o valor énoul >= 0.5. - Uma
descriptiontorna-se as instruções da pergunta, por isso uma boa descrição é o que torna a decisão precisa. Isto segue a mesma regra do guia de hooks: sê explícito sobre o que significa cada opção. - Um ramo
nullé descartado antes de o campo ser planeado, por issoOptional[X]faz exatamente a pergunta queXfaz. A chave do campo está simplesmente ausente dos valores quando não há resposta para ele, e é isso que torna seguro declarar um campo opcional sem alterar o que o modelo vê.
Ver também
- Hooks de predição: observa, molda, coloca em cache ou aplica gating às decisões que isto produz.
- Primitivas de decisão:
choice,scoreenoulem profundidade. - LangChain e LangGraph:
LayaDecisioné esta chamada como runnable numa chain.