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まわりのことを書いています。