Dokumentation

Schema-gesteuerte Entscheidungen

Verwandle ein JSON-Schema oder ein pydantic-Modell in Laya-Fragen und erhalte typisierte Werte mit kalibrierter Konfidenz. Das ist die Brücke, die Laya zu einer Engine für strukturierte Ausgaben macht: Du beschreibst die gewünschte Form, Laya beantwortet sie in einem einzigen Vorwärtspass.

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}

Mit pydantic (installiere 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)

Die unterstützte Teilmenge

Die oberste Ebene muss ein Objekt mit properties sein. Jede Eigenschaft wird zu einer Frage.

Jede Zeile unten ist ein echtes Schema: tests/test_structured_docs.py kompiliert die erste Spalte und prüft, welche Frage der Compiler tatsächlich erzeugt, sodass diese Tabelle nicht vom Code abweichen kann. Eine Zelle ist entweder ein Eigenschafts-Schema für sich oder ein Aufruf eines Einstiegspunkts.

JSON-Schema Laya-Frage Zurückgegebener Wert
{"enum": ["billing", "support"]} choice der gewählte Wert, mit seinem ursprünglichen Typ
{"const": "billing"} choice dieser eine Wert
{"type": "boolean"} noul true / false
{"type": "integer", "minimum": 0, "maximum": 5} score der Level mit der höchsten Wahrscheinlichkeit, als Ganzzahl
{"type": "number", "minimum": 0, "maximum": 5} score der Level, als Ganzzahl
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} choice How urgent? ist die Frageformulierung
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} choice wie die einfache enum-Zeile; ohne Antwort lässt es den Schlüssel weg
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} noul wie die einfache boolean-Zeile
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} score wie die einfache begrenzte Ganzzahl-Zeile

Literal[...] und Optional[...] sind die pydantic-Schreibweisen der enum- und anyOf-Zeilen: questions_from_pydantic rendert sie in diese Formen, und dieselben Zeilen gelten.

title wird nicht gelesen. pydantic legt jedem Feld von model_json_schema() eines bei, ob du danach gefragt hast oder nicht, und ein Name pro Eigenschaft kann nicht die Optionen der choices beschriften, aus denen eine Frage gebaut wird, deshalb ist der Formulierungshebel description — siehe Wie es intern abbildet weiter unten.

Die Projektion ist exakt: ein enum: [1, 2, 3] gibt 2 zurück, nicht "2"; eine begrenzte Ganzzahl gibt einen Level zwischen minimum und maximum zurück; ein Boolean ist noul >= 0.5.

Ablehnungen

Ein Schema, das nicht aus einer festen Optionsmenge beantwortet werden kann, wirft laya.structured.SchemaError (ein ValueError) und nennt den genauen Pfad. Jede Zeile wird ebenfalls ausgeführt, mit dem Feld name:

Eigenschafts-Schema Meldung
{"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

Die Einstiegspunkte selbst lehnen diese ab:

Aufruf Meldung
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

Limits: MAX_PROPERTIES = 32, MAX_OPTIONS = 32, MAX_SCORE_LEVELS = 10.

Die API

Funktion Zweck
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) die freie Funktion, funktioniert für Agent und Router
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) Komfortmethoden
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) dasselbe über viele Zustände, ein gebatchter Aufruf
questions_from_json_schema(schema) Schema zu Laya-Fragen
questions_from_pydantic(model) pydantic-Modell zu Fragen (erfordert pydantic)
answers_to_json(answers, schema) rohe Antworten auf Schema-Werte projizieren
answer_to_pydantic(model, answers) rohe Antworten in eine pydantic-Instanz projizieren
plan_from_json_schema(schema) der validierte Feldplan (fortgeschritten)

Gib genau eines von schema oder questions an. Mit questions gibt decide die rohen Antworten zurück, statt zu projizieren. Zusätzliche Keyword-Argumente werden an predict weitergegeben, sodass Hooks, model=, task= und das Token-Budget alle funktionieren:

router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])

Viele Zustände bewerten

decide_batch ist die Durchsatz-Form: Das Schema wird einmal geplant und seine Fragen laufen über jeden Zustand durch predict_batch, sodass Zustände Forward-Pässe teilen, statt einen pro Aufruf. Ergebnisse kommen in Eingabereihenfolge zurück, exakt so projiziert wie decide, und return_details=True gibt ein DecisionResult pro Zustand:

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)

