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?

# 【続編】# AWS BedrockでYouTubeライブ配信コメントをリアルタイム判定してみた話

0
Posted at

はじめに

本記事では、YouTubeライブ配信のコメント自動判定で、Bedrockの誤検知をルールとプロンプトで改善した結果を解説します。あわせて、YouTube Data API v3のクォータ枯渇でコメント取得が止まっていた問題を、liveChatMessages.streamへの移行で改善した話も書きます。

本記事は前回記事の続編です。システムの全体像と技術選定の根拠(ECS vs Lambda、Claude Haiku 4.5、CRIS)は前回記事に書いてみましたので、ぜひ見てみてください。


この記事で得られること

  • Bedrockの誤検知をルールとプロンプトで潰した結果と、プロンプト1ブロックが判定に与えた想定外の影響
  • 正解ラベルを削除・BANアクションから直接取る方法
  • YouTube Data API v3のクォータ枯渇をliveChatMessages.streamで解いた結果(実測値つき)
  • 実配信8,573件の実測値(レイテンシ中央値2.86秒・フォールバック0件)

対象読者

  • YouTube Data API v3でライブチャットを扱う方
  • LLMによるテキスト判定の精度評価に取り組んでいる方
  • AWS(ECS / Lambda / Bedrock)で常時稼働のパイプラインを組む方

目次

  1. 前回の課題と、今回やったこと
  2. 改善①:ルールによるスタンプ誤検知の除去
  3. 改善②:systemプロンプトへのゲームタイトル注入
  4. 改善③:APIクォータ枯渇でコメント取得が止まる問題
  5. 精度評価に必要な正解データ
  6. 実測結果(コメント8,573件)
  7. 今回の実装から得た学び
  8. 使用技術スタック

前回の課題と、今回やったこと

前回記事の末尾で挙げた課題は3つです。今回はそれぞれに手を入れました。

前回挙げた課題 今回の対応 結果
スタンプパターン(:_*:)がFPの主因だった ルールベースとAI側の2段でスタンプを除去 解決(AI到達を14.2%削減)
ジャンル固有語を誹謗中傷と誤判定する(「タネマシンガン」問題) 配信の概要欄からゲームタイトルを取得し、systemプロンプトに注入 部分的(対照実験でBLOCK推奨73→22件。死亡表現は残る)
正解ラベルが取れず精度評価が成立しない 削除・BANアクションを直接読む方式に作り直し 取得できるようになった
配信を最後まで取得できていなかった(原因はAPIクォータの枯渇) 取得方式をliveChatMessages.listのポーリングからstreamへ移行 解決(4時間20分の全区間を取得)

2つ目が今回の中心です。プロンプトに文脈を1ブロック足した結果、狙っていない領域の判定まで動きました。3つ目については、精度が測れなかった原因が評価スクリプトではなく、システム側が配信を最後まで取得できていなかったことでした。その原因はAPIクォータの枯渇で、4つ目として対応しています。


改善①:ルールによるスタンプ誤検知の除去

誤検知の主因

前回のFP(システムがBLOCK推奨としたが実際は問題なかったケース)は169件でした。主因はYouTubeのスタンプ機能が使う:_*:形式のテキストパターンです。スタンプの種類によっては、AIがこのパターンを何らかの単語として解釈し、有害と判定していました。

前処理によるスタンプ除去

rule_engine.pyにスタンプ除去の正規表現を追加しました。除去後の残りがOKワードだけなら、その時点でCLEARに確定します。

STAMP_PATTERN = re.compile(r":_[^:\s]+:|:[a-z][a-z0-9-]*:")

without_stamp = STAMP_PATTERN.sub("", message)
had_stamp = without_stamp != message
# 除去後の残りがOKワードだけなら CLEAR で確定させる
return ["stamp"] if had_stamp else ["ok_word"]

AI Moderator(Lambda)側にも同じ除去を実装し、除去後が空文字の場合はBedrockを呼びません。

clean_message = STAMP_PATTERN.sub("", comment.get("message", "")).strip()
if not clean_message:
    # スタンプ/絵文字のみのコメントは Bedrock を呼ばず CLEAR とする
    return BedrockResult(ai_score=0, ai_reason="スタンプ", ai_result="CLEAR", ...)

同じ処理が2箇所にあるのは冗長に見えます。ルール側のOKワードリストはSSMで運用中に変更されるため、ルールをすり抜けたスタンプのみのコメントがAIに届きます。AI側の除去は判定を正すだけでなく、Bedrock呼び出し自体を止めるのでコストも下がります。

実配信での結果

後述する実配信8,573件のうち、stamp理由でCLEARになったコメントは1,221件(14.2%)です。この1,221件はAIに到達していません。

