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
Literalwerden zuchoice-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 istminimum + argmax. - Ein Boolean wird zu einer
noul-Frage; der Wert istnoul >= 0.5. - Eine
descriptionwird 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, sodassOptional[X]genau die Frage stellt, dieXstellt. 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
- Vorhersage-Hooks: die von alldem erzeugten Entscheidungen beobachten, formen, cachen oder gaten.
- Entscheidungs-Primitives:
choice,scoreundnoulim Detail. - LangChain und LangGraph:
LayaDecisionist dieser Aufruf als Runnable in einer Chain.