Xのアルゴリズムを全部読んだら、「リプライは13.5倍」がどこにも無かった

Xの推薦アルゴリズムの公開コードを216ファイル、全部読んだ。攻略法は書かれていなかった。代わりに見つけたのは、みんなが引用している「13.5倍」という数字が、実はどこにも存在しないという話。

Share

Xがアルゴリズムを公開している。7月15日にはイーロン・マスクが「セキュリティレビューが終わり次第、Xのコードベース全体を、例外なくオープンソースにする」と表明した(※本日時点、これはまだ実行されていない。理由は後で書く)。

発信をしている身として、これは当然読みたくなる。コードが読めるなら、リーチの決まり方は「たぶんこう」ではなく、事実として語れるはずだ。

なので実際にgit cloneして、全部読んだ。Rust 139ファイル、Python 68ファイル。比較のために2023年の旧Twitter版(Scala)も読んだ。

結論から書く。リーチを決める数字は、1つも公開されていなかった。 そして日本語圏で広く引用されている「リプライはいいねの13.5倍」という数字は、2026年版にも2023年版にも存在しない。

コードは書けない。49歳。でも読むことはできる。だから読んだ。


これは4本目の話

僕はここ最近、同じ話を3回書いている。

  • 押せているように見えるボタン(登録フォームの配線が抜けていた話)
  • サイトが「5冊」と表示し続けていたtypo
  • 動いているように見えるものを、疑う

今回はその4本目。ただし今回は自分のミスの話ではなく、他人(というか世界で一番読まれているコードの1つ)の話だ。

公開されている、と、読める、は違う。 この記事はその実例。


§1 まず、何が公開されているのか

公開されているのは「式」と「構造」。

Xのフィードは、投稿ごとに「いいねしそうな確率」「リプライしそうな確率」「滞在しそうな確率」……をGrokベースのtransformer(Phoenix)が予測し、それぞれに重みを掛けて足し合わせたスコアで並び替えている。

変数名は全部読める。FavoriteWeight(いいねの重み)ReplyWeight(リプライの重み)DwellWeight(滞在の重み)……中核のスコア式はここにある。

// home-mixer/scorers/ranking_scorer.rs:146-172
let combined_score = Self::apply(scores.favorite_score, weights.favorite)
    + Self::apply(scores.reply_score, weights.reply)
    + Self::apply(scores.retweet_score, weights.retweet)
    + Self::apply(scores.dwell_score, weights.dwell)
    + Self::apply(scores.not_dwelled_score, weights.not_dwelled)
    // ...ほか17シグナル

ここで1つ、読者に伝えておきたい再定義がある。モデルが見ているのは「実際についたいいねの数」ではなく「この読者がいいねしそうな確率」だ。だから正確に言うなら、狙うべきは「エンゲージを集める」ことではなく「この読者は反応しそうだ、とモデルに思わせる」ことになる。

ここまでは、読めばわかる。問題はここからだ。


§2 その「重み」が、リポジトリに無い

上のコードのweights.favoriteweights.reply——この数値がどこから来るのか追いかけると、こうなっている。

// home-mixer/scorers/ranking_scorer.rs:3
use crate::params::*;

paramsというモジュールを読み込んでいる。ところが、