今回の配信は正解データを取得していないため、FPが何件減ったかは示せません。示せるのはAIに渡さずに処理できた件数だけです。


改善②:systemプロンプトへのゲームタイトル注入

ジャンル固有語の誤判定

前回のFPで2番目に多かったのは、ジャンル固有語の誤判定です。典型例が「タネマシンガン」で、ポケモンの技名が「マシンガン(銃器)」と解釈され、スコア8がついていました。ゲーム配信では「〇〇を倒した」のような表現も日常的に流れます。

コメント単体をAIに渡す限り、この種の誤判定は原理的に避けられません。配信の文脈をAIに渡すようにしました。

処理の流れ

配信の取得開始時に配信メタデータを取得し、分類結果をSSMへ保存します。AI Moderatorはその結果を読んでプロンプトに注入します。

[配信の取得開始]
   │ ① videos.list で title / description / tags と
   │    liveStreamingDetails.activeLiveChatId を取得
   │ ② Bedrock で {category, game_title} に分類
   │ ③ SSM のパラメータに JSON 保存
   │ ④ activeLiveChatId で取得開始
   ▼
[AI Moderator Lambda]
   │ ⑤ SSM の配信コンテキストを TTL キャッシュ付きで読込
   │ ⑥ カテゴリから固定文言を組み立て、system プロンプトの2ブロック目に注入

設計上の判断

判別はAIに任せ、注意文はコードで固定しました。

分類器の出力は{"category": "chat|singing|game|other", "game_title": "..."}の2項目だけです。注意点の文章までAIに生成させると配信ごとに文言が揺れ、判定の挙動が安定しません。プロンプトに入れる文言はカテゴリから固定文で組み立てます。

効果検証の設計

文脈ブロックが誤判定を解消するかを確かめます。同じコメントを、文脈あり/なしの両方で判定して比較します。

本番8,573件の判定は、文脈ブロックなしの条件で記録されていました。文脈ブロックなしのプロンプトで再実行すると、記録値と74件中74件で一致します。文脈ブロックありでは74件中20件です。

再実行の条件 本番記録値との一致
文脈ブロックなし 74 / 74
文脈ブロックあり(ゲーム名あり) 20 / 74
文脈ブロックあり(ゲーム名なし) 7 / 24

build_context_text() は、categoryother のときとSSM未設定のときに空文字を返します。本番はこの経路を通っていました。

本番データはそのまま対照群として使えます。同じコメントを文脈ありで再判定すれば、その差分が文脈ブロックの効果になります。

検証結果と副作用

AI判定済み5,379件を文脈ブロックありで再判定しました。モデル・temperature・max_tokensは本番と同一です。

指標 文脈なし 文脈あり
BLOCK推奨(スコア7以上) 73件 48件
うち誤検知が解消 42件
うち新たに7以上へ上昇 17件

誤検知は42件が解消しました。狙いどおりの効果です。一方で、7未満だった17件が新たに7以上になっています。

上昇した17件のうち11件は容姿と人格に関するものでした。文脈ブロックの文面はゲーム用語のことしか書いていませんが、ゲーム以外の判定基準まで厳格化されています。

コメント 文脈なし 文脈あり
子持ちの人妻 0(中立) 7(不適切)
歯茎だけはダメだって… 2(感想) 7(不適切)
綺麗でも清潔感がないんじゃない? 6(外見批判) 8(容姿批判)

プロンプトに1ブロック足すと、意図した範囲を超えて判定が動きます。

プロンプト短縮による副作用の抑制

文言を5案作り、535件の評価セットで比較しました。評価セットは本番7以上の73件、新たに7以上になった17件、閾値直下(スコア4〜6)の45件、低スコア帯からの無作為抽出400件です。閾値直下を入れたのは、6点のコメントが文言変更で7に跳ねるためです。

採用したのは次の文言です。

if category == "game":
    if game_title:
        return (
            f"【配信の文脈】「{game_title}」のゲーム実況。"
            "ゲーム用語・ゲーム内の出来事は有害としない。"
            "判定は実在の人物に向けた発言に限る。"
        )

変更前との差は2点です。緩和の説明を圧縮し、「判定は実在の人物に向けた発言に限る」を追加しました。この1文が容姿・人格への厳格化を抑えます。

全5,379件で検証した結果です。

指標 文脈なし 変更前の文言 採用した文言
BLOCK推奨 73件 48件 22件
新たに7以上(既知の誤検知を除く) 0件 0件
文脈ブロックのトークン数 105 73
1コメントあたり入力トークン 186 291 259

