ドキュメント

スキーマ駆動の意思決定

スキーマ駆動の意思決定

JSON スキーマ、または pydantic モデルを Laya の質問に変換し、キャリブレーション済みの信頼度が 付いた型付きの値を受け取ります。これが Laya を構造化出力エンジンにする橋です。欲しい形を 記述すれば、Laya が 1 回のフォワードパスで答えます。

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}

pydantic を使う場合(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)

対応する部分集合

トップレベルは properties を持つオブジェクトでなければなりません。各プロパティが 1 つの Laya の質問になります。

以下の各行は実際のスキーマです。tests/test_structured_docs.py が最初の列をコンパイルし、 コンパイラが実際に生成する質問を検証するので、この表がコードからずれることはありません。 セルは、それ自体が 1 つのプロパティスキーマであるか、エントリポイントの呼び出しです。

JSON スキーマ Laya の質問 返される値
{"enum": ["billing", "support"]} choice 選ばれた値(元の型のまま)
{"const": "billing"} choice その 1 つの値
{"type": "boolean"} noul true / false
{"type": "integer", "minimum": 0, "maximum": 5} score 確率が最も高いレベルを整数で
{"type": "number", "minimum": 0, "maximum": 5} score そのレベルを整数で
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} choice How urgent? が質問の文言になります
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} choice 通常の enum の行と同じ。答えがない場合はキーが出力に含まれません
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} noul 通常の boolean の行と同じ
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} score 通常の上限付き整数の行と同じ

Literal[...] と Optional[...] は、enum と anyOf の各行に対応する pydantic での書き方です。 questions_from_pydantic がそれらをこれらの形に描画し、同じ行が当てはまります。

title は読み取られません。pydantic は要求の有無にかかわらず model_json_schema() の すべてのフィールドにこれを付けるうえ、プロパティ単位の名前では、1 つの質問が組み立てられる 元になる選択肢ごとの choice にラベルを付けられません。したがって文言を操作するレバーは description です —— 後述の内部でどう対応するかを参照してください。

射影は正確です。enum: [1, 2, 3] は 2 を返し、"2" は返しません。上限付き整数は minimum と maximum の間のレベルを返します。真偽値は noul >= 0.5 です。

拒否

固定の選択肢集合から答えられないスキーマは、laya.structured.SchemaError(ValueError)を 送出し、正確なパスを名指しします。各行も実行され、フィールド名は name です:

プロパティスキーマ メッセージ
{"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

エントリポイント自身はこれらを拒否します:

呼び出し メッセージ
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

制限:MAX_PROPERTIES = 32、MAX_OPTIONS = 32、MAX_SCORE_LEVELS = 10。

API

関数 用途
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) 自由関数で、Agent と Router の両方に使えます
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) 便利メソッド
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) 多数の状態に対して同じことをする、1 回のバッチ呼び出し
questions_from_json_schema(schema) スキーマから Laya の質問へ
questions_from_pydantic(model) pydantic モデルから質問へ(pydantic が必要)
answers_to_json(answers, schema) 生の答えをスキーマの値へ射影
answer_to_pydantic(model, answers) 生の答えを pydantic インスタンスへ射影
plan_from_json_schema(schema) 検証済みのフィールド計画(上級)

schema か questions のどちらか一方だけを渡します。questions を渡すと、decide は射影せず に生の答えを返します。余分なキーワード引数は predict へ転送されるので、フック、model=、 task=、token 予算はすべて機能します:

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

多数の状態をスコアリングする

decide_batch はスループット重視の形です。スキーマは 1 回だけ計画され、その質問は predict_batch を通じてすべての状態にわたり実行されるので、状態は呼び出しごとではなく フォワードパスを共有します。結果は入力順に返り、decide が射影するのとまったく同じに射影 されます。return_details=True は状態ごとに 1 つの DecisionResult を返します:

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)

Router では各状態は依然として個別にルーティングされるので、1 回の呼び出しが複数の チェックポイントにまたがることがあります。キーワード引数は predict_batch に届くので、 batch_size=、model=、フックは decide と同様に機能します。Agent、ONNXAgent、Router はいずれもこれを持ちます。predict_batch を持たない runner は、黙ってループにフォールバック するのではなく TypeError を送出します —— その場合は状態ごとに decide を呼んでください。 バッチ処理は predict_batch と同じように境界付近の argmax をずらしえます。README には両方の デバイスで実測された高速化が記録されています。

信頼度と確率

既定では decide は値だけを返します。return_details=True を渡すと DecisionResult が得られ、 フィールドごとの信頼度、確率、生の答え、そしてその呼び出しの usage とルーティングが含まれます:

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": 42, "output_tokens": 0}
result.routing                         # the Router decision, when a Router answered

confidence と answer_confidence は別の量であり、名前はそれぞれの定義に従っています。 answer_confidence は max(p)、つまり報告されている答えに乗っている確率の質量です。これこそが 温度スケーリングがフィットする対象であり、このリポジトリのあらゆるキャリブレーションの数値が その上で計算される対象であり、min_confidence が比較される対象です —— これが confidence では なくこちらでゲートすべき理由です。confidence は正規化エントロピーで、質問がいくつの選択肢を 持っていたかに依存します。tests/test_confidence.py は、2 選択肢の分布が noul では 0.90、 等価な choice では 0.53 で返ることを固定しているので、これをしきい値と比較することは できません。使える answer_confidence を報告しなかったフィールドは None に対応し、これは 0.0 が報告された場合と同じではありません。

ゲートには、ゲート自身が使うのと同じ量を使ってください:

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

answer_confidence がフィルタに使うのに適した数値であることは、それが信頼できる確率で あることとは違います。「c で返された答えのうち約 c の割合が正しい」と読めるのは、その チェックポイントと質問形について、温度がフィットされ、ホールドアウトデータで検証された後に だけです。同梱のチェックポイントは、同梱状態のままでは過信傾向があり、laya-multilingual には フィット済みの温度がまったく同梱されていません —— README の Calibration と Honest limits の各節、そしてフィットの ループについては ファインチューニング notebook を参照してください。このレベルに依拠する前にフィットしてください。これを報告するのは、 ゲートと評価 harness の両方が使う量だからです。

内部でどう対応するか

  • Enum と Literal は、値を文字列ラベルとする choice の質問になります。ラベルは返すときに 元の値へ写し戻されるので、整数は整数のままです。
  • 上限付き整数は、値ごとに 1 レベルの score の質問になります。返される値は minimum + argmax です。
  • 真偽値は noul の質問になります。値は noul >= 0.5 です。
  • description は質問の指示になります。したがって、よい記述こそが意思決定を正確にします。 これはフックガイドと同じ規則に従います。各選択肢が何を意味するかを明示して ください。
  • null の枝はフィールドが計画される前に落とされるので、Optional[X] は X が尋ねる質問を ちょうど尋ねます。そのフィールドに答えがないときは、キーが値から単に欠けます。これが、 モデルが見るものを変えずにフィールドを任意と宣言しても安全な理由です。

参照