vatt'ghern jaskier's ballads
本文 4 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

LinkedIn 把 search 這個產品改寫的時候,最大的工程決定不是「換一個更強的 ranker」,而是承認傳統 NER 分類器加啟發式串成的 query understanding 層已經到了天花板——他們把它整個換成一個 1.5–4B 參數的微調 LLM,然後再用 0.6B 學生模型把 ranking 也吃下來。這篇拆四層:query understanding、embedding-based retrieval、SLM cross-encoder ranking、跟讓上述全部可服務的那套蒸餾與壓縮管線。

LinkedIn 把 search 棧改成 LLM-centric——重寫的不是 ranker,是「query 怎麼被理解」這層

LinkedIn 工程團隊近期公開了一篇關於 search stack 重寫的長文,主軸不是「我們把 ranker 改成 LLM」這種一句話的故事——他們把 query understanding、retrieval、ranking 三層全部換成 transformer-based,外加一整套蒸餾、壓縮、評估管線把成本壓回可服務的範圍。這篇文章按 query 在系統裡走的順序拆開四個 component:query understanding 層的 1.5–4B 微調 LLM、dual-tower bi-encoder 做 embedding-based retrieval、0.6B 學生 SLM 做 cross-encoder ranking、以及讓整體在「每秒百萬級 query」的場景下還能存活的 efficiency stack(pruning、context summarization、embedding compression)。每一層都有自己的 latency budget、自己的離線 / 線上評估方法、跟自己被砍掉的 fallback。

點任一層查看責任邊界——四層管線,從 query 進來到結果送出

query → understanding → retrieval → ranking → auction query in L1 · query understanding 1.5 – 4 B param LLM intent · facet · rewrite 取代 NER + 啟發式 reasoning cache (Couchbase) L2 · EBR dual-tower bi-encoder exhaustive kNN on CUDA GPU > 1.6 B docs NDCG@10 = 0.838 L3 · ranking 0.6 B SLM cross-encoder P(yes) / (P(yes)+P(no)) distilled 7B→1.7B→0.6B multi-task: 6+ actions NDCG@10 = 0.9239 L4 · serving depth controller pruning RL summary embed compress 22,000 items/s/GPU 點任一層 ── 每個 component 自己負責自己的 latency budget 與 fallback 底層 storage:Venice (KV) · Spark + Flyte (offline) · Flink (nearline)

click a layer above

L1 · query understanding

1.5–4B 參數 fine-tuned LLM 取代過去多支 NER + heuristic 組成的 brittle pipeline。一個模型同時做 intent classification、facet extraction、跟根據查詢者 profile 的 query rewriting;輸出是結構化 JSON 給下游用。Prompt template 採用 instruct-style:Instruct: ... Query: {query} {Aspect}: {value}

Reasoning cache:模型對 query 產生一段「thinking state」,存進 Couchbase,後續語意接近的 query 直接命中——對熱門 query 等於跳掉這層計算成本。

不負責:候選 document 的 score。L1 只輸出「query 想要什麼」,不碰任何 document 內容。

L2 · embedding-based retrieval

Dual-tower bi-encoder:query tower 與 document tower 各自把輸入 map 到共享語意空間,retrieval 是兩邊 embedding 做 dot product。Training loss 是 contrastive InfoNCE 加 margin-based ranking loss,hard-negative mining 從 production log 抽出「被模型誤判為相關的 non-relevant」拿來監督。

Exhaustive 不近似:所有 job embedding precompute 放進 GPU-backed index,retrieval 對 1.6B+ 文件做 exhaustive kNN——不走 IVF / HNSW 等 ANN。代價是 GPU 成本,得到的是 recall 上界。Baseline NDCG@10 = 0.838。

不負責:精細排序。L2 只負責「召回對的候選集合」,最終順序交給 L3。

L3 · SLM cross-encoder ranking

0.6B 參數 decoder-only SLM 做 cross-encoder:每組 (query, document) 拼成單一 prompt 餵進模型,讀 yesno token 的 logit,relevance = P(yes) / (P(yes) + P(no))。蒸餾鏈是 7B teacher → 1.7B intermediate → 0.6B production student;loss 是 KL divergence 對 teacher 的 soft target。

Multi-task:除了 relevance,同時訓練 6+ 個 member action 目標——job view、apply、recruiter accept 等——讓單一模型既懂「相關」也懂「會被採取行動」。

不負責:召回。L3 只對 L2 召回的候選集做 rerank。

L4 · serving efficiency

讓上面三層在 millions of QPS 的場景能存活。三道壓縮:model pruning(去除 FC layer 與整層 transformer block)、context summarization(RL 訓出簡短 summary 取代長 job description)、embedding compression(用單一 embedding 取代多數 description 文字)。throughput 從 raw 290 items/s/GPU 推到 22,000——76 倍。

Ranking-depth controller:根據負載即時決定要對多少候選跑 L3——尖峰時段 graceful degradation。

不負責:modelling。L4 只在 L3 既定模型上做 efficiency engineering。

互動圖表

LinkedIn 把 query understanding、EBR、SLM ranking、serving 串成四層管線。

四層的順序就是 query 在系統裡走的順序:

query 進 L1 變成結構化意圖、L1 的輸出進 L2 變成 dense embedding 做召回、L2 召回的候選集進 L3 做精排、L3 的順序進 L4 的 auction 層做最終呈現。

這四層的拆法跟「每層各自移除一段 fixed overhead」的 cold-start 解法不一樣——LinkedIn 的拆解更接近「每層各自取代一段傳統 IR pipeline」:

  • L1 取代 NER + heuristic;
  • L2 取代 BM25 / lexical retrieval;
  • L3 取代 GBDT ranker;
  • L4 取代「我們不能 deploy LLM 因為它太慢」這個工程信念。

本文後面依層深入,每層補上自己的 input、output、latency budget、跟「去掉這層會回到什麼狀態」這四個面向。

先看一張全局圖——下面這張圖把 raw SLM 到優化後 SLM 的 throughput 曲線跟對應 NDCG 一起畫出來,讓你直接看到「砍多少 quality 換多少 throughput」的兌換率。

把 raw SLM 一路壓縮到 production:throughput 從 290 飆到 22,000 items/sec/GPU(76×),NDCG@10 只從 0.9432 掉到 0.9239(2% 相對差)。資料:LinkedIn engineering blog。

把 raw SLM 一路壓縮到 production:throughput 從 290 飆到 22,000 items…

SLM 壓縮後 throughput 從 290 飆到 22,000 items/s/GPU,NDCG@10 僅降 0.02。

曲線往右上跑越遠越好——可是它沒有「往上」走,它往右走了將近兩個 order,往下只掉 0.02。

LinkedIn 把這個曲線當設計依據:

  • raw SLM 在 NDCG 上最高 0.9432,但只能跑 290 items/s/GPU,沒辦法服務 production;
  • 做完三道壓縮的 0.9239 在 NDCG 上「夠好」、在 throughput 上「夠多」。

他們的判斷是「NDCG 從 0.94 掉到 0.92 對最終 click AUC 的影響在誤差範圍內,可是 throughput 差 76 倍直接決定服務能不能上線」。

這個曲線形狀是整個 deep-dive 的隱含 thesis:transformer 進 search 不是「能不能」的問題、是「怎麼把它壓回 serving budget」的工程問題。後面四個小節分別把這四層拆開講。

query understanding:用 1.5 – 4 B LLM 取代 NER + 啟發式拼盤

傳統 search 的 query understanding 層長這樣:

  • tokenizer;
  • spell correction;
  • NER 抽 person / company / skill;
  • intent classifier 判 navigational / informational;
  • 加幾條 hand-written 規則處理 "engineer at google" 這種兩元組查詢。

每個 component 自己訓自己的模型,每個 component 有自己的 false positive 模式,每加一個新查詢類型就要訓一個新分類器。

LinkedIn 寫得很直接:「fine-tuned 1.5–4B parameter models...replaces multiple brittle NER and heuristic components」——把整堆東西換成一個微調 LLM。

選 1.5–4B 不是隨便挑的。再大延遲撐不住,再小就會在 facet extraction 的長尾上掉精度(「looking for a remote ML researcher who worked on RLHF at Anthropic or OpenAI」這種 query 對 NER 分類器是惡夢,對 4B LLM 是日常)。Prompt 是固定模板:

Instruct: Given a job search query, retrieve relevant job postings
Query: {query}
{Optional Aspect, eg. Company}: {company}

// 模型輸出
//   - intent: { job_search | profile_lookup | post_search | ... }
//   - facets: { skills: [...], companies: [...], locations: [...] }
//   - rewritten_query: string  ← profile-aware 重寫
//   - thinking_state: string   ← 給 reasoning cache 用

"profile-aware rewriting" 這個能力是傳統 NER pipeline 給不出來的——LLM 在 prompt 裡看得到查詢者的 profile context(資深軟體工程師、過去搜過 staff-level 職缺),可以把 "ml engineer" 自動補成 "senior ml engineer"。

NER 抽 entity 抽不出這層 context、heuristic 規則也寫不到這個粒度。

但延遲怎麼撐?兩個機制:

第一,LinkedIn 在前面架了一個 lightweight encoder——「a lightweight encoder classifies query types at high QPS and performs policy-based safety checks」——對顯然不需要重型 LLM 的 query(例如 navigational 的「My LinkedIn」、安全層擋掉的)直接 short-circuit。

第二,reasoning cache:LLM 對 query 產的「concise thinking state」存進 Couchbase,後續語意接近的 query 直接命中 cache 跳掉計算。對 head queries 這層的攤銷成本接近零;只有 tail queries 真的吃 LLM forward。

下面這個小工具讓你直接玩這個 budget——調 prompt 的 token 長度,看 latency vs quality 怎麼動。曲線基於文章揭露的 prompt 長度上界(2048 tokens)跟 job description 長度分布(中位 ~900 tokens、max > 2300)推算,左邊「短 prompt」省 latency 但被迫截掉 job description 結尾——「removing descriptions severely degrades relevance quality, confirming they carry essential semantic information」——右邊「長 prompt」保留語意但 latency 直接 quadratic 上升(self-attention 對 sequence length 是 O(n²))。

prompt token budget 1024
latency (相對) 1.0×
quality 保留 92%
建議 折衷區間
滑桿沿 token budget 移動。latency 走 O(n²)(self-attention),quality 走 S-curve(短 prompt 截掉 description 語意、長 prompt 邊際效益趨零)。Sweet spot 落在中段——不是無限長越好。

滑桿沿 token budget 移動

prompt token budget 的 latency 成 O(n²),quality 走 S-curve,甜蜜點在 400–1100 token。

2048 不是隨便挑的上界——LinkedIn 在 prompt 中明寫:「truncate them to ensure the total prompt length does not exceed 2048 tokens」。Job description median ~900 tokens、max > 2300——把 2048 設成 hard cap,等於對 long-tail(大公司放的鉅細靡遺 JD)做 truncation。他們也說明了不可省:「removing descriptions severely degrades relevance quality, confirming they carry essential semantic information」——把整段 description 拿掉測一輪,relevance 明顯垮,證明 description 是必要的語意載體;但對長度設一個 hard cap 在語意上接近無損。這個 prompt budget 是「LLM 進 search box 但不爆 latency」的核心 trick——後面 L3 SLM ranking 用同一個 budget 概念再做一次。

EBR:dual-tower bi-encoder 在 GPU 上的 exhaustive kNN

第二層的 retrieval 用 dual-tower bi-encoder——一個 query encoder、一個 document encoder,各自把輸入 map 到共享語意空間,retrieval 變成查 query embedding 跟所有 document embedding 的 dot product。

LinkedIn 的關鍵選擇是 exhaustive 不近似

  • 「performs embedding-based retrieval (EBR) on CUDA-enabled GPUs using exhaustive vector search」
  • 「>1.6B exhaustive search on GPU」

傳統 dense retrieval 都用 IVF(inverted file index)、HNSW、ScaNN 這類 ANN(approximate nearest neighbor)把 query 限縮到子空間做搜索,犧牲 recall 換 latency。LinkedIn 直接走 brute-force——所有 1.6 B 文件的 embedding precompute 放進 GPU 記憶體,每個 query 都做完整 dot product。

會這樣做的理由是 quality ceiling:

ANN 的 recall@K 永遠低於 exhaustive,差距通常 1–5 個百分點,這 1–5% 對最終 NDCG@10 的衝擊是十倍級放大(因為 L3 ranking 是條件機率——若候選集裡根本沒有正確文件,再強的 ranker 也沒辦法)。

GPU 的 dense matrix multiply 對這種「N 個 query × M 個 document」的 batch 是天生擅長的——每個 SM core 都在做 dot product,沒有 branch、沒有 cache miss、float16 / bfloat16 算力滿載。

對 LinkedIn 這個規模,「我們已經買了 GPU 來服務 L3 ranking 了,順手做 retrieval 沒有額外硬體成本」這個邏輯成立。

Training loss 是兩段組合:

L_total = L_infoNCE + α * L_margin

L_infoNCE(q, d_pos, [d_neg]) =
    -log( exp(sim(q, d_pos) / τ)
        / Σ_d exp(sim(q, d) / τ) )       // contrastive over batch + hard negs

L_margin(q, d_pos, d_neg) =
    max(0, sim(q, d_neg) - sim(q, d_pos) + m)   // pairwise margin

// hard negatives = "non-relevant jobs the model ranks high" from prod log
// τ ≈ temperature; m ≈ margin (LinkedIn unspecified)

單獨 InfoNCE 在 batch 內 negative 用完之後 gradient 變稀;margin loss 對每個 hard negative 都產生 explicit 梯度,補上長尾的鑑別力。Hard negative 來源是 production log——「Hard negatives are non-relevant jobs that the model ranks high」——把模型自己搞錯的 case 反餵回訓練。Training infra 用 Hugging Face Accelerate + PyTorch FSDP(Fully Sharded Data Parallel)跨多 GPU 訓——bi-encoder 本身不大,但 batch 要夠大才能讓 InfoNCE 的 negative sampling 有意義(batch 內每個 positive 的「其他樣本」都是 in-batch negative)。EBR 這層的 baseline NDCG@10 是 0.838——比 L3 ranking 的 0.92+ 低很多,這在 retrieval 是預期的:retrieval 只需要把對的東西放進候選集就行,精排交給 L3。

兩座 tower 各自編碼、共享語意空間裡比對 query "remote ML researcher" query encoder fine-tuned transformer e_q ∈ R^d document job posting document encoder fine-tuned transformer e_d ∈ R^d GPU-resident embedding index exhaustive kNN — sim(e_q, e_d) = e_q · e_d > 1.6 B documents, no ANN approximation Training: contrastive InfoNCE + margin loss · hard negatives from production log Infra: HuggingFace Accelerate + PyTorch FSDP, multi-GPU end-to-end fine-tune
左右 tower 對稱、不共用參數——「dual」的核心是 query 與 document 走不同 encoder。training 時 contrastive loss 把對應 (query, document) pair 在共享空間裡拉近、跟 negative 拉遠。inference 時 query 即時 encode、所有 document embedding 預先算好放 GPU。

Embedding 預算成本決定一切——把 1.6 B 文件的 embedding 放上 GPU memory 不是免費的。假設每個 embedding 是 768 維 float16,每筆 1.5 KB,1.6 B 筆需要 ~2.4 TB——遠超單一 GPU memory,必須 shard 到多 GPU、靠 NVLink / network 拼回。後面 L4 efficiency 那層的「embedding compression」就是直接攻這個 cost——把 description 的多個 embedding 壓成單一,shard 數隨之下降。「Precompute and store all job embeddings in GPU-backed indexes」這句話背後是一整套 offline pipeline——Spark 跑 batch 抽 job、Flyte 排程、把 embedding 寫進 GPU index;增量更新走 Flink nearline,每次新 job 上架 / 既有 job 改寫,nearline 重新 encode、把該文件的 embedding 替換進 index。bi-encoder 設計天然支援這個——document side 只算一次、改了再算一次,跟 query side 完全解耦。

SLM cross-encoder ranking:0.6 B 學生模型怎麼吃下 NDCG@10 = 0.9239

L3 是整套 stack 的算術重心。

Bi-encoder 的限制是 query 跟 document 各自 encode、最後 dot product——這意味著 document 的 embedding 不知道 query 是什麼。

Cross-encoder 反過來:把 (query, document) 拼成單一序列,整段一起餵進 transformer,模型對每個 token 都能看到完整 context。

代價是 retrieval 不可行(每個 query 都要對所有 document 跑一次 forward,計算複雜度 N × M),但對 retrieval 之後已經縮到 K 個候選的 reranking 是天生適合的。

LinkedIn 的 SLM 是 decoder-only,relevance score 從 token logit 直接算:

prompt = format_prompt(query, document)
logits = SLM(prompt)
last_logits = logits[-1]                    // 序列最後一個 position
P_yes = softmax(last_logits)[yes_token_id]
P_no  = softmax(last_logits)[no_token_id]

relevance = P_yes / (P_yes + P_no)          // ∈ [0, 1]

// "yes" / "no" 不是 fine-tune 後新加的 token,是原 vocabulary 的
// LinkedIn 把整個 ranking 問題 cast 成 binary token-level prediction

這個招式叫 token-level classification on a generative model——比起加一個 classification head,直接用 vocabulary 中的 token 當輸出可以完全保留 generative 預訓練的 representation;fine-tune 只需要把 yes/no 的 logit 推到合理位置,不必重新學 representation。其他 alternative(加 [CLS] head、加 linear projection)都會多一層需要從頭學的 weight、需要更多監督資料才會收斂。

0.6B 從 7B 蒸下來:「Teacher: 7B parameter → distilled to 1.7B → Student: 0.6B parameter for production」。

三段式蒸餾不是浪費——直接 7B → 0.6B 的 capacity gap 太大,中間插 1.7B 作為「teaching assistant」可以分兩階段把容量限制傳遞下去。

每階段 student 學 teacher 的 soft target,loss 是 KL divergence:「Supervised fine-tuning (SFT) to minimize the Kullback–Leibler (KL) divergence between the teacher's soft targets and the SLM's predicted probabilities」。

Soft target 比 hard label 攜帶更多資訊——它編碼了 teacher 對「邊界 case」的不確定性,這些不確定性對 student 是非常珍貴的監督信號。

多任務這層也很關鍵:

「Trained on over 6 tasks of member actions」——relevance、job view、apply、recruiter accept 等。

同一個 0.6B 模型同時學「這個工作對這個人相關嗎」跟「這個人會點進去看嗎」跟「會 apply 嗎」跟「recruiter 會 accept 這個 apply 嗎」。

多任務的好處在這種 search-and-action 系統很直接:最終 ranking 想要的不是「relevance」單一目標、而是「下游 action 發生機率」。

把多任務做進 SLM 等於把整條 action funnel 內化到 ranking 模型,online 的 click AUC 提升從 0.61 升到 0.67——這是離線 NDCG 看不到的 production gain。

蒸餾後的 NDCG@10 對照:

Model               NDCG@10
─────────────────   ───────
Teacher (7B)         0.9484    // upper bound (cost-prohibitive)
Student (0.6B)       0.9432    // raw, before efficiency stack
Student + pruning +  0.9239    // production, after L4 compression
  RL summary +
  embed compression

EBR baseline (L2)    0.838     // retrieval only, no rerank
Click AUC (online)   0.61 → 0.67   // A/B test, before vs after

0.9484 → 0.9432 是 7B → 0.6B 純蒸餾的代價,1% 不到。0.9432 → 0.9239 是後面三道 efficiency 壓縮的代價,2%。整條鏈拉下來相對 teacher 跌 2.6%,throughput 漲 76 倍——這就是把 LLM 塞進 production search 的算術。

下面這張圖讓你拖一個 handle 沿著「壓縮深度」走,看 throughput 跟 NDCG 對應如何變化——左到右四個錨點:raw 0.6B、加 pruning、加 RL summary、加 embedding compression。最左邊是「Quality optimum」、最右邊是「Production」,中間是各種 hybrid 工作點。把 handle 拖到任何位置都對應一個實際工程選擇——LinkedIn 選最右邊不是因為它最強,而是因為它是唯一能撐住 millions of QPS 的選擇。

拖動橙色 handle 沿壓縮深度移動——四個錨點對應四種工程選擇

raw 0.6B + pruning + RL summary + embed compress 290 / GPU/s ~900 2,200 22,000 NDCG 0.9432 0.9380 0.9320 0.9239 Quality optimum 壓縮深度 ──> production 工作點在最右
throughput
290 items/s/GPU
NDCG@10
0.9432

raw 0.6B 蒸餾 student——quality 最強的工作點,但 throughput 撐不住 production traffic。選這裡等於不上線

互動圖表

三道壓縮讓 SLM throughput 累積升 76 倍;pruning、RL summary、embedding compression 逐步疊加。

Multi-teacher distillation 是另一個工程巧思——student 不只跟 single relevance teacher 學,還跟「product policy LLM」(決定一個 job 是否符合 LinkedIn 政策的另一個 LLM)跟「其他預測 member action 的大模型」(apply 預測、view 預測)一起學。Student 在 forward 時對每個 (q, d) 同時產出多個 head 的預測,loss 是各 teacher 的 KL 加總。這讓 student 變成一個 multi-objective 的 unified ranker——不必為 relevance、policy、action 各跑一個獨立模型再拼起來。

SLM ranking 還有兩個 production 細節值得記。Job description length:median ~900 tokens、max > 2300,prompt total 截到 2048——也就是說對長尾 job description 會丟尾巴。LinkedIn 已實證「移掉 description 整個拿掉,relevance 顯著下降」,但對「截到 2048」這種 partial truncation 沒給數字——這是 latency / quality 的 implicit knob。Evaluation 信度:產品經理人工 label 的 Cohen's Kappa weighted 必須 ≥ 0.8 才算「golden」——這是個高標準,正常 inter-annotator agreement 在 search 領域 0.6–0.7 已可用。把標準拉到 0.8 意味著 LinkedIn 把 reviewer 訓練到對「相關 / 不相關」有共識的程度才產 label,這對下游所有 fine-tune 跟 LLM-as-judge 校準都是基礎。

serving:pruning、RL summary、embedding compression 三道壓縮

L4 不是新的 model,是把 L3 變得能服務的一整套 efficiency engineering。

Raw SLM 在 GPU 上單卡 290 items/sec——對 LinkedIn 的 traffic(「serves millions of real-time queries per second」)這個數字差三個 order。

要把它推到可服務範圍,LinkedIn 套了三道串聯壓縮,每道都針對 transformer 計算的不同瓶頸。

第一道:model pruning——「Fully Connected Layers and remove whole transformer layers」。

砍 FC 是常規操作;砍掉整層 transformer 是比較激進的 structured pruning,相當於「教 student 在更少層」做相同事情,搭配 KL distillation 補回損失。

一般 pruning 的代價是 0.5–2% 質量,這裡的數字(raw 0.6B 290 → pruned ~900,三倍)顯示壓縮效率很高,可能跟「整層砍」對 GPU 的 latency reduction 是 linear 有關(FC 砍只省 matmul、整層砍同時省 memory 與 latency 兩種成本)。

第二道:context summarization——「RL to produce concise summaries」。

長 job description 在 prompt 裡占大量 token,self-attention 的 O(n²) 直接被 description 主導。

Summarization 模型把 description 壓成 concise summary——但用 RL 而不是 supervised,是因為 supervised summary 的 ground truth 通常是「人寫的摘要」、品質高但不一定對下游 SLM 有用。

RL summarizer 的 reward 直接綁 SLM 在壓縮後 prompt 上的 ranking quality——summarizer 學的不是「人覺得好的 summary」,是「讓 SLM 還能正確排序的 summary」。

這個 RL setup 在 instruction-tuning 領域常見、用在 search context summarization 是相對少見的——它把 efficiency 工具跟 quality 工具綁在一起。

第三道:embedding compression——「replaces most of the job description with just a single embedding」。

這是最暴力的一道,等於把長 text 變成一個固定維度向量直接灌進 SLM 的 input。

它的可行性建立在「joint training of text embedding + ranking」——「Two jointly-trained 0.6B models (text embedding + ranking)」。

Embedding model 跟 ranking model 一起訓,embedding model 知道 ranking model 需要哪些語意特徵、ranking model 知道 embedding 帶來什麼形式的 input。

這道從 2,200 推到 22,000 items/s/GPU(10×),但只掉 NDCG 0.008(0.9320 → 0.9239)——就「壓縮投資報酬率」而言,是三道中最划算的。

// Conceptual prompt layout 對比

// Naive — raw text
[SLM input]
  Query: senior MLE at fintech
  Document: // ~1500 token job description here
  ...
[/SLM input]

// + RL-summarized
[SLM input]
  Query: senior MLE at fintech
  Summary: // ~300 token RL summary
  ...
[/SLM input]

// Production — embedding compression
[SLM input]
  Query: senior MLE at fintech
  Doc embedding: <EMBED_TOKEN>       // 單一向量直接填進 token stream
  Salient phrases: senior, fintech, ML  // 仍保留少數關鍵字面
[/SLM input]

整體 throughput 從 raw 290 推到 production 22,000 是 76 倍——可是這個數字要小心讀:throughput 是「single-GPU items/sec」,不是 end-to-end QPS。一個 user query 在 ranking 階段要對 K 個候選跑 SLM(K 可能是 hundreds)。如果 K=200、單 GPU 22,000 items/sec、一個 query 吃 ~9 ms 的 SLM 時間——這就是 user-perceived latency 的構成。LinkedIn 沒有公開 p50 / p99 數字,但揭露「low-latency serving」是設計目標、「ranking-depth controller to manage how many candidates progress」表示尖峰時段會動態縮 K 來保住 tail latency——這個 graceful degradation 是 production search 的標準作法,但很少有人公開承認。

底層 infra 是另一條值得記的事:offline pipeline 用 Spark + Flyte(embedding 預計算、teacher 訓練、student 蒸餾、評估),nearline 用 Flink(新 job 上架、既有 job 改寫的增量 embedding update),snippet 生成從 Venice key-value store 拉 phrase embedding 做 lazy load。Reasoning cache 在 Couchbase——選 Couchbase 而非 Redis 是因為 Couchbase 對「大量 KV with 中等 TTL、跨 datacenter 複製」的場景做了優化。這些選擇單看不性感,但合在一起決定了 LLM-centric search 在 LinkedIn 規模下能不能存活。

evaluation:把 LLM-as-judge 紮進 release loop

把 LLM 塞進 search box 是一回事,知道它有沒有變得更好是另一回事。傳統 search 的 evaluation 主要是 click-through rate + 人工 label——成本高、cycle 慢、對「relevance 的 calibration」很弱。LinkedIn 改成 LLM-as-judge 為主、人工 label 校準的五步 pipeline:

1. stratify-sample or synthesize a diverse set of queries
2. retrieve the documents returned from executing those queries
3. decorate documents with additional info (member context, recency, etc)
4. grade documents using the LLM judge
5. aggregate → precision, recall, NDCG @ K

// LLM judge calibration:
//   - 對 golden set 跑 judge,跟 PM 人工 label 比對
//   - weighted Cohen's Kappa ≥ 0.8 才算 "fit for purpose"
//   - 校準後可以對任意 query set 自動跑 evaluation, no human in loop

第一步「stratify or synthesize」是 distribution coverage——production query 分布有長尾(少數高頻 head + 大量罕見 tail),單純 random sample 會被 head 主導、看不到 tail regression。

LinkedIn 對 query 分類,每類抽固定數量、外加「synthesize aspirational queries」(自己造一些「未來想支援好的 query」),這讓 evaluation 同時覆蓋 current production 跟 strategic direction。

Synthesis 通常本身也是 LLM 做的——這意味著 evaluation pipeline 從 query generation 到 grading 到 metric 都被 LLM 包圍。

第三步「decorate」是把 query 跟 document 補上 LLM judge 需要的 context——例如「這個 query 是一個 senior software engineer 在 SF Bay Area 發的,他過去 30 天看過 25 個 staff-level 職缺」——這些 context 在 production 是隱性的,evaluation 時必須顯式給 judge,否則 judge 對「relevance」的判斷會跟 production user 對 relevance 的判斷脫節。

第四步「LLM judge」本身需要 calibration。

LinkedIn 對 weighted Cohen's Kappa 訂 ≥ 0.8 的門檻——這是嚴標。一般 inter-annotator agreement 在 search 領域 0.6 算可用、0.7 算強。

要把 LLM judge 推到 0.8 通常需要:

  • 對 judge 仔細調 prompt;
  • 對「邊界 case」給多個 example;
  • 不時用 PM label 重新校準 judge。

Calibration 一過,就可以對任意 query set 自動跑 evaluation——release cycle 從「等 PM label 兩週」變成「每次 commit 跑 LLM judge 兩小時」。

這是 search team 開發節奏的本質改變,跟「LLM 做 ranking」同樣關鍵但更少被討論。

還有兩個重要的 online / counterfactual 層:「Counterfactual evaluation」對 production log 做反事實重放——把新 model 套用在歷史 query、看它對歷史 click 的相符度;「offline KNN simulations」對 retrieval 做模擬召回測試;「online serving stack on a subset of traffic」是真正的 A/B test。三層形成完整 evaluation 漏斗:offline 抓 obvious regression、counterfactual 抓 distribution shift、online 抓 long-tail surprise。Click AUC 從 0.61 升到 0.67 就是 online 層產的數字——「online metrics」永遠是 ground truth,但要走到 online 之前先過 offline 跟 counterfactual 才能避免大規模 regression。

把這個 evaluation pipeline 講完之後,剩下的就是周邊系統——結果展示(snippet)、底層 storage 分工、跟 serving 的 traffic shaping。這些都不在主 stack 上但工程精度同樣高,也都套用同一套「LLM 想用的地方都用、但每處都打磨成 lazy compute + cache」的哲學。

結果展示也被改了。Search result 下面的「snippet」(顯示為什麼這個 document 跟 query 相關的那段摘要)以前是 BM25 + heuristic 在 document body 找匹配 query 的 sentence;現在改成 phrase embedding 相似度——unigrams 跟 bigrams 的 embedding 預先算好放 Venice key-value store,runtime 算 query embedding 跟這些 phrase embedding 的 cosine,挑最近的 phrase 構成 snippet。

Venice 是 LinkedIn 自己的 KV——選它而不是把 phrase embedding 灌進 EBR GPU index 的理由是「snippet 是 per-result 的事、不需要 global kNN」:每個展示的 result 只需要拉它自己的 phrase embedding 子集做 local matching。lazy loading + caching 確保只有真的被展示的 result 才會吃這個 cost——對 deep tail(user 翻到第 10 頁的 result)甚至不用算。

這個小設計反映了整套 stack 的工程哲學:把 LLM 想用的地方都用、但每一處都打磨成精準的「lazy compute + cache」。Query understanding 的 reasoning 進 Couchbase cache;snippet 的 phrase embedding 進 Venice lazy load;EBR 的 document embedding 進 GPU index 預計算;ranking 的 (query, document) 過 SLM 但 SLM 已經被三道壓縮捏到 22k items/sec。每一處都是「LLM forward 該做的時候做、不該重複做的時候 cache、不該全做的時候 lazy」。

底層的 storage 跟 compute 分工也乾淨:offline pipeline 用 Spark + Flyte 處理「整批重算」(重新訓練、整 index 重建),nearline 用 Flink 處理「增量更新」(新 job 上架、既有 job 修改),online serving 處理「per-query 計算」(query LLM、EBR query、SLM rerank)。三層之間靠 content-addressed 儲存與版本化模型 artifact 保持一致——每個 job 的 embedding 都帶上「用哪版 encoder 算的」標籤,nearline 上 encoder 換版本時可以 incrementally 重算而不需要 cold rebuild。

怎麼把這篇讀成自己團隊的 checklist

如果你正在面臨「我們的 search box 該不該塞 LLM」的決策,這套 stack 提供的不是現成 recipe(你大概沒 LinkedIn 的 GPU 預算),但提供了清晰的決策維度。每一層都是一個獨立決定,跟其他層之間有 fallback。

Query understanding 層

若你的 query distribution 很乾淨(電商商品搜尋、navigational 為主的內部工具),fine-tuned 1.5B 太貴、傳統 NER 加 intent classifier 就夠。

若你有大量 long-tail natural language(「remote ML researcher who worked on RLHF」這種 multi-facet conversational query),fine-tuned LLM 帶來的 quality lift 才划算。

Reasoning cache 是降本關鍵——head queries 永遠都該 cache。

EBR vs lexical

dense retrieval 對「語意接近但詞面不一致」的 case("engineering manager" vs "tech lead")有實質 lift;但對「精確詞面要求」(搜公司名、產品 SKU、人名)lexical / BM25 仍是 baseline。

混合 retrieval(dense + sparse fusion)是中位團隊的合理起點——LinkedIn 直接 all-in dense + exhaustive GPU kNN 是因為他們的 query 分布偏向語意搜尋,且 GPU 預算允許。

Cross-encoder ranker

除非你已經有 7B teacher、distillation infra、跟 RL summarizer 的訓練能力,否則不要在這層放 0.6B SLM。

中位團隊更實際的工作點是 cross-encoder DeBERTa / BGE-reranker 這類 100–300M 模型——quality 仍顯著高於 GBDT ranker、infra 成本可控、不需要自己跑 distillation。

LLM-as-judge evaluation

這層回報最快、進入門檻最低。對 Cohen's Kappa 校準到 0.7 都已經足以替代「等 PM label 兩週」的 release loop——任何 search team 今天就可以開始建 LLM judge pipeline,跟模型 stack 是否升級到 LLM 完全解耦。

這篇文章如果只能借一件事走,就借這個。

整套 stack 真正的訊息是:「LLM 進 search 的 latency / cost 問題不是被解決,是被分散到四層的 efficiency engineering 上——pruning、summarization、embedding compression、cache、lazy load、shed load 都同時在做,才能把 22k items/sec/GPU 在 NDCG 0.9239 的工作點存活下來」。沒有單一銀彈、有的是六七個小手段堆出來的 budget——而這正是 production search team 該學的最有價值的事。

What this enables:「LLM 進 search box」從 5 年前的「研究 demo」變成 LinkedIn-scale production——關鍵不是模型本身,是把 query understanding、retrieval、ranking、evaluation 四層全部 LLM 化的同時,用 distillation、pruning、RL summary、embedding compression 把每層的 latency 跟 cost 各自壓回 budget,加上 LLM-as-judge 把 release loop 從週縮到時。對中位 search team,最容易拿走的是 evaluation 那層——它不需要你升級 ranker。