トークン数はBedrockのレスポンスusage.input_tokensから取得しました。文脈ブロックは30%、1コメントあたりの入力は11%減ります。AI判定6,660件の配信で約21万トークンの削減です。

短縮しても誤検知の解消は進みました。

コメント 文脈なし 適用後
キノコ取って死のう 9(自殺示唆) 0(ゲーム内)
踏むだけじゃ足りない 8(暴力的) 0(ゲーム内容)
やばい殺される 7(暴力的表現) 0(ゲーム内)
アクロバティック死 7(暴力的表現) 0(ゲーム内)
ばかぢから覚えるのかこいつ 7(誹謗中傷) 3(軽い悪口)
しねんとかみつくが抜群か 7(誹謗中傷) 0(ゲーム用語)

判定理由のラベルも整理されました。5,379件のユニーク数は、文脈なしで344種、変更前の文言で207種、採用した文言で120種です。文言を絞ると理由の表現も収束します。

残った誤判定

採用後もBLOCK推奨は22件あります。うち8件はゲーム内の出来事を指すと読めるものです。

コメント 文脈なし 適用後 実際の意味
死に急ぎ野郎が… 9 7 マリオが何度も死ぬことへのツッコミ
生き急ぎ野郎 7 7 同上
いきるのを諦めてませんか? 8 8 ゲーム内キャラへの言及
サイコが漏れる幼女 10 8 「サイコ」はポケモンの技名

「死」「命」「自殺」を含む表現は、ゲーム文脈を与えてもスコアが下がりきりません。安全側に倒す挙動としては妥当ですが、ゲーム実況では継続的に誤検知が出ます。

配信文脈を理由にこれらを一律CLEARへ倒すと、本物の自殺示唆を見逃す穴になります。見逃しリスクを1配信のデータで評価できないため、対応は保留にしています。

なお上記の分類は筆者の目視によるもので、正解ラベルではありません。


改善③:APIクォータ枯渇でコメント取得が止まる問題

前回記事では、コメント取得をliveChatMessages.listのRESTポーリングとして紹介しました。この方式では、配信開始から約1.1時間で403 quotaExceededに達して取得が止まります。listpartの指定内容によらず1回5ユニット固定で、日次10,000ユニットを2秒間隔で回すと約2,000回で使い切るためです。前回「配信を最後まで取得できていなかった」原因はこれでした。

取得をliveChatMessages.stream(サーバーストリーミング)へ移行しました。1本の接続でサーバーpushを受け続けるため、リクエストは接続の張り直しでしか発生しません。RESTバインディングがあるのでrequestsのまま実装でき、レスポンス型はlistと同一のLiveChatMessageListResponseです。partも変えていないため、判定処理以降とDBスキーマは無改修で済みました。

streamの単価は公式に文書化されていません。GCPの「割り当てと上限」のQueries per day実績(7,036 / 10,000)と実リクエスト数から、listと同じ5ユニット/回と確定しました。

list stream
1リクエストの周期 3.21秒 11.7秒(接続10.3+再接続1.4)
リクエスト数 約1,110回/時 約310回/時
消費ユニット 5,550ユニット/時 1,550ユニット/時
10,000ユニットの持ち 約1.8時間 約6.4時間

1本の接続は約10.3秒でサーバー側から切られ、クライアントが約1.4秒で張り直しています。保持時間はサーバーが決めるため、クライアントからは指定できません。

リクエスト回数は約1/3.6になりましたが、1日6.4時間という上限は残ります。4時間超の配信を1本取ると、同じ日に2本目が取れません。


精度評価に必要な正解データ

前回の評価手法の限界

前回記事の精度評価では、配信後にライブチャットを再取得し、システムの判定結果と突合しました。配信中にブロックされたコメントは再取得できません。「システム側にあって再取得側にないコメント」を擬似的なブロックラベルとして扱いました。

結果は混同行列でTP=0、FN 202件です。FNを目視しても有害表現は見つからず、再取得漏れの可能性が高いという結論でした。正解ラベルの取得精度が低く、評価そのものが成立していません。

正解データの取得方式の変更

アーカイブのライブチャットには、削除・BANの操作がアクションとして記録されています。今回はこのアクションを直接読む方式に変えました。

アクション 意味
markChatItemAsDeletedAction 個別コメントの削除
markChatItemsByAuthorAsDeletedAction 投稿者のBAN(そのユーザーのコメント一括削除)

削除理由のテキストで削除種別も区別できます。retractを含む場合は、投稿者本人による取り下げです。[message deleted]などは、モデレーターまたは配信主による削除を表します。

