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?

LAN内AIボットの「嘘(ハルシネーション)」を許さない。CI/CDで回す事実整合性ベンチマークテスト自動化スイート(TC01〜TC10)

1
Posted at

はじめに:プロンプトをいじった後、AIの全回答を手動で確認していませんか?

LAN内の資産管理や情シス業務向けにLLM(チャットボット)を構築し、現場に公開したとします。
ユーザーから「もっと丁寧に答えて」「要約して」と言われ、システムプロンプトを少し書き換えた経験はありませんか?

その直後、恐ろしい悲劇が起きます。

  • 「プロンプトを少し変えたせいで、LAN内のPC台数を嘘八百で答えるようになった」
  • 「存在しないIPアドレスを自信満々にでっち上げる(ハルシネーション)」
  • 「要リプレース端末を聞いたのに、正常な端末を勝手に故障と判定する」

Webアプリのコードであれば単体テスト(Unit Test)を書くのが当たり前なのに、なぜかLLMのプロンプト変更だけは「人間がチャット画面で数回テストしてヨシ!」でリリースされがちです。

AIの事実整合性を担保するには、「実データベースの正解データと照合し、期待される重要キーワードが回答に含まれているかを自動採点するベンチマークスイート」 をCI/CDで回す必要があります。

本記事では、筆者のプロジェクトで実際に運用している 10項目の自動精度評価フレームワーク の設計と実装を公開します。


1. 標準テストケース10項目(TC01〜TC10)の定義

テストケースは、あらかじめ実際のDBの正解値(グラウンドトゥルース)と一致させて定義します。

テストID テスト質問 検証内容・正解キーワード 判定基準
TC01 「スマートフォンは何台ありますか?」 ['4'] 端末一覧内のスマホ台数(4台)を正しくカウント
TC02 「NASのメーカー名は何ですか?」 ['Synology'] ストレージのベンダー名を正確に特定
TC03 「要リプレースと判定されている端末のIPは?」 ['192.168.1.236'] EoL端末のIPを正確に列挙
TC04 「IP 192.168.1.200 のOSは何ですか?」 ['Ubuntu'] 対象ホストのOSバージョンを回答
TC05 「複合機のIPアドレスとメーカー名を教えて」 ['192.168.1.233', 'Epson'] 機器種別からIPとベンダーを同時特定
TC06 「全端末の合計台数は何台ですか?」 ['20'] 全台数(20台)をハルシネーションせず回答
TC07 「Amazon端末のIPアドレスは何ですか?」 ['192.168.1.227'] ベンダー名からIPを逆引き
TC08 「192.168.1.9 のデバイス分類は何ですか?」 ['スマート'] スマートTV等の種別を回答
TC09 「192.168.1.236 のOS判定はどうなっていますか?」 ['特定不可'] 不明な端末を強引に推測せず「特定不可」と正しく保留
TC10 「Synology製NASの全IPを教えてください」 ['192.168.1.12', '192.168.1.13'] 複数存在する機器の全IPを漏れなく列挙

特に TC09(不明なものを知ったかぶりせず『特定不可』と答えられるか) は、ハルシネーション抑制の最重要チェックポイントです。


2. Keyword Match 判定アルゴリズムの実装

複雑なLLM-as-a-Judge(判定用LLM)を使うと、判定側のブレやコストが発生します。
CI/CDで高速・安価に回すためには、大文字・小文字を無視した「必須キーワード完全包含(Deterministic Keyword Match)」 が最も確実です。

💻 コア実装・設定スニペット(抜粋)

# コア実装ロジック(要点抜粋)
import requests
import json
import time

TEST_CASES = [
    {"id": "TC01", "q": "スマートフォンは何台ありますか?", "expected": ["4"]},
    {"id": "TC02", "q": "NASのメーカー名は何ですか?", "expected": ["Synology"]},
    {"id": "TC03", "q": "要リプレースと判定されている端末のIPはどれですか?", "expected": ["192.168.1.236"]},
    {"id": "TC04", "q": "IPアドレス 192.168.1.200 のOSは何ですか?", "expected": ["Ubuntu"]},
    {"id": "TC05", "q": "プリンター(複合機)のIPアドレスとメーカー名を教えてください。", "expected": ["192.168.1.233", "Epson"]},
    {"id": "TC06", "q": "全端末の合計台数は何台ですか?", "expected": ["20"]},
    {"id": "TC07", "q": "Amazon端末のIPアドレスは何ですか?", "expected": ["192.168.1.227"]},
    {"id": "TC08", "q": "192.168.1.9 のデバイス分類は何ですか?", "expected": ["スマート"]},
    {"id": "TC09", "q": "192.168.1.236 のOS判定はどうなっていますか?", "expected": ["特定不可"]},
    {"id": "TC10", "q": "Synology製のNASのIPアドレスをすべて教えてください。", "expected": ["192.168.1.12", "192.168.1.13"]},
]

