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?

449MBの決定モデルで、問い合わせチケットをローカルで振り分ける

0
Posted at

チャットモデルでテキストを分類しようとすると、だいたい次のような流れになります。プロンプトの最後に「JSONのみで回答してください」と書き、返ってきた文字列をパースし、「はい!カテゴリは以下の通りです:」から始まってしまったときのためにリトライ処理を入れる。これでも動きますが、5つのラベルから1つを選ぶだけにしては周辺コードが多すぎます。

決定モデル(decision model)は別のアプローチを取ります。入力と、受け付ける回答の候補を渡すと、モデルは各候補をスコアリングし、すべての候補について確率を返します。テキストは一切生成しないので、候補にない回答が返ってくることはありません。

この記事では、ローカルで推論を行う Rust 製ライブラリ NobodyWho の Python バインディングを使って、問い合わせチケットの振り分けを実装します。決定モデルは 10 月 29 日のリリース(Python 版 v5.0.0)で追加されました。

筆者は NobodyWho のコントリビューターです(コアチームのメンバーではありません)。

インストールとモデルの読み込み

pip install -U "nobodywho>=5.0.0"
triage.py
from nobodywho import Decision, Model

model = Model(model_path="hf://ggml-org/Laya-GGUF/Laya-Q8_0.gguf")
decisions = Decision(model=model, n_ctx=1024)

使うモデルは Laya です。0.4B の ModernBERT ベースのモデルで、Q8_0 の GGUF ファイルは 449MB、ライセンスは Apache-2.0 です。推論は同じプロセス内で動くので、サーバーの起動も API キーも不要です。ダウンロード済みのファイルを使う場合は、model_path にローカルのパスを渡します。

3つの質問を一度に投げる

triage.py
message = "I was charged twice. Please refund the duplicate payment."

questions = {
    "department": {
        "type": "choice",
        "instructions": "Which team should handle this?",
        "criteria": {
            "billing": "Payments and refunds",
            "technical": "Bugs and login problems",
        },
    },
    "refund": {
        "type": "noul",
        "instructions": "Is a refund requested?",
    },
    "urgency": {
        "type": "score",
        "instructions": "How urgent is this?",
        "criteria": ["can wait", "soon", "urgent"],
    },
}

result = decisions.decide(state={"message": message}, questions=questions)

質問のタイプごとに、返ってくる値の形が異なります。

  • choice:候補から1つを選びます。result["answers"]["department"]["choice"] が選ばれた候補で、["probabilities"] に全候補の確率が入っています。
  • noul:はい/いいえの質問です。result["answers"]["refund"]["noul"] は「はい」の確率(0〜1)です。
  • score:入力を 2〜10 段階の順序付きスケール上に位置づけます。段階は 0 から番号が振られるので、この例で 1.6 なら「soon」と「urgent」の間です。値は確率の加重平均なので、「urgent」そのものの確率を知りたい場合は probabilities["2"] を読みます。

また、result["usage"]["output_tokens"] は常に 0 です。

一番確率の高い答えだけでなく、確率そのものを使う

実際のパイプラインで決定モデルが役に立つのはここです。全候補の確率が得られるので、判断に自信がないケースだけを人間に回せます。

triage.py
dept = result["answers"]["department"]
if dept["confidence"] < 0.6:
    send_to_human(message)
else:
    route(dept["choice"], message)

send_to_human と route は自分のコードに置き換えてください。0.6 はあくまで例なので、自分のデータで調整します。

ドキュメントにも書かれている注意点として、confidence は確率がどれだけ一つの候補に集中しているかを表す値で、高いからといって答えが正しいとは限りません。この閾値を信用する前に、実際のチケット数百件で確かめておくことをおすすめします。

非同期で使う

import asyncio
from nobodywho import DecisionAsync

async_decisions = DecisionAsync(model=model, n_ctx=1024)

async def classify():
    return await async_decisions.decide(state={"message": message}, questions=questions)

result = asyncio.run(classify())

DecisionAsync のコンストラクタ自体は同期的に呼び出します。

知っておきたい制限

  • 対応している決定モデルは今のところ Laya だけです。Laya 自身の README によると、型付き判定のベンチマークでの正解率はゼロショットで 0.362、ドメインに合わせてファインチューニングすると 0.766 です。重要な振り分けに使う前に、自分のデータで必ず測ってください。
  • 日本語の入力での精度は未検証です。 この記事の例はすべて英語です。日本語のチケットで試した方がいれば、結果をコメントで教えてもらえると助かります。
  • 選ぶことしかできません。理由の説明やテキストの抽出が必要なら、生成モデルは引き続き必要です。
  • 決定モデルは Python のほか、Swift・Kotlin・Flutter・React Native・Godot のバインディングにも同じリリースで入りました。モバイルアプリ内で同じ判定を動かすこともできます。

ドキュメント:https://docs.nobodywho.ooo/python/decision-models
リポジトリ:https://github.com/nobodywho-ooo/nobodywho

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?