Documentação

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 Literal viram perguntas choice com 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 score com um nível por valor; o valor retornado é minimum + argmax.
  • Um booleano vira uma pergunta noul; o valor é noul >= 0.5.
  • Uma description vira 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ão Optional[X] faz exatamente a pergunta que X faz. 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