AIREITER

22プラットフォーム・241コマンドで、統一レスポンスモデルを作らなかった理由

最終更新日: 2026-07-31 06:25:16

20以上のプラットフォームから公開データを集めるなら、まず統一モデルを描きたくなる。Postを1つ、Userを1つ用意し、各サービスのレスポンスをそこへ変換する設計だ。bilibiliの動画もtiktokの動画も、zhihuの回答もlinkedinの投稿も、見方によっては「著者がいるコンテンツ」である。最初の数プラットフォームでは、この抽象化は驚くほど気持ちよく機能する。だが20番目あたりでは、むしろ開発を圧迫し始める。

結局、私は統一モデルを作らなかった。20超のプラットフォーム、200超のコマンドを扱った結果、残ったのは一見すると素朴な「各プラットフォームは各自で完結させる」という分割だった。

統一モデルはなぜ徐々に壊れていくのか

破綻は一気には来ない。8つ目のプラットフォームを追加する頃には、Postにはオプショナルなフィールドが十数個もぶら下がっている。danmaku数を持つサービスもあれば持たないサービスもある。「公開日時」が秒単位のタイムスタンプで返る場合もあれば、「3 days ago」のような文字列で返る場合もある。20超まで増えると、統一層はコンパイルエラーを出して壊れるのではなく、何も省力化してくれなくなる形で壊れる。

下流のコードは毎回、「このプラットフォームはそもそもこのフィールドを埋めているか」を判定しなければならない。その分岐は生レスポンスを読むより長くなり、統一層は作業のために迂回すべき障害物になる。書き込み側でも同じ負債を払う。新しいプラットフォームを統合するたび、古いプラットフォームに合わせて作られた箱へフィールドを押し込むために、統一モデルへ戻ることになる。

プラットフォームごとに独立した境界を持たせる

持続する設計は逆だった。統一モデルを抽象化せず、各プラットフォームをそれぞれ完結させる。カタログ上では、各プラットフォームを<platform>_reverse/というコンテキストに置き、以下の4つをそのコンテキストだけが所有する。

  • 入力検証。IDの形式、許可されるパラメータの組み合わせを知っているのは、そのプラットフォームだけだ。

  • プロトコル。HTTPを直接叩くのか、ローカルのJS断片で署名するのか。どのドメインを使い、どのヘッダーを付けるのか。すべてプラットフォーム固有である。

  • 署名。署名機構はプラットフォームごとに大きく異なる。共通のsignerに詰め込めば、if-elseだらけの怪物になるだけだ。

  • レスポンス正規化。生レスポンスを、そのコンテキストが所有する構造へ整形する。グローバルな統一構造へ押し込むためではない。

誤解されやすいのは4番目だ。「統一モデルを作らない」は、「正規化しない」という意味ではない。もちろん各プラットフォーム内では正規化する。ただし、その変換先の形は各プラットフォーム自身が決めるものであり、共有モデルに強制されるものではない。

統一が有効なのは、本当に同じものを扱っている範囲に限られる。同一プラットフォーム内の2つのエンドポイントが投稿構造を共有するなら、それは正当な共通化だ。実際に同じドメインオブジェクトだからである。問題は、そのプラットフォーム内の共通化を、プラットフォーム横断へ持ち出すことにある。

共有層に置くのは、本当に横断的な機能だけ

では共有層には何を置くべきか。見た目が似ているものではなく、すべてのプラットフォームで実際に同じように振る舞う機能だ。私の共有層にあるのは、次の3つだけである。

  1. インターフェースのread-model。各プラットフォームのargparse宣言から、統一された機能カタログを生成する。ここで統一するのは、コマンドの見つけ方と説明の仕方であって、コマンドの返り値ではない。前者は本当に横断的だが、後者はプラットフォーム固有である。この「宣言そのものがインターフェースになる」という考え方は、interface-as-codeの記事で詳しく扱っている。

  2. ローカルループバックのトランスポート。認証済みリクエストはローカルのWebSocketセッションサービスを経由する。ここではすべてのプラットフォームを同じように扱い、個別のビジネスフィールドには一切触れない。

  3. ディスパッチの入口。プラットフォームを検出し、コマンドをそのコンテキストへ渡す。それ以上はしない。

