AIREITER

22 個平台、241 個指令:我為什麼沒有做統一回應模型

最近更新: 2026-07-31 06:26:43

要從二十幾個平台抓公開資料,大多數人的第一步大概都是先抽象化:畫出一個統一的 Post、一個統一的 User,再把各平台回傳的資料都映射進去。bilibili 的影片、tiktok 的影片、zhihu 的回答、linkedin 的貼文,乍看之下不就是「一筆內容加上一位作者」嗎?前面接三個平台時,這個設計確實很順;接到第二十個平台時,卻足以把整個人埋進去。

最後我沒有做統一模型。在二十幾個平台、兩百多個指令的規模下,看似更笨的做法——「每個平台各管各的」——反而是唯一撐得住的結構。

統一模型是怎麼一步步失效的

它不是突然壞掉,而是慢慢失去價值。接到第八個平台時,Post 上已經掛著十幾個 optional 欄位:有的平台有彈幕數,有的平台沒有;有些平台的「發布時間」是精確到秒的 timestamp,有些則只是「3 days ago」這種字串。到了二十幾個平台,問題不會表現成編譯錯誤,而是這層模型再也沒有替你省下任何事。所有下游程式都得先判斷「這個平台到底有沒有填這個欄位」,判斷邏輯甚至比直接讀原始回應還長;統一層最後成了做事時必須繞過的障礙。

寫入端也得付出同樣代價。每多整合一個新平台,你都得回頭修改它,把新欄位硬塞進原本為舊平台量身做出的格子裡。

每個平台都該是一個 bounded context

真正能撐住的切法恰好相反:不要抽象出統一模型,讓每個平台管理自己的事。在目錄結構裡,每個平台都是一個 <platform>_reverse/ context,獨自擁有四件事,不對外借出:

  • 輸入驗證:這個平台的 id 長什麼樣子、哪些參數組合合法,只有它自己知道。

  • 協定細節:要直接走 HTTP,還是執行一段本地 JS 簽名;該打哪個網域、帶哪些 headers,全部都屬於平台私有實作。

  • 簽名機制:各平台的簽名差異非常大,硬塞進一個共用 signer,只會養出滿是 if-else 的怪物。

  • 回應正規化:把原始回應整理成它自己擁有的結構,而不是硬轉成全域統一結構。

第四點最容易被誤解。「沒有統一模型」不代表「不做正規化」。每個平台當然都需要正規化,只是目標結構應由平台自己定義,而不是被迫符合一個共用模型。真正可以統一的地方,僅限於本來就是同一件事的情境:例如同一平台內的兩個 endpoint 共用同一種貼文結構,這種統一完全合理,因為它們確實是同一個 domain object。錯誤在於把平台內的統一,硬拉成跨平台的統一。

共用層只放真正跨平台的能力

那共用層裡該放什麼?答案是:每個平台都以完全相同方式運作的能力,而不是表面看起來相似的東西。我的共用層只有三項:

  1. 介面 read-model:從各平台的 argparse 宣告推導出統一的能力目錄。這裡統一的是指令如何被發現與描述,而不是指令回傳什麼資料。前者真正跨平台,後者則是平台私有。(「宣告就是介面」這個概念,可參考 interface-as-code 這篇文章。)

  2. 本機 loopback transport:經過驗證的請求會透過本機 WebSocket session service 傳遞;它以相同方式處理所有平台,完全不碰任何平台的業務欄位。

  3. 分派入口:找出目標平台,把指令交給對應 context,除此之外不做多餘事情。

判斷標準很簡單:要進共用層,就必須真的在每個平台上都以相同方式運作。Transport、dispatch,以及介面描述的產生方式,對所有平台都是一致的;但 bilibili 與 linkedin 所謂的「一筆內容」根本不是同一種行為,因此不該放進共用層。「看起來很像」是抽象化最常見的陷阱:兩支影片看起來都是影片,所以你會想統一它們;但表面相似不等於行為相同,把它當成可共用的 domain model,正是統一模型崩解的根源。

指令分布,決定抽象化值不值得

還在猶豫要不要做統一模型?看一眼真實的指令數分布,答案就很清楚。22 個平台、241 個指令,而且分布極度不平均:

平台

指令數

tiktok

34

bilibili

26

linkedin

18

zhihu

18

douyin

17

xiaohongshu

16

其餘 16 個平台

各有 1 到 13 個

前六個平台合計 129 個指令,超過全部的一半;剩下的一半則分散在 16 個長尾平台,其中很多只有兩三個指令,有些甚至只有一個。

這種分布直接決定了抽象化的成本效益:統一模型的成本是固定的——每個整合者都得填欄位、檢查 null、繞過模型限制;但效益卻是按平台分攤。對一個只有兩三個指令的長尾平台來說,抽象化的效益甚至是負的,因為為了讓它塞進統一模型而寫的 adapter 程式,比它所有業務程式還長。

不要替唯一的實作預留抽象層

順著這個分布,還有一條規則:不要為只有一個實作的東西預留抽象層。當某個平台只有一種實作時,不要因為「以後可能會有第二種」就先加 repository、factory 或 interface layer。新增一個平台,就只是新增一個 <platform>_reverse/ context,不需要先去修改共用 base class。

介面層的價值,在於讓多個實作可以互換;只有一個實作時,它的價值是零,維護成本卻是正數。替尚未存在的第二個實作預留位置,和替尚未存在的跨平台共通性預留位置,本質上是同一個錯誤。跨語言遷移時也再次驗證了這點:舊 registry 的幾百個指令中,有一批被明確保留為未遷移狀態,沒有留下 stub,也沒有 compatibility proxy,因為空殼的成本比缺口更高,它會讓下一個接手的人誤以為裡面真的有東西。預留的抽象也是一樣。

跨平台正規化時,模型也要吃到平台脈絡

「依平台切分,不做統一模型」這套思路,用在模型協助正規化時同樣成立。要把二十幾個平台的原始回應整理成可分析的結構,很自然會想交給模型處理;而這裡最容易犯的錯,正好和程式碼層一樣:先定義一個統一 schema,再把每個平台的 raw JSON 丟進去,附上一句「請映射到這個 schema」。

這會失敗,因為模型不知道 bilibili 的播放數欄位和 tiktok 的播放數欄位是否真的是同一件事。強迫它塞進最低共同分母的 schema,不是丟掉平台真正依賴的欄位,就是把欄位填得似是而非。

正確做法是逐平台提供脈絡:告訴模型「這是 bilibili,這些欄位各自代表什麼,我希望這個平台整理成什麼結構」,一次正規化一個平台,跨平台合併則留給分析層處理。整個流程可拆成幾個步驟,每一步對模型的要求都不同:

步驟

所需能力

選擇

model id

讀懂單一平台完整原始回應的結構

長上下文,一次吃下完整回應與欄位說明

Kimi K3

kimi-k3

界定正規化邊界:哪些欄位真正跨平台,哪些是平台專屬

推理能力強,不會過度統一

Claude Opus 5

claude-opus-5

批次逐平台擷取欄位,逐筆映射

成本低,可高併發執行數百到數千次呼叫

Claude Sonnet 5

claude-sonnet-5

說明兩個平台中同名欄位為何對不起來

中等推理能力,能依欄位解釋差異

GPT-5.6 Sol

gpt-5.6-sol

第二步是唯一會因為換模型而明顯改變結果的地方。它測試的是:你是否願意承認兩個欄位其實不是同一件事。這和演算法家族辨識中的反證段落一樣:較弱的模型會順著你「統一」的暗示走,較強的模型則會指出真正的邊界。

真正的障礙是切換成本

這四個層級來自三家供應商、三套 SDK、三種驗證方式,以及三種錯誤格式。為了在不同步驟切換模型而重寫三次 client,不值得;因此多數人會從頭到尾只跑同一個層級,而且往往在「界定邊界」這一步使用只會壓平差異的模型,最後做出一套到了二十幾個平台又再次崩解的 schema。

AIReiter 把這一層攤平:一把 key、一套 OpenAI-compatible 介面,背後接上全部四個層級;只要修改 request body 裡的 model 欄位就能切換。

# 界定正規化邊界:使用推理層
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": "<單一平台回應範例 + 請模型標示哪些欄位為平台專屬>"}]
  }'

# 批次逐平台擷取欄位:只改 model 欄位,其餘不動
#   "model": "claude-sonnet-5"
# 欄位差異歸因:
#   "model": "gpt-5.6-sol"

如果你已經在使用 OpenAI SDK,只要把 base_url 指向 https://aireiter.com/api/v1,其他設定都不用改。使用 Anthropic SDK 時,則以同一把 key 呼叫 POST /api/v1/messages。

價格方面,Claude 模型享有定價 7 折,GPT 模型半價,Kimi K3 也可使用同一把 key 呼叫。折扣剛好落在最主要的成本上:批次逐平台擷取欄位是呼叫最密集的步驟,二十幾個平台、每個平台數百到數千筆紀錄,每筆一次呼叫,使用最便宜的 Sonnet,再加上 7 折。用 Kimi K3 讀完整長回應時,每個輸入有數十萬 token,也是另一塊成本;至於用推理層界定邊界,呼叫次數很少,幾乎不花多少錢。

  • 取得 API key

  • 免註冊試用:先手動拿一個平台的回應跑一次,看看模型會不會誠實標出差異,還是急著把它們壓成同一種結構,再決定要不要接進流程。

結語

跨平台蒐集資料時,第一反應往往是抽象出統一的 Post/User;小規模時感覺很好,但到了二十幾個平台幾乎必然崩解:成本固定、效益按平台分攤,加上指令分布極度長尾。能撐住的結構,是一個平台一個 bounded context,各自擁有輸入驗證、協定、簽名與回應正規化。共用層只放所有平台都真正以相同方式運作的東西——transport、dispatch、介面描述產生——而不是只因表面相似就硬湊出來的 domain model;也不為唯一的實作,或根本不存在的共通性預留抽象。

模型處理也是同一句話:以每個平台的脈絡進行正規化,不要餵它統一 schema;跨平台合併只該發生在分析層。完整的四階段工作流程會更詳細說明四個層級如何分工;而透過同一套統一介面串接後,切換成本就不再是不用它們的理由。