背景
本アプリにはAI関連機能(論題提案、論題分析、AIディベート対戦、AI判定)があるが、現状は「形式的なバリデーション/サニタイズ」はある一方で、意味的に明らかに不正な入力や判定不能なディベートでも外部LLM呼び出しが走り、トークン/遅延が無駄になり得る。
また、Queue Job の再実行/多重 dispatch により、同一debate/turnでの二重課金・二重生成が起こり得る。
対象(コード上の入口)
- 論題提案/論題分析:
app/Http/Controllers/Api/TopicSuggestionController.php → app/Services/OpenRouter/TopicGeneratorService.php
- AIディベート対戦(応答生成):
app/Jobs/GenerateAIResponseJob.php → app/Services/AIService.php → app/Services/OpenRouter/DebateOpponentMessageBuilder.php
- AI判定:
app/Jobs/EvaluateDebateJob.php → app/Services/AIEvaluationService.php → app/Services/OpenRouter/DebateEvaluationMessageBuilder.php
目標
- 明らかな不正入力/判定不能ケースでは 外部LLMを呼ばずに早期リターンする
- 多重実行に対して 冪等性を担保し、同一debate/turnでの二重生成を防ぐ
- プロンプトに載る履歴の**合計サイズ上限(文字数cap)**を追加し、長文1発話等での爆発を防ぐ
- 早期リターン理由/入力規模をログで観測できるようにする
共通ガードレール案(全AI機能)
- 意味不明入力の軽量検出(ヒューリスティック)
- 空/記号のみ/同一文字連打/実質的な文字が極端に少ない/文字種がほぼ無い 等
- 目的は「明らかに違うものを弾く」で、厳密判定はしない
- 論題形(resolution shape)の検査
- 例: JA「〜すべき/是非/禁止/導入/廃止/規制/義務/許可/優先」等、EN「should/ban/legalize/abolish/is better than/THBT」等
- サイズ制限(合計文字数cap)
- 既存の
history_limit は発話数上限のみなので、合計文字数も別途必要
- 冪等性/排他
debate_id + turn 単位の Cache lock
- 「既に生成済み/保存済みならスキップ」のDBチェック
- 観測
request_type 単位で skip_reason / 入力文字数 / 履歴文字数 / lock 取得可否 をログ化(本文はログしない)
機能別の提案
A. 論題提案(keywords → generate)
- 原則: 422で落とすより keywordsを無視して一般生成へフォールバック(UX維持)
- ただし明確なスパム(記号のみ/連打/極短)だけは 422 も可
- 同一条件の連打対策として短TTLキャッシュを検討
B. 論題分析(base_topic → info)
- 対象が必須なので 不正なら422でreject(フォールバックしない)
C. AIディベート対戦(応答生成)
GenerateAIResponseJob 実行前に lock + 既存AI発話チェックで 二重生成を防止
- 質疑ターンは多重dispatchされやすいので、直前質問の存在などを検証
DebateOpponentMessageBuilder に 合計文字数cap(古い履歴を落とす/1発話を切る)
D. AI判定(評価)
AIEvaluationService::evaluate の OpenRouter 呼び出し前に、論題/議事録の薄さ等を検証し LLMを呼ばずに is_analyzable=false を返す(トークン節約の主目的)
- lock + 既存評価チェックで冪等化
DebateEvaluationMessageBuilder に合計文字数cap
- JSON repair は既に1回/低max_tokensだが、そもそも「評価不能」ケースでは repair に入る前に早期リターンする
実装候補(入口別)
- 判定の早期リターン:
app/Services/AIEvaluationService.php
- 対戦の冪等性:
app/Jobs/GenerateAIResponseJob.php(+必要なら app/Livewire/Debates/MessageInput.php)
- 論題分析の422:
app/Http/Controllers/Api/TopicSuggestionController.php(type=info)
- AIディベート作成時の論題検証:
app/Http/Requests/AI/AIDebateCreationRequest.php
- 履歴文字数cap:
app/Services/OpenRouter/DebateOpponentMessageBuilder.php / app/Services/OpenRouter/DebateEvaluationMessageBuilder.php
受け入れ条件(例)
- 意味不明な論題/議事録が薄い場合、AI判定で外部LLMが呼ばれず
is_analyzable=false が保存される
GenerateAIResponseJob が同一 debate_id + turn で重複して走っても、AI発話が複数保存されない
- 履歴の合計文字数が上限を超える場合でも、リクエストが破綻せずに短縮された履歴で実行される
- 早期リターンが発生したとき、
skip_reason がログに残る
追加メモ
app/Jobs/EvaluateDebateJob.php::failed() のフォールバックは debate_evaluations のカラムと一致しないキー(summary 等)を使っているため、今回の整備と合わせて整合を確認する。
背景
本アプリにはAI関連機能(論題提案、論題分析、AIディベート対戦、AI判定)があるが、現状は「形式的なバリデーション/サニタイズ」はある一方で、意味的に明らかに不正な入力や判定不能なディベートでも外部LLM呼び出しが走り、トークン/遅延が無駄になり得る。
また、Queue Job の再実行/多重 dispatch により、同一debate/turnでの二重課金・二重生成が起こり得る。
対象(コード上の入口)
app/Http/Controllers/Api/TopicSuggestionController.php→app/Services/OpenRouter/TopicGeneratorService.phpapp/Jobs/GenerateAIResponseJob.php→app/Services/AIService.php→app/Services/OpenRouter/DebateOpponentMessageBuilder.phpapp/Jobs/EvaluateDebateJob.php→app/Services/AIEvaluationService.php→app/Services/OpenRouter/DebateEvaluationMessageBuilder.php目標
共通ガードレール案(全AI機能)
history_limitは発話数上限のみなので、合計文字数も別途必要debate_id + turn単位の Cache lockrequest_type単位でskip_reason/ 入力文字数 / 履歴文字数 / lock 取得可否 をログ化(本文はログしない)機能別の提案
A. 論題提案(keywords → generate)
B. 論題分析(base_topic → info)
C. AIディベート対戦(応答生成)
GenerateAIResponseJob実行前に lock + 既存AI発話チェックで 二重生成を防止DebateOpponentMessageBuilderに 合計文字数cap(古い履歴を落とす/1発話を切る)D. AI判定(評価)
AIEvaluationService::evaluateの OpenRouter 呼び出し前に、論題/議事録の薄さ等を検証し LLMを呼ばずに is_analyzable=false を返す(トークン節約の主目的)DebateEvaluationMessageBuilderに合計文字数cap実装候補(入口別)
app/Services/AIEvaluationService.phpapp/Jobs/GenerateAIResponseJob.php(+必要ならapp/Livewire/Debates/MessageInput.php)app/Http/Controllers/Api/TopicSuggestionController.php(type=info)app/Http/Requests/AI/AIDebateCreationRequest.phpapp/Services/OpenRouter/DebateOpponentMessageBuilder.php/app/Services/OpenRouter/DebateEvaluationMessageBuilder.php受け入れ条件(例)
is_analyzable=falseが保存されるGenerateAIResponseJobが同一debate_id + turnで重複して走っても、AI発話が複数保存されないskip_reasonがログに残る追加メモ
app/Jobs/EvaluateDebateJob.php::failed()のフォールバックはdebate_evaluationsのカラムと一致しないキー(summary等)を使っているため、今回の整備と合わせて整合を確認する。