判断基準は単純だ。共有層に入れるには、すべてのプラットフォームで本当に同じように動作しなければならない。トランスポート、ディスパッチ、インターフェース記述の生成は共通だ。一方、bilibiliの「コンテンツ」とlinkedinの「コンテンツ」は同じようには振る舞わないため、共有層には置かない。

「似て見える」は抽象化における最大の罠である。動画と動画は似ているから統一したくなる。しかし表面的な類似は、振る舞いの同一性ではない。それを共有可能なドメインモデルだと扱うことこそ、統一モデルが崩れる根本原因になる。

コマンド数の偏りを見ると、抽象化の損得が分かる

統一モデルにまだ迷いがあるなら、実際のコマンド数の分布を見るとよい。22プラットフォーム、241コマンドは、極端に偏っている。

プラットフォーム

コマンド数

tiktok

34

bilibili

26

linkedin

18

zhihu

18

douyin

17

xiaohongshu

16

残り16プラットフォーム

各1〜13

上位6プラットフォームだけで129コマンドとなり、全体の半分を超える。残り半分は16のロングテールなプラットフォームに分散しており、その多くは2つか3つのコマンドしか持たず、1つだけのものもある。

この分布が、抽象化の採算を決める。統一モデルのコストは固定だ。すべての統合担当者がフィールドを埋め、nullを確認し、モデルを迂回する処理を書く必要がある。一方、その利益はプラットフォーム単位でしか得られない。コマンドが2つか3つしかないロングテールのプラットフォームでは、抽象化の利益は負になる。統一モデルに合わせるアダプターコードのほうが、そのプラットフォーム本来のビジネスコード全体より長くなるからだ。

実装が1つしかない段階で抽象化を予約しない

この分布から、もう1つルールが導ける。実装が1つしかないもののために抽象化を予約しないことだ。あるプラットフォームに実装が1つしかないなら、「将来もう1つ増えるかもしれない」ためのrepository、factory、interface層は作らない。プラットフォームを追加するなら、<platform>_reverse/コンテキストを1つ追加すればよい。最初に共有ベースクラスへ手を入れる必要はない。

インターフェース層の価値は、複数の実装を交換可能にすることにある。実装が1つなら価値はゼロで、保守コストだけが残る。存在しない2つ目の実装に備えることと、存在しないプラットフォーム横断の共通性に備えることは、同じ誤りだ。

クロス言語移行でも、同じことを再確認した。旧レジストリにあった数百のコマンドのうち、移行しなかった一群は明示的に未移行のままとし、スタブも互換プロキシも残さなかった。空の殻は欠落よりコストが高い。次に触る人へ、そこに何かがあると思わせてしまうからだ。予約された抽象化も同じである。

モデルで正規化する場合も、文脈はプラットフォームごとに渡す

「プラットフォームごとに分け、統一モデルを作らない」という考え方は、モデルを使った正規化にもそのまま当てはまる。20超のプラットフォームから来る生レスポンスを分析可能な構造へ整えるなら、モデルに任せたくなる。ここで最も陥りやすい間違いも、コード層と同じだ。統一スキーマを定義し、各プラットフォームの生JSONに「このスキーマへマッピングして」と付け加える。

これはうまくいかない。モデルには、bilibiliの再生数フィールドとtiktokの再生数フィールドが本当に同じ概念かどうか分からない。最小公倍数的なスキーマへ押し込めば、プラットフォームにとって必要なフィールドを落とすか、中途半端に埋めるかのどちらかになる。

正しい方法は、プラットフォームごとに文脈を渡すことだ。「これはbilibiliで、このフィールドはこういう意味を持ち、このプラットフォームではこの形にしたい」と伝え、1プラットフォームずつ正規化する。プラットフォーム横断の統合は分析層に任せる。この処理はいくつかのステップに分かれ、それぞれモデルに求める能力が異なる。

ステップ

必要な能力

選択

model id

1プラットフォーム分の生レスポンス全体の構造を読む

長いコンテキスト。レスポンス全体とフィールド注記をまとめて扱える

Kimi K3

kimi-k3