Auf einem Router wird jeder Zustand weiterhin eigenständig geroutet, sodass ein Aufruf Checkpoints überspannen kann. Keyword- Argumente erreichen predict_batch, sodass batch_size=, model= und Hooks so funktionieren wie bei decide. Agent, ONNXAgent und Router haben es alle; ein Runner ohne predict_batch wirft TypeError, statt still auf eine Schleife zurückzufallen — rufe dort decide pro Zustand auf. Batching kann Grenz-Argmaxes verschieben, genauso wie predict_batch es tut; das README hält die gemessenen Beschleunigungen für beide Geräte fest.

Konfidenz und Wahrscheinlichkeiten

Standardmäßig gibt decide nur die Werte zurück. Gib return_details=True für ein DecisionResult mit Konfidenz pro Feld, Wahrscheinlichkeiten, den rohen Antworten sowie Nutzung und Routing des Aufrufs:

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 ist der Block, den predict() gebaut hat, unverändert weitergegeben: input_tokens summiert den State über eine Zeile pro Frage, und output_tokens ist immer 0, weil nichts generiert wird, während die anderen vier Schlüssel der Truncation-Report (#174) sind – state_tokens ist, was der ganze serialisierte State braucht, state_tokens_dropped das, wovon der Head irgendeiner einzelnen Frage am meisten aufgegeben hat, und truncated / truncated_questions sagen, welche. Ein siebter Schlüssel, options, ist nur vorhanden, wenn das Head-Budget die Optionen irgendeiner Frage einen Token-Span teilen ließ (#538); docs/http-api.md dokumentiert sowohl diesen Block als auch dieses Feld als Response-Schlüssel, und tests/test_structured_docs.py hält die Liste dieser Seite an den Code, der sie baut.

confidence und answer_confidence sind verschiedene Größen, und die Namen folgen den Definitionen. answer_confidence ist max(p), die Wahrscheinlichkeitsmasse auf der gemeldeten Antwort. Sie ist das, worauf die Temperatur-Skalierung fittet, worauf jede Kalibrierungszahl in diesem Repository berechnet wird, und womit min_confidence verglichen wird — was der Grund ist, darauf zu gaten statt auf confidence. confidence ist normalisierte Entropie, die davon abhängt, wie viele Optionen die Frage hatte: tests/test_confidence.py hält fest, dass eine Zwei-Optionen-Verteilung als 0.90 bei einer noul und 0.53 bei einer äquivalenten choice zurückkommt, also vergleicht sie nicht gegen einen Schwellenwert. Ein Feld, das kein nutzbares answer_confidence meldete, bildet auf None ab, was nicht dasselbe ist wie ein gemeldetes 0.0.

Gate auf dieselbe Größe, die das Gate verwendet:

if result.answer_confidence["department"] < 0.6:
    result.values["department"] = "human-review"

Dass answer_confidence die richtige Zahl zum Filtern ist, ist nicht dasselbe wie eine vertrauenswürdige Wahrscheinlichkeit. Sie als „etwa c der bei c zurückgegebenen Antworten sind korrekt“ zu lesen, gilt erst, nachdem Temperaturen gefittet und an zurückgehaltenen Daten für diesen Checkpoint und diese Frageform validiert wurden. Die ausgelieferten Checkpoints sind im Auslieferungszustand überkonfident, und laya-multilingual wird ganz ohne gefittete Temperaturen ausgeliefert — siehe die README-Abschnitte Calibration und Honest limits sowie das Fine-Tuning-Notebook für die Fitting-Schleife. Fitte, bevor du dich auf den Level verlässt; melde ihn, weil er die Größe ist, die Gate und Evaluierungs-Harness beide verwenden.

Wie es intern abbildet

  • Enum und Literal werden zu choice-Fragen mit den Werten als String-Labels; das Label wird auf dem Rückweg auf den ursprünglichen Wert abgebildet, sodass Ganzzahlen Ganzzahlen bleiben.
  • Eine begrenzte Ganzzahl wird zu einer score-Frage mit einem Level pro Wert; der zurückgegebene Wert ist minimum + argmax.
  • Ein Boolean wird zu einer noul-Frage; der Wert ist noul >= 0.5.
  • Eine description wird zu den Frage-Instruktionen, sodass eine gute Beschreibung das ist, was die Entscheidung genau macht. Das folgt derselben Regel wie der Hooks-Leitfaden: sei explizit darüber, was jede Option bedeutet.
  • Ein null-Zweig wird verworfen, bevor das Feld geplant wird, sodass Optional[X] genau die Frage stellt, die X stellt. Der Schlüssel des Feldes fehlt einfach in den Werten, wenn es keine Antwort dafür gibt, was es sicher macht, ein Feld optional zu deklarieren, ohne zu ändern, was das Modell sieht.

Siehe auch