🤖

MCPでもハルシネーションは起きる ~MCPサーバを作ってみて~

に公開

久しぶりの投稿です。たけうちひでゆき(@chimerast)です。

最近の流行りに乗って、試しに自社サービスでMCPサーバを軽く作ってみたところ、ハルシネーションが発生してこれどうするんだとお悩みの投稿です。

知見が足りないので解決法やベストプラクティスを集めたいという願望で書いています。

プロダクトについて

弊社イエソドでは、企業における業務効率化やシステムのアカウント管理の源泉とする、企業に所属する全ての人や組織の「過去・現在・未来」を管理するIdentityマスタを構築するSaaS「YESOD」を提供しています。

起きた事象


MCPサーバ「イエソド君」との会話

ダミー従業員データを返すMCPサーバから、木内 覚さんの情報を引き出す際に「木内 覚さんの情報を教えてください。」というよくありそうなメッセージでLLM経由でMCPサーバに問い合わせたところ、名前と性別以外、全然違う情報が返ってきました。s.kiuchi@yesod.coというMCPサーバのレスポンス上にどこにも存在しない情報まで勝手に生成されました。

「木内 覚さんのレコードを返してください。」というメッセージに対しては、正しい情報を返してくれています(たぶん)。

また、従業員数を聞いたところ、実際には500以上のレコードがあるにもかかわらず、「200人です。」ときっぱりと答えてくれます。こちらは、LLMの性質上正しく返すには、本当は従業員数を返す専用のエンドポイントを用意するべきかもしれません。

MCPサーバの構成

バックエンド

MCPサーバ自体の実装は比較的簡単です。弊社ではKotlinで開発しており、WebフレームワークとしてKtorを利用しています。MCPの公式レポジトリにkotlin-sdkというものがあり、Ktor用のプラグインも用意されているためこちらを使ってみています。まだ枯れているライブラリではないため、運用には注意が必要だと思います。

    server.addToolWithContext(
        name = "list_members",
        description = """
            企業に所属するメンバー一覧を返します。
            entity_idはメンバーのIDを表し、valid_startとvalid_endはレコードの有効期間を表します。
        """.trimIndent()
    ) { request, context ->
        val results = EntityHistoryApplication(context).membersHistory(listOf())
            .map { member -> TextContent(member.toJson()) }

        CallToolResult(
            content = results
        )
    }

このようなコードに対し、MCPサーバのレスポンスとしては下記のようなものとなります(実際の情報量はもう少し多いです)。

{"entity_id":"q7u2n-_oTJKZPpvXP7s19Q","valid_start":"2019-01-01T00:00:00.000Z","valid_end":"9999-12-31T23:59:59.999Z","姓":"児玉","名":"千咲","メールアドレス":"chisaki.kodama@yesod.co","会社":[{"会社":"株式会社イエソド","社員番号":"YD0026"},{"会社":"株式会社YDC","社員番号":"UV0001"}]}
{"entity_id":"sVnmlu2HRou_GzfNiSMG-g","valid_start":"2019-01-01T00:00:00.000Z","valid_end":"9999-12-31T23:59:59.999Z","姓":"大淵","名":"拓也","メールアドレス":"takuya.oobuchi@yesod.co","会社":[{"会社":"株式会社イエソド","社員番号":"YD0027"}]}
{"entity_id":"EZOEf6rNToKKNuOWmLEgeA","valid_start":"2019-01-01T00:00:00.000Z","valid_end":"9999-12-31T23:59:59.999Z","姓":"三上","名":"希","メールアドレス":"nozomi.mikami@yesod.co","会社":[{"会社":"株式会社イエソド","社員番号":"YD0031"}]}
...

Slackでの会話

一旦プロトタイプなので、n8nを利用して簡単にチャットボットを作っています。Slackの入力を元にAI Agentが動いて、その結果をSlackにメッセージとして返すという単純なワークフローを組みました。

Open AIのモデルはgpt-4.1-miniを使用しています。モデルをより高性能なものにすれば正しい情報を返してくれるかもしれませんが、LLMの性質上、問題は何かしらの形で再現すると思います。

Gemini Deep Researchで問題について調べた

Gemini Deep Resarchを使用してMCPのハルシネーションについて聞いてみたところ、RAGの事例や研究などを基に回答してくれました。RAGもMCPも推論時に外部の関連情報をLLMに提供し、応答を現実に「Grounding」させるという点では同等のことを行っている技術と思っています。

Gemini Deep Rearchによる結果の全文は以下になります。
https://docs.google.com/document/d/1r71wS4olFl0nQyrNz65FCaztOtKNhdLhQ3BHmjWFiwU

忠実性のハルシネーション vs. 事実性のハルシネーション

RAGシステムの出力が誤っている場合、その原因は根本的に異なる2つのタイプに分けられます。

  • 忠実性のハルシネーション(Faithfulness Hallucination): 生成された出力が、提供されたソースコンテキストと矛盾する場合です。この問題の根源は、生成器(Generator)、つまりLLM自体、あるいはプロンプトの構築プロセスにあります。モデルが提供されたコンテキストを無視したり、誤解したり、あるいは過度に装飾したりすることで発生します 。これは「教科書に書いてあることを読み間違える」ことに相当します。
  • 事実性のハルシネーション(Factuality Hallucination): 生成された出力はソースコンテキストに忠実ですが、そのソースコンテキスト自体が誤っている、古い、または偏っている場合です。その結果、出力は現実世界の事実と矛盾します。この問題の根源は、**ナレッジベース(Knowledge Base)または検索器(Retriever)**にあります 。これは「教科書自体が間違っている」ことに相当します。

今回の事象は「忠実性のハルシネーション」の方にあたります。LLMが勝手に装飾したことにより、他のデータを読み込んだり、ありもしないメールアドレスを生成してしまったりしたものと考えられます。

じゃあどうするべきか

上記Gemini Deep Rearchによる結果を読む限り、「高度なプロンプトエンジニアリング」や「MLOpsスタイルの評価パイプラインへの投資」が必要なようです。

主要な結論

  • RAGはLLMを接地させるための強力なアーキテクチャパターンですが、単純なプラグアンドプレイの解決策ではありません。それはハルシネーションの問題を変容させますが、根絶はしません。
  • RAGにおけるハルシネーションは、システムレベルの課題です。失敗は、データ取り込みから最終的な生成まで、パイプラインのあらゆる段階で発生し得ます。
  • これらの失敗を診断するには、特に事実性(悪いデータ)と忠実性(悪い生成)の区別という、ニュアンスに富んだ理解が必要です。
  • 信頼できるRAGシステムを構築することは、データ品質、検索アルゴリズム、プロンプトエンジニアリング、そして最も重要な堅牢な評価にわたる、継続的な改善の反復プロセスです。

このあたりは多くの経験や事例が必要な領域になるため、少なくとも安易にMCPサーバを提供するのは避けた方が良い気がします。また、MCPサーバを利用する際にもハルシネーションが起きる可能性を考慮して、少しでも変だと感じたら、元データをあたるようにした方が良いと思います。

まとめ

実プロダクトにおいて、MCPサーバのハルシネーションを根絶するためのテクニックを知りたいです。

弊社プロダクトでは、マスタデータを扱うという性質上、確実に正しいデータを格納・提供する必要があり、LLMとは相性悪いのかなと思っていますが知見が欲しいです。

Discussion