はじめに
自己紹介
株式会社LITALICOでWEBエンジニアとして働いている @ti_aiuto です。
普段は主に個人向けのWEBサービスの開発のサポートを担当していて、特にモノリスなアプリケーションを持続可能な形に保つことや、データモデリング・データ分析に関わる諸々の整備に関心があります。
最近は技術の面からプロダクト・事業を進化させる仕事にも取り組んでいて、レコメンデーション・全文検索・生成AIの活用といった技術の検証〜導入も行っています。
なんの話か
生成AIを活用したUGC機能の「コンテンツモデレーション」(法令や規約に違反するコンテンツの自動検知)を実装した話です。事前の調査・検証から、リリース後の運用まで一連の流れをご紹介できればと思います。
課題
弊社のメディア・ポータルサイトの中には、ユーザ同士がコメントやレビューを投稿し合ったり、質問・回答などのコンテンツを投稿しあったりといったUGC機能を持つものがあります。
そうしたUGC機能において、一部のユーザが利用規約に反する投稿を行うことがあり、中でも他のユーザを意図的に不安・不快にさせるような悪質な投稿への対応に苦慮していました。
ユーザによる違反報告機能や社内のカスタマーサポート担当者による巡回により、発見したものに関してはその都度対応していたのですが、人力での対応には限界があり、特に連休には対応が遅れる状況にありました。
そうした中、昨今の生成AIの進歩もあり、明らかに利用規約に反していると分かるものに関しては、自動で検知する仕組みを作れるのではないか?という話が挙がり、導入に向けて動いていくことになりました。
調査・検証
作りたいのは 「投稿内容」を入力として与えて、「違反判定結果」を出力として得る という仕組みです。これを実現するために検討した内容をご紹介します。
出力の構造化の方法
生成AIを使って判定結果を得る上では、出力をJSONなどの何らかの構造化された形式で取得できたほうが便利です。例えば次のようなイメージです。
{
"summary": "<投稿本文の要約>",
"result": <判定結果フラグ>,
"reason": "<判定理由>"
}
調べてみた結果、 Tool Use (Function Calling) という仕組みを使えば簡単に実現できることが分かりました。
簡単に言うと、生成AIがTool(関数のようなもの)を呼び出すための引数を生成する仕組みがあり、この仕組みを利用することで、事前に与えたデータ構造の定義(JSON Schema)の通りに結果をJSON形式で出力させられるというものです。
次の記事ではメールの内容を要約してJSON形式で出力させる例が紹介されています。
Generating JSON with the Amazon Bedrock Converse API
Learn how to return JSON output from large language models using Amazon Bedrock Converse API’s tool use capabilities.
既に社内で活用事例があったAmazon Bedrock(AWSが用意している生成AIを呼び出せる便利サービス)では、このTool Useも含めてAPIでサポートされていることが分かったので、この方法を活用して実装できそうなことが分かりました。
リスク・懸念の検討
生成AIを実プロダクトに導入するうえで懸念される点として次のような点があると思います。
個人情報や機微情報の入力による情報流出の危険性
これに関しては、入力データがモデルの学習に利用されないよう設定するのはもちろんのこと、元々投稿されたコンテンツは全会員が閲覧可能なので、その全会員が閲覧可能なコンテンツを生成AIが読み込むこと自体に新たな危険性は無いと判断しました。
誤出力への対処
誤出力の問題は、偽陽性(問題のないコンテンツが問題ありと判定される)と偽陰性(問題のあるコンテンツが問題なしと判定される)の2つに分けられます。
偽陰性については、現状の違反報告機能や巡回を廃止するわけではないので、仮に発生したとしてもそこでカバーできるはずです。
偽陽性については、仮に発生すれば公開可能なコンテンツの公開が遅れてしまいますが、そのリスクよりも利用規約に反したコンテンツを一刻も早く非公開にすることのメリットが大きいと判断し、偽陽性を許容して導入することにしました。
プロンプトインジェクションによる不正操作の危険性
一般的に推奨される対策を行うのはもちろんのこと、仮にプロンプトインジェクションにより不正な操作が成功したとしても、想定しているアーキテクチャ・実装においては、最悪のケースでも仕組みが正常に動作せず非公開にするべきコンテンツが非公開にできないというだけなので、大きな懸念はないと判断しました。
以上のことから、新たな仕組みを導入するうえでの大きなリスク・懸念は無いと判断し、実装に向けた準備を進めていくことにしました。
正解データの準備とヒアリング
作った仕組みを評価するための正解データを揃えていきます。過去の投稿について、社内での協議により規約違反だと判定されたものと、されなかったものをそれぞれをピックアップした上で、投稿内容・判定理由・他のユーザのリアクションといった要素を踏まえて、想定内の・典型的な事例か?生成AIで判定するのに向いていそうか?という点から正解データとして使いやすそうな投稿を抽出していきました。
過去に違反判定がされたものの中には、利用規約のどの部分に違反しているのか分かりづらいものもあったため、カスタマーサポートの担当者の方々に利用規約の解釈や運用の考え方、当時の判断の背景や根拠も含めて教えていただき、プロンプト作成の参考にしていきました。
混同行列による評価とプロンプトの改善
正解データがある程度揃ったところで、試作したプロンプトを使って実際に判定を行い、偽陽性・偽陰性が発生した箇所について、想定の範囲内・許容範囲内かを確認していきました。
初回の検証の結果を混同行列としてまとめたものがこちらです。
個人的な感覚としては、思ったよりも精度が出ていた印象でした。
その後何回か利用規約や運用の実態を踏まえてプロンプトを調整したり、JSONの構造を調整したりして、現実的に実導入が可能な構成を模索していきました。
次の例では、偽陰性は減りましたが偽陽性が増えています。ある程度偽陰性と偽陽性の両方を減らせないか工夫した上で、偽陰性と偽陽性のどちらに寄せるかを考えながら最終的な傾向を調整していくという流れで進めていきました。