正規化の境界を決める。本当に横断的なフィールドと、プラットフォーム固有のフィールドを分ける

強い推論力。過剰な統一に流されない

Claude Opus 5

claude-opus-5

プラットフォーム単位でフィールドを一括抽出し、アイテムごとにマッピングする

低コスト。数百〜数千件の呼び出しを高並行で実行できる

Claude Sonnet 5

claude-sonnet-5

同名フィールドが2プラットフォーム間で一致しない理由を説明する

中程度の推論力。フィールドに沿って差異を説明できる

GPT-5.6 Sol

gpt-5.6-sol

モデルの切り替えで結果が目に見えて変わるのは、2番目のステップだけである。ここで問われるのは、2つのフィールドが実際には同じものではないと認められるかどうかだ。これはアルゴリズムファミリー識別における反証のセクションと同じで、弱いモデルは「統一せよ」というヒントに従う。強いモデルは、その境界を指摘する。

本当の障害はモデルを切り替える手間

4つの階層は3つのベンダー、3つのSDK、3つの認証方式、3種類のエラーフォーマットにまたがる。ステップごとにモデルを変えるため、クライアントを書き換えるのを3回繰り返す価値はない。そのため多くの人は全工程を同じ階層で回してしまう。そして「境界を決める」段階でしか使えない階層を使い、20超のプラットフォームで再び崩れるスキーマを作ってしまうことが多い。

AIReiterは、この層をフラットにする。キーは1つ、インターフェースはOpenAI互換で1つ。その背後に4つの階層があり、リクエストボディのmodelフィールドを変えるだけで切り替えられる。

# Set the normalization boundary: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<one platform response sample + have it mark which fields are platform-specific>"}]
  }'

# Extract fields per platform in bulk: change the model field, leave the rest
#   "model": "claude-sonnet-5"
# Field-difference attribution:
#   "model": "gpt-5.6-sol"

すでにOpenAI SDKを使っているなら、base_urlをhttps://aireiter.com/api/v1に向けるだけで、ほかは変える必要がない。Anthropic SDKでは、同じキーでPOST /api/v1/messagesを呼び出せる。

価格面では、Claudeモデルは定価から30%オフ、GPTモデルは半額で、Kimi K3も同じキーで呼び出せる。割引が効くのは、まさに主要コストの部分だ。プラットフォームごとのフィールド一括抽出は最も呼び出し密度が高く、20超のプラットフォームそれぞれに数百〜数千件のレコードがあり、レコードごとに1回呼び出す。最も安価なSonnetを使い、そこからさらに30%オフとなる。

Kimi K3で長いレスポンス全体を読む際も、1入力あたり数十万トークンになり、別の大きなコストになる。一方、境界を決めるための推論階層は呼び出し回数が少なく、コストはほとんどかからない。

  • APIキーを取得する

  • 登録せずに試す:まずは1プラットフォームのレスポンスを手作業で流し、モデルが差異を正直に指摘するのか、それとも急いで均質化しようとするのかを確認してから、組み込むか判断するとよい。

まとめ

クロスプラットフォーム収集で最初に思い付く統一Post/Userの抽象化は、小規模では快適でも、20超のプラットフォームでは必ず崩れる。コストは固定で、利益はプラットフォーム単位にとどまり、しかもコマンド数は極端なロングテール分布になっているからだ。

持続する分割は、プラットフォームごとに1つの境界づけられたコンテキストを置くことだ。各コンテキストが入力検証、プロトコル、署名、レスポンス正規化を所有する。共有層には、トランスポート、ディスパッチ、インターフェース記述の生成など、すべてのプラットフォームで本当に同じように振る舞うものだけを置く。見た目が似ているだけのドメインモデルは置かない。実装が1つしかないものや、まだ存在しない共通性のために抽象化を予約することもしない。

モデルを使う場合も結論は同じだ。統一スキーマを渡すのではなく、プラットフォームごとの文脈を渡して正規化する。横断的な統合は分析層で初めて行う。4段階の完全なワークフローでは、この4階層の使い分けをさらに詳しく説明している。1つの統一インターフェースで接続すれば、切り替えコストは使い分けない理由ではなくなる。