1
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?

「北広島市」は広島県じゃない──都道府県判定を文字列一致から新しい AI モデル Jev に置き換えてみた

1
Last updated at Posted at 2026-10-05

サイバーセキュリティ関連のニュースサイトから記事を集めるサイトを個人で制作しているのですが、そこにJevを導入しようと思い検証した記録です。


作ってるもの

簡単に作っているものの解説です。

  • サイバーセキュリティ情報収集用のWebアプリ
  • 毎日3時間ごとに各サイトから新着記事を取得してDBに保存
  • 概要と元記事へのリンクをダッシュボード表示
  • 記事の内容からどの県で起きたインシデントなのか分類、都道府県に該当すれば日本地図上にヒートマップで表示
  • ヒートマップはビジュアル的に面白そうという理由で作った機能であり、都道府県別の発生数の正確な傾向を示すものではないです

image.png

今回やったこと

これまで都道府県分類は、文字列一致でやっていたのですがJevでやってみることにしました。現状に不満というよりJevをサービスに使ってみたいというモチベーションが大きかったです。

Jevとは

Jevは、TypeSafe AIが提供する「System One」と呼ばれるAIモデルの1つです。一般的なLLMのように文章を生成するのではなく、こちらが用意した選択肢から答えを選び、確率つきで返してくれます。

  • 質問の型(プリミティブ)は3種類: Choice(選択肢から1つ選ぶ)、Noul(はい/いいえ)、Score(段階評価)
  • 答えは型の決まったデータで返ってくるので、プログラムからそのまま使える
  • 公式ドキュメント: https://docs.typesafe.ai/

今回は47都道府県+「該当なし」から1つ選ばせたいので、Choiceを使いました。

事前検証

  • 導入を決める前に以下のような簡単なスクリプトでテストしました。
from typesafe_sdk import Choice, TypeSafeClient
import argparse


def build_parser():
    parser = argparse.ArgumentParser(description="インシデント情報に都道府県ラベルをつける")
    parser.add_argument("--message", type=str, default="神奈川の株式会社ABCでランサム被害が発生しました")
    return parser

def return_prefecture_name(message):
    client = TypeSafeClient()
    response = client.system_one(
        state=message,
        questions={
            "prefectures": Choice(
                instructions="インシデントが発生したのはどの都道府県内ですか",
                criteria={
                    # 北海道・東北
                    "北海道": None,
                    "青森県": None,
                    "岩手県": None,
                    "宮城県": None,
                    "秋田県": None,
                    "山形県": None,
                    "福島県": None,
                    # 関東
                    "茨城県": None,
                    "栃木県": None,
                    "群馬県": None,
                    "埼玉県": None,
                    "千葉県": None,
                    "東京都": None,
                    "神奈川県": None,
                    # 中部
                    "新潟県": None,
                    "富山県": None,
                    "石川県": None,
                    "福井県": None,
                    "山梨県": None,
                    "長野県": None,
                    "岐阜県": None,
                    "静岡県": None,
                    "愛知県": None,
                    # 近畿
                    "三重県": None,
                    "滋賀県": None,
                    "京都府": None,
                    "大阪府": None,
                    "兵庫県": None,
                    "奈良県": None,
                    "和歌山県": None,
                    # 中国
                    "鳥取県": None,
                    "島根県": None,
                    "岡山県": None,
                    "広島県": None,
                    "山口県": None,
                    # 四国
                    "徳島県": None,
                    "香川県": None,
                    "愛媛県": None,
                    "高知県": None,
                    # 九州・沖縄
                    "福岡県": None,
                    "佐賀県": None,
                    "長崎県": None,
                    "熊本県": None,
                    "大分県": None,
                    "宮崎県": None,
                    "鹿児島県": None,
                    "沖縄県": None,
                    "該当なし": None,
                },
            ),
        },
    )
    return response.answers["prefectures"].choice

def main():
    parser = build_parser()
    args = parser.parse_args()
    message = args.message
    prefecture = return_prefecture_name(message)
    print("都道府県:", prefecture)

if __name__ == "__main__":
    main()

--message引数に判定したいメッセージを入れて実行すると結果となる都道府県が出力されます。

  • 作成にあたり以下の記事の内容が大変役立ちました。
  • 記事によると、すべてのパターンを網羅できない場合は逃げ道を作る、とのことだったので47都道府県 + 「該当なし」の選択肢を設けました。

テストパターン1 内包パターン

単純な文字列一致だと、「東京都」の中の「京都」にも一致してしまいます(旧方式では正規表現で個別に対策していました)。Jevではこうした個別の対策なしで正しく判定できました。

$ python jev-test.py --message "東京都でランサム被害急増"
都道府県: 東京都

テストパターン2 文字列一致との比較

