RAGチャットを作っていて、苦労したのは「ハルシネーション検知」
先のブログでRAGを用いてチャットシステムを作った話をした。 今回はその続きで、返ってきた回答がどこまで信じられるかを判定する話。
前回のブログはこちら
社内向けのドキュメントをチャンク化してEmbedding化し、 pgvectorで類似検索して、 その結果をLLMに渡して回答させる。
ここまでは割とよくある話だと思う。
ただ、実際に運用を始めると、 一番問題になったのは検索精度よりも、
「LLMが、コンテキストに無いことを自然に喋る」
ことだった。いわゆるハルシネーションだ。
「それっぽいウソ」が本当に厄介
例えば、検索結果には書いていない内容を、 LLMが勝手に補完して説明してしまう。
しかも厄介なのが、 完全なデタラメではなく、
- 半分正しい
- 一般知識としては正しい
- 文脈的には自然
というケースが多いことだった。
つまり、 人間が読んでもすぐには分からない。
RAGって、結局最後はLLMなので、 検索で正しい情報を持ってきても、 生成時に「盛る」。
ここが思った以上に難しい。
最初は「LLMに判定させればいい」と思った
最初に考えたのは単純で、
回答がコンテキストに沿っているか、 別のLLMに判定させればいい
という方法だった。
実際、 そういう評価ライブラリもいくつか試した。
ただ、やってみると、 これだけでは安定しない。
同じ回答でも、 微妙に判定が揺れる。
temperature を 0 にしても、 完全には安定しない。
あと当然ながらAPIコストも重い。
この辺りから、
「1個の評価軸だけでは危険」
という方向に考えが変わっていった。
最終的には「複数の指標」を組み合わせた
最終的には、
- LLMベースの整合性評価
- NLI系モデル
- BERTScore系の類似度
を組み合わせて、 複数の観点からスコア化していた。
特にBERTScoreは、 単純な文字列一致ではなく、 Embeddingベースで意味的な近さを見られるので、 「言い換え」にそこそこ強かった。
ただ逆に、 BERTScoreだけだと、 “意味は近いけど実は嘘” を取りこぼす。
なので、
- 統計的類似度
- 推論モデル
- LLM判定
を混ぜる構成になっていった。
XGBoost を噛ませたのは、かなりエンジニアリング的判断だった
ここで面白かったのが、 最終判定にニューラルネットではなく、XGBoostを使ったこと。
理由は単純で、
- 学習データがそこまで多くない
- 推論を軽くしたい
- 「特徴量の組み合わせ」を見たかった
から。
例えば、
- BERTScoreは低い
- でもNLI系は高い
みたいな、微妙なパターンが結構ある。
この辺は、決定木系の方が扱いやすかった。
この辺りは、 「AIシステム」というより、 普通に機械学習システムを組んでる感覚に近かった。
意外だったのは「正常な拒否」の扱い
あと実装していて面白かったのが、「回答拒否」の扱い。
最初は、
「分かりません」
は失敗だと思っていた。
でも実際には逆だった。
コンテキストに無いなら、 適当に埋めるより、
「情報がありません」
と返す方が正しい。
なので途中からは、
- ハルシネーション
- 異常
- 正常
を分けて扱っていた。
これ、 実際に運用してみないと、 なかなか気づかないポイントだった。
今振り返ると、「RAGは検索より評価の方が難しい」
RAGというと、 どうしても
- ベクトル検索
- Embedding
- chunking
の話になりがち。
もちろんそこも重要なんだけど、 実運用で本当に苦労したのは、 むしろ「評価」だった。
検索が多少ズレても、 LLMがうまく補正してくれるケースはある。
逆に、 検索が正しくても、 LLMが自然に嘘を混ぜることもある。
だから結局、 最後は
「この回答をどこまで信用するか」
という問題になる。
今でも、 ここはRAGの一番難しい部分だと思っている。
あとがき
キャリア的には、自分はAIの専門家ではない。 インフラ、フロント、バックエンドと、 わりとフルスタック寄りでやってきた人間だ。
ただ、学生時代に ある程度、数値計算をやっていた経験があったので、 この案件に参画させてもらえた、という感じだった。
最初は本当に分からないことだらけで、 正直しんどかった。
それでも、 やってみると面白かった。
まあ、正直にいうと、ここで紹介した構成なんかもAIの教科書をお手本にしたもので、オリジナリティなど何もない。 私は研究者ではなく技術者なのだからそこは割り切っていた。
今はもう、 AIを「実装で使う」側ではあるけれど、 AIそのものを作る仕事はしていない。
機会があれば、 またこういう仕事をやってみたいと思う。