RAGチャットを作っていて、苦労したのは「ハルシネーション検知」

先のブログでRAGを用いてチャットシステムを作った話をした。 今回はその続きで、返ってきた回答がどこまで信じられるかを判定する話。

前回のブログはこちら

社内向けのドキュメントをチャンク化してEmbedding化し、 pgvectorで類似検索して、 その結果をLLMに渡して回答させる。

ここまでは割とよくある話だと思う。

ただ、実際に運用を始めると、 一番問題になったのは検索精度よりも、

「LLMが、コンテキストに無いことを自然に喋る」

ことだった。いわゆるハルシネーションだ。


「それっぽいウソ」が本当に厄介

例えば、検索結果には書いていない内容を、 LLMが勝手に補完して説明してしまう。

しかも厄介なのが、 完全なデタラメではなく、

というケースが多いことだった。

つまり、 人間が読んでもすぐには分からない。

RAGって、結局最後はLLMなので、 検索で正しい情報を持ってきても、 生成時に「盛る」。

ここが思った以上に難しい。


最初は「LLMに判定させればいい」と思った

最初に考えたのは単純で、

回答がコンテキストに沿っているか、 別のLLMに判定させればいい

という方法だった。

実際、 そういう評価ライブラリもいくつか試した。

ただ、やってみると、 これだけでは安定しない。

同じ回答でも、 微妙に判定が揺れる。

temperature を 0 にしても、 完全には安定しない。

あと当然ながらAPIコストも重い。

この辺りから、

「1個の評価軸だけでは危険」

という方向に考えが変わっていった。


最終的には「複数の指標」を組み合わせた

最終的には、

を組み合わせて、 複数の観点からスコア化していた。

特にBERTScoreは、 単純な文字列一致ではなく、 Embeddingベースで意味的な近さを見られるので、 「言い換え」にそこそこ強かった。

ただ逆に、 BERTScoreだけだと、 “意味は近いけど実は嘘” を取りこぼす。

なので、

を混ぜる構成になっていった。


XGBoost を噛ませたのは、かなりエンジニアリング的判断だった

ここで面白かったのが、 最終判定にニューラルネットではなく、XGBoostを使ったこと。

理由は単純で、

から。

例えば、

みたいな、微妙なパターンが結構ある。

この辺は、決定木系の方が扱いやすかった。

この辺りは、 「AIシステム」というより、 普通に機械学習システムを組んでる感覚に近かった。


意外だったのは「正常な拒否」の扱い

あと実装していて面白かったのが、「回答拒否」の扱い。

最初は、

「分かりません」

は失敗だと思っていた。

でも実際には逆だった。

コンテキストに無いなら、 適当に埋めるより、

「情報がありません」

と返す方が正しい。

なので途中からは、

を分けて扱っていた。

これ、 実際に運用してみないと、 なかなか気づかないポイントだった。


今振り返ると、「RAGは検索より評価の方が難しい」

RAGというと、 どうしても

の話になりがち。

もちろんそこも重要なんだけど、 実運用で本当に苦労したのは、 むしろ「評価」だった。

検索が多少ズレても、 LLMがうまく補正してくれるケースはある。

逆に、 検索が正しくても、 LLMが自然に嘘を混ぜることもある。

だから結局、 最後は

「この回答をどこまで信用するか」

という問題になる。

今でも、 ここはRAGの一番難しい部分だと思っている。


あとがき

キャリア的には、自分はAIの専門家ではない。 インフラ、フロント、バックエンドと、 わりとフルスタック寄りでやってきた人間だ。

ただ、学生時代に ある程度、数値計算をやっていた経験があったので、 この案件に参画させてもらえた、という感じだった。

最初は本当に分からないことだらけで、 正直しんどかった。

それでも、 やってみると面白かった。

まあ、正直にいうと、ここで紹介した構成なんかもAIの教科書をお手本にしたもので、オリジナリティなど何もない。 私は研究者ではなく技術者なのだからそこは割り切っていた。

今はもう、 AIを「実装で使う」側ではあるけれど、 AIそのものを作る仕事はしていない。

機会があれば、 またこういう仕事をやってみたいと思う。