0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

llama.cpp v0.6.0の判断APIをアプリへ組み込む前に:choice・noul・scoreの受け取り境界

0
Posted at

llama.cpp v0.6.0には、自由文を生成するAPIとは別に、型付きの質問へ答える POST /v1/systemone が収録されています。問い合わせの担当候補を選ぶ、条件が真かを評価する、優先度を順序段階で表す、といったアプリの部品として読むと、出力を受け取る実装の境界が見えてきます。

この記事は、v0.6.0の公式READMEから入出力を確認し、アプリ側の検証と保留処理を設計するための整理です。モデルやllama-serverは取得・起動していません。後半で実行するのは、架空の確率を入力するPython標準ライブラリだけの例です。リリースの公開時刻は2026年10月5日16:56:22 UTC、日本時間では10月6日01:56:22です。情報確認日は2026年10月6日です。公式リリース

入出力の型で、受け取れる意味を固定する

公式server READMEは、このエンドポイントをTypeSafe互換のSystem One APIと説明しています。TypeScriptの型検査や、外部処理を安全に実行できることまで保証する表現としては扱いません。

state は評価対象で、文字列・オブジェクト・配列を渡せます。文字列以外はJSONテキストとしてモデルへ渡されます。questions は質問IDをキーとするオブジェクトで、各質問が type、instructions、criteria を持ちます。次の表はREADMEの仕様を整理したものです。

type criteria 結果の意味 アプリで避ける解釈
choice 選択肢名→説明のオブジェクト。説明はnullも可 最大確率の選択肢、分布、confidence 許可リスト外の処理も実行できる
noul 任意でtrue/falseの説明 真と判断する確率 bool値、命題の正しさの保証
score 低い順に2〜10段階の説明 段階インデックスの確率加重期待値、legend、分布、confidence 必ず整数になる順位

score は段階の間の値を返せます。たとえば0・1・2の段階へ0.1・0.2・0.7の確率を置けば、期待インデックスは1.6です。「今日」の整数インデックス2へ自動的に丸める仕様ではありません。丸めるなら、アプリの別の決定規則として説明できる必要があります。

noul の0.63を「63%の正答率」と表示するのも避けます。モデルが真へ割り当てた確率と、評価データで測る正答率は分母と意味が異なります。READMEはモデルファイル内のtemperatureで確率が調整される一方、利用者のデータへの校正を保証しないと明記しています。

JSONを受け取ったあとに残る実装

想定するアプリは、問い合わせを請求・配送・技術の担当候補へ振り分けます。以下の境界はこの記事の設計案です。

まず、質問IDと期待する型をリクエスト時の定義へ照合します。choiceの選択肢が許可した集合に属すること、確率が有限で0〜1に収まること、分布の合計が1であることを確認します。JSONの構文が正しくても、アプリが想定しないラベルやNaN相当の値を、そのまま操作へ渡さないためです。

次に、候補を返す場合でも、担当を変更する権限や対象レコードの状態は通常のアプリ処理で確認します。以下のコードは「候補を出す」と「実行を許可する」を分け、候補にも確認必須のフラグを付けます。請求取消しやアカウント削除の実行処理は含みません。

ネット不要の受け取り境界を動かす

decision_boundary.py として保存し、Pythonで実行できます。検証環境はCPython 3.9.6、macOS 27.0.1、arm64です。追加パッケージ、ネット接続、APIキーは不要です。

これはアプリ独自の正規化済み分布を入力する例で、公式レスポンス全体のパーサーやllama-serverのクライアントではありません。confidence の算出や判断モデルも再実装していません。0.80と差0.20は保留の仕組みを見せるための仮閾値で、実データで校正した値ではありません。

"""架空の判断結果を使う、アプリ側の境界の例。モデル/API呼び出しなし。"""
from __future__ import annotations
from dataclasses import dataclass
from math import fsum, isclose, isfinite

OPTIONS = ("billing", "shipping", "technical")
LEVELS = ("can_wait", "this_week", "today")

@dataclass(frozen=True)
class Proposal:
    route: str | None
    reason: str
    requires_approval: bool = True

def probability(value: float) -> float:
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError("probability must be a number, not bool")
    if not isfinite(value) or not 0 <= value <= 1:
        raise ValueError("probability must be finite and in [0, 1]")
    return float(value)

def distribution(values: dict, expected: tuple) -> dict:
    if set(values) != set(expected):
        raise ValueError("unexpected or missing labels")
    checked = {label: probability(values[label]) for label in expected}
    if not isclose(fsum(checked.values()), 1.0, abs_tol=1e-8, rel_tol=0):
        raise ValueError("probabilities must sum to 1")
    return checked

def propose_route(values: dict) -> Proposal:
    checked = distribution(values, OPTIONS)
    ranked = sorted(checked, key=checked.get, reverse=True)
    first, second = ranked[:2]
    # 説明用の仮閾値。校正や正答率を確認した値ではない。
    if checked[first] < 0.80 or checked[first] - checked[second] < 0.20:
        return Proposal(None, "manual_review")
    return Proposal(first, "candidate_only")

def expected_level(values: dict) -> float:
    checked = distribution(values, LEVELS)
    return fsum(index * checked[label] for index, label in enumerate(LEVELS))

