<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>推荐系统 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 22 Oct 2026 23:10:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的</title>
      <link>https://ftmao.com/blog/posts/tiktok-recommendation-math-walkthrough/</link>
      <pubDate>Thu, 22 Oct 2026 23:10:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/tiktok-recommendation-math-walkthrough/</guid>
      <description>&lt;h1 id=&#34;从余弦定理到-tiktok一个用户的推荐流是怎么被算出来的&#34;&gt;从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的&lt;/h1&gt;
&lt;p&gt;上一篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;《千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构》&lt;/a&gt;把推荐系统的骨架拆完了：离线、近线、在线三层分工，召回、粗排、精排、重排四层漏斗。但通篇是框图和分工表，读完知道&amp;quot;系统长什么样&amp;quot;，却未必有体感——&lt;strong&gt;我的数据，到底是怎么变成我刷到的这一屏的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇换个讲法：造一个用户，给他一晚上的真实行为，跟着他走完全程。线索有三条，正好串起三十年：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;吴军 2006 年起在 Google 黑板报连载、2012 年成书的《数学之美》——推荐算法的数学原点；&lt;/li&gt;
&lt;li&gt;Netflix 2013 年公开的离线/近线/在线三层实践——工程化的起点（上一篇的主线）；&lt;/li&gt;
&lt;li&gt;TikTok 2020 年官方披露的 For You 推荐信号，和字节跳动 2022 年发表的 Monolith 在线训练系统——这套东西被推到的工程极致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;数学的原点数学之美的两章&#34;&gt;数学的原点：《数学之美》的两章&lt;/h2&gt;
&lt;p&gt;《数学之美》第 14 章&amp;quot;余弦定理和新闻的分类&amp;quot;讲了一个漂亮的化归：给新闻里每个实词算出 TF-IDF 值，按词汇表位置排成一个向量，一篇新闻就变成了一个&lt;strong&gt;特征向量&lt;/strong&gt;；&amp;ldquo;新闻分类&amp;quot;这个看似需要理解语义的问题，变成了&lt;strong&gt;在向量空间里算余弦相似度&lt;/strong&gt;的几何问题。第 15 章&amp;quot;矩阵运算和文本处理中的两个分类问题&amp;quot;接着讲：成千上万篇新闻构成的大矩阵，可以用 SVD（奇异值分解）一次拆开，分类在分解后的小矩阵上完成。&lt;/p&gt;
&lt;p&gt;推荐系统是&lt;strong&gt;同一套数学，换了一张矩阵&lt;/strong&gt;：把&amp;quot;新闻 × 词汇&amp;quot;换成&amp;quot;用户 × 内容&amp;rdquo;。余弦相似度回答的问题，从&amp;quot;两篇新闻像不像&amp;quot;变成&amp;quot;这个用户的口味和这条内容像不像&amp;quot;；SVD 分解的那张大矩阵，就是后来协同过滤要分解的用户-物品交互矩阵。&lt;/p&gt;
&lt;p&gt;别急着说这是类比——直接手算一遍。假设全站内容只有四个类目：宠物、科技、健身、汽车。用户的口味表示成一个 4 维向量，每个分量是他在该类目上累计的行为强度。&lt;/p&gt;
&lt;h2 id=&#34;造一个用户u10086-的一晚&#34;&gt;造一个用户：U10086 的一晚&lt;/h2&gt;
&lt;p&gt;主角 U10086，昨晚刷了一小时短视频，留下这些行为：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;时间&lt;/th&gt;
					&lt;th&gt;行为&lt;/th&gt;
					&lt;th&gt;内容&lt;/th&gt;
					&lt;th&gt;类目&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;22:10&lt;/td&gt;
					&lt;td&gt;完播&lt;/td&gt;
					&lt;td&gt;A123「橘猫拆家」&lt;/td&gt;
					&lt;td&gt;宠物&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:13&lt;/td&gt;
					&lt;td&gt;点赞&lt;/td&gt;
					&lt;td&gt;B456「居家哑铃训练」&lt;/td&gt;
					&lt;td&gt;健身&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:15&lt;/td&gt;
					&lt;td&gt;1.2 秒划走&lt;/td&gt;
					&lt;td&gt;C789「新能源车测评」&lt;/td&gt;
					&lt;td&gt;汽车&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:18&lt;/td&gt;
					&lt;td&gt;停留 8 秒&lt;/td&gt;
					&lt;td&gt;D012「AI 编程工具实测」&lt;/td&gt;
					&lt;td&gt;科技&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些行为不会直接变成画像——中间隔着一层关键设计：&lt;strong&gt;内容的类目不是作者上传时选的，是平台算出来的。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;内容的标签不是作者标的是算出来的&#34;&gt;内容的标签不是作者标的，是算出来的&lt;/h3&gt;
&lt;p&gt;一个常见误解：视频的类目来自作者上传时勾选的分区。在 TikTok 这个量级的平台上不是这样。作者自选标签噪声大，还挡不住&amp;quot;标签作弊&amp;quot;——挂个热门标签蹭流量。真实做法是&lt;strong&gt;内容理解管道&lt;/strong&gt;：视频上传后先过一遍机器理解——抽帧做图像分类和目标检测、ASR 把语音转成文字、OCR 读画面上的字、NLP 分析标题和文案——最后由模型产出类目、标签和 embedding 向量。作者的 caption 和 hashtag 只是输入信号之一，没有最终决定权。TikTok 官方披露的三类信号里，&amp;ldquo;内容信息&amp;rdquo;（captions、sounds、hashtags）就是这条管道的产物：内容进流量池之前，&amp;ldquo;这条视频是关于什么的&amp;quot;已由机器定好。&lt;/p&gt;
&lt;p&gt;甚至播放之后还在算。前面提到的 item2vec，训练数据就是全站用户的观看行为——被同一批人看完的内容，向量天然相近。也就是说，&lt;strong&gt;内容的向量随每一次播放被持续校准&lt;/strong&gt;，而不是发布那一刻就定格。&lt;/p&gt;
&lt;h3 id=&#34;用户向量是内容向量的累计&#34;&gt;用户向量是内容向量的累计&lt;/h3&gt;
&lt;p&gt;所以系统手里 U10086 的口味画像——宠物 0.55、科技 0.25、健身 0.15、汽车 0.05，即向量 $u = (0.55, 0.25, 0.15, 0.05)$——不是他填的问卷，也不是对人工类目的简单计数，而是&lt;strong&gt;他过去几个月每次行为对应的机器内容向量，按行为强度加权累计出来的&lt;/strong&gt;。两边都是算出来的：内容侧机器识别，用户侧行为聚合。&lt;/p&gt;
&lt;p&gt;这个设计值两个好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;准&lt;/strong&gt;：作者标错、乱标的类目污染不了画像，用户向量锚定在内容的真实特征上——他看完的是一只真猫，不是一个挂着&amp;quot;萌宠&amp;quot;标签的广告；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;细&lt;/strong&gt;：4 个类目只是教学简化，底下是几百维的 embedding，&amp;ldquo;橘猫&amp;quot;和&amp;quot;布偶猫&amp;quot;这种类目装不下的区分，向量装得下。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他今晚每一次播放、点赞，都会乘上那条内容的机器向量、计入自己的向量——这正是后面&amp;quot;短期兴趣向量&amp;quot;一节的微观机制。&lt;/p&gt;
&lt;p&gt;今晚候选池里有三条内容（向量都出自内容理解管道）：猫视频 $v_1 = (0.9, 0.1, 0, 0)$，哑铃教学 $v_2 = (0.05, 0.1, 0.8, 0.05)$，汽车测评 $v_3 = (0, 0.05, 0, 0.95)$。&lt;/p&gt;
&lt;p&gt;余弦相似度的公式就一行：&lt;/p&gt;
&lt;p&gt;$$\cos(u, v) = \frac{u \cdot v}{|u||v|}$$&lt;/p&gt;
&lt;p&gt;给猫视频算一次：分子是点积 $0.55 \times 0.9 + 0.25 \times 0.1 = 0.52$；分母是两个向量的模长之积 $\sqrt{0.39} \times \sqrt{0.82} \approx 0.565$。结果是 $0.52 / 0.565 \approx 0.92$。三条候选全算完：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;候选&lt;/th&gt;
					&lt;th&gt;余弦相似度&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;猫视频 $v_1$&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;0.92&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;哑铃教学 $v_2$&lt;/td&gt;
					&lt;td&gt;0.35&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;汽车测评 $v_3$&lt;/td&gt;
					&lt;td&gt;0.10&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;**这就是&amp;quot;推荐&amp;quot;在最原始形态下的全部：一次点积，一次归一化。**今天 TikTok 的排序模型参数量以百亿计，打分函数的内核仍然是这个动作——只是向量从 4 维人工统计变成几百维神经网络学出来的 embedding（item2vec，2016；双塔模型），从&amp;quot;类目&amp;quot;粒度细化到&amp;quot;每条内容&amp;quot;粒度。三十年里数学没有换，换的是向量的生产方式和计算规模。&lt;/p&gt;
&lt;h2 id=&#34;行为怎么变成画像netflix-的三层分工&#34;&gt;行为怎么变成画像：Netflix 的三层分工&lt;/h2&gt;
&lt;p&gt;4 维向量是教学模型，真实系统里的用户向量没法靠人工统计类目得到——内容有几亿条，行为有几十种。工程上的解法就是上一篇展开讲过的 Netflix 三层分工，以及它背后的一整套大数据栈：用户的每一次曝光、点击、停留，客户端上报后先进 &lt;strong&gt;Kafka&lt;/strong&gt;（事件总线）；一份实时流进 &lt;strong&gt;Flink&lt;/strong&gt; 作业，秒级滚动更新短期画像；同时全量落盘到 &lt;strong&gt;HDFS&lt;/strong&gt;（Hadoop 生态的分布式存储），由 &lt;strong&gt;Hive&lt;/strong&gt;（或 Iceberg）建成离线数仓。&lt;strong&gt;离线层&lt;/strong&gt;的 Spark 批任务（天级）从数仓读数据，算物品向量、itemCF 共现矩阵、用户长期兴趣向量；&lt;strong&gt;在线层&lt;/strong&gt;只做查表和轻量推理，画像和榜单都躺在 Redis 里。重活在用户到来之前全部干完。&lt;/p&gt;
&lt;p&gt;问题是：一次&amp;quot;完播&amp;quot;和一次&amp;quot;秒划走&amp;rdquo;，该给短期画像各贡献多少？这个强弱排序不是工程师拍脑袋——TikTok 官方给过答案。&lt;/p&gt;
&lt;p&gt;2020 年 6 月 18 日，TikTok 在官方 newsroom 发表 &amp;ldquo;How TikTok recommends videos #ForYou&amp;rdquo;，是平台方罕见的一次坦白。它把推荐信号分成三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户交互&lt;/strong&gt;：点赞、分享、关注、评论、发布的内容、看完的视频；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容信息&lt;/strong&gt;：标题文案、声音、话题标签；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备与账号设置&lt;/strong&gt;：语言、国家、设备类型——官方明说这类权重更低，因为&amp;quot;用户没有主动把这些表达为偏好&amp;rdquo;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更重要的是官方对&lt;strong&gt;信号强度&lt;/strong&gt;的排序：原话是&amp;quot;用户是否把一个较长的视频从头看到尾&amp;quot;这类强兴趣指标，权重远大于&amp;quot;观众和创作者是否在同一国家&amp;quot;这类弱指标。翻译成 U10086 的表格：他完播猫视频这件事的权重，压过他的手机和品牌方在同一城市。&lt;/p&gt;
&lt;p&gt;这套强弱排序落到工程上，就是近线层滚动更新的&lt;strong&gt;短期兴趣向量&lt;/strong&gt;——最近 N 次行为对应的物品向量做加权平均：&lt;/p&gt;
&lt;p&gt;$$u_{short} = \frac{\sum_{i} w_i \cdot e^{-\Delta t_i / \tau} \cdot v_i}{\sum_{i} w_i \cdot e^{-\Delta t_i / \tau}}$$&lt;/p&gt;
&lt;p&gt;权重 $w_i$ 沿行为强度排：完播 3.0、点赞 2.0、停留 5 秒以上 1.5、点击 1.0、秒划走 -0.5（负反馈反向拉）；指数项是时间衰减，越久的行为影响越小。代入 U10086 的一晚：猫视频向量乘 3.0 主导方向，汽车视频乘 -0.5 往反方向拽。&lt;strong&gt;他昨晚的每一个动作，在他下次下拉之前，就已经被压缩成这个向量躺在 Redis 里了。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;把公式拆开每个符号是什么&#34;&gt;把公式拆开：每个符号是什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$i$：遍历用户最近 N 次行为的下标。N 常见取 50-200，只保留最近一小段——太久之前的行为对&amp;quot;今晚想看什么&amp;quot;没有预测力；&lt;/li&gt;
&lt;li&gt;$v_i$：第 i 次行为涉及的&lt;strong&gt;内容向量&lt;/strong&gt;——就是上一节内容理解管道算出来的那条视频的向量。注意它是物品的向量，不是用户的；用户向量正是由一堆物品向量合成的；&lt;/li&gt;
&lt;li&gt;$w_i$：这次行为的强度权重（完播 3.0、点赞 2.0、停留 1.5、点击 1.0、秒划 -0.5），回答&amp;quot;这个动作多能代表他的兴趣&amp;quot;；&lt;/li&gt;
&lt;li&gt;$\Delta t_i$：这次行为距今的时间差；&lt;/li&gt;
&lt;li&gt;$\tau$：时间衰减常数，控制&amp;quot;多久之前的行为算老&amp;quot;。$\tau$ 取 12 小时，则 12 小时前的行为权重衰减到 $e^{-1} \approx 0.37$，一天前只剩 0.14；&lt;/li&gt;
&lt;li&gt;$e^{-\Delta t_i / \tau}$：指数时间衰减因子，越久的行为对当前兴趣越没有发言权；&lt;/li&gt;
&lt;li&gt;分母：归一化项。没有它，刷得多的人向量模长就大，&amp;ldquo;勤奋&amp;quot;会冒充&amp;quot;兴趣浓&amp;rdquo;；除一下只留方向，不带数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话：&lt;strong&gt;短期兴趣 = 最近看过的内容的向量，按&amp;quot;行为多重 × 时间多近&amp;quot;加权平均。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;演算u10086-的短期向量&#34;&gt;演算：U10086 的短期向量&lt;/h3&gt;
&lt;p&gt;把内容向量代入他昨晚的四次行为。$\tau$ 取 0.5 小时，站在 22:30 这一刻计算（D012 的内容向量是 (0.05, 0.85, 0.05, 0.05)，科技向）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;行为&lt;/th&gt;
					&lt;th&gt;内容向量 $v_i$&lt;/th&gt;
					&lt;th&gt;强度 $w_i$&lt;/th&gt;
					&lt;th&gt;距今 $\Delta t_i$&lt;/th&gt;
					&lt;th&gt;衰减 $e^{-\Delta t_i / \tau}$&lt;/th&gt;
					&lt;th&gt;有效权重 $w_i \cdot e^{-\Delta t_i / \tau}$&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;完播猫视频&lt;/td&gt;
					&lt;td&gt;(0.9, 0.1, 0, 0)&lt;/td&gt;
					&lt;td&gt;3.0&lt;/td&gt;
					&lt;td&gt;20 分钟&lt;/td&gt;
					&lt;td&gt;0.51&lt;/td&gt;
					&lt;td&gt;1.54&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;点赞哑铃&lt;/td&gt;
					&lt;td&gt;(0.05, 0.1, 0.8, 0.05)&lt;/td&gt;
					&lt;td&gt;2.0&lt;/td&gt;
					&lt;td&gt;17 分钟&lt;/td&gt;
					&lt;td&gt;0.57&lt;/td&gt;
					&lt;td&gt;1.13&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;秒划汽车&lt;/td&gt;
					&lt;td&gt;(0, 0.05, 0, 0.95)&lt;/td&gt;
					&lt;td&gt;-0.5&lt;/td&gt;
					&lt;td&gt;15 分钟&lt;/td&gt;
					&lt;td&gt;0.61&lt;/td&gt;
					&lt;td&gt;-0.30&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;停留 AI 工具&lt;/td&gt;
					&lt;td&gt;(0.05, 0.85, 0.05, 0.05)&lt;/td&gt;
					&lt;td&gt;1.5&lt;/td&gt;
					&lt;td&gt;12 分钟&lt;/td&gt;
					&lt;td&gt;0.67&lt;/td&gt;
					&lt;td&gt;1.01&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分子是四个&amp;quot;有效权重 × 内容向量&amp;quot;之和，这里的算术只有两步：标量乘向量、向量相加。全部展开（保留三位小数）：&lt;/p&gt;
