はじめに:音楽データから紡ぐパーソナライズストーリー
毎年数億人のユーザーが待ち望むSpotify Wrapped。2025年はさらに一歩進み、単なる統計サマリーではなく、ユーザーのリスニング履歴から「特別な日」を特定し、LLMが創作したパーソナライズストーリーを提供する「Wrapped Archive」機能をリリースしました。
この機能は単なる「面白い機能」ではなく、3.5億人のユーザーそれぞれに最大5件、合計14億件のレポートを事前生成するという大規模エンジニアリングの課題でした。本記事では、Spotifyエンジニアリングブログで公開された内容を基に、彼らがどのようにこの難題を解決したか、中核となるアーキテクチャと意思決定を分析します。
本記事は Spotify Engineering Blogの原文 を一次情報として分析しています。
1. 「特別な日」を見つけるアルゴリズム:ヒューリスティック設計
すべてのユーザーにとって意味のある「一日」を定義するのは容易ではありません。Spotifyはこれを解決するために、優先順位付けされたヒューリスティック集合を設計しました。
主な発見ヒューリスティック
- 最も聴いた日 (Biggest Music/Podcast Day): 単純に聴取時間が最も長い日
- 最大の発見の日 (Biggest Discovery Day): 初めて聴くアーティストを最も多く発見した日
- 推しアーティストの日 (Biggest Top Artist Day): 特定のアーティストに最も時間を費やした日
- 最もノスタルジックな日 (Most Nostalgic Day): 古い曲や回帰的な聴取が急増した日
- 最も特異な聴取パターン (Most Unusual Listening Day): 普段の趣味から最も乖離した日
これらの候補を物語としてのポテンシャルと統計的な強度でランク付けし、ユーザーごとに最大5日を選出。分散データパイプラインで数億人のデータを集計し、結果をオブジェクトストレージに保存した後、メッセージキューを介して非同期にレポート生成ステージへ渡しました。
2. 14億件のレポートのためのLLM:プロンプトエンジニアリングとモデル蒸留
プロンプト設計:一貫性と創造性のバランス
3ヶ月以上にわたり毎日プロンプトを改善したとのこと。中核は2つのレイヤーに分かれます。
-
システムプロンプト: 創造的契約の定義
- データ駆動型ストーリーテリング(すべての洞察は実際の聴取データに基づく)
- ウィットに富み、誠実で、さりげなく遊び心のあるトーン
- 薬物、暴力などセンシティブな話題は絶対禁止
-
ユーザープロンプト: 曖昧性の排除
- 該当日の詳細な聴取ログ
- 要約された統計ブロック(LLMは計算が苦手なため)
- ユーザーのWrapped全体データ
- 興味深い日のカテゴリ
- 既に生成済みのレポート(重複防止)
- ユーザーの国(スペル・語彙の調整)
モデル蒸留:コストと品質のトレードオフ解決
高性能フロンティアモデルで14億件のレポートを生成するのは経済的に非現実的です。Spotifyは蒸留(Distillation)パイプラインを構築しました。
- フロンティアモデルで高品質な参照出力を生成
- 手作業レビューを経た「ゴールドデータセット」を構築
- より小型で高速なプロダクションモデルをこのデータでファインチューニング
- Direct Preference Optimization (DPO) を適用:A/Bテストされた人間評価で選好学習
結果として、小型モデルが大型モデルと強力な選好パリティ(Preference Parity) を達成しました。
3. 並列書き込みと並行性:データモデリングが解決した問題
14億件のレポートを生成・保存する過程で最も厄介な問題の一つは並行性制御でした。1ユーザーに対して最大5件のレポートがほぼ同時に生成され、保存される必要がありました。
Spotifyが選んだ解決策は驚くほどシンプルでした。
カラム指向キーバリューストアの設計
- 各ユーザーのデータは1行(row)に格納
- 各「特別な日」は独自のカラム修飾子(Column Qualifier) を持つ
- 日付(YYYYMMDD)をカラム修飾子として使用(例:20250315)
- 異なる日付の書き込みは同じ行内の完全に異なるセルに記録 → ロック、トランザクション、読み取り-修正-書き込みサイクル不要
# 概念的なデータモデル (Python風疑似コード)
class UserWrappedData:
def __init__(self, user_id):
self.user_id = user_id
# カラムファミリー: 'reports'
# カラム修飾子: YYYYMMDD (例: '20250315')
# 値: レポート内容
self.reports = {} # {'20250315': '...', '20250622': '...'}
# カラムファミリー: 'metadata'
# カラム修飾子: 'completed_days'
# 値: 完了した日のリスト (軽量メタデータ)
self.metadata = {}
def write_report(self, date, content):
# 1. レポート内容を先に書き込む
self.reports[date] = content
# 2. その後にメタデータを更新 (順序保証)
self.metadata['completed_days'] = list(self.reports.keys())
# この順序により、ユーザーは未完了のレポートを見ることができない
核心的インサイト: 複雑な並行性問題は、しばしばデータモデリングで解決できます。アプリケーションロジックを複雑にする代わりに、スキーマを「並列書き込みに安全な」方法で設計したことが成功の鍵でした。
4. 「ビッグバン」ローンチのための事前スケーリングと合成負荷テスト
Wrappedは全世界同時ローンチです。段階的ロールアウトはなく、1秒前はアイドル状態だったのが、次の瞬間には数百万人が殺到します。オートスケーリングはこの「スパイク」に対応するには遅すぎます。
Spotifyが選んだ方法:
- 事前スケーリング: ローンチの数時間前にコンピュートポッドとデータベースノード容量を事前確保
- モデルプロバイダー容量の事前調整: 予想トラフィックに合わせてスループット確保
- 合成負荷テスト: 実際のユーザートラフィックが到着する前に全リージョンで実行
- コネクションプールとキャッシュのウォームアップ
- データベースノードのタブレット割り当てとブロックキャッシュ準備
結果: 実際のトラフィックが到着したとき、「コールドスタート」はありませんでした。すべてが準備済みでした。
5. 大規模評価フレームワーク:「裁判官」を判断するシステム
14億件のレポートで0.1%の障害率は140万件の「壊れた」ストーリーを意味します。手動レビューは事実上不可能です。
LLM-as-a-Judge評価パイプライン
- 評価軸: 正確性、安全性、トーン、フォーマット (4軸)
- サンプリング: 全コーパスではなく約165,000件のレポートを無作為サンプリング
- 複数小規模ルールベースクエリ: 1つの巨大プロンプトではなく、複数の小規模クエリで並列評価
- 推論後のスコア: 判定前に裁判官が推論プロセスを出力するよう要求し、一貫性向上
発見された問題と構造的修正ループ
評価中に発見された代表的なバグ:「最大の発見の日」レポートが誤ったアーティスト発見数を自信満々に祝福する問題
- 原因:上流データパイプラインの微妙なタイムゾーンバグ
- 対応:SQLクエリと正規表現パターンマッチングで類似障害を探索 → バッチ削除 → パイプライン修正 → 安全に再生成
教訓: 大規模LLMシステムでは、障害は「発生するかどうか」ではなく、「どれだけ早く発見し復旧するか」の問題です。
6. この技術の限界と注意点
- コスト: 14億回のLLM呼び出しは莫大なコストです。蒸留とDPOで最適化しましたが、依然として相当なインフラコストが発生します。
- 創造性の限界: プロンプトエンジニアリングで創造性を制御することはできますが、「真の創造性」を完全に保証することはできません。特に日本語のように文脈と語調が重要な言語では、追加のチューニングが必要になる可能性があります。
- 評価の難しさ: LLM-as-a-Judge方式は、評価モデルのバイアスが結果に影響を与える可能性があります。人間による評価との継続的なクロスチェックが必要です。
7. 次のステップとしての学習方向
本記事の内容をより深く理解したい場合は、以下のトピックを学習してください。
- Direct Preference Optimization (DPO): RLHFの代替として注目されるアライメント手法です。
- モデル蒸留(Model Distillation): 大規模モデルの知識を小規模モデルに転移する様々な手法を学びましょう。
- 分散キーバリューストア設計: Bigtable、Cassandra、DynamoDBなどのデータモデリングパターンを学習すると、並行性問題の解決に大いに役立ちます。
- LLM-as-a-Judge: 評価フレームワークを実際に構築し、どのメトリクスが重要なのか経験してみてください。
まとめ:LLM呼び出しは簡単な部分だった
Spotifyの事例が与える最大の教訓はこれです。14億回のLLM呼び出し自体は簡単な部分だった。 本当のエンジニアリングの挑戦は、キャパシティ計画、リプレイと復旧、コスト規律、セーフティループ、そしてすべてが完璧に動作しなければならないたった一度のローンチの瞬間を準備することにありました。
合わせて読みたい記事
![]()