AIハルシネーションとは何か:仕組みとアプリでの検出方法

AI tutorial - IT technology blog
AI tutorial - IT technology blog

必ず直面する問題

こんな状況を想像してほしい。LLMを使って技術的な質問に答えるチャットボットをリリースしたばかり。ユーザーは満足しているが、ある日、AIが存在しないLinuxのフラグを自信満々に説明した、偽のURLのStack Overflowスレッドを引用した、標準ライブラリには存在しないPythonメソッドを列挙した、というレポートが届く。警告もなく、曖昧さもない。ただ、流暢で権威ある、しかし完全に作り話の回答があるだけだ。

これがAIハルシネーションだ。大規模言語モデルを使って開発する際に最初にぶつかる本物の問題のひとつだ。厄介なのはAIが間違えることではない——自信満々に間違えることだ。

これは初心者だけが引っかかるエッジケースではない。本番環境でも一貫して発生し、精度が最も重要な場面——技術ドキュメント、医療情報、法的サマリー、あるいは特定のバージョン番号やコマンド構文に関連するもの——で最も深刻になる傾向がある。

AIハルシネーションとは何か

AIハルシネーションとは、言語モデルがもっともらしく聞こえるが、実際には事実と異なる、でっち上げた、あるいは実データに基づいていないテキストを生成する現象だ。モデルは嘘をついているわけではない。学習データのパターンに基づいて統計的に最も可能性の高い次のトークンを予測しているだけで、そのプロセスのどこにも組み込みのファクトチェッカーは存在しない。

ハルシネーションの主な種類

  • 事実のハルシネーション: モデルが誤った情報を事実として述べる(「Python 3.12にはネイティブのswitch文が追加された」など)。
  • 引用のハルシネーション: 存在しない論文、URL、ドキュメントを引用する。
  • 命令のハルシネーション: 誤ったフラグ、メソッド名、パラメータを含むシェルコマンドやAPIコールを提示する。
  • コンテキストのハルシネーション: 入力プロンプトや提供したドキュメントに一切なかった詳細を捏造する。

なぜ起きるのか

LLMはテキストを予測するように訓練されており、事実のデータベースを参照するわけではない。モデルが知識の限界に達すると、統計的にもっともらしいテキストで空白を埋める。結果は構文的には正しく見えるが、事実はまったく間違っている場合がある。

問題が悪化する状況:

  • トピックがニッチまたは高度に専門的な場合
  • 学習データが古い(ナレッジカットオフ)場合
  • プロンプトが曖昧で、モデルが推測する余地がある場合

コードでハルシネーションを検出する4つの方法

これらのテクニックは、実装コストの低いものから複雑なものまで様々だ。現在のボトルネックに合ったものから始めよう。

1. 構造化出力を強制してバリデーションする

最もシンプルな防衛策:モデルに構造化JSONを返させ、既知のスキーマに対してバリデーションする。モデルが存在しないフィールドや不可能な値をハルシネーションしても、バリデーション層がユーザーに届く前にキャッチする。

import anthropic
import json

client = anthropic.Anthropic()

def ask_with_structured_output(question: str) -> dict:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"""この質問に答え、有効なJSONのみを返してください。
スキーマ: {{"answer": "string", "confidence": "high|medium|low", "sources": ["既知のソースのリスト"]}}

質問: {question}"""
        }]
    )

    raw = response.content[0].text.strip()
    try:
        data = json.loads(raw)
        assert "answer" in data
        assert data.get("confidence") in ("high", "medium", "low")
        return data
    except (json.JSONDecodeError, AssertionError) as e:
        return {"error": f"無効なレスポンス: {e}", "raw": raw}

result = ask_with_structured_output("Linuxの'ls'コマンドで隠しファイルを表示するフラグは何ですか?")
print(result)

モデルが "confidence": "low" を返した場合、それを免責事項を表示するか、クエリを人間のレビュアーにルーティングするシグナルとして扱おう。

2. 実際のソース資料でモデルをグラウンディングする

モデルに記憶から答えさせてはいけない。実際のドキュメントやソースコンテンツを渡し、それだけに基づいて答えるよう指示する。これがRAG(Retrieval-Augmented Generation)の核心的なアイデアだ。ここで紹介するテクニックの中で、最も少ない実装労力で最大の信頼性向上をもたらす。

def ask_with_grounding(question: str, context: str) -> str:
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system="提供されたコンテキストのみに基づいて回答してください。コンテキストに回答がない場合は'提供されたコンテキストには見つかりませんでした'と言ってください。",
        messages=[{
            "role": "user",
            "content": f"コンテキスト:\n{context}\n\n質問: {question}"
        }]
    )
    return response.content[0].text

