0
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?

AIチャットボットに「根拠がなければ答えさせない」を実装する。ハルシネーションを、諦めて止める

0
Posted at

BYOK(利用者が自分のAPIキーを入れて使う形)でAIチャットボットを作っていたとき、実装の途中で、ふと手が止まりました。

もし、誰かが悪意を持って、自動化したリクエストを一晩じゅう投げ続けたら、どうなるんだろう。そのAPIの請求は、自分ではなく、このプラグインを入れてくれたサイト運営者に届きます。自分の書いたコードのせいで、他人が、知らないうちに課金される。想像して、ぞっとしました。

そのとき気づいたんです。AIを自分のプロダクトに載せて配るというのは、機能を足す話ではなくて、他人の損失に責任を持つ話なんだ、と。

自分でAIを使うぶんには、失敗しても自分が損するだけです。トークンを溶かしても、変な回答が返ってきても、被害者は自分ひとり。でも載せる側になると、損をするのは、こちらを信用してインストールしてくれた人になる。この非対称が、設計の重さを変えました。

作りながら分かったのは、避けて通れないものが3つあることでした。利用者の鍵をどう預かるか。利用者の財布に、どう天井をつけるか。AIの出力を、どこまで信用しないか。どれも「あとで足す機能」ではなく「最初に決める設計」で、しかも紹介ページには書かれない類のものです。順に、何を選んで、どこを妥協したかを書きます。

1. 鍵:預かるなら、平文で置かない

AIを動かすにはAPIキーが要ります。ここで最初の分岐がありました。自分がまとめて払うか、利用者に自分のキーを入れてもらうか。

BYOKにしたのは、自分が代理で払うと、使われるほど自分が赤字になるからです。個人開発で、他人の使用量を自分の財布で受け止めるのは、無理があります。

そのかわり、キーを預かる責任が生まれました。update_option に平文で入れるのは、やめました。DBだけが漏れる経路は、現実にいくつもあります。他のプラグインのSQLインジェクション、バックアップの置き忘れ、共有サーバーでの露出。そのときに平文のキーがあると、そのまま使われる。

なので、wp-configの salt から鍵を導出して、AES-256-GCM で暗号化して保存しています。鍵をコードに直書きしたら誰でも復号できるし、DBに置いたら一緒に漏れる。ファイル側にある salt から導く、というのが落としどころでした。

ただ、これは万能ではありません。サーバーのファイルまで読まれたら、salt も読めるので復号されます。フルに侵入された場合には効かない。それでも入れているのは、実際の事故が「DBだけ」の形で起きることが多いからです。全部は守れない。起きやすい形だけ塞ぐ。

2. 財布:最悪いくらか、を言えるようにする

冒頭でぞっとした、あの部分です。

最初はレート制限を考えました。でも、これでは足りません。連打は止まりますが、30秒に1回を丸一日、行儀よく投げられたら素通りします。守りたいのは「連打されないこと」ではなく「請求が想定を超えないこと」でした。見るべきは頻度ではなく、総量です。

だから、1日あたりの利用上限をサーバー側で持って、超えたらAIを呼ばずに定型メッセージを返すようにしました。呼んでから弾いても、お金はもう出ているので、呼ぶ前に判定するのが肝です。カウントはDB側でアトミックに加算します。読んで足して書き戻す実装だと、同時アクセスで数え落とす。攻撃されている時ほどリクエストは重なるので、いちばん数えてほしい場面で失敗する。ここは自分が実際に踏んで、直しました。

これも完璧ではありません。匿名の訪問者はIPで数えるしかなく、同じ職場からなら複数人が1人に見えるし、IPを変えられれば回避されます。ただ、目的は個人の識別ではなくて、最悪の請求額に天井をつけることでした。上限に達したらAIを呼ばない。その一点さえ守れば、何をされても請求は決めた線で止まります。「最悪いくら」が言えるようになると、AIチャットボットは急に置きやすくなりました。

3. 出力:賢く見せることを、諦める

3つ目は、AIの答えそのものです。

チャットボットが知らないことを聞かれると、それらしい嘘を作ります。存在しない返品条件を案内する。やっていないサービスを「あります」と答える。訪問者は、それをサイト公式の回答として受け取ります。つまり、AIの嘘が、そのままサイト運営者の嘘になる。

そこで「根拠のある回答のみ」というモードを作りました。質問に対してサイト内のコンテンツを検索して、十分な根拠が見つからなければ、AIに答えさせずに「サイト内に該当情報が見つかりません」と返す。それだけです。

これは、賢く見せる機能ではありません。賢く見せることを諦める機能です。作っていて、少し変な気分でした。せっかくAIを載せているのに、答えさせない方向の実装をしている。でも、サポート窓口に必要なのは、博識な嘘つきではなく、正直な案内係のほうだと思っています。

あわせて、LLMの返答は外部入力として扱っています。ユーザー入力は疑うのに、それをLLMに通しただけの出力を信じてしまう。自分はこれを二重trust問題と呼んでいますが、実際にこれで一度やられました。返答をそのまま画面に出していて、構文としては何も間違っていないのに、危ない。

ここでの妥協は、答えられる範囲が狭くなることです。「分かりません」と言う回数が増えて、派手さは落ちる。それでも、間違ったことを自信満々に言うより、ましだと判断しました。「分からない」と言えるボットは、答えたときの信頼が上がる、という副産物もありました。

3つに共通していたもの

書き終えてから気づいたのですが、この3つの判断は、全部同じ形をしていました。

鍵は、最悪DBが漏れても平文では出ない。財布は、最悪攻撃されても上限で止まる。出力は、最悪分からなくても嘘は言わない。どれも「うまくいったときに素晴らしい」ではなく、「まずいときに、どこまでで止まるか」を設計している。最悪のケースを、利用者が見積もれるようにする、という一点で、3つはつながっていました。

面白いことに、この手の設計は、製品の紹介ページではあまり語られません。並ぶのは高精度、多機能、かんたん導入。怖さの話は、売り文句にならないからだと思います。でも、導入をためらっている人の頭の中にあるのは、機能の数ではなく、この手の不安のはずなんです。

次の自分に渡すメモ

  • 使う側と載せる側は違う。載せる側の失敗は、信用してくれた他人が損をする
  • 鍵を預かるなら平文で置かない。ただし全部は守れない。起きやすい事故の形だけ塞ぐ
  • 財布は頻度ではなく総量に天井を。呼ぶ前に判定する。カウントはアトミックに
  • 出力は外部入力として扱う。根拠がなければ答えさせない。賢く見せるのを諦める勇気
  • 3つに共通するのは「最悪のケースを、利用者が見積もれること」

AIを載せるのは、機能を足すことよりも、他人の損失に責任を持つことでした。派手さのない設計ばかりですが、この3つを最初に決めておいたおかげで、夜はよく眠れています。


実装の細部(鍵の暗号化、1日あたりの利用上限のカウント、根拠のある回答のみモード)は、別記事に書きました。

ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
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
0
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?