AIに商品を選ばせたらトイレットペーパーだらけになった話 ── 「AIの判断」の前に機械的な関連性フィルタを置くという設計パターン
前提
個人開発で、AIに商品情報を収集・審査させて紹介コンテンツを作らせる、という自動化を運用しています。ある時、AIに「良さそうな商品候補を選んでください」と任せたら、需要はあるが自分たちのテーマとはまったく関係ない一般消費財(トイレットペーパーのような季節性の高い生活必需品)ばかりが選ばれる、という事態になりました。
この記事では、固有の内部システム名やIDは出さず、原因の切り分け方と、そこから確立した実装パターンを一般化してまとめます。実際のアフィリエイトリンクや特定商品の紹介は行いません(設計パターンの話に絞ります)。
何が起きていたか
調査してわかった原因は、1つではなく複数が重なっていました。
- 検索キーワードの選び方が、季節性の強いキーワードに引っ張られていた。結果として、検索で見つかる候補自体が、季節性の一般消費財に偏っていた。
- 「関連度スコア」という名前のスコアが、実際には需要・信頼性・収益性を測る別の軸だった。名前だけ見ると「テーマとの関連性」を測っていそうに見えるが、実態は違う軸のスコアで、関連性そのものは一切評価されていなかった。
- 最終審査(AIによる判断)のプロンプト自体にも、「テーマに合っているか」を判定する基準が含まれていなかった。
つまり、「関連性を評価する工程」がパイプラインのどこにも存在しなかったのです。需要があって、信頼できて、利益率も良い商品なら、テーマと無関係でも高スコアがついて通ってしまう構造でした。
なぜAIの審査だけでは防げなかったか
最初に疑ったのは「AIの審査(最終判断)が甘いのでは」ということでした。しかし、これは的外れな疑いでした。AIに渡している判断材料(プロンプト)自体に、関連性を判定する基準が書かれていなかったので、AIがどれだけ賢くても、そもそも評価しようがなかったのです。
ここで実感したのが、「AIが後から判断してくれるから大丈夫」という設計そのものが誤りだった、ということです。AIの判断力に期待する前に、判断させたい軸そのものを明示的に評価対象として渡しているかを確認する必要があります。
採用した設計パターン: 関連性フィルタを「最初の関門」にする
対処方針は、AIによる主観的な判断(最終審査)の前に、機械的・決定論的な関連性フィルタを第一関門として置くことです。疑似コードで書くとこういう形です。
// Before: 候補を集めて、いきなりAIの主観判断(最終審査)にかける
async function selectCandidates(rawCandidates) {
const scored = rawCandidates.map(c => ({ ...c, score: computeDemandScore(c) }));
const approved = await aiJudge(scored); // 関連性の基準がプロンプトに無い
return approved;
}
// After: AIの判断より前に、安価な決定論フィルタを通す
async function selectCandidates(rawCandidates) {
// 第一関門: 機械的な関連性判定(キーワード一致・カテゴリ一致など、
// AIを呼ばない、決定論的で説明可能なロジック)
const relevant = rawCandidates.filter(c => isRelevantToTheme(c, THEME_DEFINITION));
// 関連性が無いと判定された候補は、この時点でAIへ渡さない
const scored = relevant.map(c => ({ ...c, score: computeDemandScore(c) }));
const approved = await aiJudge(scored, { themeContext: THEME_DEFINITION });
return approved;
}
ポイントは3つです。
- 関連性の判定を、AIの主観的な最終判断とは別の、独立した決定論ロジックにする。キーワード一致・カテゴリ一致など、後から「なぜ通った/弾かれたか」を説明できるロジックにする。
- この関門を、AIの判断より前(パイプラインの上流)に置く。後段のAI判断にプロンプトで頼るのではなく、そもそも関係ない候補をAIへ渡さないようにする。
- 既存のスコア(需要・信頼性・収益性など)とは、明確に別軸として扱う。名前がそれらしくても、実際に何を測っているかをコードで確認する。
実際にこのフィルタを導入した後、それまで承認されていた既存候補を新しいロジックで再評価したところ、既存候補の全件が関連性スコア0と判定され除外されました。つまり、それまで通っていた候補は、1件の例外もなく「本来は関連性が無いのに通っていた」ものだったことになります。この結果自体が、フィルタが機能している証拠にもなりました。
テストでどう検証したか
このパターンを検証する上で書いたテストケースの分類です(具体的な関数名・ファイル名は割愛します)。
| テストカテゴリ | 確認すること |
|---|---|
| 明確に無関係な候補の除外 | テーマと無関係なカテゴリの候補が、関連性フィルタで確実に弾かれる |
| 明確に関連する候補の通過 | テーマに合致する候補が、誤って弾かれない |
| 境界的な候補の挙動 | テーマとの関連が薄い(ゼロではない)候補が、意図した基準で通過/除外される |
| スコアの軸の独立性 | 需要・信頼性スコアが高くても、関連性が無ければ通過しない |
| フィルタ通過後のAI審査への影響 | 関連性フィルタを通過した候補だけが、AI審査のプロンプトへ渡る |
| 既存データでの回帰確認 | 既存の承認済み候補群を新ロジックで再評価し、除外される件数・理由が想定と一致する |
特に「既存データでの回帰確認」は、実際にそれまで通っていた候補群を新しいロジックへ通してみることで、フィルタ導入前は何が起きていたかを定量的に可視化できるという点で、単体テストとは別の価値がありました。
なぜこの種の設計ミスに気づきにくいか
- スコアの「名前」と「実際に測っている内容」がズレていても、動いているように見える。名前が「関連度スコア」であれば、それが関連性を測っていると誰も疑わずに使い続けてしまいます。
- AIの最終判断が入っていると、「そこで弾かれるはず」という安心感が生まれやすい。しかし判断材料に基準が無ければ、AIは判断のしようがありません。
- 個々の候補を見ると「まあ需要はあるし、悪くはないか」と思えてしまうため、大量の候補を俯瞰して初めて「テーマと無関係なものばかり」というパターンに気づける、という性質があります。
まとめ
- AIに「候補を選んでください」と丸投げする前に、判断材料(この場合は関連性の基準)がそもそも渡っているかを確認する。「AIが後で判断してくれる」という設計は、判断材料が無ければ成立しない。
- スコアの名前と実際に測っている内容が一致しているかを、コードレベルで確認する。名前だけで信用しない。
- AIの主観的な最終判断の前に、決定論的で説明可能な関連性フィルタを第一関門として置く設計パターンは、AIの判断力に頼らずに「そもそも文脈に合わない候補」を構造的に排除できる。
- フィルタ導入の効果は、既存データを新ロジックで再評価する回帰確認によって、定量的に確認できる。
AIに何らかの選定・審査を任せる自動化を設計している方の参考になれば幸いです。
検証条件と限界
このパターンは、実際にそれまで蓄積されていた候補群を新しい関連性フィルタで再評価し、想定どおりの判定結果になることを確認したうえで採用しています。一方で、次の点は限界として明記します。
- 「関連性」をどう定義するか(キーワード一致で十分か、より高度な意味的な近さが必要か)は扱うテーマによって異なり、この記事のロジックがそのまま流用できるとは限りません。
- 決定論フィルタ自体の基準(何を「関連あり」とみなすか)は、人が設計・メンテナンスする必要があります。フィルタ自体が古くなれば、同種の問題が形を変えて再発しうる点は、継続的な見直しが必要な限界として残ります。