&lt;p&gt;第一步，&lt;strong&gt;标量乘向量&lt;/strong&gt;——每条内容向量逐分量乘上自己的有效权重：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;          有效权重 × 内容向量                =  加权后的向量
猫视频     1.54 × (0.9,  0.1,  0,    0)     = ( 1.386,  0.154,  0,      0)
哑铃       1.13 × (0.05, 0.1,  0.8,  0.05)  = ( 0.057,  0.114,  0.904,  0.057)
汽车      -0.30 × (0,    0.05, 0,    0.95)  = ( 0,     -0.015,  0,     -0.285)
AI工具     1.01 × (0.05, 0.85, 0.05, 0.05)  = ( 0.050,  0.854,  0.050,  0.050)&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第二步，&lt;strong&gt;逐分量相加&lt;/strong&gt;——四个向量的同一列相加，每一列就是一个类目方向：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;宠物分量： 1.386 + 0.057 + 0      + 0.050 =  1.49
科技分量： 0.154 + 0.114 - 0.015  + 0.854 =  1.11
健身分量： 0     + 0.904 + 0      + 0.050 =  0.96
汽车分量： 0     + 0.057 - 0.285  + 0.050 = -0.18&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;分子向量就是 (1.49, 1.11, 0.96, -0.18)；分母是有效权重之和 1.54 + 1.13 - 0.30 + 1.01 = 3.38。最后逐分量相除：1.49 / 3.38 ≈ 0.44，1.11 / 3.38 ≈ 0.33，0.96 / 3.38 ≈ 0.28，-0.18 / 3.38 ≈ -0.05。得到：&lt;/p&gt;
&lt;p&gt;$$u_{short} \approx (0.44, 0.33, 0.28, -0.05)$$&lt;/p&gt;
&lt;p&gt;从第二步的列里能直接读出故事：宠物分量的九成来自那条完播的猫视频（1.49 里它独占 1.386）；汽车这一列，哑铃和 AI 工具带来的两个正贡献（0.057 + 0.050）加起来也抵不过一次秒划的 -0.285——&lt;strong&gt;负权重的意义就在这里：他不喜欢的内容在向量里留下的不是零，而是负坐标&lt;/strong&gt;，检索时会把相似内容往反方向推。&lt;/p&gt;
&lt;p&gt;对比他的长期画像 $(0.55, 0.25, 0.15, 0.05)$：一小时的行为已经把&lt;strong&gt;科技顶到第二位、健身翻了倍、汽车打成负值&lt;/strong&gt;。在线服务一般把长短期两个向量融合（拼接或加权和）作为最终用户向量——长期画像管&amp;quot;他是个什么人&amp;quot;，短期向量管&amp;quot;他今晚想看什么&amp;quot;。下一刷召回时，猫和科技内容能同时被捞回来，靠的就是这个融合向量。&lt;/p&gt;
&lt;h2 id=&#34;tiktok-把闭环推到极致&#34;&gt;TikTok 把闭环推到极致&lt;/h2&gt;
&lt;h3 id=&#34;官方确认的和官方从没确认的&#34;&gt;官方确认的，和官方从没确认的&lt;/h3&gt;
&lt;p&gt;除了信号分类，TikTok 那篇文章还有一句关键坦白：&lt;strong&gt;粉丝数和账号历史爆款都不是直接推荐因素&lt;/strong&gt;（原话：neither follower count nor previous high-performing videos are direct factors）。这解释了新账号为什么也能出爆款——内容进场不看出身。&lt;/p&gt;
&lt;p&gt;中文互联网流传的&amp;quot;流量池&amp;quot;机制要在这里说清边界：新视频先给 200-500 次曝光的小池子，完播率、互动率跑赢同期内容就推进千级、万级的更大池子，逐级叠加放量——&lt;strong&gt;这套具体说法出自运营圈的逆向总结，TikTok 和抖音官方从未确认过细节&lt;/strong&gt;。但它的方向和官方披露自洽：既然粉丝数不算直接因素，新内容就必须有一个&amp;quot;不看出身、先看小范围真实反馈&amp;quot;的冷启动机制，流量池只是这个必然性的具体形态。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;新视频发布&amp;lt;br/&amp;gt;初始小流量池 数百次曝光&amp;#34;] --&amp;gt; B{&amp;#34;完播率 互动率&amp;lt;br/&amp;gt;跑赢同期内容?&amp;#34;}
    B -- &amp;#34;达标&amp;#34; --&amp;gt; C[&amp;#34;二级流量池&amp;lt;br/&amp;gt;千级曝光&amp;#34;]
    B -- &amp;#34;不达标&amp;#34; --&amp;gt; D[&amp;#34;停止放量&amp;lt;br/&amp;gt;转入自然长尾&amp;#34;]
    C --&amp;gt; E{&amp;#34;数据继续跑赢?&amp;#34;}
    E -- &amp;#34;达标&amp;#34; --&amp;gt; F[&amp;#34;更大流量池&amp;lt;br/&amp;gt;万级到百万级&amp;#34;]
    E -- &amp;#34;不达标&amp;#34; --&amp;gt; D&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;（图为第三方运营经验的通行描述，非官方口径。）&lt;/p&gt;
&lt;h3 id=&#34;monolith连模型本身都在线学习&#34;&gt;Monolith：连模型本身都在线学习&lt;/h3&gt;
&lt;p&gt;Netflix 的三层架构里，模型参数是离线训练的，近线只更新特征和画像。字节跳动 2022 年发表的 Monolith（RecSys &amp;lsquo;22，arXiv:2209.07663）把这条边界也打掉了：&lt;strong&gt;推荐模型在线训练&lt;/strong&gt;——行为事件实时变成训练样本回流，模型参数近实时更新。&lt;/p&gt;
&lt;p&gt;工程难点在 embedding 表。在线场景下新内容源源不断涌进来，传统哈希表会发生碰撞：两条不同的内容被映射到同一份 embedding，互相污染。Monolith 用 cuckoo hashing 实现无碰撞的 embedding 表，再配合特征过期和低频过滤控制内存，硬是把&amp;quot;在线深度学习&amp;quot;做成了生产级系统。&lt;/p&gt;
&lt;p&gt;对比一下闭环速度：Netflix 2013 年的近线层，&amp;ldquo;行为 → 画像生效&amp;quot;做到秒级已经是当时的极致；Monolith 把&amp;quot;行为 → 模型参数更新&amp;quot;也压到了近实时。&lt;strong&gt;闭环速度，就是推荐系统的军备竞赛指标。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;跟着-u10086-走一遍四层漏斗&#34;&gt;跟着 U10086 走一遍四层漏斗&lt;/h2&gt;
&lt;p&gt;铺垫完了，现在让 U10086 在 23:05 下拉刷新，跟他的数据走完全程。四层漏斗的原理上一篇讲过，这篇只看一件事：&lt;strong&gt;每一步的数据长什么样。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;召回1-亿--8000&#34;&gt;召回：1 亿 → 8000&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：短期兴趣向量 $u_{short}$、最近行为物品列表 [A123, B456, D012]、长期画像。&lt;/p&gt;
&lt;p&gt;多路并行，每一路都是查表或检索，没有任何矩阵运算：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;召回路&lt;/th&gt;
					&lt;th&gt;动作&lt;/th&gt;
					&lt;th&gt;产出&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;itemCF 协同过滤&lt;/td&gt;
					&lt;td&gt;Redis 批量查 A123、B456、D012 各自的相似物品 Top50（相似度矩阵是 Spark 离线算好的共现余弦）&lt;/td&gt;
					&lt;td&gt;约 3000 条，大量猫、健身、科技内容&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量召回&lt;/td&gt;
					&lt;td&gt;$u_{short}$ 在 Faiss（HNSW 索引）里做近似最近邻检索，几毫秒返回&lt;/td&gt;
					&lt;td&gt;3000 条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;热门榜&lt;/td&gt;
					&lt;td&gt;直接读 Redis 里 Flink 滚动窗口算好的全站热榜&lt;/td&gt;
					&lt;td&gt;1000 条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;标签/关键词召回&lt;/td&gt;
					&lt;td&gt;内容元数据（标题、标签、类目）灌在 Elasticsearch 的倒排索引里，拿画像里的高权重标签当查询条件去检索——「搜推一体」里 ES 干的就是这路&lt;/td&gt;
					&lt;td&gt;约 500 条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新内容池 + 关注产出 + 探索流量&lt;/td&gt;
					&lt;td&gt;读离线、近线备好的榜单&lt;/td&gt;
					&lt;td&gt;各几百条&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：多路合并、去重、按路配额截断——&lt;strong&gt;8000 条候选 ID 的列表&lt;/strong&gt;，每条只带召回来源和粗分。注意一个细节：没有一路会拿 C789（秒划走的汽车视频）当种子去查相似内容，汽车类在召回阶段就已经天然稀疏了。&lt;/p&gt;
&lt;h3 id=&#34;粗排8000--1000&#34;&gt;粗排：8000 → 1000&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：8000 条候选 + 一批&amp;quot;便宜特征&amp;rdquo;——从特征存储（Feast 或自研 Feature Store，底层通常是 Redis）批量读出物品热度分、质量分、该用户的类目偏好分、历史 CTR。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算&lt;/strong&gt;：轻量模型打分（简化双塔就是一次向量点积，GBDT 就是几千次小树遍历——XGBoost / LightGBM），单条成本压到微秒级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：每条一个粗排分，截断 Top 1000：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;cat_video_8891   (A123 的相似内容)   0.81   ← 和短期向量点积极高
ai_tool_3302     (D012 的相似内容)   0.74
hot_funny_1102   (热门榜兜底)        0.62
car_review_5567                      0.05   ← 秒划负反馈直接压死&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;精排1000--200&#34;&gt;精排：1000 → 200&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：1000 条候选 + 全量特征拼接——用户画像（Redis KV 读出）、物品特征、近线实时特征（最近一小时的点击序列，Flink 产出）、实时上下文（23:05、手机、WiFi）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算&lt;/strong&gt;：深度排序模型（DIN 系，阿里 2018 年 KDD 发表）在推理服务（TF Serving 或 NVIDIA Triton，GPU 批量矩阵乘）上跑推理。DIN 的注意力机制值得演算一步：给候选 cat_video_8891 打分时，模型给 U10086 的历史行为分配权重——同为猫视频的 A123 权重拉到最高，B456 次之，C789 趋近于零，输出高 pCTR；而给 car_review_5567 打分时，被注意机制激活的恰恰是 C789——方向是负的。&lt;strong&gt;同一个用户的历史，在不同候选面前讲出不同的故事&lt;/strong&gt;，这是 DIN 相比&amp;quot;死板的平均历史向量&amp;quot;的本质进步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：每条候选一对预测值 (pCTR, 预估观看时长），加权成综合分后排序，截断 Top 200。&lt;/p&gt;
&lt;h3 id=&#34;重排200--20&#34;&gt;重排：200 → 20&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：200 条带精排分的列表。这一层几乎不算模型，全是规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;已曝光过滤&lt;/strong&gt;：查 Redis 里 &lt;code&gt;user:10086:seen&lt;/code&gt; 集合（或布隆过滤器），剔除本次 session 已刷过的 15 条；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负反馈硬屏蔽&lt;/strong&gt;：汽车类目整体降权——秒划走加历史&amp;quot;不感兴趣&amp;quot;记录；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多样性打散&lt;/strong&gt;：同作者最多 2 条、相邻 3 条不同类目——Top 200 里连着的 8 条猫视频被打散重插；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混排&lt;/strong&gt;：第 4 位插入广告（广告单独按 eCPM = 出价 × 预估点击率 排序选出），第 9 位插一条关注博主的更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务护栏&lt;/strong&gt;：过滤审核中内容，新内容扶持配额 5%——这就是流量池机制落在自然流量里的那一小块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;...20 条：16 条自然内容 + 2 条广告 + 1 条关注更新 + 1 条新内容扶持...&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;next_cursor&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;eyJscCI6MC44MSwiYmF0Y2giOiJUYjI5...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;游标里封着最后一条的排序分、本次批次号、过期时间，客户端不许解析。同时这 20 条写入已曝光集合，曝光和后续行为事件经 Kafka 回流——&lt;strong&gt;闭环合上，U10086 的下一刷已经在被这一刷改写。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;沿途的三方件总表&#34;&gt;沿途的三方件总表&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;位置&lt;/th&gt;
					&lt;th&gt;工具&lt;/th&gt;
					&lt;th&gt;角色&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;事件总线&lt;/td&gt;
					&lt;td&gt;Kafka&lt;/td&gt;
					&lt;td&gt;行为事件解耦、削峰、可回放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据底座&lt;/td&gt;
					&lt;td&gt;Hadoop（HDFS）+ Hive / Iceberg&lt;/td&gt;
					&lt;td&gt;全量行为日志落盘，离线数仓&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;离线计算&lt;/td&gt;
					&lt;td&gt;Spark&lt;/td&gt;
					&lt;td&gt;模型训练、itemCF 共现矩阵、物品向量&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;近线计算&lt;/td&gt;
					&lt;td&gt;Flink&lt;/td&gt;
					&lt;td&gt;秒级更新短期画像、滚动热榜&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;在线训练&lt;/td&gt;
					&lt;td&gt;Monolith（字节自研，论文开源）&lt;/td&gt;
					&lt;td&gt;模型参数近实时更新&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;在线 KV&lt;/td&gt;
					&lt;td&gt;Redis（或 HBase）&lt;/td&gt;
					&lt;td&gt;画像、榜单、已曝光集合、itemCF 结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量检索&lt;/td&gt;
					&lt;td&gt;Faiss / HNSWlib / Milvus&lt;/td&gt;
					&lt;td&gt;双塔向量的近似最近邻召回&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;倒排索引&lt;/td&gt;
					&lt;td&gt;Elasticsearch&lt;/td&gt;
					&lt;td&gt;标签/关键词召回路，「搜推一体」的内容检索&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;特征平台&lt;/td&gt;
					&lt;td&gt;Feast 或自研 Feature Store&lt;/td&gt;
					&lt;td&gt;离线、在线特征一致性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型推理&lt;/td&gt;
					&lt;td&gt;TF Serving / NVIDIA Triton&lt;/td&gt;
					&lt;td&gt;精排批量打分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;轻量模型&lt;/td&gt;
					&lt;td&gt;XGBoost / LightGBM&lt;/td&gt;
					&lt;td&gt;粗排&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;辅助&lt;/td&gt;
					&lt;td&gt;布隆过滤器、规则引擎&lt;/td&gt;
					&lt;td&gt;已曝光去重、重排策略&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;这套链路怎么扛住高并发&#34;&gt;这套链路怎么扛住高并发&lt;/h2&gt;
&lt;p&gt;漏斗走完了，但还有一个工程问题没回答：几十毫秒的延迟预算，乘以几十万的并发请求，靠什么扛？先把账算出来。假设 1 亿日活、人均 50 次下拉：平均约 6 万次请求/秒，晚高峰按 3-5 倍算是 20-30 万 QPS。每次下拉背后是几十上百次 KV 读取、一次向量检索、一次千级候选的模型推理。能扛住靠的不是某台快机器，是一组选型决策：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis：O(1) 查表 + 批量读。&lt;/strong&gt; 单实例简单 GET 是十万级 QPS、亚毫秒延迟；一次召回要查几十上百个 key（itemCF 种子物品、画像、热榜），用 &lt;code&gt;mget&lt;/code&gt; 合并成一次网络往返，QPS 压力直接降一个量级；集群模式水平分片，容量不够就加分片。已曝光集合的布隆过滤器也放在这里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;向量检索：这是召回里唯一真正&amp;quot;算&amp;quot;的地方，也是选型的重点。&lt;/strong&gt; 几个主流方案的性能画像（百万到十亿级向量、TopK 检索）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;形态&lt;/th&gt;
					&lt;th&gt;性能画像&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Faiss（Meta）&lt;/td&gt;
					&lt;td&gt;嵌入服务进程内的库&lt;/td&gt;
					&lt;td&gt;没有网络和序列化开销；IVF+PQ 量化后单机可扛亿级向量，检索毫秒级&lt;/td&gt;
					&lt;td&gt;召回服务内嵌，延迟敏感场景的主力&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HNSWlib&lt;/td&gt;
					&lt;td&gt;嵌入库&lt;/td&gt;
					&lt;td&gt;图索引召回率高（95% 以上），代价是全量向量驻内存，内存开销大&lt;/td&gt;
					&lt;td&gt;中等规模、高精度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ScaNN（Google）&lt;/td&gt;
					&lt;td&gt;嵌入库&lt;/td&gt;
					&lt;td&gt;各向异性量化，同等召回率下 QPS 领先&lt;/td&gt;
					&lt;td&gt;内嵌、高 QPS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Milvus&lt;/td&gt;
					&lt;td&gt;独立分布式服务&lt;/td&gt;
					&lt;td&gt;存算分离、支持 GPU 索引，十亿级横向扩展&lt;/td&gt;
					&lt;td&gt;多业务共享的向量中台&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Elasticsearch kNN&lt;/td&gt;
					&lt;td&gt;搜索引擎插件&lt;/td&gt;
					&lt;td&gt;复用现有 ES 集群，开箱即用，但 QPS 和召回率都不及专用索引&lt;/td&gt;
					&lt;td&gt;小规模、搜推一体&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;工程上的关键判断：&lt;strong&gt;召回的向量检索优先用内嵌库（Faiss / HNSWlib），省掉一次 RPC&lt;/strong&gt;——30 万 QPS 面前，每次网络往返都是命。Milvus 这类独立服务的价值在多团队共享、在线扩缩容和统一的向量管理，不在单点延迟。内容池上到十亿级，靠 PQ 量化把每条向量从几百字节浮点压成几十字节编码，再加分片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka：百万级消息/秒是常态负载。&lt;/strong&gt; 曝光点击的洪峰对它来说不算事；真正要设计的是分区数（决定下游消费者的并行上限）和消费滞后监控——滞后就是&amp;quot;画像生效延迟&amp;quot;，直接等于用户体感。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flink：状态在本地的流计算。&lt;/strong&gt; 短期画像的滚动更新是 keyed state，状态读写走本地 RocksDB，不回远程存储——这是近线层敢吞全量事件流的原因。反过来说，一旦作业挂掉从 checkpoint 恢复，RocksDB 状态的加载时间就是画像的&amp;quot;失明时间&amp;quot;，所以 checkpoint 间隔要按业务能容忍的失明时长来定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU 推理：批量摊薄。&lt;/strong&gt; 精排把 1000 条候选拼成一个大 batch 一次过 GPU，摊到每条只有几微秒；Triton 的动态批处理（dynamic batching）会在毫秒级窗口里把多个请求的 batch 攒起来再推理，用一点点延迟换几倍吞吐——这是&amp;quot;几十毫秒预算&amp;quot;能成立的关键手法之一。&lt;/p&gt;
&lt;p&gt;共同的模式只有一条：&lt;strong&gt;所有重活提前算完存起来，在线路径上只剩内存级查询和批量推理&lt;/strong&gt;——30 万 QPS 里，没有一个请求需要&amp;quot;现场思考&amp;quot;。&lt;/p&gt;
&lt;h2 id=&#34;收尾数学没变闭环变快&#34;&gt;收尾：数学没变，闭环变快&lt;/h2&gt;
&lt;p&gt;走完全程回头看，三十年里真正变化的不是数学：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2006 年&lt;/strong&gt;，吴军讲余弦定理给新闻分类——打分内核从那时起就是向量点积，今天依然如此；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2013 年&lt;/strong&gt;，Netflix 定下三层分工——把计算摊到用户到来之前，至今是主流形态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2020 年&lt;/strong&gt;，TikTok 官方坦白完播是强信号、粉丝数不是直接因素——行为强度加权第一次有了平台方背书；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022 年&lt;/strong&gt;，Monolith 把模型训练本身搬进在线链路——闭环的最后一段延迟也被压缩。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐系统真正的军备竞赛，从来不是&amp;quot;谁的公式更巧妙&amp;quot;，而是&lt;strong&gt;谁的闭环转得更快&lt;/strong&gt;：从用户划走一条汽车视频，到系统少推汽车内容，中间隔了几秒还是几天。U10086 今晚的每一次停留和划走都在投票，明早打开 App 时看到的那个&amp;quot;更懂他&amp;quot;的首页，没有一次是当场算出来的——全是昨晚就已开始的计算，刚好在他到来之前完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关阅读：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;《千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构》&lt;/a&gt;——本文的架构视角姊妹篇，讲四层漏斗的原理和无限下拉的分页协议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：吴军《数学之美》（第三版，人民邮电出版社）第 14、15 章；TikTok Newsroom, &amp;ldquo;How TikTok recommends videos #ForYou&amp;rdquo;, 2020-06-18（newsroom.tiktok.com/en-us/how-tiktok-recommends-videos-for-you）；Liu et al., &amp;ldquo;Monolith: Real Time Recommendation System With Collisionless Embedding Table&amp;rdquo;, RecSys 2022, arXiv:2209.07663；&amp;ldquo;流量池&amp;quot;分级机制为运营圈第三方经验总结，非官方披露口径。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>千人千面 &#43; 无限瀑布流：推荐 Feed 的完整实现架构</title>
      <link>https://ftmao.com/blog/posts/personalized-infinite-feed-architecture/</link>
      <pubDate>Fri, 02 Oct 2026 18:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/personalized-infinite-feed-architecture/</guid>
      <description>&lt;h1 id=&#34;千人千面--无限瀑布流推荐-feed-的完整实现架构&#34;&gt;千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：三类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后端工程师&lt;/strong&gt;：要实现一个推荐 Feed，想知道每个组件的职责边界（看&amp;quot;召回/粗排/精排/重排&amp;quot;四章和&amp;quot;离线 vs 在线分工总表&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师&lt;/strong&gt;：在做 Feed 系统选型，需要 push/pull、offset/cursor 这些关键决策的对比依据（看各节的对比表）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端/客户端工程师&lt;/strong&gt;：关心无限滚动的分页协议和预取时机（看&amp;quot;游标分页&amp;quot;和&amp;quot;预取&amp;quot;两节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阅读前提：不需要推荐算法背景，算法部分只讲工程接口，不讲模型内部。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上一篇讲 Netflix 的 Flink 扩缩容时提过一个三层结构：离线训练、近线特征、在线排序。有读者问：**首页千人千面，是每个用户打开时都跑一遍计算吗？**答案是否定的，而完整的答案就是这篇文章的主题——把微博、X 那种&amp;quot;每个人看到的都不一样、而且永远拉不到头&amp;quot;的推荐瀑布流，从客户端一路拆到数据闭环。&lt;/p&gt;
&lt;p&gt;先把两个需求拆开，因为它们的难点完全不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;千人千面&lt;/strong&gt;：难在&amp;quot;算&amp;quot;——几亿用户每人一套候选，怎么做到几十毫秒出结果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无限瀑布流&lt;/strong&gt;：难在&amp;quot;协议&amp;quot;——内容在不断新增、排序在不断变化，怎么保证用户连续下拉时不重复、不漏看、永远有货。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;总体架构重活在用户到来之前干完&#34;&gt;总体架构：重活在用户到来之前干完&lt;/h2&gt;
&lt;p&gt;整个系统的核心思想只有一句话：&lt;strong&gt;请求路径上只做轻活，所有重计算都提前发生在事件流和离线任务里&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 提前计算[&amp;#34;提前计算（用户到来之前）&amp;#34;]
        A[&amp;#34;离线层&amp;lt;br/&amp;gt;批量训练模型&amp;lt;br/&amp;gt;计算全量候选池&amp;#34;] --&amp;gt; D[&amp;#34;在线存储&amp;lt;br/&amp;gt;画像 特征 候选集&amp;#34;]
        B[&amp;#34;近线层&amp;lt;br/&amp;gt;行为事件经 Kafka&amp;lt;br/&amp;gt;Flink 实时更新画像特征&amp;#34;] --&amp;gt; D
    end
    subgraph 请求路径[&amp;#34;请求路径（用户下拉这一刻）&amp;#34;]
        C[&amp;#34;在线服务&amp;lt;br/&amp;gt;召回 排序 组装&amp;lt;br/&amp;gt;几十毫秒返回&amp;#34;] --&amp;gt; E[&amp;#34;客户端&amp;lt;br/&amp;gt;渲染一屏&amp;#34;]
    end
    D --&amp;gt; C
    E -.-&amp;gt;|&amp;#34;曝光 点击 停留事件&amp;#34;| B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线层&lt;/strong&gt;（小时/天级）：Spark 类批处理训练推荐模型，算出全量候选——比如&amp;quot;每个用户最可能感兴趣的一万条内容&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近线层&lt;/strong&gt;（秒级）：用户的每一次曝光、点击、停留、点赞都作为事件进 Kafka，流处理作业（如 Flink）实时更新用户画像和特征。你上一次刷的行为，下一刷就生效——&amp;ldquo;越刷越懂你&amp;quot;的体感来自这一层；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线层&lt;/strong&gt;（几十毫秒）：你下拉刷新这一刻，服务只做&amp;quot;取候选 → 排序 → 组装一页&amp;rdquo;，所有重活的结果早已躺在在线存储里。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三层 Netflix 在 2013 年就公开过（offline/nearline/online 的经典划分），至今仍是推荐系统的主流形态。上一篇 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Flink 自动扩缩容&lt;/a&gt;里 Netflix 的&amp;quot;个性化自定义作业&amp;quot;，干的就是近线层的活。&lt;/p&gt;
&lt;h2 id=&#34;为什么是这个分工kafkasparkflink-各自的角色&#34;&gt;为什么是这个分工：Kafka、Spark、Flink 各自的角色&lt;/h2&gt;
&lt;p&gt;架构图里三个组件的角色容易被误解，值得单独说清。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka 不是&amp;quot;记录数据&amp;quot;的，是事件总线。&lt;/strong&gt; 它解决四个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：打点的客户端不需要知道谁在消费、有几个消费者；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;削峰&lt;/strong&gt;：曝光事件的洪峰来临时，消费端可以按自己的节奏追，消息堆在 Kafka 里不会丢；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可回放&lt;/strong&gt;：特征算错了、逻辑改了，把昨天的事件流重新放一遍就能重算——消息队列变成时间机器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多消费者&lt;/strong&gt;：同一份事件流同时喂给画像更新（Flink）、数仓落盘、实时监控，各取所需互不干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有它的替代方案是客户端打点直连 Flink——Flink 一挂数据就丢。Kafka 是缓冲带加保险箱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 和 Flink 看着像，血统不同：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Spark&lt;/th&gt;
					&lt;th&gt;Flink&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;计算血统&lt;/td&gt;
					&lt;td&gt;批处理：数据是有界数据集&lt;/td&gt;
					&lt;td&gt;流处理：逐条事件，批是特例（有界流）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;流处理形态&lt;/td&gt;
					&lt;td&gt;Structured Streaming 是微批（攒一批算一次）&lt;/td&gt;
					&lt;td&gt;原生逐条处理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;典型延迟&lt;/td&gt;
					&lt;td&gt;秒级到分钟级&lt;/td&gt;
					&lt;td&gt;毫秒到秒级&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;生态主场&lt;/td&gt;
					&lt;td&gt;数仓（Hive/Iceberg）、MLlib、Python 数据科学工具链&lt;/td&gt;
					&lt;td&gt;有状态流计算：keyed state、事件时间水位线、exactly-once&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分工逻辑由此而来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;训练用 Spark，因为它是吞吐型任务&lt;/strong&gt;：几 TB 历史数据扫几遍、小时级跑完即可，不在乎单条延迟；而且机器学习工具链都在它这边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;画像更新用 Flink，因为近线层的 KPI 是&amp;quot;行为发生后多久生效&amp;quot;&lt;/strong&gt;：要求秒级，且要维护滚动状态（最近 N 次行为的加权向量）——逐条处理加状态管理正是 Flink 的主场。用 Spark 微批也能跑，代价是生效延迟从秒变分钟，&amp;ldquo;越刷越懂你&amp;quot;的体感就钝了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要诚实补一句：&lt;strong&gt;边界在模糊化&lt;/strong&gt;。Spark 能流、Flink 能批，用 Spark Streaming 做近线的公司也不少。这个分工一半是技术判断，一半是生态和组织惯性——团队的历史栈是什么，往往和&amp;quot;哪个理论最优&amp;quot;同等重要。&lt;/p&gt;
&lt;h2 id=&#34;用户偏好怎么算出来交互矩阵与-embedding&#34;&gt;用户偏好怎么算出来：交互矩阵与 embedding&lt;/h2&gt;
&lt;p&gt;讲四层漏斗之前，先看&amp;quot;偏好&amp;quot;这个东西在系统里的物理形态——它决定了每层能用什么数据。&lt;/p&gt;
&lt;p&gt;一切从一张&lt;strong&gt;用户-物品交互矩阵&lt;/strong&gt;开始：行是用户，列是内容（视频、微博、商品），格子里的值是行为强度。显式反馈（评分、点赞）稀疏但明确；隐式反馈（点击、停留时长、完播、划走速度）量大但噪声多——工业界主要靠后者。&lt;/p&gt;
&lt;p&gt;矩阵太大太稀疏（几亿用户 × 几亿内容，99.99% 是空格），没法直接用，于是有了两条经典技术路线，都是十多年前就成熟的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;矩阵分解（Matrix Factorization）&lt;/strong&gt;：把大矩阵分解成两个小矩阵——每个用户一个向量、每个物品一个向量，维度几十到几百。用户对物品的偏好 ≈ 两个向量的点积。2006-2009 年 Netflix Prize 竞赛把它打成主流，用 ALS（交替最小二乘）在集群上批量求解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;embedding（嵌入向量）&lt;/strong&gt;：矩阵分解的现代化身。item2vec（2016）把物品当&amp;quot;词&amp;rdquo;、用户行为序列当&amp;quot;句子&amp;quot;套 word2vec；双塔模型（DSSM 系）用两个神经网络分别把用户和物品映射到同一个向量空间。&lt;strong&gt;殊途同归：最终产物都是&amp;quot;每个用户一个向量、每个物品一个向量，点积或余弦相似度就是偏好分&amp;quot;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套东西直接决定了离线/在线的分工：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线算&lt;/strong&gt;：物品向量（内容发布后批量算一次，更新频率低）、用户长期兴趣向量（按天/小时批量更新）、物品相似度矩阵；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近线算&lt;/strong&gt;：用户短期兴趣向量——最常见的实现是把用户最近 N 次行为对应的物品向量做&lt;strong&gt;加权平均&lt;/strong&gt;（行为类型加权：完播 &amp;gt; 点赞 &amp;gt; 点击 &amp;gt; 秒划过；时间衰减：越久的行为权重越低）。Flink 作业消费行为事件、滚动更新这个向量，秒级生效；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线算&lt;/strong&gt;：只剩查表和点积——拿用户向量去向量库里找最近邻，或用画像特征给候选打分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，&lt;strong&gt;&amp;ldquo;用户偏好&amp;quot;在请求到来前就已经被压缩成几百维的向量躺在存储里了&lt;/strong&gt;。下面四层漏斗里所有&amp;quot;懂用户&amp;quot;的操作，本质都是在读这份预先算好的压缩包。&lt;/p&gt;
&lt;h2 id=&#34;四层排序漏斗总览&#34;&gt;四层排序漏斗总览&lt;/h2&gt;
&lt;p&gt;在线服务收到一次下拉请求，要在几十毫秒内从海量内容里选出这一屏的 20 条。对全量内容逐一打分是不可能的，工程上的标准解法是&lt;strong&gt;漏斗&lt;/strong&gt;：每一层用更贵的模型处理更少的候选。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;召回&amp;lt;br/&amp;gt;万级候选&amp;lt;br/&amp;gt;多路召回： 协同过滤 向量 热门 关注&amp;#34;] --&amp;gt; B[&amp;#34;粗排&amp;lt;br/&amp;gt;千级&amp;lt;br/&amp;gt;轻量模型快速打分&amp;#34;]
    B --&amp;gt; C[&amp;#34;精排&amp;lt;br/&amp;gt;百级&amp;lt;br/&amp;gt;深度排序模型融合全部特征&amp;#34;]
    C --&amp;gt; D[&amp;#34;重排 混排&amp;lt;br/&amp;gt;二十条一页&amp;lt;br/&amp;gt;多样性打散 插入广告 业务规则&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;先说清一个术语问题：&lt;strong&gt;这套漏斗的主力技术是深度学习排序模型，不是大语言模型（LLM）&lt;/strong&gt;。协同过滤 2000 年代就有，矩阵分解是 2009 年 Netflix Prize 的明星，GBDT+LR 是 Facebook 2014 年的论文，Wide &amp;amp; Deep 是 Google 2016 年的——整个栈在十年前就完整存在。LLM 近两年开始进入推荐链路（内容理解、生成标签、甚至生成式召回），但在线排序的主力依然是经典深度模型。别把&amp;quot;推荐系统&amp;quot;和&amp;quot;大模型&amp;quot;混为一谈。&lt;/p&gt;
&lt;h2 id=&#34;召回离线算相似度在线做检索&#34;&gt;召回：离线算相似度，在线做检索&lt;/h2&gt;
&lt;p&gt;任务：从几亿内容里捞出万级候选。关键词是&lt;strong&gt;覆盖率&lt;/strong&gt;——宁多勿漏，漏了的后面几层救不回来。&lt;/p&gt;
&lt;p&gt;召回是&amp;quot;多路并行&amp;quot;的，每一路是独立的候选来源，技术路径和离线/在线分工各不相同：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一路：协同过滤（itemCF 为主）&lt;/strong&gt;。逻辑是&amp;quot;和你喜欢过的东西相似的东西&amp;rdquo;。物品两两之间的相似度由共现矩阵算出（喜欢 A 的人也多大比例喜欢 B，余弦相似度），这个矩阵&lt;strong&gt;全离线算&lt;/strong&gt;，按天或按小时更新，结果存 KV：&lt;code&gt;item_id → 相似物品 TopK 列表&lt;/code&gt;。在线时只做一件事：拿用户最近行为过的物品列表去 KV 里批量查，合并候选。&lt;strong&gt;在线路径上没有任何矩阵运算，全是查表。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二路：向量召回（双塔 + ANN 检索）&lt;/strong&gt;。物品向量离线批量算好，灌进向量索引库（Faiss、HNSW 这类近似最近邻索引，百万级向量检索只要几毫秒）；用户向量由近线层滚动更新。在线时拿用户向量在索引里查 TopK——这一步是召回里唯一称得上&amp;quot;在线计算&amp;quot;的环节，但它也只是一次&lt;strong&gt;索引检索&lt;/strong&gt;，不是模型推理（双塔的用户塔也可以在线跑一次，把实时上下文特征算进去，看延迟预算）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三路：运营类候选&lt;/strong&gt;。热门榜、新内容池、同城/地理围栏、关注关系产出——这些本质是&lt;strong&gt;离线或近线算好的榜单&lt;/strong&gt;，在线直接读。它们的作用不是精准，而是兜底和探索：新用户没有画像（冷启动）时，热门榜就是全部；老用户的候选里也固定掺一部分，保证 Feed 有新鲜感。&lt;/p&gt;
&lt;p&gt;多路结果合并、去重、按路配额截断，凑齐万级候选交给下一层。&lt;/p&gt;
&lt;h2 id=&#34;粗排夹在中间的性价比层&#34;&gt;粗排：夹在中间的性价比层&lt;/h2&gt;
&lt;p&gt;任务：万级 → 千级。它存在的唯一理由是&lt;strong&gt;精排太贵&lt;/strong&gt;——精排模型打一万条候选，延迟和算力都吃不消，但召回结果直接用又太糙。&lt;/p&gt;
&lt;p&gt;粗排的模型选型讲究&amp;quot;够用就好&amp;quot;：早年用 LR（逻辑回归）或 GBDT，特征只取最强的一批（物品热度、用户类目偏好分、历史 CTR）；近年常用&lt;strong&gt;简化版双塔&lt;/strong&gt;——向量点积打分，几乎就是向量召回的打分器复用。特征从特征存储里批量读，模型在线跑，但都是毫秒级的小模型。&lt;/p&gt;
&lt;p&gt;工程上粗排是可选层：候选量小的业务（百万级内容池）召回直送精排也扛得住；亿级内容池才必须用它护住精排的延迟。&lt;/p&gt;
&lt;h2 id=&#34;精排深度学习但不是大模型&#34;&gt;精排：深度学习，但不是大模型&lt;/h2&gt;
&lt;p&gt;任务：对千级候选逐条打分，预测&amp;quot;这个用户看到这条内容，会不会点、会看多久&amp;quot;。这是&amp;quot;千人千面&amp;quot;真正被算出来的地方，也是算法团队的主战场。&lt;/p&gt;
&lt;p&gt;模型演进的主线清晰可见，每一步都发表在十年内：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;年代&lt;/th&gt;
					&lt;th&gt;模型&lt;/th&gt;
					&lt;th&gt;解决的问题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;2010 前后&lt;/td&gt;
					&lt;td&gt;LR&lt;/td&gt;
					&lt;td&gt;基线：特征加权和，可解释、可工程化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2012&lt;/td&gt;
					&lt;td&gt;FM / FFM&lt;/td&gt;
					&lt;td&gt;自动学习特征两两交叉（&amp;ldquo;男性 × 游戏类&amp;quot;这种组合特征不用人工配）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2014&lt;/td&gt;
					&lt;td&gt;GBDT + LR（Facebook）&lt;/td&gt;
					&lt;td&gt;GBDT 自动做特征组合和离散化，输出喂给 LR&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2016&lt;/td&gt;
					&lt;td&gt;Wide &amp;amp; Deep（Google）&lt;/td&gt;
					&lt;td&gt;Wide 侧记忆规则（&amp;ldquo;已安装的应用&amp;rdquo;），Deep 侧泛化（embedding 学没见过组合）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2017-2018&lt;/td&gt;
					&lt;td&gt;DeepFM / DIN（阿里）&lt;/td&gt;
					&lt;td&gt;DeepFM 把 FM 嵌进网络；DIN 引入注意力——给用户历史行为按&amp;quot;和当前候选的相关性&amp;quot;加权，你刷运动鞋时，你上周看鞋的权重就压过看手机的&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意这张表里&lt;strong&gt;没有一个是大语言模型&lt;/strong&gt;。它们参数量百万到亿级，输入是结构化特征向量，一次推理是几十毫秒的批量矩阵乘法。&lt;/p&gt;
&lt;p&gt;精排的在线链路是漏斗里最长的：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;候选列表 千级&amp;#34;] --&amp;gt; B[&amp;#34;特征拼接&amp;lt;br/&amp;gt;用户画像 物品特征&amp;lt;br/&amp;gt;实时上下文: 时间 设备 网络&amp;#34;]
    B --&amp;gt; C[&amp;#34;特征存储 Feature Store&amp;lt;br/&amp;gt;离线特征读 KV&amp;lt;br/&amp;gt;近线特征读 Flink 产出&amp;#34;]
    C --&amp;gt; D[&amp;#34;深度排序模型&amp;lt;br/&amp;gt;批量推理 输出 CTR/时长预测&amp;#34;]
    D --&amp;gt; E[&amp;#34;按预估分排序&amp;lt;br/&amp;gt;截断到百级&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键组件是&lt;strong&gt;特征存储（Feature Store）&lt;/strong&gt;：离线特征（用户长期画像、物品属性）和近线特征（最近一小时的点击序列、实时曝光计数）在请求时按 key 批量读出、和实时上下文（现在是几点、什么设备）拼成特征向量。&lt;strong&gt;特征的一致性是这个环节最大的坑&lt;/strong&gt;：离线训练用的特征和在线服务用的特征必须同一份定义，否则&amp;quot;训练时很准、上线后翻车&amp;rdquo;——特征平台存在的意义就是管这份一致性。&lt;/p&gt;
&lt;h2 id=&#34;重排混排从预测喜欢到设计一页&#34;&gt;重排/混排：从&amp;quot;预测喜欢&amp;quot;到&amp;quot;设计一页&amp;quot;&lt;/h2&gt;
&lt;p&gt;精排输出的是分数，但分数最高的 20 条直接端给用户往往体验很差：可能十条都是同一个作者、同一类题材。重排层的职责变了——&lt;strong&gt;不再预测&amp;quot;你多喜欢&amp;quot;，而是设计&amp;quot;这一页长什么样&amp;quot;&lt;/strong&gt;。它几乎全是规则和策略，在线执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多样性打散&lt;/strong&gt;：相邻 N 条不允许同作者/同类目；更讲究的用 MMR（每次选&amp;quot;分高且与已选差异大&amp;quot;的）或 DPP（行列式点过程，数学上保证选出的集合整体多样）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混排&lt;/strong&gt;：把广告、关注更新、运营位按规则插进自然结果。广告有自己的排序逻辑——eCPM = 出价 × 预估点击率，既要用户可能点、又要广告主出价高；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负反馈过滤&lt;/strong&gt;：你点过&amp;quot;不感兴趣&amp;quot;的作者和类目，在这里被硬性屏蔽；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务护栏&lt;/strong&gt;：去重、过滤已举报/审核中内容、新内容冷启扶持配额。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重排是业务规则密度最高的一层，通常做成规则引擎而不是模型——产品运营每周调的都是这里。&lt;/p&gt;
&lt;h2 id=&#34;离线-vs-在线一张分工总表&#34;&gt;离线 vs 在线：一张分工总表&lt;/h2&gt;
&lt;p&gt;把全链路摊开，每个环节算什么、什么时候算、存哪里，一张表收拢：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;算什么&lt;/th&gt;
					&lt;th&gt;离线/近线/在线&lt;/th&gt;
					&lt;th&gt;存储&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;模型训练&lt;/td&gt;
					&lt;td&gt;排序模型参数、双塔参数&lt;/td&gt;
					&lt;td&gt;离线（小时/天）&lt;/td&gt;
					&lt;td&gt;模型仓库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物品向量&lt;/td&gt;
					&lt;td&gt;内容的 embedding&lt;/td&gt;
					&lt;td&gt;离线（发布时 + 定期刷新）&lt;/td&gt;
					&lt;td&gt;向量索引库（Faiss/HNSW）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物品相似度&lt;/td&gt;
					&lt;td&gt;itemCF 共现矩阵&lt;/td&gt;
					&lt;td&gt;离线（天/小时）&lt;/td&gt;
					&lt;td&gt;KV&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;热门榜/新内容池&lt;/td&gt;
					&lt;td&gt;全站榜单&lt;/td&gt;
					&lt;td&gt;近线（分钟级滚动窗口）&lt;/td&gt;
					&lt;td&gt;KV&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户长期画像&lt;/td&gt;
					&lt;td&gt;长期兴趣向量、类目偏好&lt;/td&gt;
					&lt;td&gt;离线（天）&lt;/td&gt;
					&lt;td&gt;KV / 特征存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户短期兴趣&lt;/td&gt;
					&lt;td&gt;最近行为的加权向量&lt;/td&gt;
					&lt;td&gt;近线（秒级，Flink）&lt;/td&gt;
					&lt;td&gt;特征存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;召回&lt;/td&gt;
					&lt;td&gt;查相似物品、ANN 检索、读榜单&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;在线&lt;/strong&gt;（但全是查表/检索）&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;粗排/精排&lt;/td&gt;
					&lt;td&gt;特征拼接 + 模型打分&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;在线&lt;/strong&gt;（特征预先备好，只做推理）&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重排/混排&lt;/td&gt;
					&lt;td&gt;规则执行&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;在线&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;已曝光记录&lt;/td&gt;
					&lt;td&gt;用户看过什么&lt;/td&gt;
					&lt;td&gt;在线读写，近线聚合&lt;/td&gt;
					&lt;td&gt;KV / 布隆过滤器&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;规律一目了然：&lt;strong&gt;越靠近&amp;quot;知识沉淀&amp;quot;（模型参数、向量、相似度）的越离线，越靠近&amp;quot;当下上下文&amp;quot;（你刚点了什么、现在几点）的越在线&lt;/strong&gt;。在线路径上真正的计算只有两样——向量检索和模型推理，其余全是查表。&lt;/p&gt;
&lt;p&gt;注意一个工程事实：&lt;strong&gt;漏斗每一层都是&amp;quot;算法团队调模型、工程团队保接口&amp;quot;&lt;/strong&gt;。接口契约是稳定的（输入候选列表和特征，输出打分排序后的列表），模型怎么迭代都不影响上下游——这是推荐系统能持续进化的架构前提。&lt;/p&gt;
&lt;h2 id=&#34;无限瀑布流一分页协议为什么必须是游标&#34;&gt;无限瀑布流（一）：分页协议为什么必须是游标&lt;/h2&gt;
&lt;p&gt;无限下拉遇到的第一个坑是分页。直觉做法是 offset 分页（&amp;ldquo;给我第 41-60 条&amp;rdquo;），在推荐流场景下它有三个致命伤：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;offset 分页&lt;/th&gt;
					&lt;th&gt;cursor 游标分页&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;新内容插入头部&lt;/td&gt;
					&lt;td&gt;第 2 页和第 1 页之间内容错位，用户看到重复&lt;/td&gt;
					&lt;td&gt;游标锚定&amp;quot;上次最后一条&amp;quot;，新内容插进来也不影响&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;排序实时变化&lt;/td&gt;
					&lt;td&gt;每次请求重新排序，offset 语义直接失效&lt;/td&gt;
					&lt;td&gt;游标携带上次的排序上下文&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;深翻页性能&lt;/td&gt;
					&lt;td&gt;数据库 OFFSET 越大越慢&lt;/td&gt;
					&lt;td&gt;从锚点继续，代价恒定&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以推荐流的分页协议长这样：&lt;strong&gt;每次请求带一个游标（cursor），响应返回一页内容 + 下一页的游标&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;请求:  GET /feed?cursor=eyJscCI6...&amp;amp;count=20
响应:  { &amp;#34;items&amp;#34;: [...20条...], &amp;#34;next_cursor&amp;#34;: &amp;#34;eyJs...&amp;#34; }&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;游标是个不透明字符串（服务端编码，客户端不许解析），里面通常装着：上一页最后一条的标识和排序分、本次推荐的批次号、过期时间。&lt;strong&gt;&amp;ldquo;不透明&amp;quot;是关键设计&lt;/strong&gt;：服务端可以随时改游标格式和分页逻辑，客户端零感知。&lt;/p&gt;
&lt;h2 id=&#34;无限瀑布流二一次下拉请求的完整链路&#34;&gt;无限瀑布流（二）：一次下拉请求的完整链路&lt;/h2&gt;
&lt;p&gt;把上面两节合起来，用户下拉一次的完整链路：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant U as 客户端
    participant F as Feed 服务
    participant S as 在线存储
    participant R as 排序服务
    U-&amp;gt;&amp;gt;F: 下拉 带上次游标
    F-&amp;gt;&amp;gt;S: 查用户画像和已曝光记录
    F-&amp;gt;&amp;gt;S: 取候选集 多路召回合并
    F-&amp;gt;&amp;gt;R: 粗排 精排 重排
    R--&amp;gt;&amp;gt;F: 排序后的候选
    F-&amp;gt;&amp;gt;F: 过滤已曝光 取一页&amp;lt;br/&amp;gt;写入已曝光记录
    F--&amp;gt;&amp;gt;U: 返回 20 条 + 下一页游标
    U-&amp;gt;&amp;gt;U: 渲染 上报曝光事件&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;链路里有两个不那么显眼但缺一不可的组件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已曝光去重（seen filter）&lt;/strong&gt;。用户刷过的内容不能再出现，否则无限下拉就成了无限重播。实现上是在线存储里维护每用户的已曝光集合——请求时过滤、响应后写入。数据量大时用布隆过滤器压缩内存（允许极低误判率，误判的代价不过是少推一条内容），或者按天滚动窗口只记最近 N 天的曝光。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预取（prefetch）&lt;/strong&gt;。如果等用户滑到底再发请求，每一页都有加载等待，无限流就成了&amp;quot;有停顿流&amp;rdquo;。客户端的标准做法：滚动到当前列表 70%-80% 位置时就预取下一页；网络差时加大预取提前量。用户感知到的&amp;quot;永远流畅&amp;quot;，一半是服务端架构，一半是这一个提前量。&lt;/p&gt;
&lt;h2 id=&#34;永远拉不到头怎么兑现&#34;&gt;&amp;ldquo;永远拉不到头&amp;quot;怎么兑现&lt;/h2&gt;
&lt;p&gt;这是无限瀑布流真正的核心问题：个性化候选再多也是有限的，用户刷 200 屏之后呢？答案是&lt;strong&gt;分层供给，逐级降级&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;个性化候选&lt;/strong&gt;：精排得分最高的一批，前几十屏的主力；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;探索流量（exploration）&lt;/strong&gt;：故意混入你没表现出过兴趣的类目——推荐系统必须留探索比例，否则画像会收敛到死胡同（信息茧房在工程上的成因就是探索为零）。这层对用户是&amp;quot;刷到了意外之喜&amp;rdquo;，对系统是&amp;quot;采集新兴趣信号&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热门与新内容兜底&lt;/strong&gt;：当个性化候选耗尽，降级到全站热门、最新发布——这些内容池每天都在被离线任务刷新，事实上是无限的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间衰减&lt;/strong&gt;：同一条内容的推荐价值随时间衰减，昨天刷过的爆款明天权重下降，保证&amp;quot;明天再来还有新东西&amp;quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以&amp;quot;永远拉不到头&amp;quot;不是因为算出了无限多的个性化结果，而是因为&lt;strong&gt;个性化耗尽之前，系统会不露痕迹地滑向兜底池&lt;/strong&gt;。用户刷到第 100 屏时看到的&amp;quot;千人千面&amp;quot;，成分里热门内容的占比早已悄悄超过个性化——体验连续，供给无限。&lt;/p&gt;
&lt;h2 id=&#34;数据闭环这一刷决定下一刷&#34;&gt;数据闭环：这一刷决定下一刷&lt;/h2&gt;
&lt;p&gt;最后把环闭上。客户端上报的每一条曝光、点击、停留时长、划走速度，都经 Kafka 进入近线层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停留 5 秒以上 → 画像里对应类目的权重上调；&lt;/li&gt;
&lt;li&gt;秒划走 → 权重下调；&lt;/li&gt;
&lt;li&gt;点了的广告、取关的话题 → 写入负反馈特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一刷的召回和排序用的就是更新后的画像。&lt;strong&gt;推荐系统的&amp;quot;智能&amp;quot;不在某一次排序多准，而在于这个闭环转得多快&lt;/strong&gt;——从行为发生到画像生效的延迟，直接决定用户&amp;quot;它懂我&amp;quot;的体感。这也是为什么 Netflix 那样的公司要养三万个流处理作业、还专门给它们做自动扩缩容。&lt;/p&gt;
&lt;h2 id=&#34;和关注流的关系&#34;&gt;和关注流的关系&lt;/h2&gt;
&lt;p&gt;顺带澄清一个常见混淆：微博、X 都有两种流——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关注流（关注的人发的内容，按时间排序）&lt;/strong&gt;：经典问题是写扩散（发微博时推给所有粉丝）还是读扩散（刷的时候现拉），粉丝量大的大 V 是写扩散的噩梦；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐流（本文讨论的）&lt;/strong&gt;：没有&amp;quot;关注&amp;quot;这个约束，内容来自全站，靠排序漏斗筛选，游标分页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现代产品里两者早已混合：推荐流里插关注内容、关注流里插推荐内容，都在重排混排那一层完成。&lt;/p&gt;
&lt;h2 id=&#34;落地清单&#34;&gt;落地清单&lt;/h2&gt;
&lt;p&gt;如果要自己实现一套，按这个顺序检查：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;骨架&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 三层分离：离线训练、近线特征、在线排序，请求路径上只留轻活；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 排序漏斗四层：召回保覆盖、粗排保性价比、精排保准确、重排保体验，层间接口契约稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;无限流协议&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 游标分页，游标不透明、带过期时间；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 已曝光去重：在线集合或布隆过滤器，按天滚动窗口；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 客户端 70%-80% 位置预取下一页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;拉不完&amp;quot;的保障&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 分层供给：个性化 → 探索 → 热门/新内容兜底，降级要无感；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 探索流量比例显式配置，别让它收敛到零；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 内容时间衰减，保证&amp;quot;明天还有新东西&amp;rdquo;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;闭环&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 曝光/点击/停留全量上报，经消息队列进近线层；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 监控&amp;quot;行为 → 画像生效&amp;quot;的端到端延迟，这个数就是用户体感的下限。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：千人千面不是&amp;quot;为每个用户算一遍&amp;quot;，而是&lt;strong&gt;把计算摊到用户到来之前的每一秒里&lt;/strong&gt;；无限瀑布流不是&amp;quot;有无限内容&amp;quot;，而是&lt;strong&gt;个性化耗尽之前，系统早已悄悄换档&lt;/strong&gt;。两个魔术的机关，都在用户看不见的那一侧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想直观感受这套漏斗里的数据长什么样，可以看姊妹篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/tiktok-recommendation-math-walkthrough/&#34;&gt;《从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的》&lt;/a&gt;——造一个用户，带着他一晚上的行为数据走完召回、粗排、精排、重排。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：Netflix 技术博客的个性化系统架构（offline/nearline/online 三层划分）；推荐系统排序漏斗与游标分页为业界通用架构实践。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了</title>
      <link>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</link>
      <pubDate>Fri, 28 Aug 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-maps-multimodal-personalization/</guid>
      <description>&lt;h1 id=&#34;netflix-maps-笔记让推荐模型看见素材冷启动就不冷了&#34;&gt;Netflix MAPS 笔记：让推荐模型「看见」素材，冷启动就不冷了&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 应用的工程师，尤其是被冷启动、离线线上不一致折磨过的人。
阅读目标：搞清楚 Netflix 怎么用多模态向量把素材个性化的冷启动问题「绕」过去，以及那套消融实验和代理任务的方法论为什么值得抄。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;问题本身素材是个不透明的-id&#34;&gt;〇、问题本身：素材是个不透明的 ID&lt;/h2&gt;
&lt;p&gt;Netflix 把海报、视频预览这些视觉线索统称为素材（asset），给每个会员挑哪张海报，本身就是一个个性化问题。老模型回答「老标题挑哪张图」回答得很好，但「新标题刚上线怎么办」回答得很差：模型只从历史交互里学习，每张素材在它眼里只是一个不透明的 ID，图里画了什么人、什么色调，它一概看不见。于是新剧上线时素材没有任何历史，只能加大探索流量去「刷数据」，其余时候退回人气启发式——完全不认你的口味。这就是教科书式的冷启动。&lt;/p&gt;
&lt;p&gt;原文的解法一句话：&lt;strong&gt;让模型看见并听见它要推荐的东西&lt;/strong&gt;。新素材一到手就有向量，而向量空间已经承载了会员对相似素材的偏好，所以个性化在接近上线的那一刻就能启动，不用等交互慢慢堆出来。&lt;/p&gt;
&lt;h2 id=&#34;一海报个性化让模型看图&#34;&gt;一、海报个性化：让模型看图&lt;/h2&gt;
&lt;p&gt;做法直接到近乎朴素：用预训练的 CLIP 把每张海报编码成一个 768 维向量，和素材原有的可学习 ID 向量&lt;strong&gt;拼接&lt;/strong&gt;，作为素材的最终表示。就这一个改动。&lt;/p&gt;
&lt;p&gt;效果的关键在于迁移：会员对「某位喜剧演员的脸」「某种配色」的偏好，本来写在图片向量空间里而不是绑死在素材 ID 上，所以新剧海报一编码完，这些偏好立刻生效——哪怕这张图从没给任何人展示过。冷启动从盲区变成了「向量空间对此早有意见」。&lt;/p&gt;
&lt;h2 id=&#34;二五个模型合成一个&#34;&gt;二、五个模型合成一个&lt;/h2&gt;
&lt;p&gt;第二个收益更工程化。同一部剧的海报要铺到五个画布上（billboard、vertical-box、horizontal-panel、short-panel、landscape-panel），裁剪比例各不相同。ID 时代的模型不知道这些裁剪图是同一幕戏，信号无法在画布之间流动，于是历史上每个画布各训一个模型，各自承受各自的冷启动。&lt;/p&gt;
&lt;p&gt;CLIP 向量对裁剪、缩放、宽高比基本不变，同一幕戏的五个版本映射到几乎同一个向量——壁垒没了，五个模型合成一个，高流量画布学到的偏好立刻灌溉稀疏画布。收益最大的恰恰正是数据最少的画布。&lt;/p&gt;
&lt;p&gt;合并带来一个新问题：五个画布的曝光量天差地别，直接按原始计数混训，高流量画布和高频交互类型会淹没一切，本想帮扶的稀疏画布反而最吃亏；手工调权重又换来一堆拍脑袋超参。Netflix 的做法是&lt;strong&gt;奖励加权&lt;/strong&gt;：每条训练样本按其交互类型的长期奖励分（来自 Netflix 既有的长期奖励建模）加权。交互类型在各画布分布不均，按长期价值加权之后，画布配比自动重平衡——画布按「它驱动的交互的长期价值」贡献梯度，而不是按曝光量。没有手工权重，模型优化目标从「最高频的短期动作」换成了「长期会员满意度」。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    IMG[&amp;#34;新海报上线&amp;#34;] --&amp;gt; CLIP[&amp;#34;CLIP 图像编码器&amp;lt;br/&amp;gt;输出 768 维向量&amp;#34;]
    ID[&amp;#34;素材 ID 向量&amp;#34;] --&amp;gt; CONCAT[&amp;#34;拼接成素材表示&amp;#34;]
    CLIP --&amp;gt; CONCAT
    CONCAT --&amp;gt; MODEL[&amp;#34;统一模型&amp;lt;br/&amp;gt;五个画布共享&amp;#34;]
    HOT[&amp;#34;高流量画布&amp;lt;br/&amp;gt;billboard 等&amp;#34;] --&amp;gt;|&amp;#34;交互信号沿向量空间迁移&amp;#34;| MODEL
    MODEL --&amp;gt;|&amp;#34;长期奖励加权混训&amp;#34;| SPARSE[&amp;#34;稀疏画布直接受益&amp;lt;br/&amp;gt;short-panel 等&amp;#34;]
    style MODEL fill:#fff3cd
    style SPARSE fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;三离线评估ips-与探索流量&#34;&gt;三、离线评估：IPS 与探索流量&lt;/h2&gt;
&lt;p&gt;这套结果能信，前提是评估干净。用当前线上策略的日志评估新模型是有偏的：线上策略偏爱某些素材，日志记录的是「策略选了什么」，不是「会员从全集里会选什么」。和线上策略意见不合的新模型会被冤枉——它会挑的曝光在日志里几乎不存在。&lt;/p&gt;
&lt;p&gt;Netflix 用一小股&lt;strong&gt;随机探索流量&lt;/strong&gt;做 IPS（逆倾向评分）：随机策略按已知分布在候选素材里采样，倾向分在服务时刻精确记录，而不是事后估计。每条观测按倾向分的倒数加权，被探索流量压稀的曝光得到相应抬升，得到候选策略的无偏奖励估计。原文有句话值得划线：&lt;strong&gt;倾向分由构造保证已知而非事后建模，是离线数字能跟踪线上结果的最大单一原因。&lt;/strong&gt; 候选模型必须在 IPS（相对线上基线的比值）上先赢，才配拿 A/B 流量。&lt;/p&gt;
&lt;h2 id=&#34;四消融实验两个成分缺一不可&#34;&gt;四、消融实验：两个成分缺一不可&lt;/h2&gt;
&lt;p&gt;「图片向量」和「模型合并」两个想法捆在一起，Netflix 分开消融，对照老的五模型系统：V1 只加图片向量（仍是五个画布模型），V2 只合并（统一模型但纯 ID），V3 两者都要。离线指标上 ±1% 以内不算显著，V1、V2 单独做基本都在噪声带里；线上全平台跑了至少四周的 A/B 给出更清楚的分界线：&lt;strong&gt;V1、V2 双双不显著，只有 V3 拿到统计显著的提升&lt;/strong&gt;——V3 就是今天线上跑的版本。&lt;/p&gt;
&lt;p&gt;原因也讲得透：V1 告诉单画布模型素材长什么样，但稀疏画布样本太少，教不会它怎么用这条信息；V2 数据够了，但全是 ID 信号，新素材没有。V3 两者兼有，成熟画布教会共享模型「CLIP 向量如何映射到会员偏好」，这个映射直接迁移到稀疏画布。效果是相乘不是相加——V3 在 short-panel 上的提升（5.691%）超过 V1 与 V2 之和。原文的教训写得很直白：&lt;strong&gt;下结论「内容特征没用」之前，先找第二个阻塞因子。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真实考验来自产品侧：Netflix 十年来最大的电视首页改版，让 short-panel 一夜之间成为主战场——即将吃下最多曝光的画布恰好历史数据最少。V3 赶在改版前上线，配合一个月的 holdback A/B，核心发现指标和播放时长双双显著增长，且增幅比稳态消融更大。&lt;/p&gt;
&lt;h2 id=&#34;五查询感知的海报排序&#34;&gt;五、查询感知的海报排序&lt;/h2&gt;
&lt;p&gt;浏览时你的长期口味是对的信号，搜索时不是：搜某个演员时你要的是带他的海报，哪怕你的广义口味另有指向。搜索页的意图写在 query 里，海报应当响应它。&lt;/p&gt;
&lt;p&gt;因为 CLIP 把文本和图像投进了同一个向量空间，这件事近乎白送：直接算 query 的文本向量与素材图像向量的余弦相似度，和原有个性化分数按权重 α（0 到 1，线上 A/B 调）混合。第一项是「我们认为你喜欢什么」，第二项是「你刚开口要什么」。没有任何额外建模——向量本来就在素材表示里，排序时加一个相似度项而已。&lt;/p&gt;
&lt;h2 id=&#34;六视频预览mediafm&#34;&gt;六、视频预览：MediaFM&lt;/h2&gt;
&lt;p&gt;视频预览比静态图难：吸引力来自运动、节奏、对白、配乐，单帧说不清。第一版内容感知方案 SeqCLIP 只取各帧的 CLIP 向量求平均——看得见长相，听不见声音，而对白和音乐恰恰扛着预览片的大半调性。&lt;/p&gt;
&lt;p&gt;补全这块的是 MediaFM，Netflix 第一个自研多模态地基模型，在 8000 万个镜头（shot）上训练，每个镜头融合三路信号成一个向量：视觉走 SeqCLIP，音频走预训练语音音频编码器，文本是字幕经大规模文本模型编码。接入方式和 CLIP 一样：把向量拼进素材表示，不需要新基建。&lt;/p&gt;
&lt;p&gt;离线 IPS 和五周全平台 A/B 给出一致的排序：&lt;strong&gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&lt;/strong&gt;，每加一层内容感知都有收益，TV 端收益最大。音频和定时文本是 SeqCLIP 这类纯视觉编码器够不到的，这部分增量是真的。MediaFM 已作为默认视频预览向量全平台上线。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph FM[&amp;#34;MediaFM 多模态地基模型&amp;lt;br/&amp;gt;8000 万镜头训练&amp;#34;]
        V[&amp;#34;视觉&amp;lt;br/&amp;gt;SeqCLIP 帧向量平均&amp;#34;] --&amp;gt; FUSE[&amp;#34;融合成镜头级向量&amp;#34;]
        A[&amp;#34;音频&amp;lt;br/&amp;gt;预训练语音音频编码器&amp;#34;] --&amp;gt; FUSE
        T[&amp;#34;文本&amp;lt;br/&amp;gt;字幕经大规模文本模型&amp;#34;] --&amp;gt; FUSE
    end
    FUSE --&amp;gt; STORE[&amp;#34;Embedding Store&amp;lt;br/&amp;gt;训练与推理同源 无偏斜&amp;#34;]
    STORE --&amp;gt; PROBE[&amp;#34;线性探针代理任务&amp;lt;br/&amp;gt;仅凭向量预测去偏人气赢家&amp;#34;]
    PROBE --&amp;gt;|&amp;#34;跑赢随机猜测才放行&amp;#34;| AB[&amp;#34;离线 IPS + 线上 A/B&amp;lt;br/&amp;gt;MediaFM ＞ SeqCLIP ＞ 纯 ID&amp;#34;]
    style PROBE fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七便宜地挑向量线性探针&#34;&gt;七、便宜地挑向量：线性探针&lt;/h2&gt;
&lt;p&gt;新向量层出不穷，端到端试验太贵——数据工程、重训、数周 A/B 流量，不可能每个候选都走全程。Netflix 用一个便宜问题把漏斗口收窄：&lt;strong&gt;仅凭内容向量，能不能预测一个朴素无个性化策略下哪张素材会赢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;做法：固定一批标题，用探索数据按倾向分去偏后找出每个标题的人气赢家，打成二值标签；然后只用一个线性探针从素材向量恢复这个标签（标准二值交叉熵），不带标题、演员等任何元数据。保持线性和纯向量是刻意的——它隔离出「人气驱动因子有多少真的编码进了向量」。向量若抓住了人气的语义驱动，线性分类器就该能认出赢家；抓不住，探针就和随机猜测一样，而后者正是它的及格线。&lt;/p&gt;
&lt;p&gt;这个探针先把一大批候选剪到两个决赛选手（SeqCLIP 和最强 MediaFM 变体），再送全程评估。三条信号——探针准确率、离线 IPS、线上 A/B——对两个选手的排序完全一致，所以现在每个 MediaFM 新版本发布前都得先过探针这一关。&lt;/p&gt;
&lt;h2 id=&#34;八embedding-store让一切成立的地基&#34;&gt;八、Embedding Store：让一切成立的地基&lt;/h2&gt;
&lt;p&gt;所有向量——CLIP、SeqCLIP、MediaFM——都活在 Netflix Embedding Store 里（Netflix AI Platform 的组件）。地基模型把原始素材编码成向量只做一次，Store 用同一接口喂给所有下游；关键性质是&lt;strong&gt;训练时和在线推理时服务的是完全相同的向量&lt;/strong&gt;，模型学到的和线上见到的之间没有 skew。&lt;/p&gt;
&lt;p&gt;另一个性质是解耦：新向量（或老向量的新版本）注册、回填、验证全部独立进行，不碰任何消费方的训练和推理代码，下游靠配置即可启用。CLIP 进海报模型、同一批向量撑起查询感知排序、MediaFM 滚进视频预览模型，各自是独立变更而不是跨团队迁移。下一步目标是把 Store 推向图文视频统一的语义空间，做跨模态检索和跨界面统一排序。&lt;/p&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;冷启动的最优解常常是换表示，不是加探索。&lt;/strong&gt; 探索流量和人气回退都是在「ID 无知」的框架里打补丁；一旦实体有了内容向量，偏好沿向量空间迁移，问题在表示层就消掉了。做实体冷启动（新商品、新文档、新用户）时先问：这个实体有没有一个下游模型读得懂的内容表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「特征没用」往往是因为别处先堵着。&lt;/strong&gt; V1 的失败不是图片向量没信息，是稀疏画布样本不够教模型用；V2 的失败不是合并不对，是 ID 信号对不上新素材。多因素实验里单变量不显著，先怀疑第二个阻塞因子，别急着枪毙特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估体系的纯度比模型重要。&lt;/strong&gt; IPS 能跟踪线上结果，靠的是倾向分在服务时刻由构造已知——日志里事后估出来的倾向分自带一层建模误差。值得为评估专门留一股随机探索流量，这笔钱花在刀刃上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用廉价代理任务给昂贵实验把关。&lt;/strong&gt; 线性探针 + 随机基线，成本几乎为零，却和端到端 A/B 给出了一致排序。任何「候选方案多、全量验证贵」的场景（选向量、选特征、选模型结构）都该先架一道这样的探针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量服务要当成基础设施来建。&lt;/strong&gt; 训练推理同源、版本注册回填独立、下游配置即用——这三条让「换向量」从跨团队项目降级为一次配置变更。Embedding Store 这类组件才是多模态落地真正的护城河，模型本身反而不是。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：素材个性化的冷启动——老模型把素材当不透明 ID，新素材只能靠探索流量和人气启发式硬扛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;海报&lt;/strong&gt;：CLIP 768 维向量拼接 ID 向量，偏好沿向量空间即时迁移；五个画布模型合并为一个，混训按交互类型的长期奖励分加权，画布按长期价值而非曝光量贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评估&lt;/strong&gt;：专门探索流量 + IPS，倾向分服务时刻已知，离线数字才跟得上线上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消融&lt;/strong&gt;：V1（仅向量）、V2（仅合并）线上均不显著，V3（两者兼有）显著胜出，short-panel 提升 5.691% 且超过 V1+V2 之和；电视首页改版实战中 holdback A/B 证实收益更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索&lt;/strong&gt;：query 文本向量与素材图像向量的余弦相似度，按 α 与个性化分数混合，零额外建模。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频&lt;/strong&gt;：MediaFM（8000 万镜头，视觉+音频+文本三模态）＞ SeqCLIP ＞ 纯 ID，TV 端收益最大，已全平台默认上线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法论&lt;/strong&gt;：线性探针代理任务廉价筛选向量，三条信号排序一致后成为 MediaFM 发版门禁；Embedding Store 保证训练推理同源、解耦上下游。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/maps-netflixs-multimodal-asset-personalization-at-scale-32f96320785e&#34;&gt;MAPS: Netflix&amp;rsquo;s Multimodal Asset Personalization at Scale&lt;/a&gt;（原文，Netflix TechBlog）。&lt;/li&gt;
&lt;li&gt;Radford et al., &amp;ldquo;Learning Transferable Visual Models From Natural Language Supervision&amp;rdquo;（CLIP 原始论文，文中海报向量与图文共空间的来源）。&lt;/li&gt;
&lt;li&gt;Netflix TechBlog 关于长期奖励建模（long-term reward modeling）与 Netflix AI Platform / Embedding Store 的相关文章（原文内引用，用于奖励加权与向量基础设施）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生</title>
      <link>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</link>
      <pubDate>Thu, 30 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genrec-llm-native-recommendation/</guid>
      <description>&lt;h1 id=&#34;genrec-笔记netflix-怎么把推荐排序器做成-llm-原生&#34;&gt;GenRec 笔记：Netflix 怎么把推荐排序器做成 LLM 原生&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;
目标读者：做推荐、搜索、排序的后端工程师，以及关心「LLM 怎么落地进成熟业务系统」的 AI 工程师。
阅读目标：搞清楚 GenRec 的两阶段训练框架、目录感知打分头、prefill-only 推理这三个核心机制，以及它用远少于生产系统的标注数据追平甚至反超的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题推荐栈的复杂度税&#34;&gt;一、问题：推荐栈的复杂度税&lt;/h2&gt;
&lt;p&gt;Netflix 现有生产排序器是多年演化的结果：数千个手工特征、专门的序列建模结构、特征交互结构、多任务目标，支撑电影、剧集、游戏、直播、播客等多种内容类型和多个产品界面。它能跑，但** onboarding 一个新场景（新内容类型、新界面）的成本极高**——特征工程、架构改动、基础设施、实验，一样都少不了。这是典型的复杂度税。&lt;/p&gt;
&lt;p&gt;与此同时，LLM 圈已经在推荐方向有不少探索（原文点名 PLUM、GLIDE、OneRec-Think）。思路很诱人：把用户历史和物品元数据直接表示成文本，靠 LLM 的世界知识和语言理解在共享语义空间里抓关系，甚至用自然语言 prompt 去「引导」推荐。但&lt;strong&gt;现成 LLM 直接上岗是不合格的推荐器&lt;/strong&gt;：偏向全局热门、幻觉出目录外的物品、无视业务约束、个性化弱。GenRec 要回答的问题就是：把 LLM 正经后训练成排序器，能不能打过一套调了多年的生产系统？&lt;/p&gt;
&lt;p&gt;任务设定本身很标准：全目录排序（有候选集时是 top-K）。给定用户、交互历史、当前上下文（设备、界面、地区、时间），输出一个个性化排序，优化目标是&lt;strong&gt;长期会员效用&lt;/strong&gt;（满意度和留存的代理），不是短期点击。&lt;/p&gt;
&lt;h2 id=&#34;二两阶段先适配再排序&#34;&gt;二、两阶段：先适配，再排序&lt;/h2&gt;
&lt;p&gt;GenRec 的训练分两阶段，分工明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1 —— Netflix 适配的基座 LLM&lt;/strong&gt;：从开源 LLM 出发，在 Netflix 专有语料上继续训练，让它学会内容理解、会员行为与偏好模式，同时保住通用语言能力。更新频率低，作为多个应用共享的「Netflix-aware」基座。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2 —— GenRec 本体&lt;/strong&gt;：在 Phase-1 基座上用排序数据和排序目标做后训练，专注排序质量与可控性，用奖励加权损失融入多种奖励信号；更新频率高，跟踪新内容和口味漂移；并且从设计上就在推理成本约束下优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    OSS[&amp;#34;开源 LLM&amp;#34;] --&amp;gt;|&amp;#34;Netflix 专有语料继续训练&amp;lt;br/&amp;gt;低频更新&amp;#34;| P1[&amp;#34;Phase 1&amp;lt;br/&amp;gt;Netflix 适配基座 LLM&amp;lt;br/&amp;gt;多应用共享&amp;#34;]
    LOG[&amp;#34;会员交互日志&amp;lt;br/&amp;gt;播放 时长 评分 弃看 等&amp;#34;] --&amp;gt;|&amp;#34;文本化为对话&amp;lt;br/&amp;gt;user 消息 上下文与历史&amp;lt;br/&amp;gt;assistant 消息 真实行为&amp;#34;| DATA[&amp;#34;Phase 2 训练数据&amp;#34;]
    P1 --&amp;gt; DATA
    DATA --&amp;gt;|&amp;#34;排序目标 + 语言建模目标&amp;lt;br/&amp;gt;+ 奖励加权损失&amp;#34;| P2[&amp;#34;Phase 2&amp;lt;br/&amp;gt;GenRec 排序模型&amp;lt;br/&amp;gt;高频更新 成本约束&amp;#34;]
    RM[&amp;#34;独立奖励模型&amp;lt;br/&amp;gt;长期满意度代理&amp;lt;br/&amp;gt;行为再平衡&amp;#34;] -.-&amp;gt;|&amp;#34;给每条样本一个标量权重&amp;#34;| P2
    style P1 fill:#fff3cd
    style P2 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个两阶段划分是全文我觉得最值得抄的部分：&lt;strong&gt;基座慢迭代、排序快迭代&lt;/strong&gt;。基座沉淀通用能力，排序层追内容新鲜度——后面消融实验会证明，Phase-1 越陈旧，Phase-2 的相对收益越大。&lt;/p&gt;
&lt;h2 id=&#34;三训练数据即对话上下文即特征&#34;&gt;三、训练数据即对话，上下文即特征&lt;/h2&gt;
&lt;p&gt;Netflix 会员每天产生千亿级交互事件。GenRec 把日志转成单轮或多轮「对话」：user 消息是文本化的上下文、画像、历史、物品元数据和任务（比如「预测用户接下来会播放或点赞什么」），assistant 消息是会员的真实行为（播了什么、播了多久、给了什么反馈）。Phase-2 训练时，模型学的是 assistant 消息对 user 消息的条件依赖。&lt;/p&gt;
&lt;p&gt;注意一个容易误读的点：&lt;strong&gt;对话格式只为训练服务&lt;/strong&gt;。推理时不做自回归生成——把文本化的上下文喂进去，直接用打分头给候选物品算分。对话形式存在的原因是配合语言建模目标、保住模型对文本的理解能力。&lt;/p&gt;
&lt;p&gt;文本化的瓶颈是 token 预算。把用户历史里每个事件都摊开写，成本在 Netflix 规模下不可接受。于是上下文窗口成了新的「特征预算」，GenRec 的做法是给上下文做工程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完整保留&lt;/strong&gt;：高信号事件（长播放、点赞），细节给足；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接丢弃&lt;/strong&gt;：低信号事件（极短播放、快速划过）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;摘要压缩&lt;/strong&gt;：重复性行为（连续刷剧这类 binge 行为）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择性展开&lt;/strong&gt;：重要或冷启动物品（比如新上线内容）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;固定预算内优先近期的、高信号的历史，老的压缩或丢掉。prompt 结构上还要最大化共享前缀，吃满 prefix caching。&lt;strong&gt;「prompt 就是新的特征向量」&lt;/strong&gt;——这句话是全文的眼。&lt;/p&gt;
&lt;h2 id=&#34;四三个目标排序语言奖励&#34;&gt;四、三个目标：排序、语言、奖励&lt;/h2&gt;
&lt;p&gt;GenRec 的损失是三件事的组合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目录感知排序目标&lt;/strong&gt;：用高价值行为（足够长的播放、强显式反馈）做正例，带阈值和去噪逻辑；在目录或候选集上做交叉熵，让正例得分更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语言建模目标&lt;/strong&gt;：保留 LM 损失，防止模型在文本化输入上丢掉语言理解能力，也给「推荐理由解释」这类生成场景留门。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权对齐&lt;/strong&gt;：只在原始行为序列上训练，模型会学出坏习惯——过度偏向刷剧、只推单一内容类型。解法是独立奖励模型给每条训练样本算一个标量权重：长期满意度代理（这次短期行为对回访、目录探索、持续活跃有多大贡献）加行为再平衡（跨内容类型、跨上线阶段调节）。高价值样本加权，低价值样本降权。原文特别说明：这比完整 RL 简单省钱，实践中对齐效果够用；GRPO 这类 RL 方法有额外收益但成本高，留给未来工作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;五架构与-serving打分头--prefill-only&#34;&gt;五、架构与 serving：打分头 + prefill-only&lt;/h2&gt;
&lt;p&gt;架构贴着基座走：decoder-only Transformer，加一个&lt;strong&gt;目录感知打分头&lt;/strong&gt;，只对 Netflix 在库物品打分。推理管线三步：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    V[&amp;#34;Verbalizer&amp;lt;br/&amp;gt;历史 上下文 物品元数据&amp;lt;br/&amp;gt;序列化成单条文本&amp;#34;] --&amp;gt; LLM[&amp;#34;LLM 前向一次&amp;lt;br/&amp;gt;取池化隐状态 h&amp;lt;br/&amp;gt;概括当前偏好与上下文&amp;#34;]
    LLM --&amp;gt; HEAD[&amp;#34;打分头&amp;lt;br/&amp;gt;h 与物品嵌入做点积或小 MLP&amp;lt;br/&amp;gt;softmax 得到分布&amp;#34;]
    CAT[&amp;#34;目录物品嵌入&amp;lt;br/&amp;gt;与主干联合训练&amp;#34;] --&amp;gt; HEAD
    HEAD --&amp;gt; RANK[&amp;#34;排序输出&amp;lt;br/&amp;gt;天然不会幻觉出目录外物品&amp;#34;]
    style HEAD fill:#fff3cd
    style RANK fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;打分头这步设计得很实在：物品嵌入、打分头、主干&lt;strong&gt;联合训练&lt;/strong&gt;；目录极大时用 sampled softmax 或候选集。由于分数只算在目录物品上，「幻觉出目录外内容」这个 LLM 推荐的老毛病在结构上就被掐死了——不需要后处理过滤。&lt;/p&gt;
&lt;p&gt;Serving 在 Netflix 内部 LLM 栈上跑 vLLM。成本由三个因素驱动：模型大小、上下文长度、推理模式。对应三个手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;小模型/蒸馏模型&lt;/strong&gt;：用更小或蒸馏过的基座，配更大更针对性的数据，用更低的成本换大部分质量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激进压缩上下文&lt;/strong&gt;：就是第三节那套上下文工程；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prefill-only 推理&lt;/strong&gt;：不对候选集做自回归解码（那在成本上是自杀），prompt 只消费一次，单次前向给整个候选集打完分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第三条是整套方案能上线的关键。一个排序器如果按 LLM 聊天那种方式逐 token 生成推荐结果，账永远算不过来；把 LLM 当成「一次性编码器 + 打分头」，账就算过来了。&lt;/p&gt;
&lt;h2 id=&#34;六实验结果少数据反而赢&#34;&gt;六、实验结果：少数据，反而赢&lt;/h2&gt;
&lt;p&gt;对照组是调了多年的生产排序器（数千个特征、定制架构）。结果分几层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线&lt;/strong&gt;：用大约 40 分之一的 Phase-2 标注样本，MRR 反而高约 1.6%。继续加数据、加输入信号，指标继续涨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线上 A/B&lt;/strong&gt;：批量计算的推荐界面，约 10% 流量跑约 4 周，短期和长期指标均统计显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据与模型 scaling&lt;/strong&gt;：1B 和 10B 主干都随 Phase-2 数据量上涨，大模型绝对值更高、曲线形状相似；固定训练预算下，更大主干稳定更好——推荐也吃 scaling law。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-1 vs 开源&lt;/strong&gt;：用 Netflix 适配基座比直接用现成开源 LLM，离线指标高约 10%-20%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase-2 vs Phase-1&lt;/strong&gt;：在 Phase-1 刚训完的时间点，Phase-2 后训练带来 35%-50% 提升；Phase-1 越陈旧（新内容、口味漂移），Phase-2 相对收益越大，2 周后约 80%。这直接论证了两阶段划分的正确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据效率&lt;/strong&gt;：从强 Phase-1 出发，GenRec 用 10-40 倍更少的 Phase-2 标注样本追平或超过生产排序器——这点值钱在 Phase-2 更新频率高，标注效率直接决定迭代速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文压缩&lt;/strong&gt;：清洗事件、找 MRR 对事件数的「肘点」、调措辞详略，三步下来 token 预算可以压到原来的三分之一左右，离线指标几乎不掉；serving 成本约与上下文长度成正比，所以成本也同比降。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七更大的判断从特征工程到上下文工程&#34;&gt;七、更大的判断：从特征工程到上下文工程&lt;/h2&gt;
&lt;p&gt;原文最后一节不是在总结 GenRec，是在押注一个范式转移，四条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;特征工程 → 上下文工程&lt;/strong&gt;：建模范畴从「设计特征」变成「决定哪些信号进上下文、回溯多久、怎么在 token 预算内压缩」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定制架构 → 共享基座&lt;/strong&gt;：双塔、DLRM、定制 attention 各立山头的时代，让位给「一个基座 + 数据与文本化策略 + 后训练目标 + 推理优化」的差异化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling law 成为设计指南&lt;/strong&gt;：传统 RecSys 因为稀疏 ID、重工程、任务定制架构容易撞墙；LLM 化的推荐继承了清晰的数据和模型 scaling 行为，投入产出变得可规划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RecSys 基建 → LLM 基建&lt;/strong&gt;：GPU、vLLM/Triton、batching、caching——推荐 serving 越来越像通用 LLM serving。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;「prefill-only」是给 LLM 落地祛魅的一刀。&lt;/strong&gt; 很多团队评估「LLM 进生产」时默认按生成式推理算账，结论当然是不划算。GenRec 的示范是：先问这个任务是不是真的需要逐 token 生成。排序、分类、匹配这类任务，LLM 的正确用法是当编码器，一次前向出全部结果——成本模型完全不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录感知打分头是「防幻觉」的标准答案。&lt;/strong&gt; 不要在生成端防幻觉再靠后处理拦截，把输出空间在结构上约束到合法集合（打分头、受限解码、枚举候选），比训一个「不幻觉的模型」可靠得多。这个模式放之四海皆准：凡是输出必须在闭集内的场景，都该这么干。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奖励加权是 RL 的穷人版，但可能够你用很久。&lt;/strong&gt; 完整 RLHF/GRPO 的成本和组织复杂度劝退大多数团队；「独立奖励模型给样本算权重、乘进损失」用监督训练的工程成本拿到对齐收益的大部分。先把这条路走通，再谈要不要上 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文工程是可以量化的工程，不是 prompt 玄学。&lt;/strong&gt; GenRec 把它做成了有肘点曲线、有详略实验、有三分之一预算硬指标的东西。做 RAG 的人应该直接抄这个流程：清洗事件 → 画「指标 vs 上下文量」曲线找肘点 → 在保留集合上调措辞密度。上下文不是越多越好，是有预算的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「prompt 是新的特征向量」意味着你的特征基建要换。&lt;/strong&gt; 特征平台管的是 Schema、版本、回填；上下文工程管的是文本化模板、压缩策略、共享前缀。如果你的推荐/排序团队还在纯特征工程范式里，GenRec 给出的信号是：下一个五年的技能树在后者。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GenRec 是什么&lt;/strong&gt;：Netflix 的 LLM 排序器。文本化用户历史与上下文，在 Netflix 适配基座上做排序后训练，挂目录感知打分头，奖励加权对齐长期价值，prefill-only 推理控成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：两阶段（慢基座 + 快排序）、对话格式训练但非生成式推理、打分头把输出约束在目录内、奖励加权替代完整 RL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键数字&lt;/strong&gt;：40 倍更少标注样本下 MRR +1.6%；Phase-1 基座 +10%-20%；Phase-2 +35%-50%（基座陈旧 2 周后 +80%）；上下文压到三分之一指标几乎不掉；10% 流量 4 周 A/B 短期长期均显著为正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大的启示&lt;/strong&gt;：推荐的工程重心从特征工程转向上下文工程，「prompt 就是新的特征向量」；LLM 进排序系统的正确姿势是编码器 + 打分头，不是聊天生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保留意见&lt;/strong&gt;：原文没给具体基座模型、参数量细节和线上指标绝对值，成本数字也只有相对比较；「match or exceed 生产系统」的结论目前只在批量计算界面上验证过，实时低延迟场景没提。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：Ying Li, Arjun Rao, Shradha Sehgal, &lt;a href=&#34;https://netflixtechblog.com/genrec-towards-llm-native-recommendation-at-netflix-f20be6f643e3&#34;&gt;GenRec: Towards LLM-Native Recommendation at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-07-30。&lt;/li&gt;
&lt;li&gt;原文点名的相关 LLM 推荐工作：PLUM、GLIDE、OneRec-Think。&lt;/li&gt;
&lt;li&gt;推理框架：&lt;a href=&#34;https://github.com/vllm-project/vllm&#34;&gt;vLLM&lt;/a&gt;（GenRec serving 所依赖）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 方法：&lt;a href=&#34;https://arxiv.org/abs/2402.03300&#34;&gt;GRPO&lt;/a&gt;（Group Relative Policy Optimization，DeepSeekMath 论文提出）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页</title>
      <link>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</link>
      <pubDate>Mon, 29 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-genpage-generative-homepage/</guid>
      <description>&lt;h1 id=&#34;genpage-学习笔记netflix-用一个-transformer-直接生成整个首页&#34;&gt;GenPage 学习笔记：Netflix 用一个 Transformer 直接生成整个首页&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;
目标读者：做推荐、搜推广或 AI 工程的工程师；也适合想看「LLM 范式怎么落进重工业级在线系统」的人。
阅读目标：搞清楚两件事——GenPage 怎么把「首页构建」改写成「序列生成」问题，以及为了让这个改写扛住生产环境，他们必须解决哪几个工程难题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题的改写从多级流水线到一次生成&#34;&gt;一、问题的改写：从多级流水线到一次生成&lt;/h2&gt;
&lt;p&gt;Netflix 首页不是一个列表，是一个二维结构：哪些行出现、每行里放哪些实体、整体怎么排版，几乎全都个性化。传统做法是多级流水线——行级和实体级各自有召回与排序组件，模型多、阶段之间的优化目标互相对不齐，特征工程沉重。&lt;/p&gt;
&lt;p&gt;GenPage 的改写只有一句话：&lt;strong&gt;把用户历史和请求上下文当作 prompt，自回归生成整个首页作为 response。&lt;/strong&gt; 要回答的问题从「给这个候选打多少分」变成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;已知关于这个用户和这次请求的一切，应该生成什么样的首页来最大化用户满意度？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和 TIGER、HSTU、OneRec 这些只生成扁平排序列表的生成式推荐不同，GenPage 把行、实体、布局一起生成。动机四条：端到端建模（一个 transformer 替代多级堆栈，砍掉阶段间目标错位和大量特征工程）；整页级 RL 优化（捕捉行与实体之间的交互，比如顶部的 Continue Watching 满足即时意图但会压低整页浏览深度）；更好的 scaling 行为；以及对新产品形态（直播、游戏、播客、个性化 UI、逐实体艺术图）的扩展性。&lt;/p&gt;
&lt;p&gt;先说成绩再放细节：线上 A/B 对一个成熟、高度优化的多级生产系统，核心用户参与指标统计显著提升（p ＜ 0.001），同时端到端 serving 延迟降了 20%。&lt;/p&gt;
&lt;h2 id=&#34;二数据自建-tokenizer把首页变成一门语言&#34;&gt;二、数据：自建 tokenizer，把首页变成一门语言&lt;/h2&gt;
&lt;p&gt;从传统推荐迁到生成式 transformer，第一件事是重新想数据表示。GenPage 把上下文和首页都编码成一条离散 token 序列。每个训练样本对应一次首页曝光，由三部分组成：Context（用户行为历史、档案属性、请求上下文）、Page（页面上展示的行和实体，按布局顺序）、Feedback（用户与页面的交互）。只有前两者进入 token 化，Feedback 交给奖励系统产生监督信号。&lt;/p&gt;
&lt;p&gt;他们没有用现成的文本 tokenizer，而是为首页数据建了领域专用 tokenizer，换来两个关键优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算效率&lt;/strong&gt;。举例：「用户 30 天前看了 50 分钟《女子监狱》」这件事，GPT-5 tokenizer 要 16 个 token，他们的方案压到 4 个——&lt;code&gt;[Entity_ID]&lt;/code&gt;、&lt;code&gt;[Action_Type]&lt;/code&gt;、&lt;code&gt;[Action_Time_Bucket]&lt;/code&gt;、&lt;code&gt;[Action_Duration_Bucket]&lt;/code&gt;。序列变短直接降低推理成本和延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产品控制&lt;/strong&gt;。token 和产品概念（行、实体）一一对应，给业务规则的执行留了一个干净的接口（第五节会看到这条多值钱）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上下文 token 里，用户历史表示为动作序列（显式信号如播放、加片单、点赞，隐式信号如预告片观看、详情页访问），档案 token 编码语言与 profile 类型，请求上下文 token 编码时段、星期几、设备。太长的数据源（比如完整曝光历史）用摘要版本——作者坦白这是手工痕迹，本质是把 prompt engineering 塞回了流水线，端到端学压缩是未来方向。各段之间插特殊 token 作分隔，连续信号（时间戳、时长）分桶离散化。&lt;/p&gt;
&lt;p&gt;页面 token 侧：每个实体、每个行各是一个 token，首页按从左到右、从上到下序列化。词表每日更新纳新；服务时仍不在词表里的实体，靠语义 embedding 融合和 fallback token 处理（见第五节）。分页推荐也自然落在范式里：每次翻页请求前，把已生成行的 page token 连同用户在这些行上的最新实时行为追加进 prompt，模型同时看到长期偏好和会话内行为。&lt;/p&gt;
&lt;h2 id=&#34;三奖励系统一个标量两种用法&#34;&gt;三、奖励系统：一个标量，两种用法&lt;/h2&gt;
&lt;p&gt;监督信号来自 Netflix 内部的奖励系统（既有工作，通过线上 A/B 调参对齐长期满意度）：给页面上每个曝光实体打一个标量奖励——通宵刷完的剧比看了 10 分钟的电影奖励高，被曝光后放弃的实体拿负奖励。&lt;strong&gt;页面级奖励定义为页面上所有曝光实体奖励之和。&lt;/strong&gt; 这个标量同时喂给监督后训练和 RL，但用法不同，见下。&lt;/p&gt;
&lt;h2 id=&#34;四模型与训练llm-配方照搬到推荐&#34;&gt;四、模型与训练：LLM 配方照搬到推荐&lt;/h2&gt;
&lt;p&gt;架构是标准的 decoder-only transformer，理由是简单、灵活、能吃整个 transformer 训练与 serving 生态。有一个值得记的细节：&lt;strong&gt;输入 embedding 和输出投影不共享权重（untie）&lt;/strong&gt;——预训练优化的是词表上的 softmax，WBC 后训练优化的是逐 token 的 sigmoid 分数，两种目标对 logits 的要求不同，解绑给模型留出兼顾的余地。&lt;/p&gt;
&lt;p&gt;训练配方照搬 LLM：先预训练教模型「首页这门语言」，再后训练对齐用户满意度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预训练（next-token prediction）&lt;/strong&gt;：只用线上拿到正反馈的曝光样本，让模型 bootstrap 出模仿现有生产系统的能力。作者特意澄清：样本形态其实更像 LLM 的 SFT 对而不是裸文本预训练，叫它预训练是因为从零训练而非微调。局限也直说：预训练只教模仿，不优化奖励幅度；而且模型上线后反复在自己生成的页面上训练，有退化风险——所以要后训练。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 WBC（weighted binary classification）&lt;/strong&gt;：把生成变成 token 级价值预测。自定义 tokenization 在这里再次兑现——每个页面 token 直接对应一个实体或行，奖励可以干净地挂上去：奖励的符号给二分类标签（正参与 vs 放弃），幅度给权重（通宵刷剧权重高于短播放），对对应 token 的 logit 优化加权二分类交叉熵。此时 logit 可解释为「在这个位置生成这个 token 的价值估计」。推理时贪心逐 token 取最高价值。优点是构造上自带 token 级 credit assignment，不用像 RL 那样去推断每个决策对最终页面奖励的贡献。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后训练之 RL&lt;/strong&gt;：把页面生成当序列决策，直接优化页面级奖励。借鉴 RLHF 两步走：先训一个 transformer 奖励模型，预测生成页面的页面级奖励（以实体奖励之和为监督目标；注意它和奖励系统是两回事——奖励系统处理真实曝光的反馈，奖励模型不用真把页面给用户看，这让 RL 能对任意候选页面打分）；再用 Dr. GRPO 优化策略，并用 KL 惩罚把策略锚在预训练 checkpoint 附近——奖励模型只在生产策略生成的页面附近可靠，KL 约束既把页面留在奖励模型的覆盖区内，也压制 reward hacking。另加规则化的 format rewards：页面得长得像行的列表，业务关键的行和实体不能排太靠下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RL 相对 WBC 多开三扇门：整页优化（多样性、stopping power、页面级业务约束都能进奖励）；test-time reasoning（推理输出可看作自动化特征工程）；多 token 实体支持——若实体要用多个 token 表示（如 &lt;code&gt;[Show_ID]&lt;/code&gt; 加 &lt;code&gt;[Episode_#]&lt;/code&gt;），WBC 的逐 token 标注会有歧义（一个实体级奖励怎么摊到多个 token 上），RL 优化序列级 return 天然绕开这个问题。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph DATA[&amp;#34;训练样本: 一次首页曝光&amp;#34;]
        CTX[&amp;#34;上下文 token&amp;lt;br/&amp;gt;用户历史 档案 请求上下文&amp;#34;]
        PAGE[&amp;#34;页面 token&amp;lt;br/&amp;gt;行与实体 按布局顺序序列化&amp;#34;]
        FB[&amp;#34;用户反馈&amp;lt;br/&amp;gt;播放 点赞 放弃&amp;#34;]
    end
    CTX --&amp;gt; PRE[&amp;#34;预训练&amp;lt;br/&amp;gt;next-token prediction&amp;lt;br/&amp;gt;只用正反馈样本&amp;#34;]
    PAGE --&amp;gt; PRE
    FB --&amp;gt; RS[&amp;#34;奖励系统&amp;lt;br/&amp;gt;每个曝光实体一个标量奖励&amp;#34;]
    PRE --&amp;gt; WBC[&amp;#34;后训练 WBC&amp;lt;br/&amp;gt;符号作标签 幅度作权重&amp;lt;br/&amp;gt;加权二分类交叉熵&amp;#34;]
    RS --&amp;gt; WBC
    RS --&amp;gt; RM[&amp;#34;奖励模型&amp;lt;br/&amp;gt;预测生成页面的页面级奖励&amp;#34;]
    RM --&amp;gt; RL[&amp;#34;后训练 RL&amp;lt;br/&amp;gt;Dr. GRPO 加 format rewards&amp;#34;]
    PRE --&amp;gt; RL
    RL -.-&amp;gt;|&amp;#34;KL 惩罚锚定预训练 checkpoint&amp;lt;br/&amp;gt;防 reward hacking&amp;#34;| PRE
    style PRE fill:#fff3cd
    style RL fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;五上线要解决的四个工程问题&#34;&gt;五、上线要解决的四个工程问题&lt;/h2&gt;
&lt;p&gt;范式讲得再漂亮，进不了生产都是纸上谈兵。原文列的四个难题，恰好是每个想把生成式推荐搬上线的团队都会撞上的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;实体冷启动&lt;/strong&gt;。新内容没有交互数据，token embedding 学不出来。两个互补策略：context injection——把新实体或时效实体（如 Live Now 赛事）的元数据直接注入上下文 token；semantic embedding fusion——实体表示改为 ID embedding 与内容 embedding（来自简介、演职员、字幕、类型、视频内容）的融合，训练时以小概率把实体 ID token 随机换成通用 fallback token，逼模型学会只靠内容 embedding 做推荐。效果：新实体的内容元数据一到，它在潜在空间里立刻就有有意义的表示，不等交互数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型新鲜度&lt;/strong&gt;。每天从头重训大 transformer 不现实，但推荐模型必须跟上趋势和新片入库。方案是 multi-cadence incremental training：大周期在全量历史窗口上做完整预训练加后训练；两次大周期之间，每天从昨天的 checkpoint 继续后训练，数据用最新一天加历史采样的混合，防过拟合和灾难性遗忘。每日新进词表的 token 用同类型 fallback token 初始化（如新行用 &lt;code&gt;[Row_Fallback_Token]&lt;/code&gt;），训练时随机把少量已知 token 替换成 fallback，教模型优雅处理未知 token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务规则&lt;/strong&gt;。首页必须满足结构约束（行的列表）和产品逻辑（去重、行钉位置、类目一致性——喜剧行里必须都是喜剧）。训练信号只能鼓励服从，不能保证。所以规则在推理时强制执行：constrained decoding，每个自回归步先算出合规 token 的掩码打到 logits 上。自定义 tokenization 让这件事格外便宜——实体和行都是单 token，业务规则直接映射成 token 级掩码，没有文本词表上多 token 记账的麻烦。钉行就是把那个位置其他 token 全 mask 掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推理延迟&lt;/strong&gt;。逐 token 生成整个页面太贵。利用首页结构做 hybrid row decoding：每行只有前几个实体（用户注意力最集中、决定行质感的位置）自回归生成，然后基于这个前缀做一次前向，拿到所有合格实体的 logits，按分数补齐该行其余位置，同样受业务规则约束。自回归条件性保留在最值钱的地方，长行的逐 token 解码成本省掉了。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    REQ[&amp;#34;请求&amp;#34;] --&amp;gt; CTX2[&amp;#34;构造上下文 token&amp;lt;br/&amp;gt;含会话内最新行为&amp;#34;]
    CTX2 --&amp;gt; GEN[&amp;#34;自回归生成&amp;lt;br/&amp;gt;每行只生成前几个实体&amp;#34;]
    GEN --&amp;gt; MASK[&amp;#34;约束解码&amp;lt;br/&amp;gt;业务规则映射为 token 掩码&amp;#34;]
    MASK --&amp;gt; GEN
    GEN --&amp;gt; FILL[&amp;#34;整行补齐&amp;lt;br/&amp;gt;一次前向取全部合格实体&amp;lt;br/&amp;gt;按分数补满该行&amp;#34;]
    FILL --&amp;gt; OUT[&amp;#34;输出首页若干行&amp;#34;]
    OUT -.-&amp;gt;|&amp;#34;翻页时把已生成行和最新行为&amp;lt;br/&amp;gt;追加回 prompt&amp;#34;| CTX2
    style MASK fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;六离线实验真正起作用的是什么&#34;&gt;六、离线实验：真正起作用的是什么&lt;/h2&gt;
&lt;p&gt;消融在 Netflix 内部数据上做，模型约 200M 参数，只报各研究内部的相对比较。四个问题，四个答案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预训练有用吗？&lt;/strong&gt; 有。WBC 前加不加预训练对比，全指标显著提升。Entity AUC 从 0.91 到 0.92 看着小，翻译过来是随机正负实体对的错排率从 9% 降到 8%——成熟生产系统上单点改动罕见的幅度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型规模怎么 scale？&lt;/strong&gt; 从约 120M 扫到约 900M 参数，预训练 loss 和 WBC loss 都呈幂律下降，和 LLM 的 scaling 行为同构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文信息怎么 scale？&lt;/strong&gt; 固定模型规模，持续丰富 prompt（加新数据源加改进 tokenization），WBC loss 大幅下降。两组扫描不严格可比，但差距悬殊：&lt;strong&gt;模型扩约 7.5 倍只降 WBC loss 约 1.3%，上下文富化累计降约 6.9%&lt;/strong&gt;，好几次单次设计良好的上下文加入就超过整轮扩容。结论：当前阶段个性化质量的瓶颈首先是模型能看到的信息和表示，其次才是容量；等上下文榨干了，容量才会成为主驱动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RL 真在做页面级优化吗？&lt;/strong&gt; 页面奖励持续提升（作者承认这条基本是 confirmatory——奖励和策略优化用的是同一个模型）。更有意思的副产品：&lt;strong&gt;多样性根本不在 RL 目标里，但页面多样性（页内实体两两 embedding 距离）随训练上升&lt;/strong&gt;——策略在整页层面优化，而不是孤立地抠每个 token。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七线上-ab-与意外发现&#34;&gt;七、线上 A/B 与意外发现&lt;/h2&gt;
&lt;p&gt;线上对生产基线的 A/B 里，GenPage 在既有候选集上解码（候选集顺便处理掉大量资格类业务规则）。几个观察都值得记：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有训练数据配置不同的变体都在核心指标上显著提升，增益对设计选择鲁棒，不依赖某个特定配置。&lt;/li&gt;
&lt;li&gt;出现了非预期的曝光类目分布漂移（新内容 vs 老内容、剧集 vs 电影）。作者的诊断很诚实：这大概是 GenPage 个性化更锐利的表现——首页 impression 效率上升，用户用更少曝光完成参与；而更锐利的个性化把从生产系统继承来的组件（比如奖励系统）尚未与新范式对齐的问题暴露了出来。&lt;/li&gt;
&lt;li&gt;会话内响应性强且自然：最新行为迅速影响后续推荐，一两天后淡回长期偏好——模型自己在 attend 动作时间戳，生产栈里要靠大量手工特征工程才能做到的事，在生成式范式里涌现出来。&lt;/li&gt;
&lt;li&gt;延迟降 20%，和「生成式一定慢」的直觉相反：多个排序阶段和重特征计算被一个跑在原始 token 上的 transformer 取代，自定义 tokenization 与 hybrid decoding 再压缩了解码步数。作者强调这还没压榨完，余量可以再投给容量或更富的 prompt。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;prompt 工程是新的特征工程，而且 ROI 可能更高。&lt;/strong&gt; 6.9% 对 1.3% 这组数字值得贴在每个推荐团队的墙上：在堆模型容量之前，先问自己信息瓶颈有没有解决——模型看不见的信号，再大的容量也学不到。这和我们日常给 LLM 应用调 prompt 的经验完全同构：多数时候收益不在换更大的模型，在把上下文喂对。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 tokenizer 本质是一次领域建模。&lt;/strong&gt; 4 token 对 16 token 的压缩只是表面收益，真正值钱的是「token 与产品概念一一对应」——它让业务规则从模型输出后的事后修补，变成解码期的一层掩码。凡是需要严格控制生成内容的场景（合规、结构约束、配额），这个思路可以直接搬：把控制点设计进词表，而不是指望 prompt 约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则兜底放在解码时，别指望训练。&lt;/strong&gt; 训练信号只能鼓励、不能保证——这句坦白适用于所有生成式系统。凡是硬约束（不能出现的内容、必须出现的位置），都应该有推理期的机制级保障，RLHF 式的软对齐管软偏好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模仿学习起步、对齐训练接棒，是离线冷启动新系统的通用配方。&lt;/strong&gt; 先用生产系统的正反馈样本 bootstrap，避开没有策略就没有数据的死锁；再承认模仿的天花板（不优化奖励幅度、自蒸馏会退化），用后训练接棒。WBC 和 RL 的二选一也很务实：先用简单可优化的对齐拿下实体级指标，把 RL 留给页面级目标，不上来就啃最硬的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「单模型替代流水线」的延迟账要重新算。&lt;/strong&gt; 直觉上生成式更慢，实际是把 N 个模型加 N 套特征计算换成一次前向序列。系统总成本从来不在单个模型的 FLOPs 上，在组件数量、数据依赖和特征管线上——架构简化的收益经常大于模型本身的开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题改写&lt;/strong&gt;：首页构建从「多级召回排序」改写为「prompt 进、页面出」的自回归序列生成，行、实体、布局一次生成；这是与 TIGER、HSTU、OneRec 等扁平列表生成式推荐的分界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据&lt;/strong&gt;：领域专用 tokenizer 把上下文和页面编码成一条 token 序列，换来 4 倍级序列压缩和「token 即产品概念」的控制接口；反馈不进序列，进奖励系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练&lt;/strong&gt;：照搬 LLM 配方——正反馈样本上 next-token 预训练，后训练二选一：WBC（token 级价值预测，自带 credit assignment）或 RL（奖励模型 + Dr. GRPO + KL 锚定，解锁整页优化、test-time reasoning、多 token 实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上线四件套&lt;/strong&gt;：冷启动靠 context injection 加 semantic embedding fusion；新鲜度靠多节奏增量训练加 fallback token；业务规则靠 constrained decoding（规则即 token 掩码）；延迟靠 hybrid row decoding（每行只自回归前几个实体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：线上核心指标显著优于成熟多级基线且延迟降 20%；离线两条最值得记——丰富 prompt 的收益远大于模型扩容（6.9% 对 1.3%），RL 在不含多样性目标时反而提升了页面多样性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未竟之事&lt;/strong&gt;：长上下文仍靠手工摘要，语言、多模态、reasoning 能力尚未接入；作者看好的方向是领域 token 与通用文本 token 的混合 tokenization——概念上等于给 LLM 增加一个「推荐模态」，LLM 和推荐系统的边界会继续模糊。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇最打动我的不是架构，是那份诚实：明说预训练样本其实更像 SFT，明说长上下文还是手工摘要，明说奖励提升是 confirmatory，明说线上出现了没优化的分布漂移。工业系统的论文能写到这个坦白程度，比指标本身更值得学。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lequn Wang, Jiangwei Pan, Linas Baltrunas, &lt;a href=&#34;https://netflixtechblog.com/genpage-towards-end-to-end-generative-homepage-construction-at-netflix-77146fba8a08&#34;&gt;GenPage: Towards End-to-End Generative Homepage Construction at Netflix&lt;/a&gt;, Netflix TechBlog, 2026-06（本文唯一原文来源）。&lt;/li&gt;
&lt;li&gt;原文提及的对照工作：TIGER、HSTU、OneRec（生成扁平排序列表的生成式推荐系统）。&lt;/li&gt;
&lt;li&gt;原文提及的 RL 算法：Dr. GRPO（GRPO 的变体，缓解训练目标中的偏差）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录</title>
      <link>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</link>
      <pubDate>Tue, 03 Mar 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</guid>
      <description>&lt;h1 id=&#34;netflix-用-jdk-vector-api-优化推荐系统一次教科书级的性能工程实录&#34;&gt;Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API&lt;/a&gt;
目标读者：写 Java 后端、关心 JVM 性能工程的工程师；也适合任何在“要不要引入原生库/SIMD”上纠结过的人。
阅读目标：看清一次完整性能优化的正确顺序——先改计算形状，再改数据布局，最后才轮到换计算内核。以及理解为什么 BLAS 这种“标准答案”在真实生产路径里会失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一背景ranker-里的-75-热点&#34;&gt;一、背景：Ranker 里的 7.5% 热点&lt;/h2&gt;
&lt;p&gt;Ranker 是 Netflix 最大、最复杂的服务之一，首页那些个性化推荐行就是它算出来的。看 CPU profile 时有一个特征始终扎眼：&lt;strong&gt;视频惊喜度打分（video serendipity scoring）&lt;/strong&gt;——回答一个简单问题：“这部新片和你最近看的东西有多不一样？”这个特征吃掉每个节点约 &lt;strong&gt;7.5% 的总 CPU&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;算法本身不复杂：候选视频和用户历史里的每个视频都表示成 embedding 向量；对每个候选，算出它与所有历史向量的余弦相似度，取最大值，用 1 减去它得到“新颖度”分数，作为下游推荐逻辑的输入特征。原始实现非常直白：对每个候选取 embedding，遍历历史逐对算余弦，维护最大值。容易理解，但在 Ranker 的规模下意味着大量串行工作、重复查找、分散的内存访问和糟糕的缓存局部性。火焰图确认了这一点：服务里最热的热点之一就是惊喜度编码器里的 Java 点积。算法层面，这是一个 &lt;strong&gt;M 个候选 × N 个历史&lt;/strong&gt;的嵌套循环，产生 O(M×N) 次独立点积。&lt;/p&gt;
&lt;p&gt;一个容易被忽略但决定了整个优化方向的数据：他们埋点统计了流量形状，发现&lt;strong&gt;约 98% 的请求是单视频请求，剩下 2% 是大批量请求&lt;/strong&gt;——但那 2% 的批次实在太大，最终处理的视频总量在单视频和批处理之间大约是 &lt;strong&gt;50:50&lt;/strong&gt;。所以批处理值得做，即使它对中位数请求毫无帮助。&lt;/p&gt;
&lt;h2 id=&#34;二第一步把嵌套循环看成矩阵乘&#34;&gt;二、第一步：把嵌套循环看成矩阵乘&lt;/h2&gt;
&lt;p&gt;第一个想法是换“计算形状”：不再想成“很多次小点积”，而是当成一次矩阵运算。设 embedding 维度为 D：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把所有候选 embedding 装进矩阵 A（形状 M×D）；&lt;/li&gt;
&lt;li&gt;把所有历史 embedding 装进矩阵 B（形状 N×D）；&lt;/li&gt;
&lt;li&gt;所有行归一化成单位向量；&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;，C 就是 M×N 的余弦相似度矩阵。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样 &lt;strong&gt;M×N 次独立点积变成一次矩阵乘&lt;/strong&gt;——而这正是 CPU 和优化过的计算内核最擅长的东西。工程上通过同时支持 &lt;code&gt;encode()&lt;/code&gt;（单视频）和 &lt;code&gt;batchEncode()&lt;/code&gt;（批量）接入现有框架，保持向后兼容。到这里看起来像“做完了”。但并没有。&lt;/p&gt;
&lt;h2 id=&#34;三第二步批处理反而慢了-5&#34;&gt;三、第二步：批处理反而慢了 5%&lt;/h2&gt;
&lt;p&gt;上了批处理版本跑金丝雀，结果出人意料：&lt;strong&gt;性能回退约 5%&lt;/strong&gt;。算法没问题——把点积归并成矩阵乘在数学上是等价的。问题出在第一版实现自己引入的开销：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个批次都新建 &lt;code&gt;double[][]&lt;/code&gt; 矩阵装候选、历史和结果。这些又大又短命的对象制造了 GC 压力；而且 &lt;code&gt;double[][]&lt;/code&gt; 在内存里本身就不连续，多一层指针跳转，缓存行为更差。&lt;/li&gt;
&lt;li&gt;第一版 Java 矩阵乘是朴素的标量实现，用不上 SIMD。换句话说，&lt;strong&gt;付了批处理的成本，却没拿到批处理该有的计算效率&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;教训来得很快：算法层面的改进，如果实现细节——内存布局、分配策略、计算内核——跟你作对，就一文不值。这一步定下了后面的路线：先把数据布局改成缓存友好的，消灭每批次分配，然后再回头解决矩阵乘内核。&lt;/p&gt;
&lt;h2 id=&#34;四第三步扁平-buffer-加-threadlocal-复用&#34;&gt;四、第三步：扁平 buffer 加 ThreadLocal 复用&lt;/h2&gt;
&lt;p&gt;重做数据布局。&lt;code&gt;double[m][n]&lt;/code&gt; 换成&lt;strong&gt;行主序的扁平 &lt;code&gt;double[]&lt;/code&gt;&lt;/strong&gt;：内存连续，访问模式可预测。然后引入 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt;，持有候选、历史等可复用 buffer——&lt;strong&gt;buffer 只增长不收缩&lt;/strong&gt;，避免每请求分配，同时每个线程持有自己的一份，无锁无争用。核心逻辑就几行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;class BufferHolder {
  double[] candidatesFlat = new double[0];
  double[] getCandidatesFlat(int required) {
    if (candidatesFlat.length &amp;lt; required) {
      candidatesFlat = new double[required];
    }
    return candidatesFlat;
  }
  // historyFlat 同理
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;光是这一步，批处理路径就变得可预测多了：分配少了，GC 压力小了，缓存局部性好了。剩下的问题回到最初的那个：矩阵乘到底用什么算？&lt;/p&gt;
&lt;h2 id=&#34;五第四步blas测试里很好生产里不行&#34;&gt;五、第四步：BLAS——测试里很好，生产里不行&lt;/h2&gt;
&lt;p&gt;显而易见的下一步是 BLAS（Basic Linear Algebra Subprograms）。隔离的微基准看起来很有希望，但一旦接进真实的批量打分路径，收益就没兑现。几个因素在作对：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认的 &lt;code&gt;netlib-java&lt;/code&gt; 路径走的是 F2J（Fortran 转 Java）的 BLAS，不是真正的原生实现；&lt;/li&gt;
&lt;li&gt;就算用原生 BLAS，也要付 setup 和 JNI 边界切换的开销；&lt;/li&gt;
&lt;li&gt;Java 的行主序和很多 BLAS 例程期望的列主序对不上，会引入转换和临时 buffer；&lt;/li&gt;
&lt;li&gt;这些额外的分配和拷贝在完整流水线里很扎眼，尤其是旁边还有 TensorFlow 的 embedding 计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BLAS 不是白试——它帮忙看清了时间到底花在哪。但它不是那种即插即用的胜利。他们需要的是：&lt;strong&gt;纯 Java、贴合扁平 buffer 架构、还能用上 SIMD&lt;/strong&gt; 的东西。&lt;/p&gt;
&lt;h2 id=&#34;六第五步jdk-vector-api&#34;&gt;六、第五步：JDK Vector API&lt;/h2&gt;
&lt;h3 id=&#34;先补一课jdk-vector-api-到底是什么&#34;&gt;先补一课：JDK Vector API 到底是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;起点是 SIMD。&lt;/strong&gt; 现代 x86 CPU 都有向量寄存器，一条指令可以同时算多个数：AVX2 的 256 位寄存器一次装 4 个 double，AVX-512 的 512 位一次装 8 个。一条向量乘加指令顶 4-8 条标量指令——这就是 SIMD（Single Instruction, Multiple Data，单指令多数据），点积、矩阵乘这类规则的数值计算是它的主场。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 的痛点是：这些指令一直在硬件里，Java 程序员却够不着。&lt;/strong&gt; JIT 有自动向量化，但只对形状简单、能被它证明安全的循环生效，代码稍微复杂一点就悄悄退回标量，而且没有任何方式强制它发生；想要确定性的 SIMD，只能走 JNI 调原生库——代价就是上一步 BLAS 踩过的那些坑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vector API 是 Project Panama 的一部分&lt;/strong&gt;，从 JDK 16 起以孵化模块 &lt;code&gt;jdk.incubator.vector&lt;/code&gt; 的形式提供，到原文写作时仍在孵化（所以运行时要显式 &lt;code&gt;--add-modules&lt;/code&gt;）。它把 SIMD 变成了普通 Java 代码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VectorSpecies&lt;/code&gt; 描述「一次算几个 lane」（lane 就是向量寄存器里的一个槽位），&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 让运行时按当前 CPU 挑最优宽度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DoubleVector.fromArray(...)&lt;/code&gt; 从数组加载一组 lane，&lt;code&gt;fma(b, acc)&lt;/code&gt; 做向量融合乘加，&lt;code&gt;reduceLanes(ADD)&lt;/code&gt; 把各 lane 归约成一个标量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一份代码的可移植性由 JIT 兜底&lt;/strong&gt;：编译时映射到宿主机器最好的指令集，AVX-512 机器跑 8 lane，AVX2 机器跑 4 lane，都不支持就退回标量。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph S[&amp;#34;标量路径 一条指令算一个数&amp;#34;]
        A1[&amp;#34;a0 乘 b0&amp;#34;] --&amp;gt; R1[&amp;#34;acc 逐个累加&amp;#34;]
        A2[&amp;#34;a1 乘 b1&amp;#34;] --&amp;gt; R1
        A3[&amp;#34;a2 乘 b2&amp;#34;] --&amp;gt; R1
        A4[&amp;#34;a3 乘 b3&amp;#34;] --&amp;gt; R1
    end
    subgraph V[&amp;#34;SIMD 路径 AVX2 一条指令算四个数&amp;#34;]
        B1[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;a0 a1 a2 a3&amp;#34;] --&amp;gt; F[&amp;#34;一条 fma 指令&amp;#34;]
        B2[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;b0 b1 b2 b3&amp;#34;] --&amp;gt; F
        F --&amp;gt; R2[&amp;#34;acc 四个 lane 同时累加&amp;#34;]
    end
    style F fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话总结：&lt;strong&gt;它把「手写 intrinsics 的活」变成了「写普通循环的活」&lt;/strong&gt;——你不用知道目标机器是 SSE 还是 AVX-512，JIT 替你选。对他们来说更关键的是：&lt;strong&gt;纯 Java，没有原生依赖，没有 JNI 切换，写起来就是普通 Java 代码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;跟他们的负载是绝配：embedding 已经搬进了扁平连续的 &lt;code&gt;double[]&lt;/code&gt; buffer，热循环被海量点积占满。最后一步就是用基于 Vector API 的纯 Java SIMD 实现换掉 BLAS——藏在一个小工厂后面，类加载时由 &lt;code&gt;MatMulFactory&lt;/code&gt; 选最优实现：&lt;code&gt;jdk.incubator.vector&lt;/code&gt; 可用就用 Vector API，否则退回一个高度优化的标量实现——循环展开的点积（同事 Patrick Strawderman 实现，灵感来自 Lucene 里的模式）。&lt;/p&gt;
&lt;p&gt;Vector API 版本的内层循环：用 &lt;code&gt;fma()&lt;/code&gt;（fused multiply-add）把 &lt;code&gt;a * b&lt;/code&gt; 累加进向量累加器；&lt;code&gt;DoubleVector.SPECIES_PREFERRED&lt;/code&gt; 让运行时自己挑适合机器的 lane 宽度（AVX2 上通常 4 个 double，AVX-512 上 8 个）。简化版：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; M; i++) {
  for (int j = 0; j &amp;lt; N; j++) {
    DoubleVector acc = DoubleVector.zero(SPECIES);
    int k = 0;
    for (; k + SPECIES.length() &amp;lt;= D; k += SPECIES.length()) {
      DoubleVector a = DoubleVector.fromArray(SPECIES, candidatesFlat, i*D + k);
      DoubleVector b = DoubleVector.fromArray(SPECIES, historyFlat, j*D + k);
      acc = a.fma(b, acc);  // 融合乘加
    }
    double dot = acc.reduceLanes(VectorOperators.ADD);
    // 处理 k..D-1 的尾部
    similaritiesFlat[i*N + j] = dot;
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;原来的一长串标量乘加，变成少量向量 &lt;code&gt;fma()&lt;/code&gt; 加一次归约——算法没变，CPU 向量单元的利用率完全不同。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph OLD[&amp;#34;原始路径 标量&amp;#34;]
        O1[&amp;#34;候选 embedding&amp;#34;] --&amp;gt; O3[&amp;#34;逐个算余弦相似度&amp;#34;]
        O2[&amp;#34;历史 embedding 逐个取&amp;#34;] --&amp;gt; O3
        O3 --&amp;gt; O4[&amp;#34;M 乘 N 次独立点积&amp;lt;br/&amp;gt;分散内存访问 缓存不友好&amp;#34;]
    end
    subgraph NEW[&amp;#34;优化后路径 SIMD&amp;#34;]
        N1[&amp;#34;候选装入扁平 buffer A&amp;lt;br/&amp;gt;M×D 行主序&amp;#34;] --&amp;gt; N3[&amp;#34;归一化为单位向量&amp;#34;]
        N2[&amp;#34;历史装入扁平 buffer B&amp;lt;br/&amp;gt;N×D 行主序&amp;#34;] --&amp;gt; N3
        N3 --&amp;gt; N4[&amp;#34;一次矩阵乘 C = A × Bᵀ&amp;lt;br/&amp;gt;Vector API fma 累加&amp;#34;]
        N4 --&amp;gt; N5[&amp;#34;每行取最大值&amp;lt;br/&amp;gt;novelty = 1 - maxSim&amp;#34;]
    end
    O4 -.-&amp;gt;|&amp;#34;重构计算形状&amp;#34;| N1
    style O4 fill:#ffcccc
    style N4 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七回退与安全孵化特性的生产姿势&#34;&gt;七、回退与安全：孵化特性的生产姿势&lt;/h2&gt;
&lt;p&gt;Vector API 还在孵化，运行时要加 &lt;code&gt;--add-modules=jdk.incubator.vector&lt;/code&gt; 才能用。他们不想让正确性和可用性绑在这个 flag 上，所以把回退行为显式设计出来：启动时探测 Vector API 支持，有就走 SIMD 批量矩阵乘，没有就走优化过的标量路径，单视频请求继续用逐条实现。运营故事很干净：服务可以选择启用 Vector API 拿满性能，不启用系统也照样安全可预测。&lt;/p&gt;
&lt;h2 id=&#34;八生产结果&#34;&gt;八、生产结果&lt;/h2&gt;
&lt;p&gt;完整方案（批处理 + 扁平 buffer + ThreadLocal 复用 + Vector API）在跑生产流量的金丝雀上测得：&lt;strong&gt;CPU 利用率下降约 7%，平均延迟下降约 12%&lt;/strong&gt;；为了归一化吞吐差异还跟踪了 CPU/RPS（每 RPS 消耗的 CPU），&lt;strong&gt;改善约 10%&lt;/strong&gt;——同样的流量可以少用约 10% 的 CPU，全量上线后数据保持住了。在函数级别，这个特征占的 CPU 从最初的 &lt;strong&gt;7.5% 降到约 1%&lt;/strong&gt;。汇编层面的变化也很直白：从循环展开的标量点积，变成 AVX-512 硬件上的向量化矩阵乘。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;p&gt;这篇文章值得读两遍：第一遍看技术，第二遍看顺序。沉淀下来的启示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;优化顺序是：计算形状 → 数据布局 → 分配策略 → 计算内核。&lt;/strong&gt; 他们最后换的才是内核（Vector API），但前面三步缺任何一步，换内核都拿不到收益。那个 5% 的回退也说明：跳过布局直接批处理，等于同时引入新开销和新瓶颈。做性能优化先问“形状对不对”，再问“数据在内存里躺得对不对”，最后才轮到“用什么算”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微基准通过不等于生产有效。&lt;/strong&gt; BLAS 在隔离测试里很有希望，一进真实路径就被 JNI、布局转换、临时分配吃光收益。任何库的 benchmark 都只对它自己的测试负责；决策必须在真实流量路径里做，最好带着上下游（他们旁边还跑着 TensorFlow 的 embedding 计算）一起测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;double[][]&lt;/code&gt; 到扁平 &lt;code&gt;double[]&lt;/code&gt; 是 Java 性能工程的经典动作。&lt;/strong&gt; 二维数组是“数组的数组”，行指针分散，缓存局部性差；扁平数组行主序连续、访问可预测，配合 ThreadLocal buffer 只增不缩，每请求分配降成零——这套组合在 Lucene、Netty 里到处都是，值得变成肌肉记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量形状决定优化对象。&lt;/strong&gt; 98% 的请求是单视频，只看中位数的话批处理毫无价值；但那 2% 的大批量扛了一半总计算量。看 p50 优化和看总量优化是两个问题，先埋点搞清楚自己的流量长什么样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孵化/预览特性的正确姿势：工厂加探测加回退。&lt;/strong&gt; 不赌 &lt;code&gt;--add-modules&lt;/code&gt; 一定加得上，启动时探测、随时退回标量实现，“试用新 JDK 特性”就从一次豪赌变成一个配置项。这个模式可以复用到任何“新能力可用性不确定”的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺带一个私货：这篇文章最好的地方是诚实——它把“我们以为做完了结果慢了 5%”和“BLAS 没成”都写了出来。性能工程的真实过程就是这样：每一步的正确性都要靠金丝雀和生产数据确认，不存在一步到位。那些只写成功结果的优化文章，参考价值反而低。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：Netflix Ranker 的视频惊喜度打分（候选与历史 embedding 的余弦相似度取 max）占单节点 CPU 约 7.5%，热点是 O(M×N) 的逐对点积；流量形状 98% 单视频、2% 大批量，但计算总量 50:50。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径&lt;/strong&gt;：M×N 次点积重构成矩阵乘 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;（行归一化后即余弦相似度矩阵）→ 金丝雀回退约 5%（&lt;code&gt;double[][]&lt;/code&gt; 分配与 GC、非连续布局、标量内核）→ 扁平 &lt;code&gt;double[]&lt;/code&gt; 行主序加 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt; 复用（buffer 只增不缩）→ BLAS 因 F2J 非原生、JNI 开销、行列主序不匹配而放弃 → 终解 JDK Vector API（&lt;code&gt;fma()&lt;/code&gt; 累加、&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 自适应 lane 宽度、JIT 映射 SSE/AVX2/AVX-512）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全姿势&lt;/strong&gt;：&lt;code&gt;MatMulFactory&lt;/code&gt; 启动时探测，不可用则回退到 Lucene 风格的循环展开标量实现，不依赖 &lt;code&gt;--add-modules&lt;/code&gt; 一定在场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产结果&lt;/strong&gt;：CPU 降约 7%、平均延迟降约 12%、CPU/RPS 改善约 10%、该特征的函数级 CPU 占比从 7.5% 降到约 1%；汇编从标量循环展开点积变成 AVX-512 向量化矩阵乘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：优化拼的不是“最快的库”，而是把计算形状、内存布局、分配策略这些基本功做对——这些就位之后，Vector API 才成为那个“顺其自然的选择”。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API — Netflix Technology Blog&lt;/a&gt;（原文，作者 Harshad Sane，2026-03-03）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://openjdk.org/jeps/338&#34;&gt;JEP 338: Vector API (Incubator) — OpenJDK&lt;/a&gt;（Vector API 的 JEP 入口，含 SPECIES、fma、lane 等概念定义；后续孵化迭代见 JEP 417/426/448/460）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/fommil/netlib-java&#34;&gt;netlib-java — GitHub&lt;/a&gt;（文中提到的 Java BLAS 库，默认 F2J 路径的出处）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lucene.apache.org/&#34;&gt;Apache Lucene&lt;/a&gt;（回退路径标量点积实现的灵感来源，Lucene 在向量检索里大量使用循环展开和 Panama Vector API）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