def demo() -> None:
    strong = propose_route({"billing": .86, "shipping": .09, "technical": .05})
    tied = propose_route({"billing": .45, "shipping": .45, "technical": .10})
    print(f"strong: {strong.route}; approval={strong.requires_approval}")
    print(f"tie: {tied.reason}; approval={tied.requires_approval}")
    print(f"score expected_index: {expected_level(dict(zip(LEVELS, (.1, .2, .7)))):.2f}")
    p_true = probability(.63)
    print(f"noul p_true: {p_true:.2f}; no action executed")
    invalid = [
        {"billing": float("nan"), "shipping": .5, "technical": .5},
        {"billing": .9, "shipping": .9, "technical": .1},
        {"billing": .9, "shipping": .1, "delete_account": 0},
        {"billing": True, "shipping": 0, "technical": 0},
    ]
    rejected = 0
    for values in invalid:
        try:
            propose_route(values)
        except ValueError:
            rejected += 1
    assert rejected == len(invalid)
    assert strong.route == "billing" and strong.requires_approval
    assert tied.route is None and tied.requires_approval
    assert isclose(expected_level(dict(zip(LEVELS, (.1, .2, .7)))), 1.6)
    print(f"invalid distributions rejected: {rejected}/{len(invalid)}")

if __name__ == "__main__":
    demo()

実行コマンドは python3 decision_boundary.py です。実際の標準出力は次のとおりでした。

strong: billing; approval=True
tie: manual_review; approval=True
score expected_index: 1.60
noul p_true: 0.63; no action executed
invalid distributions rejected: 4/4

同率の候補は保留し、NaN、合計不一致、未知ラベル、boolを確率として渡した4ケースは例外で拒否しました。これはローカルな入力処理の動作確認です。問い合わせの分類精度、閾値の有効性、実APIとの互換性を確認した結果にはなりません。実APIへ接続するときは、質問ID・type・必須フィールド・choiceと分布の整合なども別の境界で検証します。

サーバーの互換条件を先に確認する

READMEではstreaming非対応です。一般の生成モデルでこのAPIを使うと501、不正なリクエストは400、画像非対応モデルまたはprojector未読込での画像要求は501です。実装では入力不備、モデル互換性、業務上の保留を同じ状態へ潰さず、呼出し元へ区別して返す設計が考えられます。

質問は原則独立して回答されますが、Clefは全質問を一つのプロンプトで読み、まとめて判断します。したがって「全モデルで質問を追加しても既存質問の答えへ影響しない」というテスト前提は置けません。layaとClefではプロンプト全体を一括評価し、--ubatch-size に収まる必要があります。layaでは長い質問や選択肢が学習時のトークン予算に合わせて切り詰められることもREADMEにあります。

また usage.output_tokens は常に0と説明されています。自由文の生成量を測るAPIとは意味が違うため、既存のチャットAPI向け集計をそのまま適用せず、実際のサービス側契約を確認します。0から処理時間やメモリ消費、外部サービスの料金まで推定しません。

モデル選択とMac性能は、APIの型とは別に確認する

このAPIを呼べることと、手元の用途に適したモデルであることは別の確認です。layaの標準モデルは英語向けで、多言語版は別配布です。多言語版のカードに日本語が記載されていても、この記事で日本語品質を測定したことにはなりません。

画像入力には対応する判断モデルとmultimodal projectorが必要です。READMEは --mmproj を案内し、画像はdata URLを受け付けると説明しています。Clefについては、新templateに合わせて既存の公開済みGGUFの再変換が必要とのPRレビューがあります。現在の配布ファイルすべてが未更新と断定せず、採用するファイルと変換時点を確認する必要があります。モデル取得・変換・画像推論は今回未実施です。

Macの性能値も判断の正しさやAPIの応答時間を保証しません。Metal最適化PR #29869には、M3 Ultraの60-core GPU、macOS 15.7.9、Qwen3.8-27B Q4_0、master 836d57176 と当該PRの比較、2回の交互実行の平均という条件で次の値が載っています。

同時系列数 比較元のTG 当該PRのTG 読み方
B=1 32.9 t/s 32.9 t/s 1系列の生成
B=8 38.6 t/s 119.7 t/s 8系列の合計生成スループット

開発者の測定を引用したもので、この記事の独自ベンチマークではありません。1人のチャットが3倍になる値でも、v0.5.0とv0.6.0を比較した値でもありません。後半のコメントにはM5以降でも対象経路を有効化した説明があるため、初期のM1〜M4の説明を最終的な対応機種の上限に固定するのも避けます。

ライセンスはllama.cppのMITコードとモデルの利用条件を分けて確認します。今回確認したlaya、多言語laya、Julia-1、ClefのカードにはApache-2.0の記載がありますが、それを別配布物や任意のモデルへ一般化しません。オープンな重みから、学習パイプラインすべての公開や利用条件の省略を推定しないことも必要です。

導入時に残す検証記録

ここまでの一次資料から、次の順に実装検証を組み立てるのが扱いやすいと考えます。

  1. 採用モデル、重みの版、変換履歴、サーバーの版、質問定義を固定する。
  2. 実レスポンスの型と意味を、アプリの正規化層で検証する。
  3. 自分の言語・業務データで、誤分類、保留率、確率の校正を別々に測る。
  4. 権限確認と実行処理を判断モデルの出力から分離する。
  5. 1要求の遅延と同時処理の合計スループットを別に測る。

この順番は使い魔工房の設計上の考察です。今回確認したのは公式仕様と開発者測定の条件、および掲載Pythonのローカル実行だけです。GPUベンチマーク、日本語評価、モデルの取得・推論は未実施です。AIを原稿補助に用い、記載した一次資料と実行結果へ照合しました。資料の照合は一次資料の直接確認で行いました。Geminiによる資料分析は行っていません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?