API_ENDPOINT = "http://localhost:8000/api/v1/chat"

def run_accuracy_benchmark():
    passed = 0
    results = []

    print("[*] AI事実整合性ベンチマークテストを開始します...\n")

    for tc in TEST_CASES:
        payload = {"message": tc["q"], "reset_history": True} # 会話履歴を毎回クリア
        start_time = time.time()

        try:
            res = requests.post(API_ENDPOINT, json=payload, timeout=20)
            res.raise_for_status()
            reply = res.json().get("reply", "")
        except Exception as e:
            reply = f"ERROR: {str(e)}"

        elapsed = round(time.time() - start_time, 2)

        # Keyword Match 判定: 全ての期待キーワードが含まれているか
        is_pass = all(kw.lower() in reply.lower() for kw in tc["expected"])
        if is_pass:
            passed += 1

        status_mark = "✅ PASS" if is_pass else "❌ FAIL"
        print(f"[{tc['id']}] {status_mark} ({elapsed}s) Q: {tc['q']}")
        if not is_pass:
            print(f"   期待キーワード: {tc['expected']}")
            print(f"   実際のAI返答: {reply[:100]}...\n")

        results.append({
            "id": tc["id"],
            "pass": is_pass,
            "elapsed_sec": elapsed,
            "expected": tc["expected"],
            "reply": reply
        })

    accuracy_rate = (passed / len(TEST_CASES)) * 100
    print(f"\n==========================================")
    print(f"総合スコア: {passed}/{len(TEST_CASES)} 合格 ({accuracy_rate:.1f}%)")
    print(f"==========================================")

    # 結果をJSONとして出力
    with open("accuracy_report.json", "w", encoding="utf-8") as f:
        json.dump({"accuracy_rate": accuracy_rate, "results": results}, f, indent=2, ensure_ascii=False)

    # 合格率100%未満ならCI/CDを落とす
    if accuracy_rate < 100.0:
        exit(1)

まとめ:プロンプトエンジニアリングを「ソフトウェア工学」にする

AIアプリケーションの開発において、最も危険なのは「感覚的なチューニング」です。

  • プロンプトを1行修正したら、自動テストを一括実行する
  • 10項目の必須要件が1つでも欠落したらプルリクエストをマージさせない
  • ハルシネーション(嘘)をCI環境で機械的にブロックする

このテスト自動化スイートを1つ導入しておくだけで、「プロンプトをいじったら他の機能が壊れた」というデグレを100%防止でき、安心して本番環境のAIを改善し続けることができます!


📚 さらに詳しいアーキテクチャ設計・パラメータ表・トラブルシューティング

最後までお読みいただきありがとうございます!
本記事が少しでも「役に立った」「あとで試したい」と感じていただけましたら、ぜひQiitaの 「いいね(♡)」 と 「ストック」、および アカウントのフォロー をいただけると今後の検証・執筆の大きな励みになります!

また、本記事のさらに踏み込んだ 「深層アーキテクチャ解説」「主要パラメータの技術的根拠」「現場アンチパターン・トラブルシューティング対応表」 は、以下のGitHub技術リファレンスにて公開しています。

👉 【第21弾 詳細技術リファレンス (QIITA_ARTICLE_DETAIL_21.md)】

⭐ 全14層・200パターンの自立型インフラ&オンプレAIリファレンス集(おじさんの武器庫)を順次公開中です。ぜひGitHubリポジトリへの Star (★) と X (@OjisanTechnical) のフォローもお願いいたします!
👉 GitHub リポジトリ: ojisantech/ojisan-arsenal

💡 【検証環境・個人研究(PoC)に関する注記】
本記事は、筆者個人の自宅検証環境(ホームラボ/仮想環境)において、公開情報およびOSS(オープンソースソフトウェア)を用いて独自に構築・検証した個人研究(PoC)の記録です。特定の企業・団体の実環境や業務上の非公開情報とは一切関係ありません。

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?