$ find . -name "params*" -not -path ./.git/*
(0件)
$ grep -rl "crate::params" --include="*.rs" . | wc -l
60

60ファイルが参照しているモジュールが、リポジトリの中に存在しない。

決定打はこれだ。home-mixer/lib.rsは全17行、コメントなしのシンプルなファイルで、13個のモジュールを宣言している。

// home-mixer/lib.rs
pub mod ads;
mod candidate_hydrators;
pub mod candidate_pipeline;
mod filters;
mod for_you_server;
pub mod models;
mod query_hydrators;
mod scored_posts_server;
pub mod scorers;
mod selectors;
pub mod server;
mod side_effects;
mod sources;

13個の中にparamsが無い。なのにfor_you_server.rsscored_posts_server.rsparams::TEST_USER_IDSを参照している。

公開されたコードは、そのままではコンパイルできない。 「動くものを公開した」のではなく、「動かないものを公開した」というのが正確な言い方になる。

さらに言うと、2026年5月版では重みは定数ですらなく、実行時に読み書きできるフィーチャースイッチから取得される仕組みに変わっていた。つまり仮にparamsが公開されていたとしても、それは「キーの定義」でしかなく、実際の運用値はサーバ側でいつでも変更できる。「コードを読めば重みがわかる」という前提自体が、2026年版ではもう成立しない。


§3 手で抜いた跡が残っている

ここが今回、僕が一番強い証拠だと思った部分。

grox/tasks/task_filters.pyの56行目。

class TaskSpamFilter(TaskFilterWithPost):
    FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = ""

本来は整数が入るはずの変数が、空文字になっている。ところが同じファイルの122行目・124行目では、この値を整数と比較している。

# grox/tasks/task_filters.py:122, 124
follower_count > cls.FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION

Pythonで文字列と数値を>で比較すれば、実行時にTypeErrorになる。このコードは、このままでは動かない。

「たまたま空欄だった」ではなく、「元は数字が入っていて、公開前に手で空文字に置き換えた」以外の説明が難しい。抽象的に「隠している」と言うより、消し忘れが残っていると言うほうが正確だし、こっちのほうがずっと生々しい。

同じパターンがTaskReplyRankingFilterにもある(:138)。「フォロワー何人から"大アカウント"扱いになるか」という、リプライ営業をする人が一番知りたいであろう閾値も、同じ理由で公開されていない。


§4 では「13.5倍」はどこから来たのか

これが今回の記事を書くきっかけになった話。

日本語圏でよく見る「リプライはいいねの13.5倍の価値がある」という説明。これがコードのどこに書いてあるのか確かめようとして、両方のリポジトリを全文検索した。

$ grep -rn "13\.5" --include="*.rs" --include="*.py" .   # 2026年版
(0件)
$ grep -rn "13\.5" --include="*.scala" .                 # 2023年版
(0件)

どちらにも、1件も無い。

念のため2023年版(Scala)の重み定義も読んでみた。ScoredTweetsParam.scalaに17個の重みが定義されているが、これが全部1.00.0のプレースホルダだった。

object FavParam
    extends FSBoundedParam[Double](
      name = "scored_tweets_model_weight_fav",
      default = 1.0,
      min = 0.0,
      max = 100.0
    )

極めつけはNegativeFeedbackV2Paramdefault = 1.0なのにmin = -1000.0, max = 0.0と定義されている。デフォルト値が、自分で決めた許容範囲の外にある。 これは実運用の値ではなく、単なる仮置きの数字だという何よりの証拠だ。

つまり2023年版が公開したのも「重みを入れる箱と、その上下限」であって、実際の値ではなかった。2026年版もまったく同じ構造で、値だけがさらに徹底して非公開になっている。

ここで大事なことを1つはっきりさせておく。「13.5倍は誰かが捏造した」とは書かない。 事実として言えるのは「両方の公開コードを全文検索したが出所を辿れなかった」まで。言えないことは言わない。それがこの記事の唯一の価値だと思っている。


§5 それでも、構造から言えることはある

重みの値はわからない。でも、式の「形」からは言えることがある。数値の大小は不明という前提つきで、5つ。

# 言えること コード上の根拠
1 素通りされることは、0点でなくマイナスに働く。滞在は「したか(dwell)」と「秒数(cont_dwell_time)」で二重に加点され、さらに「止まらずスクロールされる確率(not_dwelled)」が明示的な減点として引かれる ranking_scorer.rs:159-170
2 同じ投稿者の2投稿目以降は指数的に減衰する。式は(1-floor)×decay^position+floor。連投は本数に比例してリーチしない ranking_scorer.rs:186-217
3 同じ会話(スレッド)はフィード上で1本に潰される。最高スコアの1件だけが残り、他は全部除外される。伸ばしても表示枠は増えない dedup_conversation_filter.rs:14-38
4 フォロー外の投稿だけ、最後に一律の係数が掛け算される。フォロー内は無変換。同じ品質なら、フォロー内の方が構造的に有利 ranking_scorer.rs:272-275
5 大きいアカウントへのリプライは、Grokが0〜3点で品質採点する。小さいアカウントへのリプライは採点自体がスキップされ、低フォロワーからのリプライは専用のスパム判定に回る。リプライ営業は「量」でなく「採点を通るか」の勝負になっている task_filters.py:137-201

§6 通説の答え合わせ

読者が一番知りたいであろう部分を、表にした。

よく聞く説 コードでの検証
リンクを貼るとリーチが落ちる ❌ 2023年版・2026年版とも、ランキング処理にURL/リンクという概念自体が存在しない。ただし「外部に離脱しやすい→素通り扱いされやすい」という間接的な経路はありうる。これは推論であって、コードで確認した事実ではない
日本語は不利 ❌ 言語別の重み・分岐は両バージョンとも皆無。言語は「モデルに渡す特徴量の1つ」であって、ハードコードされたペナルティではない
連投はペナルティ 「1日に何本投稿したか」に対するペナルティは見つからなかった。実際にあるのは§5の2(同一応答内の減衰)と3(会話の一本化)で、体感の「連投は伸びない」はこの2つで説明できる
認証・Premiumだと伸びる 2023年版にはBlueVerifiedAuthorInNetworkMultiplierParam = 4.0が実数で存在した。2026年版には対応する加点が見当たらないverified``premium``blueいずれもランキング処理にヒットしない)

§7 一次情報を読んだ人にしか書けない段落

もう1つ、これは書いておきたい。

tweet_type_metrics_hydrator.rsというファイルには、フォロワー数の区切り(100人未満、100〜1000人、1000〜1万人……)がハードコードで並んでいる。これを「フォロワー数の階層でリーチが変わる証拠」として紹介している解説をいくつか見た。

読んでみると、これはランキングに一切関与しない。関数名の通り、ただの監視用の指標(メトリクス)を作っているだけで、出力はスコアではなく統計データとして流れていく。

一見おいしそうな発見に見えるけど、コードを最後まで追うとハズレだった。これが今回、216ファイルを実際に読んだことの証明になる部分だと思う。


§8 限界を先に書いておく

ここまで「読めばわかったこと」を書いてきたけど、この記事の信頼性のために、限界も先に書いておく。

  • xAI自身が「本番はもっと大きなモデル、もっと多くの層を使っている」「これは学習プロセスの中の、ある時点の凍結スナップショットにすぎない」と明記している。公開コードが本番と一致する保証はない。
  • xAI自身のドキュメントの中にも矛盾がある。ルートのREADMEは同梱モデルを「2層」と書き、Phoenix側のREADMEは「4層」と書いている。
  • 「4週間ごとに更新する」というマスクの公約は、これまでのところ守られていない。1月20日から5月15日まで115日(約16週)、5月15日から今日まで66日(約9週)、更新は止まっている。
  • 7月15日に表明された「コードベース全体の公開」も、本日時点で未実行。GitHub APIで確認したところ、xai-orgの公開リポジトリは9件のままで、7月15日以降に新しく作られたものは1つもない。

隠したい部分ではなく、正直に書く部分だと思っている。


§9 それで、何が言えるのか

公開されたのは「アルゴリズム」であって、「答え」ではなかった。

でも、読んだから、「読まないと言えないこと」は言えるようになった。「13.5倍はどこにも無い」も、「監視用コードをランキング根拠と誤読してはいけない」も、実際にcloneして読まないと出てこない話だ。それで十分だと思う。

公開されている、と、読める、は違う。


この記事で引用したファイル・行番号・再現コマンドは全て一次情報(実際の公開リポジトリ)に基づく。検証したい人は誰でも同じコマンドで追試できる。 → こういう検証と「作り方」を、実際に手を動かして学びたい人向けにまとめているのがスターターキット