ソリューション本部エンタープライズ事業部スポーツプロダクト部開発グループ のあるぱかです。ベイスターズ戦 観戦アプリ 『BAYSTARS STAR GUIDE』 のサーバサイド開発を担当しています。
『BAYSTARS STAR GUIDE』 に、試合中にAIが解説を届ける「BASE☆BLUE」を実装しました!各打席ごとに数秒で、データに基づいた熱のある解説を生成します。本記事では、Gemini × Cloud Tasks × Cloud Runで構築したスケーラブルな基盤と、運用を通じて磨いた4つのプロンプト設計の問題点と解決策を紹介しようと思います。
ハマスタでも試合中継でも観戦体験を届けたい
『BAYSTARS STAR GUIDE』 は、元々「ハマスタで観戦を思いっきり楽しむなら」というコンセプトのもと、横浜スタジアムでの観戦体験を豊かにする機能を提供してきました。ただ、すべてのファンが横浜スタジアムに足を運べるわけではありません。
この課題を解決し、「ハマスタでも試合中継でも。ベイスターズ戦 観戦アプリ」という新しいコンセプトを実現するため、「BASE☆BLUE」を開発しました。「BASE☆BLUE」では、試合中にAIが解説を自動生成してリアルタイムに配信することで、横浜DeNAベイスターズを勝利に導くためのポジティブな解説を目指しました。
今回は具体的な設計や技術的な話は控えめにして、プロンプト設計に重点を置いてお話ししようと思います。
本記事のポイント
AIも人間と同じです。情報は整理して渡し、状況に応じた指示を出し、間違えたら具体的に教えます。そして良い見本を少しだけ見せる方が、大量の見本より効果的になります。
ベイスターズ戦 観戦アプリ 『BAYSTARS STAR GUIDE』 の新機能「BASE☆BLUE」は、試合中にAIが数秒で熱い解説を届けるシステムです。運用を通じて磨いたプロンプト設計の工夫は以下の通りです。
- プロンプトに与える情報は階層で整理する
- 状況ごとにプロンプトを分ける
- AIに「型」を与えることで自由と制御を両立する
- プロンプトだけに頼らず、アプリケーションでも制御が必要
AI解説を支える全体像
ここでは、プロンプト設計を中心に据えつつ、それを支えるシステムの全体像を整理します。 リアルタイム性とスケーラビリティを両立するため、アプリ上の他機能の実装でも実績のある技術スタックを中心に構築しました。
全体の処理フロー
「BASE☆BLUE」の解説生成は、試合データの受信から配信まで、以下の4つのステップで処理されます。
AI解説で使っているモデルは gemini-2.5-flash
です。
- 選手が打席に立ったなどの試合中のイベントをトリガーに、Firestoreの試合データが更新されます。この更新を受けて、Cloud Tasksに非同期処理のタスクが投入されます。
- Cloud Tasksから、Cloud Run上のアプリサーバ(コールバックAPI)が呼び出されます。
- Cloud Run内で、Gemini 2.5 Flash APIを呼び出し、解説文の生成を依頼します。生成された解説文は、独自の品質チェック(詳細は後述)を通過しない場合、最大3回まで再生成を試みます。
- 最終的な解説文はPostgreSQLに履歴として永続化され、Firestore Realtime Databaseを通じてリアルタイムにアプリに配信されます。
すでに他機能で使っていた技術をベースに、信頼性のある構成を取りました。 Cloud Tasksはミッション機能の更新などの非同期処理で安定して動いており、Firestoreも試合状況表示で使っていて、アプリへの即時反映が可能です。
Cloud TasksによるインテリジェントなAI生成制御
Cloud Tasksは、試合データ更新のレスポンスを待たずに非同期に処理を進めます。しかし、「BASE☆BLUE」では単なる非同期処理以上の役割を担っています。
試合状況のデータは、BSOカウント(ボール、ストライク、アウト)の変化だけでも配信されます。ただし、カウントの変化ごとにAI解説を毎回再生成すると頻度が高すぎるため、打者、投手、走者状況が変わった時にフォーカスしてAI解説を生成する方針にしました。この制御を Cloud Tasks のtaskID機能で実現しました。打者の選手ID、投手の選手ID、各塁のランナー状況から changeKey を生成し、SHA-256でハッシュ化します。このハッシュ値を含む taskID で Cloud Tasks への投入を試みると、taskID が重複すれば自動的にタスク投入が失敗します。つまり、監視対象外のデータ変更(BSOカウントのみの変化など)では、システムが自動的に処理をスキップする仕組みです。
function generateCommentaryTaskId(
gameId: string,
pageId: string,
situation: CurrentSituation
) {
const keyString = `${situation.batter.playerId}-${situation.pitcher.playerId}-${situation.runners.firstBase}-${situation.runners.secondBase}-${situation.runners.thirdBase}`;
const hash = crypto.createHash('sha256').update(keyString).digest('hex');
return `ai-commentary-${gameId}-${pageId}-${hash}`;
}
この設計により、重複したAI解説生成が完全に排除され、コストとレスポンスタイムを最適化しています。Gemini 2.5 Flash は、高速に生成し、平均約3秒のリアルタイム性を達成しています。(データ更新〜解説文タスク投入〜生成〜配信の総遅延を考慮し、次打者交代から初球が投じられるまでのインターバル内に収まるという観点で妥当と判断)
情報の階層化(データの渡し方)
人間が何かを説明するとき、自然に「全体 → 中間 → 詳細」という順序で情報を整理します。しかし、プログラムでデータを扱うとき、私たちはデータベースから取得した情報をそのまま平坦に並べがちです。
野球の試合には、膨大な量のデータが存在します。
- 現在のスコア、イニング、両チームの先発投手情報などの試合全体に関わるデータ
- より細かい粒度に各イニングの得点状況、現在の表裏といったイニングデータ
- イニングよりももっと細かく、打者と投手の基本成績、対戦成績、状況別成績(対左右投手、得点圏打率など)、カウント、や試合の塁情報などの打席ごとのデータ
これらすべてのデータをプロンプトに詰め込むと、数千文字の巨大なプロンプトになります。どのデータが試合全体に関するもので、どのデータが現在の打席だけに関するものか、AIが正確に判断できなくなり大量のデータの中から、今の打席で本当に重要な情報をAIが見落としてしまいます(人間が見ても同じだと思います)
例えば、こんな状況を想像してください
現在3回表、スコアは3-2でベイスターズがリード。
打者は~~選手、打率.285、本塁打15本、対左投手打率.320...
投手は~~投手、防御率3.50、対右打者被打率.280...
1回表の得点は...、2回裏は...、3回表は...(延々と続く)
すべてのデータを並列に並べると、AIは「今、どの情報が一番重要なのか?」を判断できません。
そのため、プロンプトに魔法をかけました。
プロンプトを3つの階層(試合レベル、イニングレベル、打席レベル)に分けて構造化したのです。
階層1: 試合レベルの情報
試合全体の文脈を提供します。この層では、どういう試合なのかといった大局的な情報を提供します。
## 試合全体の情報
- 試合日: 2025-09-27
- 開始時刻: 14:00
- スタジアム: 横浜スタジアム
- ビジターチーム(先攻): 相手チーム, 先発投手: XX投手
- ホームチーム(後攻): ベイスターズ, 先発投手: ジャクソン投手
..etc
階層2: イニングレベルの情報
現在のイニング状況を提供します。この層では、「今どのイニングか」「このイニングで何が起きているか」を伝えます。直前のイニング情報も渡すことで、そのイニングだけではなく直前の状況を踏まえて出力させるようにしています。
## イニング情報
- 現在のイニング: 1回表
- 攻撃チーム: ~~~
- 守備チーム: ~~~
- この回の先頭打者の打順: 1番
- この回の打順レンジ: 1 ~ 3
- この回に予想される打者: ~~選手、~~選手、~~選手
## 直前のイニング情報
- 得点が入ったか: true
..etc
階層3: 打席レベルの情報
現在の打席の詳細情報を提供します。この層では、具体的な投手や打者のデータ、その塁情報や成績などを明確にわたします。
## 打席状況
- イニング: 1回表
- アウトカウント: 2
- 打者: ~~選手、右打ち
- 投手: ~~投手、左投げ
- 塁状況:
- 1塁: ~~選手
- 2塁: None
- 3塁: ~~選手
..etc
AIに対しても、人間が理解しやすい形で情報を構造化することが、最も効果的かなと感じました。AIだから特別な形式でと考えるのではなく、「人間の解説者ならどう情報を整理するか?」を考えることが、良いプロンプト設計につながります。
モデル選択
基盤からGoogle Cloud を使っているので Geminiは導入しやすく、モデル選定では最初から Gemini系列を中心に考えていました。そのときに出てくるのが、「Flash と Pro、どちらを使うか?」という話です。
生成AIモデルの選定にあたっては、速度、品質を主軸に評価しました。以下は検証結果の比較表です。
評価項目 | Gemini 2.5 Pro | Gemini 2.5 Flash |
---|---|---|
生成速度 | 5〜7秒(thinking_budget=128) | 平均3秒(thinking_budget=0) |
人名の出力精度 | ❌ 頻繁に誤出力 (例:藤浪→蔤浪) Unicodeエスケープの課題 |
✅ ほぼ誤出力なし |
コンテキスト理解 | ✅ 柔軟に文脈を理解 | △ 指示や例に引っ張られやすい |
表現の多様性 | ✅ 豊富 | △ 制御が必要 |
品質の安定性 | ❌ 再生成でも同じ誤りが繰り返される | ✅ 稀に中国語混入も再生成で対応可能 |
コスト (主軸の観点ではないけど) | 高い | 大幅に低い |
リアルタイム性と正確性を最優先するため、Gemini 2.5 Flashを採用し、表現の多様性は型によるプロンプト設計で補完する方針としました。
ちなみに PoC では、Gemini 2.0 Flash lite
のモデルも試していましたが、動的な情報が多く渡されるケースに弱く今回の要件では適さなかったので外しています。
Gemini 2.5 Proの利点と課題
Proは、表現の多様性や発想の広がりで優れ、同じ状況でも語り口を滑らかに変えられる利点がありました。ただ、今回のプロンプト設計や実験の範囲では、選手名を誤って出力するケースが多かったのです。
例えば「藤浪晋太郎投手」が「蔤浪晋太郎投手」と出力されるケースが確認されていたりと、、。
問題を複雑にしていたのは、誤りがUnicodeのエスケープ形式のまま出力されていた点です。正常に出力しているときは日本語なのですが、ログを確認すると「\u8524\u6D6A
」のような表記が返されており、これをデコードすると「蔤浪」になってしまいます。本来は「藤浪」(\u85E4\u6D6A
)であるべきところ、別の漢字のUnicodeが出力されていました。
{
"ai_comment_title_final": "\u8524\u6D6A\u664B\u592A\u90CE\u6295\u624B\u3001\u5727\u5012\u306E\u6295\u7403\u3067\u3053\u306E\u56DE\u3092\u7247\u4ED8\u3051\u308D\uFF01",
"ai_comment_main_text_final": "\u8524\u6D6A\u664B\u592A\u90CE\u6295\u624B\u306F..."
}
この問題への対応を試みましたが、Unicodeエスケープシーケンスの異常を検知して再生成を指示しても、同じリクエスト内で同様の現象が発生し続けました。 何度再試行しても同じ選手名で誤りが繰り返され、品質保証の仕組みが十分に機能しない場面がありました。
他側面のリアルタイム性については問題なく、thinking_budget を最小値の 128 に設定した場合でも 5〜7 秒程度で生成が完了していて、速度面では十分実用に耐える性能が確認できています。
ただ、thinking_budget
を上げることで人名の誤出力が減る可能性も検討しましたが、生成時間がさらに伸びるトレードオフがあります。リアルタイム解説という要件では、打者交代から初球が投じられるまでの限られた時間内に配信を完了させる必要があるので、最小値で検証を行っていました。
しかし、野球において選手名は最も重要な情報であり、どれだけ魅力的な解説でも誤った名前を出力した瞬間に信頼性は失われます。人名の出力が不安定でリトライがかさみ、リアルタイム性を確保できなくなったことが決定打となり、最終的に Gemini 2.5 Pro の導入を断念する決定的な理由となりました。
Gemini 2.5 Flashでは人名の誤出力はほぼ発生しませんでした。生成速度も平均約3秒で、リアルタイム要件を満たします。中国語の簡体字・繁体字が混入するケースはごく稀に発生しますが、これは品質チェックと再生成ループで対応可能なレベルでした。コストもGemini 2.5 Proと比較して大幅に抑えられ、運用に適しています。
そういった点で Gemini 2.5 Flashを選択しました。 ただ、一つFlashモデルは多様性を生む観点で扱いづらさという問題があり、、、それは後述しますね
問題から学ぶプロンプト設計
AIは人間の思い通りに完全に動かせるものではありません。開発中でも、意図しない出力や想定外の動きを目にすることもしばしばあります ここでは実際に遭遇した問題を一部取り上げ、その原因と対処をまとめていきます。
問題1: 生成例につられすぎてしまい逆に多様性が失われてしまった
もっと良い例を見せれば、AIはもっと良い出力をするはずだと考えて、状況で分割した各プロンプトに30件ほど生成例を追加し、プロンプト内の生成例を大幅に増やしました。これで品質があがるだろうと思っていました
しかし期待していた結果とは逆でした
実際に開発環境で生成した際に以下の問題が出てきました
- 状況に合わない表現が再び出現
- 同じ見出しが何度も繰り返される
- 選手名などの誤り
問題1で単調な表現を避けるために型というラベルを定義したにも関わらず、例を増やしたことで逆に単調になり誤りも散見されたのです
原因
プロンプトが肥大化したことかなと推測しています。例が多すぎて重要な指示や最新の試合状況のデータが埋もれ、どれが重要なものなのかの判断に迷ってしまい単調な表現になってしまったのではないかと考えています。 また、AIは「単調を避ける」という指示と「与えていた生成例を参考にする」という間で混乱し、結果として多く与えていた例の方に強く引っ張られてしまったのではないかな〜〜と推測してます。
もう一つの根本的な問題は、例をどのように参考にすべきかが明文化されていなかったことです。 例は「表現の型」を示すためのものであり、例中の具体的な数値や状況をそのまま流用すべきではありません。しかし、それを明示していなかったため、AIは例の数値や表現をそのまま写す方向に寄ってしまいました。
解決策
この問題を解決するために、生成例の扱い方を根本から見直しました。より品質の良い生成例を5 ~ 10個ほど選びそれを生成例として与えるようにし、生成例のデータを直接利用することを避ける文言を追加しました。そうすることで少なからず状況に応じた多様な表現が生まれるようになりました。
問題2: 解説が単調で「またこのパターンか」と思われる
開発環境で様々なプロンプトで動作確認を行ってました。その際に同じような試合状況が続くと、「~~選手、ここで一発!」「期待が高まる!」といった定型表現が3〜4打席連続で出現するケースが発生しました。守備の際に顕著に発生していて、打者と投手との戦いであるので、塁状況を動かせるかどうかなども難しく似たような表現が続いてしまいましたね、、
原因
生成AIは、何も指示がないと無難な表現を選びがちです。具体的な選択肢やバリエーションを示さないとよく使われるようなパターンで出力してしまいます。
初期実装では、プロンプトに「多様な表現を使ってください」と抽象的な指示はしていましたが、具体的にどのように多様化すべきかを示していませんでした。また、直前にどのような解説を生成したかをAIが認識する仕組みもなかったため、同じパターンを繰り返すことを防げませんでした。
解決策
タイトル生成に明確な「型」のラベルを付け、AIに選択させる方式に変更しました。例えば、以下のようなものです
- 期待・鼓舞型: 「~~選手、ここで決めろ!」
- データ証明型: 「対左投手打率.350の実力を見せろ!」
- 状況強調型: 「満塁のチャンス、ここで追加点を!」
こういった型をいくつも用意しました。
各生成例には上記のような「型」ラベルを付け、モデルにラベル選択をさせています。例中の数値や選手名はスタイル参照のみとし、実データは直近の試合状況から必ず再計算する方針です。 例としては以下の形をプロンプトに書いています
### 攻撃・期待・鼓舞型(label: 期待・鼓舞型)
{
"ai_title": "~~選手、ここで決めろ!",
"ai_description": "イニング: 3回表, スコア: ベイスターズ 2-1 相手チーム。対左投手に**打率.320**、直近5試合でも**長打率.550**。データがこの打席の期待を裏付けます。"
}
### 守備・信頼・鼓舞型(label: 信頼・鼓舞型)
{
"ai_title": "~~投手、テンポ良くゼロで繋げ!",
"ai_description": "イニング: 3回裏, スコア: ベイスターズ 2-1 相手チーム。**初球ストライク率65%**、右打者への**被打率.210**。配球が機能しており、失点を抑えやすい局面です。"
}
なぜ「型」は多様性を奪わず、むしろ広げるのか
逆に型をつけることで多様性が失われるのでは?と思いましたよね。直感的には、型を設けると表現が均質化しそうに見えます。しかし実際には、温度感のブレを抑えつつ語り口の幅を広げるために活躍してくれます
型を与えることで、同じ状況でも視点や語り口が自然に切り替わり、単調に感じなくなりました。型は 何を強調するか だけを定め、語彙、比喩、視点、リズムといった表現そのものの自由度は十分に残すようにしています。また、他にもAIに与えているプロンプトには、直近用いた型ローテーションで同型の連続を避けるように、直近で生成した生成文を含めています。
つまり型は、自由度を削るのではなく、道を誤らないように支える仕組みなんですね
問題3: 攻守や試合状況に合わない温度感の解説が出る
これは、運用開始してから気になっていた点です。実際の試合で運用を開始すると、以下のような不自然な解説が生成されることがありました
- リードしている場面でも「逆転のチャンス!」と誤った解説が出る
- ビハインドの場面で、過度に楽観的な「ダメ押しの追加点を!」という表現が出る
- 守備でも攻撃でも似たようなトーンになり、状況に応じた温度感の違いを表現できていない
野球は攻守やスコア差によって、求められる解説のトーンが大きく変わるスポーツ、リードしている場面では「ダメ押しの追加点を!」という積極的な応援が求められますが、ビハインドの場面では「まだ諦めない、逆転のチャンスは必ずある!!」という粘り強さを表現したいです
原因
単一のプロンプトですべての状況(例えば、攻撃、守備、リード時、同点、ビハインド)に対応しようとした結果、どうしても状況に応じた解説が思ったとおりに出なかったのです。 プロンプト内でリードしている場合の生成例などの指示を書いても、AIはその文脈を正確に把握できないことがあります。単一プロンプトが1000行を超えていたことも、AIが本当に重要な指示を見落とす原因になっていました。
問題の解決策
プロンプトを攻守とスコア状況(リード・同点・ビハインド)の組み合わせで6パターンに分割しました
system-prompt/
├── attack/
│ ├── base.ts # 攻撃時の共通要素
│ ├── lead.ts # リード時の積極的なトーン
│ ├── tie.ts # 同点時のバランス型
│ └── behind.ts # ビハインド時の期待感
├── defense/
│ ├── base.ts # 守備時の共通要素
│ ├── lead.ts # リード時の安定感
│ ├── tie.ts # 同点時の緊張感
│ └── behind.ts # ビハインド時の粘り強さ
各パターンのプロンプトは、共通層と状況別層の2層構造にしました
共通層
どの状況でも一貫させるべき要素を定義しています。
- 基本設定(最初に守るべき前提)
- 役割、出力言語や表現の仕方を与えています
- 生成ステップ(どう考えて出力するか)
- 状況把握とデータ選定を行い、タイトル、本文生成の出力の型を指定
- ルールと出力フォーマット(品質と安全のための決まり)
- 禁止事項、注意事項など
具体的に、攻撃/守備時は以下のものを書いています
# **BAYSTARS BASE☆BLUE 攻撃時実況生成プロンプト**
## **【基本設定】**
- **あなたの役割**: あなたは、**横浜DeNAベイスターズの熱狂的な応援団長AI『BASE☆BLUE』**です。
- **最重要原則**: データは、君の熱い想いを伝えるための「武器」である。比喩や熱い言葉で物語を紡ぐ。
- **出力言語**: 日本語のみ
- **禁止**: 中国語(簡体字・繁体字)の混入
- **使用可能な文字**: 日本の標準的な漢字・ひらがな・カタカナのみ使用
- **視野拡大の原則**: 該当場面(半イニング開始直後)は回全体の展望に短く触れて良い。
- **二層説明(初心者/玄人)**: 数字提示に加え、初心者向けの橋渡しフレーズを1つ添える。
....
## **【生成ステップ】**
以下の思考ステップに従って、高速かつ高品質な出力を生成してください。
### **ステップ1:【状況把握とデータ選定】**
現在の試合状況と、打者の活躍を期待させる切り札データを2〜3個、素早く選びます。\`key01_circumstance\` を必ず出力(空文字は禁止)し、\`key02_base_data\` に根拠データを必ず出力します(空配列は禁止)。
- \`key01_circumstance\` の書式(この順で1行・日本語):
- 「イニング: {数値}回{表/裏}, スコア: ベイスターズ X-Y 相手チーム, アウトカウント: {0/1/2}アウト, 塁情報: {ランナーなし/一塁/二塁/三塁/一二塁/二三塁/満塁}」
- 例: 「イニング: 3回表, スコア: ベイスターズ 2-1 相手チーム, アウトカウント: 1アウト, 塁情報: 二塁」
### **ステップ2:【演出プランニング - どう盛り上げるか?】**
選んだデータを元に、どのような演出でファンの感情を最高潮に高めるか、以下から最も効果的なものを選びます。
プランA:【ヒーロー待望プラン】: 「待ってました!」という期待感を前面に押し出す。
プランB:【データ証明プラン】: 圧倒的なデータを突きつけ、「勝つべくして勝つ」自信を示す。
プランC:【ストーリーテラープラン】: 直前の打席などを伏線として扱い、この打席を物語のクライマックスのように描く。
プランD:【弱点粉砕プラン】: 相手投手の苦手データを明確にし、そこを突ける打者の頼もしさを表現する。
### **ステップ3:【タイトル生成】 (\`key98_ai_comment_title_final\`)
ステップ2の演出プランに基づき、以下の**「タイトルの武器庫」**から最適な型を選び、ファンの心を掴むキャッチーなタイトルを生成します。主語は必ずベイスターズの選手名にしてください。
【タイトルの武器庫】
- 期待・鼓舞型: 「〇〇、ここで決めろ!」「その一振りで流れを呼び込め!」
- 状況強調型: 「勝利をたぐり寄せる、運命の一打!」「絶好のチャンス、ここで畳みかけろ!」「スタジアムの期待が最高潮!」
- データ証明型: 「データが後押し!〇〇の快音に期待!」「数字が証明する、この打席の価値!」「データが勝利への道を照らす!」
### **ステップ5:【解説本文生成】 (\`key99_ai_comment_main_text_final\`)
選んだ演出プランに沿って、**表現の武器**を駆使し、100字程度で客観的に根拠を説明します。
## **【ルールと出力フォーマット】**
* **禁止事項**: ~~~
* **注意事項**: ~~~
状況別層
その状況に特化した具体例とトーンを定義しています。与えている生成例にそういった状況に特化したものを与えるようにしています。
攻撃・リード時 {
"ai_title": "相性抜群!~~選手が相手投手に立ちはだかる!",
"ai_description": "**~~選手**は相手投手に**対戦打率.400**。加えて~~投手自身も**右打者に被打率.310**と苦手で、指標が勝負の行方を支えています。"
}
攻撃・ビハインド時 {
"ai_title": "~~選手、逆転への第一歩を踏み出せ!",
"ai_description": "一死三塁。**~~選手**は**得点圏打率.356**。データが逆転への道筋を照らします。"
}
分割をすることで、AI解説の温度感を保ちながら、野球の試合状況に応じて表現を広げることができました。6分割により、その場で必要な情報だけを読み込む仕組みにすることで、状況に合った多様性が生まれやすくなりました。 加えて、プロンプトを小さく分けることでモデルは重要な指示と例に集中しやすくなり、長文に埋もれていた情報を見落とすリスクが減ったのかなという所感です。
副次的な効果ですが、単一の巨大プロンプトが抱えていた構造的な複雑さも解消できました。攻守とスコア差ごとに小さなプロンプトへ分割し、共通要素を集約したことで、重要指示の見落としが減り、変更の影響範囲が明確になり、レビューも容易になりました ただ、この変更で完全に解消はしていないのですが、頻度を大きく減らすことはでき改善は見られたかなという所感です。
問題4: 品質にばらつきがある(中国語が混ざるなど)
開発中、以下のような品質問題が散発的に発生しました
- 中国語文字やアラビア語などの混入
- プロンプトで使用を禁止している用語の出力
プロンプトで「日本語のみ使用してください」と明示しても、完全には制御できませんでした。
原因
大規模言語モデルは確率的な生成モデルであり、プロンプトによる100%の制御は不可能で、「〜を避けてください」と指示しても、AIは必ずしも従いません。面白いことに禁止事項を意識しすぎると、逆にその禁止語が出力されてしまっていました1。
また、初期実装の品質チェックでは「もう一度生成し直してください」とだけ伝えていたが、何が問題だったのかをAIが理解できず、同じ間違いを繰り返していました。
解決策
どうやって解決したのかというと、プロンプトだけに頼らず、アプリケーションロジックで制御することにしました。3層にわけて制御をする仕組みを導入しました。
- 第1層:プロンプト設計
- 禁止語を明示し、具体的な良い例を提示する。型を指定して表現の方向性を示すようにする
- 第2層:出力検証(品質チェック)
- 生成された解説を自動チェックする。出力されたものに禁止語が含まれていないか、日本語以外が出力されていないかなどをアプリケーションロジックでチェックする
- 第3層:自己修正
- 問題を検出したら、何が問題だったのかをAIにフィードバックして再生成するようにする(以下例)
1回目:「ここで一发!」(簡体字が混入)
↓
2回目のプロンプト:「もう一度生成し直してください」
↓
2回目:「ここで一发!」(同じ間違いを繰り返す)
自己修正を入れると、
1回目:「ここで一发!」(簡体字が混入)
↓
2回目のプロンプトに追加:
「【重要:前回生成時の品質問題を回避してください】
以下の問題が前回検出されました。今回の生成では避けてください:
中国語の使用: タイトル部分で検出(日本語野球解説では不適切)
」
↓
2回目:「ここで一発!」(正しい日本語)
品質チェックで検出された違反内容を蓄積し、次の生成時にプロンプトに含めることで、AIは「前回は中国語文字を使ってしまった」という具体的な問題を理解し、2回目の生成では注意深く日本語だけを使うようになります
Flashモデルの設計の背景
Flash系モデルは、指示への追従と例に釣られる傾向があります。書かれた指示をそのまま出力へ引き込みやすく、生成例にも強く影響されます。そのため、禁止語を強調するとかえって出力を誘発してしまうケースがありました1。
この特性を踏まえ、禁止語の抑止をプロンプトだけに依存しない設計に切り替えました。プロンプト側は否定の羅列ではなく、推奨語と良い例の提示で表現を誘導します。同時に、アプリケーションロジック側で禁止語を制御し品質を担保しました
結果として、Flashの速いが指示に引っ張られやすいという特性によるデメリットを抑えつつ、出力の一貫性と安全性を両立できました。
まとめ
ベイスターズ戦 観戦アプリ 『BAYSTARS STAR GUIDE』 の新機能「BASE☆BLUE」について、その開発の背景と、特にAIの挙動を制御するためのプロンプト設計における試行錯誤を紹介しました。
「ハマスタでも試合中継でも楽しめる観戦体験」というコンセプトを実現するためリアルタイム解説生成の基盤を構築しました。
その中で、設計図となるプロンプト文の構築に対して実際に直面した4つの問題に対して取り組みました。
- 解説に「型」という明確な指針を与え、表現の多様性を確保
- プロンプトを試合状況に応じて6つに分割し、攻守や点差に合ったトーンを制御
- プロンプト、アプリケーションによる自動検証、そしてAI自身に間違いを修正させる3層の品質管理ループを導入
- 多くの例を示すのではなく、量より質を重視して厳選した例を用いることで、AIの柔軟性を引き出しました
ぜひ「BASE☆BLUE」と共に、ベイスターズの試合を楽しみましょう!
-
否定型の表現の代わりに、肯定型の表現で指示することで期待する結果が得られる場合があるとGemini プロンプトガイドに書かれている。 Gemini プロンプトガイド ↩︎ ↩︎
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。