特に難しいと思ったのは、一口に「利用規約に反している」と言っても、「明らかに悪意があると分かる」というものから、「医療や服薬など専門的な知識が必要な内容について誤解を招きかねない表現がある」「地域名や診断の詳細な情報を記載することで投稿者の不利益につながる恐れがある」といったものまで、様々な観点や根拠、考え方があるということです。どこまでを今回の検知対象にするべきなのか?その線引きは本当に可能なのか?という点で、検討と試行錯誤が続きました。
プロンプトは自然言語で書いているので、ある意味ではとっつきやすいですが、細かい言い回しの違いでここまで変わるのか…と気を遣う部分もありました。
モデルの選定
「判定結果と判定根拠について実用レベルの精度で出力ができるか」「指定した形式通りのJSONを安定的に出力できるか」「利用料金と呼び出し制限(1分あたり◯回など)は許容範囲内か」といった観点からいくつかの基盤モデルを比較検討した結果、Claude 3.7 Sonnetを使うことに決定しました。
モデルによっては正常に動くときは動くのに、時々エラーを返したり、指定したJSONの形式に従わなかったりすることがあり、安定的に・予測可能な挙動をするのか?という点も重要な要素だということが分かりました。
実装
違反検知後の運用の検討
実際に生成AIにより違反が検知された後に、ユーザにどのように見せるのか?社内のカスタマーサポート担当者がどのように対応するのか?といった、実際の運用を検討・設計していきます。
この点については、プロダクトマネージャーとアプリケーション側の実装を担当したもう一人のエンジニアが進めてくれました。
最終的に決まった運用の流れとしては、違反判定となったコンテンツはまず自動的に非公開に変更し、カスタマーサポートの担当者が後日社内で協議を行ったうえで、最終的な公開・非公開を決定するというものです。あくまで生成AIの補助を受けるだけで、最終判断は人間が行うという点は変わりません。
実導入を想定したアーキテクチャ設計
初期リリースの時点では、最終的に次のような構成を採用しました。
ポイントを簡単にご紹介します。
非同期呼び出し
生成AIによるコンテンツモデレーションですが、検証の段階で一回当たり20秒前後の時間がかかることが分かっていました。これだと投稿のリクエストの処理と同時に実行するのは現実的ではないため、Sidekiqによる非同期呼び出しを行うことに決めました。
リリース後には生成AIの呼び出しが稀に失敗したり想定よりも長時間かかることもあったため、非同期化と自動的な再試行が実装できていたのが功を奏したと思います。
API Gateway & Lambdaによる基盤整備
次の2つの理由から、API GatewayとLambdaを使ったAPIを間に挟みました。
- 生成AIが想定以上に青天井で呼び出されることを防ぎたい
- Amazon Bedrockには、「月あたり・一日当たり何回」のような上限を設定できる機能が見当たらなかったため、その代わりにAPI Gatewayの「使用量プラン」という機能で一定時間たりの呼び出し回数の上限を設定するようにしました
- 指定したJSON Schemaへの準拠を保証したい
- 生成AIがTool Useにより出力したJSONですが、モデルによっては稀に指定した形式を逸脱した結果が返ってくることがありました。アプリケーション側でこれを毎回チェックするのは煩雑なので、API Gateway/Lambda側でJSONがスキーマに準拠しているかバリデーションする処理を入れて、「必ず指定したJSON Schemaの通りに返却してくれるAPI」として実装することにしました
- これにより、仮にプロンプトインジェクションで生成AIが予期しない挙動をしたとしても、想定外の値がアプリケーション側に返ることも防げます
後々このAPIは廃止することになるのですが、初期リリースのミニマム構成としては一定の役割を果たしてくれたと思います。(単にAPI Gatewayと使用量プランを使ってみたかったというのもありますが…)
モニタリング体制
生成AIの呼び出し状況や呼び出し結果を後からモニタリング・検証するために、次の2つの観点から実装を行いました。
- どのようなリクエストに対してどのようなレスポンスを返したのか
- 誤判定やバグがあった際に調査できるよう、呼び出し履歴のログを一種の監査ログとしてBigQueryに集約することにしました
- 処理時間・エラーの発生状況
- 事前の検証では20秒前後の想定かつ基本的にはほぼ失敗しないと見込んでいましたが、実態としてどうなのかをDatadogのAPMで記録するようにしました
アプリケーション側の実装とリリース
プロンプトの作成とアーキテクチャ設計、ユーザ・社内向けの運用設計ができたところで、実アプリケーションの実装に入っていきました。
ここについてはもう一人のエンジニアが実装して自分は主にレビューを担当するのみだったのですが、新たな仕様と運用を的確に理解した上で考慮漏れについて改善案を提案したり、状態遷移図など図に整理したり、古のコードを読み解き適宜リファクタリングしながら新仕様の実装を追加したりなど、大変な開発だったと思います。見事にやりきってくれました。
その後
無事にリリースを迎えた後、追加で対応した内容についてご紹介します。
不安定なレスポンス時間
リリース後、基本的には想定通り安定的に稼働していたのですが、稀に生成AIのレスポンスが異様に遅く、API Gateway側の2分間のタイムアウトを超過してしまう場合がありました。
その後検証してみると、投稿内容が特定の内容だと、事前の想定の20秒を大幅に超過することが分かりました。内容によっては生成AIが考え込んでしまうということでしょうか…
一時的な対処としては、このケースの場合だけは、若干精度は落ちるものの高速に処理できる別のモデルにフォールバックすることで対応することにしました。
モデルのEoL対応
生成AIの基盤モデルにもEoL(サポート終了)というものがあるらしく、新たなバージョンに追随していく必要があります。
その対応のため、モデルを変更したときに判定精度が悪化していないか検証するスクリプトを実装して、モデルごとに性能を比較できる手順を準備しました。
次がモデルごとの判定結果の一例です。
これにより、新たに採用するべきモデルを具体的な根拠に基づいて決定できるようになり、モデル変更に対応しやすくなりました。
LiteLLM(LLMプロキシ)の導入
その後数ヶ月運用を続けていく中で、また、他の機能における生成AIの導入の話がいくつか持ち上がってきたタイミングで、既存のAPI Gatewayによる単純な基盤だけでは不十分な点が浮かび上がってきました。
- 別の機能でも生成AIを呼び出すときに、使用するモデルや必要なトークン量・呼び出し頻度などの呼び出し特性が異なるため、単純な呼び出し回数だけでQuotaを管理するのは困難になってきそう
- 生成AIの呼び出しの失敗時の対応や別モデルのフォールバックについて、アプリケーション側の各機能で個別に考慮するのは煩雑
- 生成AIの利用方法について、単なるJSON化だけではなく単純なテキスト返却や複数Toolの選択、テキストではなく画像をプロンプトに渡すなど、より多様な使用方法が出てきそう
こういった課題が見えてきた時点で、他社事例も含めて調査していくと、LiteLLMという一種のプロキシを導入することが適切だと分かりました。仕組みとしては、アプリケーションとAmazon Bedrockの間に「LLMプロキシ」を挟むことで、使用量の管理やエラー処理などをよしなにやってくれるというものです。
このプロダクトの機能のうち、特に便利なのは次の点です。
- 事前に発行したAPIキーごとに、使用可能なモデル・期間あたりの課金量の上限を設定したり、キーごとの使用状況を確認したりできる
- Amazon Bedrock等の生成AIのAPIがエラーを返したときに、エラーのステータスコードに応じてリトライしたり、別のモデルにフォールバックするよう設定したりできる
- Datadogと統合して生成AIの呼び出し状況をモニタリングできる
変更後の構成は次のようになります。
この変更により、API Gateway/Lambdaで行っていたJSONのバリデーションをアプリケーション側で行うよう変更する必要がありましたが、別件でJSON Schemaを検証する仕組みを既に導入していたため、その流用により最小の工数で新方式に切り替えることができました。
最後に
担当者からの声
新たな仕組みを導入してから、カスタマーサポート担当の方からはポジティブな声を頂くことができました。これまでの人力のパトロールではチェックしきれていなかった部分も含めてチェックできるようになったという声も頂き、よりユーザが安心して利用できるサービスに一歩前進したのではないかと思います。
生成AIの使いどころ
この企画を推進しておきながら、個人的にはAIに監視されながらサービスを使うというあり方には違和感を感じないでもないなと思っていたのですが、今回のように明確に利用規約に反している投稿を検知するために使うことに関しては、利点はとても大きいと感じました。
ユーザの皆さんがサービスを安心して使える環境を損なわないという意味でもそうですし、社内のカスタマーサポートの方にとっても、「悪質な投稿が自分の知らないうちにまた増えているかもしれない」と考えるだけでも、安心して休まらない部分もあると思います。(実際過去には休日出勤してまで対応くださっていたこともあったようです。)
今回の開発を足がかりにして、人間の強みと生成AIの強みを活かした技術の活用・プロダクトの進化の方向性を考えていけたらなと思うところです。