文字列一致では、「北海道」「広島県」の両方に分類しますがJevでは正しく分類しました。

$ python jev-test.py --message "北海道北広島市教育委員会は、中学校の修学旅行中に、旅行会社の添乗員が生徒や引率教員の個人情報を含む資料を紛失したことを明らかにした。"
都道府県: 北海道

テストパターン3 市区町村名だけでも判定できる

$ python jev-test.py --message "渋谷区恵比寿のABC株式会社で情報漏洩"
都道府県: 東京都

驚いたのが都道府県の明記がなくても都市名だけでも行ける点です。

$ python jev-test.py --message "恵比寿のABC株式会社で情報漏洩"
都道府県: 東京都
$ python jev-test.py --message "横浜のABC株式会社で情報漏洩"
都道府県: 神奈川県
$ python jev-test.py --message "大宮のABC株式会社で情報漏洩"
都道府県: 埼玉県

なんとなくマイナーなイメージの市でも試しましたが、全部正解でした。

$ python jev-test.py --message "飯田市のABC株式会社で情報漏洩"
都道府県: 長野県
$ python jev-test.py --message "刈谷市のABC株式会社で情報漏洩"
都道府県: 愛知県
$ python jev-test.py --message "大垣市のABC株式会社で情報漏洩"
都道府県: 岐阜県
$ python jev-test.py --message "守山市のABC株式会社で情報漏洩"
都道府県: 滋賀県
$ python jev-test.py --message "太田市のABC株式会社で情報漏洩"
都道府県: 群馬県

テストパターン4 地名なのか紛らわしいパターン

長野は人名なので正解は「該当なし」。これはまあ許容範囲かと。

$ python jev-test.py --message "ABC株式会社が情報漏洩。長野社長が謝罪"
都道府県: 長野県

メッセージの内容によっては正しく分類してくれました。

$ python jev-test.py --message "ABC株式会社が情報漏洩。社長の長野氏が謝罪"
都道府県: 該当なし

以上、検証でした。
簡単なテストでしたが県名の記載がなくても学習済みの知識から都道府県を推測してくれる点に満足し次に進みました。

ちなみに、ここまでの検証で49回呼び出して約5.4万トークン、費用は$0.0014でした。1回あたり約$0.00003(1ドル150円換算で約0.004円)です。

実データでの比較

テストで十分効果を実感できたので実際にサービスで集計してきた記事データ(200件)を使ってJevと旧方式(文字列一致)を比較しました。

  • 対象: 直近200件の記事(脆弱性情報のデータベースであるJVNの記事は、都道府県とほぼ関係がないので除外)
  • プロンプト: 事前検証で見つかった人名の誤判定(テストパターン4)に対応するため、「地名と同じ文字を含む人名は判断材料にしない」旨を加えたもの
  • 正しいかどうかは、差分の記事を1件ずつ目で確認して判断しました

集計

判定 件数
両者「該当なし」 163
回答した都道府県が両者一致 26
Jevのみ都道府県を回答 9
旧のみ都道府県を回答 1
旧が複数回答(Jevの回答含む) 1

差分11件の評価

差分は全件 Jev が正しい or 妥当となり、旧方式が勝ったケースは0件でした。

  • Jevのみ(9件): 旧方式の取りこぼしを拾った
    • 辞書にない地名・表記: 宝塚市立病院→兵庫県、コープやまぐち→山口県、JA遠州夢咲→静岡県
    • 組織名から本社所在地を推定: 京王電鉄・京王プラザホテル→東京都、日大→東京都、関西学院大→兵庫県、西武鉄道→埼玉県(被害は予約サイトでしたが、本社所在地としては正しいので妥当と判断)
  • 旧のみ(1件): 旧方式で「リコーブラックラムズ東京」のチーム名に部分一致した誤判定です。Jev の該当なしが正しい
  • 旧が複数(1件): これは旧方式で「山口東京理科大」を「東京」「山口」の両方に分類していた例となります。Jev は山口県のみと正しく分類しました
  • JevのChoiceプリミティブでは単一選択しかできませんが、本当に複数県にまたがる記事は200件中0件なので、実用上問題なさそうでした(公式ドキュメントでは、複数当てはまる場合はラベルごとにNoulを使う方法が紹介されています)

方針決定

記事中に地名がなくても、被害組織の本社所在地から推定するのは認めるという方針でJevへの切り替えを決定しました。


以上、今回は導入を決定したところまでです。

注意点として、文字列一致と違って、Jevの結果はプロンプトの書き方や記事の書き方で変わります。実際、人名の誤判定を直そうとプロンプトを調整したら、今度は別のケースで誤判定が出たりしました。

そこでさらに工夫をして導入したのですがそれは別記事で書きます。

ここまで読んでいただいてありがとうございました。

1
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
1
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?