# 実際のmanページスニペットを使ったグラウンディングの例
man_page_snippet = """
ls - ディレクトリの内容を表示
  -a  .で始まるエントリを無視しない
  -l  長い形式で表示する
  -h  -lと組み合わせて、サイズを人間が読みやすい形式で表示する
"""

answer = ask_with_grounding("lsで隠しファイルを表示するにはどうすればよいですか?", man_page_snippet)
print(answer)

モデルは渡されたものだけを扱う。コンテキストに回答がない場合はそう伝える——回答を作り上げるのではなく。

3. 自己一貫性チェックを実行する

同じ質問を何度か送信し、回答を比較する。複数回の実行で一貫した結果が出れば、モデルがよく知っている内容を想起していることを示す。大きなばらつきは危険信号だ——モデルは確固とした知識に基づいているのではなく、推測しているのだ。

def consistency_check(question: str, runs: int = 3) -> list[str]:
    answers = []
    for _ in range(runs):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=256,
            messages=[{"role": "user", "content": question}]
        )
        answers.append(response.content[0].text.strip())
    return answers

question = "f文字列が導入されたPythonのバージョンは何ですか?"
results = consistency_check(question)

for i, ans in enumerate(results, 1):
    print(f"実行 {i}: {ans[:100]}")

# 不一致をハルシネーションリスクとしてフラグ
unique_answers = set(a[:60] for a in results)
if len(unique_answers) > 1:
    print("\n[警告] 回答が一致しません — 注意して扱ってください")

4. 検証プロンプトを使う

最初の回答を得た後、モデルに自分の回答を批評するよう2回目のリクエストを送信する。組み込みのレビューパスとして考えるとよい。すべてをキャッチするわけではないが、ユーザーに届く前に怪しい主張を浮かび上がらせる。

def verify_answer(question: str, initial_answer: str) -> dict:
    verification_prompt = f"""
元の質問: {question}
提案された回答: {initial_answer}

この回答を慎重にレビューしてください:
1. あなたの知識に基づいて、事実として正確ですか?
2. 不確かかもしれない具体的な主張はありますか?
3. 信頼度を評価してください: high / medium / low

JSONで回答してください: {{"accurate": true/false, "uncertain_claims": [], "confidence": "high|medium|low"}}
"""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{"role": "user", "content": verification_prompt}]
    )
    try:
        return json.loads(response.content[0].text.strip())
    except json.JSONDecodeError:
        return {"error": "検証レスポンスを解析できませんでした"}

実際のパイプラインに統合する

本番環境では、コストと効果の順にこれらを重ねていく:

  1. グラウンディングを優先: 常に関連するドキュメントやコンテキストをプロンプトに注入する。モデルに事実に関する質問を記憶だけから答えさせてはいけない。
  2. 構造化出力: JSONスキーマを使用して、返ってきた内容をプログラム的にバリデーションできるようにする。
  3. 信頼度によるルーティング: 信頼度が低い、または複数回の実行で一貫性がない場合は、フォールバックにルーティングする——よりシンプルなルールベースの回答、「この件に関する信頼できる情報がありません」というメッセージ、あるいは人間によるレビューキューなど。
  4. すべてをログに記録: モデルへのすべての入出力をログに記録する。ユーザーが誤った回答を報告したとき、何が起きたのかを理解するためにフルトレースが必要になる。

最も重要なマインドセットの転換:LLMの出力をユーザー入力と同じように扱うことだ。盲目的に信頼してはいけない。バリデーションし、実データに基づかせ、アプリケーション層に信頼度チェックを組み込む。モデルは自分がハルシネーションしているかどうかわからない——それはあなたのコードの仕事だ。

どこから始めるか

まずグラウンディングを追加しよう。各質問とともに実際のドキュメントやデータベースコンテンツを渡す。ドメイン固有のアプリ——ドキュメントアシスタント、サポートBot、バージョン対応CLIヘルパー——では、その一つの変更でハルシネーションの頻度を半分以上削減できる。

グラウンディングが整ったら、構造化出力バリデーションを追加して、アプリケーションが予期しない形式不正な応答を検出できるようにしよう。検証プロンプトは、精度が重要で多少のレイテンシが許容される高リスクなクエリのために取っておく。

ハルシネーションは言語モデルの仕組みに組み込まれている——次のリリースで修正されるバグではない。しかしこれらのテクニックを使えば、基盤となるモデルが時々間違えても信頼性を維持できるAI機能を構築できる。

Share: