
野球解説AI AgentをWebのプロダクトとして完成させました!
本ブログは日本語版の解説記事です。
英語版は別途Mediumで「Bringing AI to Every Dugout — The Ambitious Quest to Build an AI Agent for All of Baseball」というタイトル*1で公開中です。
なお、スクリーンショットは海外版優先のため英語となっております。
日本語版のデモ動画はこちらをどうぞ。
日本語版は近日中にブログで公開する or Xでつぶやきます。
TL;DR
野球に関わるすべての人のバディとして、AI Agentが傍らにいる未来を実現したい。
以下はオリジナルコンテンツ(英語版と同等)をお楽しみください&最後に日本語版のみの結びを入れています。
- TL;DR
- 野球をAIでいい感じにしようぜ — ファンも解説者もコーチも、すべての野球人にAI Agentを提供する野望のはじまり
- 結び
野球をAIでいい感じにしようぜ — ファンも解説者もコーチも、すべての野球人にAI Agentを提供する野望のはじまり
デモ
筆者について
中川伸一(shinyorke)。本業は株式会社LayerX AI Workforce事業部のSRE。個人のライフワークとして野球データ分析に取り組んでおり、自称「野球AI Engineer」。
2012年からセイバーメトリクスを元にした野球データ分析を開始。2018年には、スポーツ科学とITで「全てのアスリートにイノベーションを届ける(INNOVATIONS FOR ALL ATHLETES)」ベンチャー企業「ネクストベース」に一人目のEngineer/CTOとして参画し、約1年半にわたりNPB/MLB向けのシステム開発に従事した。現在もライフワークとして野球データ基盤の構築・運用、成績予測といったチャレンジを継続中。本記事で紹介する「The Scouter 3」は、その集大成となるプロジェクトである。
Links: GitHub / X (Twitter) / Blog / LinkedIn
はじめに
MLB(メジャーリーグベースボール)の試合を観戦する際、「この投手と打者の過去の対戦成績はどうだったか」「もし大谷翔平と佐々木朗希が対戦したらどうなるか」といった疑問が浮かぶことがある。Baseball SavantやFangraphsといった既存の統計サイトには膨大なデータが公開されているが、セイバーメトリクスの知識がなければ数字の羅列から意味を読み取ることは難しい。
そこで、LLM(大規模言語モデル)をAI Agentとして活用し、Statcastのトラッキングデータを自然言語で解説するWebアプリケーション「The Scouter 3」を開発した。
本記事の概要と読みどころ
The Scouter 3は、選手検索・AIレポート生成・実績対戦の深掘り分析・仮想対戦予測の4つの機能を日英バイリンガルで提供するWebアプリケーションである。中でも仮想対戦予測は、対戦したことがない選手同士、あるいは「投手・大谷翔平 vs 打者・大谷翔平」のように実際の野球では絶対に実現し得ないカードをLLMでシミュレーションする、AI Agentだからこそ可能な機能である。
本記事は、以下の4章構成となっている。
| 章 | 対象読者 | 内容 |
|---|---|---|
| 1章 The Scouter 3 とは何か | 野球ファン・野球関係者 | AI Agentが提供する4つの分析体験の詳細。野球データに馴染みのある方であれば、技術的な知識がなくても楽しめる内容 |
| 2章 PoCからプロダクトへ | プロダクトマネージャー・エンジニア | Marimo PoCの限界、プロトタイプ駆動開発、Design Docによる設計管理など、プロダクト化の判断と過程 |
| 3章 技術スタック | エンジニア | Next.js 15 + FastAPI + Vertex AI + GCP Cloud Runのアーキテクチャ詳細、CI/CD、インフラ構成 |
| 4章 得られた知見 | プロダクトマネージャー・エンジニア | PoCからプロダクトへの移行で得た3つの実践的な教訓 |
筆者(shinyorke)は、このプロジェクトを「ちょっとした個人開発」ではなく、本気で野球の見方を変えるプロダクトとして開発している。現時点の対象ユーザーはセイバーメトリクスに馴染みのある中〜上級ファン・アナリストだが、その先には、中継で裏付けデータを即座に引き出したい解説者、次の打席の作戦を練るコーチなど、野球に関わるすべての人の傍らにAI Agentがいる未来を見据えている。6週間で27本のPRをマージし、約35,200行・202ファイルのコードベースを構築した。その規模感と開発プロセスの詳細は2章の末尾で紹介する。この野望の全体像は「おわりに」で改めて語る。
1. The Scouter 3 とは何か
1.1 プロジェクトの目的
The Scouter 3は、MLBのStatcast/Baseball Savantデータを活用したAI駆動の野球分析Webアプリケーションである。ユーザーに見えるサイト名が「The Scouter 3」。3はストライク、3アウトの野球メタファーである。
プロジェクト名は「Betts」。Mookie Betts選手に由来する。データの土台となるMLB統計データAPI基盤は「Zobrist」という名前で、こちらはBen Zobrist選手に由来する1。両者はいずれもMLBを代表するユーティリティ・プレーヤーであり、どこのポジションでも高水準のパフォーマンスを発揮する万能性を持つ。プロジェクト名にユーティリティ・プレーヤーの名前を冠するのは、「あらゆる分析シーンで使える万能な基盤」でありたいという筆者のリスペクトと願いの表れである。
対象ユーザーは、wOBA、xBA、BABIP、ISO、K%、BB%といったアドバンスド指標2を理解し、日常的にMLBデータを分析する中〜上級ファン・アナリストを想定している。
1.2 4つのページで構成される分析体験
The Scouter 3は4つのページで構成され、選手検索から詳細分析まで段階的に深掘りできる構造になっている。
| ページ | 概要 |
|---|---|
| Top(選手検索) | 日本語・英語での選手名検索 + サマリー表示 |
| 選手詳細 | AIレポート + 統計テーブル + チャート可視化 |
| AI対戦分析 | 実績対戦データの深掘り + AI対戦レポート |
| 仮想対戦予測 | 未対戦カードのLLMシミュレーション |
Topページ(選手検索)

最初に目にするページである。日本語(漢字・ひらがな・カタカナ)でも英語でも選手名を入力して検索できる。たとえば「大谷翔平」と入力すれば、LLMが「Ohtani, Shohei」に対応づけ、基本成績のサマリーカードを表示する。

サマリーカードには打率・出塁率・長打率・OPS・本塁打数・三振数・四球数の7指標を掲載し、そこから詳細ページに遷移する導線を提供する。
選手詳細ページ

選手の1シーズンを俯瞰するページである。画面の上部にはAIレポートが表示される。5つのセクション(選手スタイル、強みと課題、対右傾向、対左傾向、総合評価)に分かれており、Server-Sent Events(SSE)によるストリーミング配信でセクション単位で逐次表示される。LLMの応答を待つ間もユーザーは最初のセクションから読み始めることができるため、体感的な待ち時間が短い。
統計・可視化は2つのタブで切り替える。
| タブ | 内容 |
|---|---|
| 統計 | 基本成績・SABR指標・球種別成績・打球傾向の4カテゴリ |
| 可視化 | Plotly.jsによるインタラクティブチャート |
可視化タブでは、投手なら球種分布(円グラフ)・球速推移・投球位置・リリースポイント・打球位置の8種のチャートを4サブタブで表示する。

打者ならBarrel分布・打球種別分布・打球位置の4種を2サブタブで表示する。いずれも投打左右(対右/対左/全体)のフィルタで絞り込みが可能である。

AI対戦分析ページ

実績のある対戦カードを深掘りするページである。選手のトラッキングデータから対戦相手を抽出し、対戦打数・打率・本塁打・三振・四球・1打席あたりの投球数をテーブルで一覧表示する。ページネーションとソートに対応している。
特定の対戦相手を選択すると、以下の情報が表示される。
| セクション | 内容 |
|---|---|
| 対戦成績サマリー | カウンティング成績 + 率成績の2テーブル |
| 球種別成績 | 球種ごとの投球数・打率・K%など |
| トラッキング可視化 | 球速推移・投球位置・打球位置のチャート |
| 球種別比較 | 打率 vs 三振率の比較チャート |
| AI対戦レポート | 総合分析・球種別分析・推奨戦略の3セクション |
AI対戦レポートは単なるデータの要約ではない。「この打者はスライダーの打率が低いため、カウントを稼ぐ場面ではスライダーを中心に組み立てるのが有効」といった具体的な戦略提案が含まれる点が特徴である。
仮想対戦予測ページ

The Scouter 3の最も独創的な機能であり、AI Agentだからこそ実現できる分析体験である。対戦したことがない選手同士、あるいは対戦しようがない選手同士の仮想対戦をLLMでシミュレーションする。
たとえば、以下のようなユースケースが考えられる。
| ユースケース | 例 |
|---|---|
| リーグ違い・対戦機会の少ない組み合わせ | NL所属の大谷翔平は、AL所属の村上宗隆や岡本和真と年に何回対戦するだろうか? インターリーグでしか実現しないカードを、データに基づいて予測する |
| 同一チーム所属の選手同士 | 大谷翔平 vs 山本由伸。同じドジャースに所属する以上、公式戦で対戦することはない。しかし、両者のStatcastデータから「もし対戦したら」を予測できる |
| 同一人物の投手 vs 打者 | 投手・大谷翔平は、打者・大谷翔平を抑えられるか? 二刀流選手ならではの究極の仮想対戦。投手としてのトラッキングデータと打者としてのトラッキングデータの両方をLLMに渡し、球種別の有利度や予想打率を算出する |
特に「大谷 vs 大谷」の対戦は、実際の野球では絶対に実現し得ない。しかし、Statcastには投手・大谷翔平の全投球データと打者・大谷翔平の全打席データが別々に記録されている。

AI Agentはこの両方のデータを読み解き、「大谷のSweeper(被打率.180)に対して、打者・大谷のブレーキングボール打率は.260」といった具体的な根拠をもとに対戦を評論する。
人間の解説者では主観的な推測に留まりがちなこの種の問いに、データドリブンな分析を提供できる点が、AI Agentならではの価値である。
画面には以下の要素が表示される。
| UI要素 | 説明 |
|---|---|
| Tug-of-Warバー | 投手(青)vs 打者(赤)の有利度を横棒グラフで表示。55%以上で投手有利、45%以下で打者有利、その間は五分 |
| 予測サマリー | 予想打率・三振確率・四球確率・長打確率の4指標(予測幅付き) |
| 球種別有利度バー | 球種ごとの有利度。「Sweeper: 75%投手有利」「4-Seam: 55%打者有利」のような粒度 |
| AI分析レポート | 総合予測・球種別攻略可能性・推奨戦略・相手の脅威・予想シナリオの5セクション |
モード選択機能も用意している。「真剣分析」モードでは客観的・プロフェッショナルな分析、「エンタメ」モードではドラマチックでファン向けの語り口に切り替わる。同じデータであっても、読み手の目的に合わせた表現を選べる設計である。

1.3 日英バイリンガル対応
The Scouter 3は日本語と英語の両方に対応している。ヘッダーのJA/ENボタンで言語を切り替えると、UIラベルだけでなく、AIレポートの生成言語も連動して切り替わる。108以上のi18nキーを独自実装の軽量i18nシステムで管理しており、外部ライブラリに依存しない構成としている。
日本語をデフォルト言語とした理由は、主要ターゲットが日本在住のMLBファン・アナリストであるためである。英語は補助言語として提供し、海外のユーザーや英語記事を書く際にも活用できるようにしている。
2. PoCからプロダクトへ — 開発の経緯
2.1 Marimoで始めたPoC
プロジェクトの出発点は、PythonのリアクティブノートブックフレームワークであるMarimoを使ったPoCであった。Google Cloud Vertex AI(Gemini 2.5 Flash)とPolars、Plotlyを組み合わせ、以下の機能を検証した。
- 自然言語によるMLB統計データ検索
- LLMによる日本語クエリの解析と選手名マッチング
- 打者・投手の統計データの可視化
- 選手レポートの自動生成(AIレポート)
- 対戦傾向分析と対戦レポート生成
- シーズン選択(2023-2025)
PoCはインタラクティブなウェブインターフェースとして動作し、開発者自身が分析に使う分には十分な機能を備えていた。Cloud Run上にOAuth2 Proxyを併設してデプロイし、betts-poc.shinyorke.infoのカスタムドメインで限定公開した。Terraformでインフラを管理し、本番環境の運用まで一通り経験できた。
しかし、PoCにはいくつかの課題があった。
2.2 PoCの限界と、プロダクト化の動機
PoCを運用する中で、以下の4つの課題が明確になった。
| 課題 | 詳細 |
|---|---|
| UIの制約 | Marimoはノートブック環境であり、モバイル対応が不十分。球場でスマホから確認するユースケースに対応できなかった。タブ切り替えやアコーディオンUIの実現も困難 |
| パフォーマンス | サーバーサイドレンダリング主体のため、クライアントサイドのフィルタリングやソートのたびにサーバーとの往復が発生 |
| i18nの困難さ | PoC段階では日本語UIのみ。Marimoのアーキテクチャ上、多言語対応の後付けは容易ではなかった |
| スケーラビリティ | 1つのCloud Runサービスに全機能を詰め込んでおり、フロントエンドとバックエンドの独立スケーリングが不可能 |
これらの課題を踏まえ、Next.js + FastAPIでフロントエンドとバックエンドを分離した本格的なWebアプリケーションとしてフルリビルドすることを決定した。PoCで検証済みのLLMロジック(選手名マッチング、レポート生成、仮想対戦予測)は、Python側に再実装してFastAPIのバックエンドに移植する方針とした。
2.3 プロトタイプ駆動の開発プロセス
フルリビルドにあたり、最初にHTML/CSS/JSのプロトタイプ(prototype)を作成した。Tailwind CSS 3.x(CDN版)とPlotly.js 2.35をビルド不要で直接利用し、4ページすべてのUI/UXをブラウザ上で検証した。
この「prototype → frontend」の開発フローには明確なルールを設けた。
- prototypeをUI/UXデザイン検証の「正」とする
- デザイン変更はまずprototypeで検証し、確定後にfrontend(Next.js)へ実装する
- prototypeのHTML構造・CSSクラス名・レスポンシブ設計をfrontend実装の仕様として扱う
- カラーパレット(CSS変数)・ブレークポイントはfrontendのTailwind設定に反映する
- チャート定義はfrontendのReactコンポーネントと1:1対応を維持する
prototypeにはPlaywrightによるE2Eテスト(121テスト)を整備しており、変更のたびにデグレがないことを自動検証する体制を構築した。こうすることで、「デザインの正」としてのprototypeの信頼性を維持しつつ、Next.jsへの移植における仕様のブレを防止した。
2.4 Design Docによる設計管理
本プロジェクトでは、各機能・各フェーズの設計判断をDesign Docとして文書化した。ファイル命名規則(ISSUE番号_YYYYMMDD_説明.md)を定め、GitHub Issueと紐づけて管理している。以下に主要なDesign Docを挙げる。
- #11 LLM Backend API設計 — 8つのAPIエンドポイント(ヘルスチェック、選手リスト、日本語名検索、トラッキングデータ、選手サマリー、選手AIレポート、対戦AIレポート、仮想対戦予測)のスキーマ・データフロー・SSE設計・キャッシュ戦略を定義
- #12 GCPインフラ設計 — VPC、Cloud Run 2サービス構成、IAM、Artifact Registry、Cloud NAT等のインフラ全体像を設計
- #16 Backend実装計画 — FastAPIの4層アーキテクチャ(ルーティング・サービス・クライアント・モデル)と段階的実装フェーズを定義
- #17 Frontend実装計画 — Next.js 15 App Routerのファイル構成、Server/Client Componentの使い分け、API Routeプロキシ設計を定義
- #26 i18n実装 — LanguageProvider(React Context)による多言語切替の配線設計
- #27 Terraformインフラ構築 — VPC/IAM/Artifact Registry/Workload Identity等の基盤リソース構築
- #28 CI/CDパイプライン — GitHub ActionsによるGAR push + Terraform deployフロー
- #29 Cloud Runデプロイ — OAuth2 Proxyサイドカー構成、Domain Mapping、本番運用構成
このように設計を文書化しておくことで、PoCの設計判断を振り返り、プロダクトとして何を変更し何を踏襲するかを明確にできた。特にPoCとの差分(たとえば、Zobrist APIの呼び出し元をFrontendからBackendに変更した判断など)は、Design Docの「RFPからの変更点」セクションで明示的に記録している。
2.5 開発規模
PoCからプロダクトへの再構築に要した期間は約6週間(2026年2月14日〜3月29日)。その間のリポジトリ活動を数字でまとめる。
| 指標 | 数値 |
|---|---|
| コミット数 | 45 |
| マージ済みPR数 | 27 |
| 管理ファイル数 | 202ファイル |
| コード総量 | 約35,200行 |
平均すると約1.5日に1本のPRをマージしたペースである。コンポーネント別の内訳は以下の通り。
| コンポーネント | 行数 | 備考 |
|---|---|---|
| frontend/ | ~17,300行 | 最大。lockfile含む |
| docs/ | ~5,000行 | RFP・Design Doc等 |
| backend/ | ~5,000行 | Python + lockfile |
| prototype/ | ~3,900行 | HTML/CSS/JS |
| terraform/ | ~960行 | HCL |
| .github/ | ~630行 | CI/CDワークフロー |
| scripts/ | ~46行 | pre-commitフック |
lockfileを除いた実質コード量は概算で2万行前後となる。ドキュメント(docs/)が5,000行を占めるのは、RFPと8本のDesign Docを丁寧に書いた結果である。
なお、実装にはClaude Codeを全面的に活用し、コードレビューにはOpenAI CodexおよびGitHub Copilotを併用した。AIコーディングツールを開発プロセスに組み込むことで、一人開発でも6週間という短期間でこの規模のプロダクトを形にできた。
6週間の開発は、生活の中に溶け込む形で進んだ。平日は仕事が終わった後、馴染みの居酒屋でお酒を飲みながらプロトタイプの設計と実装を行った。毎週末は地元のスターバックスで、シロップ少なめのアーモンドミルクのカフェモカを片手にフロントエンドとバックエンドの実装・テストに集中した。そしてWBCのアメリカ代表戦 — 日本時間では朝から昼にかけての放送である — を見ながらデバッグに勤しむ日もあった。野球を見ながら野球のプロダクトを作る、これ以上のドッグフーディングはない。
3. 技術スタック
ここからは、エンジニアリングの観点からThe Scouter 3の技術スタックを詳述する。
3.1 全体アーキテクチャ

2つのCloud Runサービスを同一VPC内にデプロイし、BackendへのアクセスをVPC内部に限定する構成である。Frontendは外部からのアクセスを許可し、OAuth2 Proxyで認証を行う。Backendはingress: internal-onlyでVPC内部からのアクセスのみ受け付ける。
3.2 Frontend — Next.js 15 + TypeScript
フレームワーク選定の背景 — PoCのMarimoからの移行先として、Next.js 15(App Router)を採用した。理由は、将来のServer Component化によるSSR/SEO最適化を見据えたこと、App RouterによるServer/Client Componentの柔軟な使い分け、そしてReactエコシステムの成熟度である。
主要なライブラリ構成は以下の通りである。
- Next.js 15(App Router) + React 18 + TypeScript — ルーティング・データフェッチ・レンダリング
- Tailwind CSS v4 + @tailwindcss/typography — スタイリング。v4ではCSS内の
@themeディレクティブで設定し、tailwind.config.tsは不要 - Plotly.js(react-plotly.js + plotly.js-basic-dist-min) — チャート描画。scatter/pieを含み本プロジェクトの要件を満たす基本ディストリビューション(約1MB)を使用。将来の3Dチャート等の要件に備え、フルバンドル(約3.5MB)への切り替えも考慮済み
- react-markdown — AIレポートのMarkdownレンダリング
- 独自i18n実装(
lib/i18n.ts+ JSON辞書 +LanguageProvider) — 外部ライブラリ不使用。4ページ分のラベル(108キー以上)に対して外部ライブラリは過剰と判断
API Routeプロキシの設計 — FrontendからBackendへの通信は、Next.js API Route(/app/api/v1/[...path]/route.ts)を透過プロキシとして経由する単一パターンに統一した。ブラウザは/api/v1/*を叩き、API RouteがBackendのCloud RunサービスURLに転送する。現時点での中核部分を以下に示す。
// frontend/src/app/api/v1/[...path]/route.ts export async function GET( request: Request, { params }: { params: Promise<{ path: string[] }> }, ) { const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8000"; const { path } = await params; const url = new URL(request.url); const targetUrl = `${backendUrl}/api/v1/${path.join("/")}${url.search}`; const response = await fetch(targetUrl); return new Response(response.body, { status: response.status, headers: { "Content-Type": response.headers.get("Content-Type") ?? "application/json", }, }); } export async function POST( request: Request, { params }: { params: Promise<{ path: string[] }> }, ) { const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8000"; const { path } = await params; const url = new URL(request.url); const targetUrl = `${backendUrl}/api/v1/${path.join("/")}${url.search}`; const body = await request.text(); const response = await fetch(targetUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body, }); const contentType = response.headers.get("Content-Type") ?? "application/json"; return new Response(response.body, { status: response.status, headers: { "Content-Type": contentType }, }); }
見ての通り、プロキシの中核はこれだけである。catch-allルート([...path])でパスを受け取り、BackendのURLに転送し、response.body(ReadableStream)をそのまま返す。SSEレスポンス(text/event-stream)もContent-Typeヘッダーごと透過するため、ストリーミング対応のための特別な処理は一切不要である。
この設計には以下の利点がある。
| 利点 | 説明 |
|---|---|
| セキュリティ | ブラウザからBackendの内部URLが見えない |
| CORS不要 | 同一オリジン通信のため、CORSの設定が不要 |
| 統一的なAPI呼び出し | Client/Server Componentのどちらからでも同じ/api/v1/*パスで呼び出し可能 |
一方、トレードオフとして、全リクエストがAPI Routeを経由するためレイテンシが1ホップ分増加する点がある。また、Cloud RunのリクエストタイムアウトはSSEの長時間接続に影響し得る。現時点ではCloud Runのタイムアウトを60秒に設定しており、LLMのレポート生成がこの範囲に収まることを確認済みである。Vercel等のサーバーレス環境ではストリーミングのタイムアウト制約がより厳しいケースがあるが、Cloud Runではこの問題は発生していない。
Server ComponentとClient Componentの使い分け — 設計上はページコンポーネント(page.tsx)をServer Componentとし、初期データを取得してClient Componentにpropsとして渡す構成を想定した。ただし、i18n対応(LanguageProvider = React Context)の導入により、全ページが"use client"を宣言したClient Componentとして動作している。
Server Component化は将来の最適化として位置づけている。なお、Plotly.jsのチャートコンポーネントはモジュールimport時にwindowを参照するため、next/dynamic + ssr: falseでSSRを無効化している。
3.3 Backend — Python + FastAPI
フレームワーク選定の背景 — LLMとの連携にVertex AI(Google Cloud)のPython SDKを利用するため、PythonベースのWebフレームワークとしてFastAPIを採用した。PoCのMarimoアプリ内のLLMロジックをFastAPIに移植する形で再実装している。ただし、CLAUDE.mdの制約としてPoCコードの直接importは禁止しており、ロジックを理解した上での再実装(移植)が方針である。
主要なライブラリ構成は以下の通りである。
- Python 3.14+ + FastAPI + Pydantic v2 — APIフレームワーク。
CamelModel(alias_generator)でcamelCase↔snake_caseの自動変換を実現 - google-cloud-aiplatform(Vertex AI / Gemini 2.5 Flash) — LLM推論
- httpx — Zobrist APIへの非同期HTTPクライアント
- google-cloud-storage — GCSキャッシュバケットへのアクセス
- Polars — データ処理(PoCから踏襲)
- パッケージマネージャ: uv — lockfile(
uv.lock)による依存管理
4層アーキテクチャ — Backendは以下の4層で構成している。
- ルーティング層(
api/) — HTTPリクエスト/レスポンスの受け渡しのみを担当。ビジネスロジックは持たない - サービス層(
services/) — ビジネスロジック。選手名マッチング、レポート生成、対戦予測を実装 - クライアント層(
clients/) — 外部サービスとの通信。Vertex AI、Zobrist API、GCS/Localキャッシュ - モデル層(
models/) — Pydanticスキーマ定義。リクエスト・レスポンス・内部型
さらに、core/に横断的な基盤コード(LLMClient Protocol、SSEヘルパー)、config/に環境変数管理を配置している。os.getenvはsettings.py内のみで使用し、他のモジュールからの直接使用を禁止するルールを設けている。これにより環境変数の管理ポイントが一箇所に集約される。
DI(依存性注入)パターン — FastAPIのlifespanイベントで共有リソースを初期化し、app.stateに格納する。実装は以下の4フェーズで段階的に進めた。
| Phase | 初期化するリソース | 解禁されるエンドポイント |
|---|---|---|
| A | Settings | health |
| B | + CacheBackend + ZobristClient | players, tracking, summary |
| C | + LLMClient | search, report/player |
| D | 全結合 | report/matchup, predict/virtual |
この順序には意図がある。Phase Aでヘルスチェックが通ればデプロイの正常性を確認でき、Phase Bで外部API連携(Zobrist API)を検証し、Phase Cで初めてLLMを導入する。依存関係の少ないものから順に積み上げることで、各フェーズの完了時点で動作するエンドポイントが増え、段階的にデプロイ・検証できる設計とした。
8つのAPIエンドポイント — Backendは8つのエンドポイントを提供しており、レスポンス形式で大きく2種類に分かれる。
| カテゴリ | エンドポイント数 | レスポンス形式 | 役割 |
|---|---|---|---|
| データ取得系 | 5 | JSON | ヘルスチェック、選手リスト取得、日本語名検索、トラッキングデータ取得、選手サマリー |
| AIレポート生成系 | 3 | SSE | 選手AIレポート、対戦分析AIレポート、仮想対戦予測 |
データ取得系はZobrist APIへのプロキシやLLMによる選手名マッチングなど、即時レスポンスを返すJSON APIである。一方、AIレポート生成系の3エンドポイントはSSE(Server-Sent Events)でセクション単位のストリーミング配信を行う。PoCでも同様の方式を採用しており、LLMの応答を待たずに先行セクションを表示するUXを実現する。
キャッシュ戦略 — ヘルスチェック以外の全エンドポイントにcacheパラメータを設けている。デフォルトはtrueでGCSバケットのキャッシュを参照し、falseの場合はキャッシュを無視して再処理する。LLM呼び出しのコスト削減とレスポンス速度の改善が目的である。キャッシュバケットは7日間のライフサイクルポリシーで自動削除される。
3.4 インフラストラクチャ — GCP + Terraform
Infrastructure as Code — すべてのインフラはTerraform(1.14+)で宣言的に管理している。フラット構成(モジュール分割なし)を採用し、サービス数が少ない段階ではシンプルさを優先した。Google Cloud Provider ~> 6.0を使用している。
主要なインフラ構成要素は以下の通りである。
| カテゴリ | コンポーネント | 用途 |
|---|---|---|
| コンピュート | Cloud Run × 2 | Frontend + Backend |
| ネットワーク | VPC + Subnet | 内部通信の隔離(10.0.0.0/24) |
| ネットワーク | Serverless VPC Access Connector | Cloud Run ↔ VPC接続(10.8.0.0/28) |
| ネットワーク | Cloud Router + Cloud NAT | Backend → 外部API通信用 |
| 認証 | OAuth2 Proxy v7.6.0 | エンドユーザーGoogle OAuth認証 |
| 認証 | Workload Identity Federation | GitHub Actions → GCPのkeyless認証 |
| IAM | Service Account × 4 | frontend-sa / backend-sa / deploy-sa / admin-sa |
| AI/ML | Vertex AI(Gemini 2.5 Flash) | LLM推論 |
| ストレージ | GCS | キャッシュストア(7日lifecycle) |
| シークレット | Secret Manager | APIキー・OAuthシークレット管理 |
| コンテナ | Artifact Registry | Dockerイメージ保管(最新10保持) |
VPCとネットワーク設計 — BackendからのEgress通信経路は宛先に応じて異なる。SubnetのPrivate Google Accessを有効化することで、この使い分けを実現している。
| 通信先 | 経路 | 備考 |
|---|---|---|
Vertex AI(*.googleapis.com) |
Private Google Access | Googleプライベートネットワーク内で完結 |
| GCS キャッシュ | Private Google Access | 同上 |
| Zobrist API | Cloud NAT経由 | 外部REST API。パブリックインターネットを通過 |
OAuth2 Proxyサイドカー構成 — PoCと同じ方式をそのまま踏襲した。Cloud Run v2 APIのマルチコンテナ機能を利用し、以下の2コンテナ構成としている。
| コンテナ | ロール | ポート | CPU | Memory |
|---|---|---|---|---|
| OAuth2 Proxy v7.6.0 | Ingress(認証ゲートウェイ) | 8080 | 0.5 | 256Mi |
| Next.js App | サイドカー(アプリ本体) | 3000 | 0.5 | 512Mi |
OAuth2 ProxyはSecret Managerからクライアントシークレットを読み取り、Google OAuth 2.0で認証を行う。Terraformでの定義を抜粋する(シークレット関連の環境変数は省略)。
# terraform/cloud_run.tf(抜粋) resource "google_cloud_run_v2_service" "frontend" { name = "betts-frontend" location = var.gcp_region template { service_account = google_service_account.frontend.email timeout = "60s" # OAuth2 Proxy container (ingress, port 8080) containers { name = "oauth2-proxy" image = "${local.gar_url}/oauth2-proxy:v7.6.0-amd64" ports { container_port = 8080 } env { name = "OAUTH2_PROXY_UPSTREAMS"; value = "http://localhost:3000" } # ... Secret Manager参照によるOAuth設定(省略) resources { limits = { cpu = "0.5"; memory = "256Mi" } } } # Next.js container (sidecar, port 3000) containers { name = "betts-frontend" image = "${local.gar_url}/betts-frontend:${var.frontend_image_tag}" env { name = "BACKEND_URL"; value = google_cloud_run_v2_service.backend.uri } resources { limits = { cpu = "0.5"; memory = "512Mi" } } depends_on = ["oauth2-proxy"] # サイドカーはIngressコンテナの後に起動 } vpc_access { connector = google_vpc_access_connector.connector.id egress = "ALL_TRAFFIC" } } ingress = "INGRESS_TRAFFIC_ALL" }
ポイントは、OAuth2 Proxyがportsブロックを持つIngressコンテナであり、Next.jsアプリはportsブロックを持たないサイドカーである点。OAuth2 ProxyのUPSTREAMSをhttp://localhost:3000に向けることで、同一インスタンス内のlocalhostでNext.jsに転送する。BACKEND_URLにはBackendのCloud Run URIが動的に注入され、VPC内部通信でBackendに到達する。
3.5 CI/CD — GitHub Actions + Terraform
ブランチ戦略 — feature/#123-xxx → main → prdの3層ブランチモデルを採用している。mainへのマージはSquash merge、main → prdへのマージはMerge commit(リリース単位でのリバートを可能にするため)と使い分けている。
CIパイプライン — コンポーネントごとにワークフローを分離している。
- backend-ci.yml — ruff check → ruff format --check → ty check → pytest → Docker build → GAR push
- frontend-ci.yml — ESLint → Next.js build → vitest → Docker build → GAR push
- terraform-ci.yml — fmt → validate → plan(PRコメント出力)
CDパイプライン — prdブランチへのmerge commitをトリガーとする。CDパイプラインの動作は以下の3ステップで構成される。
- 変更検出 —
frontend/・backend/の差分有無を判定 - タグ解決 — 変更ありのコンポーネントはGARから最新のGit SHA短縮タグを取得、変更なしのコンポーネントはterraform stateの現在値を維持
- デプロイ —
terraform applyでCloud Runを更新。tf stateと実態の整合性を保証
Workload Identity Federation — GitHub ActionsからGCPへの認証にはkeyless認証を採用している。サービスアカウントキーを生成せず、OIDCトークンで一時的な認証を行う。
| SA | 用途 | 主要なIAMロール |
|---|---|---|
deploy-sa |
GAR push専用 | roles/artifactregistry.writer |
admin-sa |
Terraform plan/apply | roles/run.admin, roles/compute.networkAdmin 等 |
pre-commitフック — コミット時に変更されたコンポーネントのCIチェックを自動実行するpre-commitフックを整備している。
| 変更対象 | 実行される検証 |
|---|---|
backend/ |
ruff check → ruff format --check → ty check → pytest |
frontend/ |
ESLint → Next.js build → vitest |
terraform/ |
terraform fmt -check → terraform validate |
prototype/ |
Playwright E2Eテスト(121テスト) |
3.6 基調カラーとデザインシステム
技術スタックの話題から少し外れるが、The Scouter 3のビジュアルアイデンティティについても触れておきたい。基調カラーとしてフォレストティール(#115e59、Tailwind teal-800)を採用した。既存の野球データサイトとの差別化を意図した選択であると同時に、アスレチックスのチームカラー(グリーン&ゴールド)の色味も意識している。筆者はマイケル・ルイス著『マネー・ボール』をきっかけにセイバーメトリクスと出会い、そのままアスレチックス・ファンになった。データ分析の原点にあるチームの色がプロダクトに宿っているのは、個人的に気に入っているポイントである。
| サイト | 基調カラー |
|---|---|
| Fangraphs | 緑 |
| Baseball Reference | 赤 |
| Baseball Savant | ネイビー |
| The Scouter 3 | フォレストティール |
ヘッダー、フッター、テーブルヘッダーなどの主要UI要素にフォレストティールを適用し、統一感のあるブランドカラーを形成している。投手を青(#2563eb)、打者を赤(#dc2626)で表現するカラーコードは、Tug-of-WarバーやAI対戦レポートなど、アプリケーション全体で一貫して使用している。
カラーパレットはCSS変数で一元管理し、prototypeのcss/style.cssをfrontendのTailwind設定に1:1で反映する運用としている。
4. PoCからプロダクトへの移行で得た知見
4.1 PoCのコードは捨てる、設計は活かす
PoCのMarimoアプリのコードを直接importすることは、プロジェクトルールとして禁止した。Marimoに固有のUIフレームワークの制約やモノリシックな構成をそのまま引きずることを避けるためである。一方で、PoCで検証した設計判断(SSEによるストリーミング配信、GCSキャッシュ、OAuth2 Proxyサイドカー構成など)はDesign Docを通じてプロダクトに引き継いだ。「コードは捨てる、設計は活かす」というアプローチが、クリーンな再実装につながった。
4.2 prototypeを「正」とするルールの有効性
HTML/CSS/JSの静的プロトタイプをデザインの「正」として位置づけ、Next.jsの実装はprototypeに準拠するルールを設けた。これにより、デザインの議論と実装の議論を分離でき、「UIの仕様はprototypeを見ればわかる」という単一の参照先が確立された。Playwrightテストによるprototypeの品質保証も、この仕組みを支える重要な要素であった。
4.3 Design Docの蓄積が開発速度を上げる
8本のDesign Docを作成した結果、「この設計判断はなぜこうなったのか」を後から参照できる資産が残った。特に、PoCとの差分を明示的に記録しておくことで、新しい実装が既存のPoC設計を意図的に踏襲しているのか、意図的に変更しているのかが一目瞭然になる。将来のメンテナンスや新規メンバーのオンボーディングにおいて、大きな価値を持つと考えている。
おわりに
The Scouter 3は、MLBのStatcastデータとLLMを組み合わせた野球解説AI Agentである。自然言語での選手検索、AIによるレポート生成、実績対戦の深掘り分析、そして未対戦カードの仮想シミュレーションという4つの分析体験を、日英バイリンガルで提供する。
技術的には、Next.js 15 + FastAPI + Vertex AI(Gemini 2.5 Flash)のスタック上に、OAuth2 Proxyサイドカー構成のCloud Run 2サービスアーキテクチャを構築した。TerraformによるInfrastructure as Code、GitHub Actions CI/CDパイプライン、pre-commitフックによる品質保証、Design Docによる設計管理など、プロダクション品質のWebアプリケーションに求められる周辺プラクティスも整備した。
PoCからプロダクトへの移行においては、「コードは捨てる、設計は活かす」「prototypeを正とするルール」「Design Docの蓄積」の3つが開発を円滑に進める鍵となった。
野球データとAIの組み合わせは、まだ多くの可能性を秘めている。今後の拡張として、以下のような方向性を考えている。
- 自然言語クエリ検索 — 「去年30本以上打った左打者は?」のような問いかけに対し、データベースから該当選手を検索して回答する機能
- リアルタイム試合データとの連携 — 進行中の試合のトラッキングデータを取り込み、リアルタイムで対戦分析やレポートを更新する仕組み
- 予測モデルの高度化 — 現在は球速・打球速度・投球数と傾向を中心に分析しているが、ボールの変化量(横変化・縦変化)やピッチトンネル(異なる球種が打者の手元までどれだけ同じ軌道を通るか)といったより高度な概念を予測モデルに組み込むことで、「なぜこの球種が打ちにくいのか」をより精緻に解説できるようになる
そして、The Scouter 3が届けたい価値は野球ファンの楽しみだけに留まらない。AIによるデータドリブンな分析は、人間の判断を補強する第三者的な視点を提供できる。野球に関わるすべての人のバディとして、AI Agentが傍らにいる未来を実現したい。
- 解説者 — 試合中継で「この対戦カードの過去の傾向は?」と問われたとき、裏付けデータとしてAIレポートを即座に参照する
- 実況アナウンサー — 次の打席に向けて、対戦カードの背景や球種別の有利不利を瞬時に把握し、視聴者に伝える
- コーチ — 選手に「この投手のSweeper、お前は打率.150だから初球は見逃せ」と伝える際の根拠資料として活用する
- ファン — 球場で試合を見ながら、目の前の対戦をデータで読み解き、野球の楽しみをもう一段深くする
The Scouter 3は、その第一歩である。
この取り組みに共感していただける方、一緒に野球×AIの未来を作りたいという方がいれば、ぜひ気軽に声をかけてほしい。個人・企業を問わず、shinyorke(X (Twitter) / LinkedIn)へのご連絡をお待ちしている。
結び
ここまでお読みいただいた皆様、本当にありがとうございます。
この「The Scouter 3」、プロジェクト「Betts」は「ファンタジーベースボールに『テクノロジーとエンジニアリングを駆使して』勝ちたいんや」という思いで2012年からスタートした野球データ分析、野球エンジニア改め野球AIエンジニアとして一つの完成形ができたなと感じています。
じゃあこれでこの営みはおしまいか?と言われるとおしまいじゃない、むしろ新しいスタートだと思っています。
せっかく作った野球AI Agent、まずはアウトカムを出していくのとまだまだやりたいことがあるので「AIをバディにして進める」Bet AIをいい感じにやっていきです。
shinyorke先生の次回作にご期待ください、ではでは。
- Zobrist APIは筆者が個人開発しているMLBデータAPI基盤。Cloud Run + Cloud Pub/Subによるサーバレスなマイクロサービス構成で、Terraformで管理している。詳細は登壇資料「Cloud RunとCloud PubSubでサーバレスなデータ基盤2024 with Terraform」およびブログ記事「仕事も個人開発も周りがドン引きするまでガチでエンジニアをやっていきましょ - デブサミ2023登壇報告」を参照。↩
- いずれもBaseball SavantやFangraphsで用いられるセイバーメトリクス系の統計指標。詳細はFangraphs Glossary等を参照。試合前のスカウティング、選手評価、対戦傾向の考察、SNS投稿のネタ探しといったシーンでの利用を念頭に置いた。↩
*1:日本語タイトルをそのまま訳したもの、この文脈で「野球」は「Dugout」と訳すといいらしいです。へえーー。

