def classify_deletion(deleted_state_message_text):
    if not deleted_state_message_text:
        return "moderator_or_owner", "モデレーター/配信主による削除(推定)"
    if "retract" in deleted_state_message_text.lower():
        return "self", "投稿者本人による削除"
    return "moderator_or_owner", "モデレーター/配信主による削除"

「残っていないから消されたのだろう」という推測より、削除アクションを直接読むほうが確度は上がります。投稿者の自主削除をモデレーション成果として数える誤りも避けられます。

この取得はアーカイブ経由のため、配信中のリアルタイム取得には影響しません。


実測結果(コメント8,573件)

実際のライブ配信でシステムを稼働させました。配信内容と配信者情報は公開しません。

基礎集計

指標 実績値
総コメント数 8,573件
計測時間 4.34時間(4時間20分)
平均コメントレート 0.55件/秒
ルールOK(AI不要) 4,699件(54.8%)
└ OKワードのみ 3,478件
└ スタンプ除去でCLEAR 1,221件
GRAY(AI送り) 3,874件(45.2%)
BLOCK推奨 45件(全体の0.52% / GRAY比1.16%)
フォールバック発生 0件

4時間20分ぶんのコメントを、最初から最後まで取得できました。

GRAY率は45.2%で、前回の54.2%より下がっています。配信の性質による差も含むため、スタンプルールだけによる差とは言い切れません。スタンプ由来のCLEARは1,221件(14.2%)記録されており、この配信でAI呼び出しを1,221回減らしています。

処理性能

設計目標は「コメント発生からブロック推奨フラグ保存完了まで30秒以内」です。起動直後10秒間はpageToken引き継ぎのバックログで遅延が跳ねるため、定常状態のみで集計しました(n=8,443)。

指標 中央値 95%ile 99%ile 最大値 標準偏差
コメント発生 → システム受信 1.90秒 2.33秒 2.59秒 3.66秒 0.24
コメント発生 → 判定結果のDB保存完了 2.86秒 3.86秒 4.23秒 8.15秒 0.55

経路別の内訳(DB保存完了までの中央値)は、ルール確定(OK/NG)が2.90秒、AI判定(GRAY)が2.84秒です。AIを通しても通さなくてもほぼ同じ速度で終わっています。SQSとBedrockの往復が約0.9秒に収まっているためです。

受信ラグの標準偏差は0.24秒、最大値でも3.66秒です。中央値だけでなく分散も小さく収まっています。

前回の実測値は受信4.0秒/DB保存完了6.0秒(いずれも中央値)です。別配信・別コメントレートのため直接比較はできません。前回は8.29件/秒、今回は0.55件/秒で、負荷が2桁違います。上の表は、設計目標30秒に対して1桁の余裕がある点を示すものです。

AIスコアの分布

スコア 件数 割合(GRAY内)
0 2,915 75.2%
1 379 9.8%
2 376 9.7%
3 116 3.0%
5 2 0.1%
6 41 1.1%
7以上(BLOCK推奨) 45 1.16%

スコア0〜2に94.7%が集中しています。閾値7以上のBLOCK推奨は45件で、AI判定件数に対して1.16%、全コメントに対して0.52%です。前回はAI判定件数に対して0.9%で、ほぼ同じ水準です。

スコア4は0件、5は2件で、3と6の間に谷があります。Claude Haiku 4.5は「無害」と「有害」をはっきり分ける傾向があり、閾値7の妥当性は分布からも見て取れます。これは1配信の傾向です。

この配信での精度評価の限界

この配信では混同行列を出せていません。DB側で観測した人間による削除・BANが0件で、アーカイブからの正解データ取得も未実施です。

前回とは状況が違います。前回は配信の一部しか取得できておらず、突合の土台がありませんでした。今回は4時間20分の全区間がシステム側に揃っています。あとは荒れた配信のデータを取って突合するだけです。


今回の実装から得た学び

プロンプト1ブロックの影響範囲

systemプロンプトに文脈ブロックを1つ足すと、書いていない領域の判定まで動きました。ゲーム用語の緩和だけを狙った文言が、容姿や人格への判定を厳格化しています。プロンプトを変えたら、狙った範囲だけでなく全体を測り直す必要があります。


使用技術スタック

  • 言語:Python 3.14
  • コンテナ:Amazon ECS on EC2(t3.small)
  • AI:Amazon Bedrock(Claude Haiku 4.5 / CRIS)
  • キュー:Amazon SQS
  • DB:Amazon RDS PostgreSQL 17(db.t3.micro)
  • 設定管理:AWS Systems Manager Parameter Store / AWS Secrets Manager
  • IaC:AWS CDK
  • コメント取得:YouTube Data API v3(liveChatMessages.stream
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?