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?

自動判定の「OK」を信じる前に — 例外条文で崩れる精度と「上位N件だけ人手照合」の実装

0
Posted at

環境情報

  • Python 3.11.8
  • FastAPI 0.111.0
  • PostgreSQL 16 + pgvector 0.7.0
  • LLM: 構造化出力(JSON Schema)に対応したチャットAPI
  • 判定対象: 社内規程・利用規約 約1,200条 × 申請データ 300件
  • 評価指標: Accuracy / Precision@1 / Precision@3

何が起きるか

規程や利用規約の適合判定を自動化すると、まず全体のAccuracyが目に入ります。ところが実運用で痛いのは平均ではなく上位の誤りです。人間は一覧の上から見るので、1位が間違っていれば、その後の正解率がどれだけ高くてもシステムは信用されません。

そして上位に誤りが固まりやすい典型パターンが、条件の例外節です。

第12条 以下の物品の持ち込みを禁止する。(1) 発火性物品 (2) 刃物類。ただし、業務上必要な工具であって、所属長の承認を得たものについては、この限りでない。

「禁止」という主文だけを拾えばNG、「ただし」以降だけを拾えばOK。判定がどちらに倒れるかは、モデルの気分やプロンプトの書き方で簡単に反転します。平均精度が高く見えても、こうした条文に当たった瞬間に上位が壊れます。

平均精度が嘘をつく

筆者の検証では、全体Accuracy 0.96に対してPrecision@1は0.71でした。つまり最初の1件は3割外れている。この差は指標の作り方の問題ではなく、見ている場所の違いです。

指標 値 現場での意味
Accuracy(全体) 0.96 ほぼ正しい、ように見える
Precision@1 0.71 最初に見る1件が3割外れ
Precision@3 0.83 上位3件で1件弱が怪しい
Recall(例外節のみ) 0.62 ただし書きを4割取りこぼす

例外節だけを抽出してRecallを測ると、全体Accuracyの陰で0.62まで落ちていました。平均は、少数の厄介な条文を埋めてしまうのです。

実装1: 判定根拠を必ず出力に残す

対策の第一歩は、判定結果に根拠を残すことです。後から人が照合できない出力は、検証不能なブラックボックスになります。

from typing import Literal
from pydantic import BaseModel, Field

class Judgement(BaseModel):
    decision: Literal["allow", "deny", "needs_review"]
    rationale: str = Field(description="判定理由を1〜2文で")
    cited_clause_ids: list[str] = Field(description="根拠にした条文ID")
    cited_quotes: list[str] = Field(description="原文からの抜粋。改変・要約は禁止")
    exception_checked: bool = Field(description="ただし書きを確認したか")
    confidence: float = Field(ge=0.0, le=1.0)

cited_quotes に原文抜粋を改変せず出させるのがポイントです。要約を許すと、存在しない条文をそれらしく書けてしまいます。抜粋なら原文と機械的に突合できます。

実装2: 上位N件+境界帯を抽出する

全件を人が見るのは現実的ではありません。見るべきは「上位」と「境目」です。

def sample_for_review(rows, top_k: int = 3, band: float = 0.05):
    """rows: [{"id":..., "score":..., "decision":...}, ...]"""
    ordered = sorted(rows, key=lambda r: (-r["score"], r["id"]))
    picked = {r["id"] for r in ordered[:top_k]}

    # スコアが閾値近傍にある「境目」も拾う
    for r in ordered:
        if abs(r["score"] - 0.5) <= band:
            picked.add(r["id"])

    return [r for r in ordered if r["id"] in picked]

ソートキーに id を足しているのは、同点で順位が入れ替わると検証結果が再現しないためです。地味ですが、回帰テストを回すなら必須です。

実装3: 引用の突合を自動化する

人が照合する前に、明らかな不一致は機械で落とします。

import re
import unicodedata

def normalize(s: str) -> str:
    s = unicodedata.normalize("NFKC", s)
    s = re.sub(r"[\s\u3000]", "", s)
    s = s.replace("・", "").replace(",", "").replace(".", "")
    return s

def is_verbatim(quote: str, clause_text: str, min_len: int = 15) -> bool:
    q = normalize(quote)
    if len(q) < min_len:
        return False
    return q in normalize(clause_text)

これで「引用が原文に存在しない」ケースを弾けます。残ったものだけを人が読みます。

照合の手順

  1. 判定を全件実行し、スコア付きで保存する
  2. 上位3件+閾値±0.05の境界帯を抽出する
  3. 人が元の条文と1件ずつ突き合わせる(1件あたり2〜3分)
  4. 不一致を「例外の見落とし」「引用のずれ」「同点による順位入れ替え」に分類する
  5. 分類に応じてプロンプト・ルールを修正し、回帰テストに追加する

この流れなら、300件のうち照合対象は30〜50件程度に収まります。

ハマりポイント

  • 表記ゆれ: 全角/半角、中黒、括弧の種類で突合が落ちる。NFKC正規化を先に挟む
  • 条文番号の自動採番: 改行やルビの扱いでIDがずれ、引用が別条文に当たる
  • 「ただし」の検出漏れ: キーワード検索だけでなく、条文を木構造で持ち、ただし書きを子ノードとして管理する
  • 同点問題: sorted のキーを固定しないと、同じ入力でも上位Kが変わる
  • 入れ子の例外: 「ただし書きのただし書き」は2段階判定に分けた方が安定する

FAQ

Q. 全件、人が見ればいいのでは?
A. 1,200条×300件を毎回照合するのは回りません。上位と境界に絞るだけで、体感としてリスクの大半を拾えます。

Q. temperatureは0にすべき?
A. 0を推奨します。ただし同点問題は残るので、順位の再現性はソートキーで担保してください。

Q. 例外判定だけ別モデルに分ける?
A. 有効です。主文判定と例外判定を分けると、Recallの計測もやりやすくなります。

まとめ

  • 例外節は判定を反転させる。平均精度では見えない
  • 判定には根拠条文の原文抜粋を必ず残す
  • 照合対象は「上位K件+境界帯」に絞る
  • 引用の突合は機械で前処理し、人は判断に集中する
  • 一覧の上位は最初に見られる場所。平均より、上位の正しさを先に確認する

この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 業務自動化サービス — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

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?