一个推荐系统是如何“长大”的(工程演进)

从 Embedding Demo 到生产级推荐系统

很多推荐系统文章,一上来就是:

  • 协同过滤
  • 深度学习
  • CTR / CVR 预估
  • Wide&Deep / DIN / Transformer

看完你只有一个感觉:

我现在就算想做,也完全不知道第一步该干嘛。

但真实世界里,推荐系统几乎从来不是一开始就复杂的

它通常从一个非常简单的版本开始,然后一年一年慢慢长大。

这篇文章不讲算法推导,只讲一件事:

如果你今天要做推荐系统,可以怎样一步一步把它做出来。

我们从 最小可用版本(MVP) 开始。

0️⃣ 推荐系统最小闭环

无论多复杂的推荐系统,本质只有一条链路:

text 复制代码
用户 → 用户向量  
内容 → 内容向量  
相似度 → 召回  
排序 → 返回

所有推荐系统,本质只是在不断优化三件事:

1️⃣ 内容如何向量化(Embedding)

2️⃣ 如何找到"可能喜欢的内容"(Recall)

3️⃣ 如何从候选中挑最好的(Rank)

我们先做一个 完全不依赖用户行为 的推荐系统。

1️⃣ 第一版:只有 Embedding 的推荐系统

这是几乎所有推荐系统的起点。

没有点击

没有用户画像

没有模型训练

只有一句话:

找和用户当前想看的内容最像的内容

换句话说:

语义搜索 = 推荐系统 v1

很多公司第一版推荐,就是这样上线的。

1.1 内容准备

假设我们做一个类似小红书的信息流:

python 复制代码
contents = [  
  {"id":1, "text":"日本东京旅行攻略|涩谷+浅草一日路线"},  
  {"id":2, "text":"MacBook 提升效率的10个工具"},  
  {"id":3, "text":"新手如何开始健身"},  
  {"id":4, "text":"上海咖啡馆探店合集"},  
  {"id":5, "text":"Python 自动化办公技巧"},  
]

1.2 内容向量化(Content Embedding)

第一步:把所有内容变成向量。

python 复制代码
from sentence_transformers import SentenceTransformer, CrossEncoder

bi_encoder = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

doc_vectors = bi_encoder.encode(  
  [c["text"] for c in contents],  
  normalize_embeddings=True  
)

数据库现在变成:

text 复制代码
内容 → 向量(768维)

这一步在真实公司中不是脚本,而是:

一个长期运行的离线 pipeline

通常包含:

  • 新内容实时入库
  • 批量重新 embedding
  • 向量版本管理
  • 索引定期重建

1.3 建立向量索引(Vector Index)

使用 FAISS 构建向量检索:

python 复制代码
import faiss

dim = doc_vectors.shape[1]
index = faiss.IndexFlatIP(dim) # 使用 Inner Product(点积)
index.add(doc_vectors)

很多人第一次看到这里会问:

为什么用 点积(Inner Product) ,而不是 Cosine 相似度

这背后其实是向量检索里一个非常重要的知识点。

1️⃣ 三种最常见的向量相似度

向量检索通常有三种距离/相似度:

方法 直觉理解 FAISS 支持
L2 距离 空间距离 IndexFlatL2
点积(Inner Product) 方向 + 长度 IndexFlatIP
Cosine 相似度 只看方向 (可用IP实现)

推荐系统 & 语义搜索中最常用的是:

Cosine 相似度

因为我们关心的是:
语义方向是否一致,而不是向量有多长。


2️⃣ Cosine 相似度在计算什么?

Cosine 相似度本质是:

text 复制代码
cos(θ) = A·B / (|A| |B|)

它衡量的是:

两个向量夹角的余弦值

直觉理解:

角度 Cosine 含义
1 非常相似
90° 0 无关
180° -1 完全相反

所以:

text 复制代码
东京旅游 → 日本旅行攻略   ✔
东京旅游 → Python教程   ✖

我们只关心 方向一致


3️⃣ 为什么工程里不用直接算 Cosine?

看公式:

text 复制代码
cos(θ) = A·B / (|A| |B|)

它包含三步计算:

1️⃣ 计算点积

2️⃣ 计算向量长度

3️⃣ 再做除法

这在大规模检索中 非常慢

而 FAISS 的设计目标是:

在千万级向量中毫秒级搜索

所以工程上做了一个非常巧妙的优化:


4️⃣ 关键技巧:向量归一化

在 embedding 时我们做了这一步:

python 复制代码
normalize_embeddings=True

这一步会把所有向量变成:

python 复制代码
|A| = 1
|B| = 1

也叫 L2 Normalization

此时 Cosine 公式变成:

css 复制代码
cos(θ) = A·B

🎉 Cosine 相似度 = 点积

这就是为什么我们可以使用:

python 复制代码
faiss.IndexFlatIP

来实现 Cosine 搜索。


5️⃣ 一句话总结工程做法

真实系统中几乎都会采用:

text 复制代码
先做向量归一化 → 再用点积检索

原因只有一个:

点积检索可以被高度优化,速度极快

这就是推荐系统里一个非常经典的小技巧:

text 复制代码
Cosine 相似度(理论)
=  
Normalized 向量 + 点积(工程实现)

6️⃣ 向量长度其实是"强度信号"

前面我们做了向量归一化:

python 复制代码
normalize_embeddings=True

这一步会把所有向量长度变成 1。

但你可能会问:

那向量的"长度"还有用吗?

答案是:

非常有用,只是被我们暂时隐藏了。


向量的两部分信息

一个向量其实包含两种信息:

text 复制代码
向量 = 方向(兴趣类型) + 长度(兴趣强度)
组成 表示什么
方向 喜欢什么
长度 喜欢程度

Cosine 相似度只使用:

text 复制代码
方向

但在真实推荐系统里:

强度信息非常重要


7️⃣ 深度用户 vs 轻度用户

想象两个用户:

text 复制代码
用户A:每天刷 2 小时  
用户B:每周刷 10 分钟

他们的兴趣方向可能完全一样:

text 复制代码
都喜欢:旅游 + 健身

但对平台来说,这两种用户完全不同:

用户类型 商业价值
深度用户 高留存、高广告价值
轻度用户 容易流失

如果我们不做归一化,用户向量通常这样生成:

text 复制代码
user_vec = sum(clicked_item_vectors)

那么:

python 复制代码
用户A 向量长度 >> 用户B 向量长度

向量长度自然就变成了:

用户活跃度 / 兴趣强度


8️⃣ 广告系统特别依赖"向量长度"

在广告推荐里,长度甚至可能代表:

text 复制代码
付费能力
转化概率
商业价值

举个典型场景:

广告系统的目标不是点击,而是:

text 复制代码
eCPM = CTR × CVR × 出价

如果两个用户兴趣一样:

用户 Cosine 相似度 向量长度
用户A 很长
用户B 很短

广告系统更愿意把广告给谁?

向量更长的用户

因为他更可能:

  • 停留更久
  • 点击更多
  • 产生转化

所以在广告排序中常见做法是:

text 复制代码
score = 向量点积 × 用户价值系数

甚至直接使用 未归一化向量做点积


9️⃣ 推荐系统里的一个经典取舍

现在你会看到一个很真实的工程选择:

场景 是否归一化
语义搜索 / 内容召回 ✅ 归一化(只看兴趣方向)
广告 / 商业排序 ❌ 不归一化(需要兴趣强度)

这也是为什么很多推荐系统会同时保存:

text 复制代码
normalized embedding
raw embedding

一个用于 召回 ,一个用于 排序 / 商业化


🔚 小总结

text 复制代码
Cosine 相似度 → 找"像不像"
向量长度 → 衡量"有多重要"

这就是向量在推荐系统里的完整信息。


现在你已经拥有了一个:

毫秒级语义搜索引擎


1.4 用户 Query → 推荐

用户打开 App 输入:

python 复制代码
query = "东京旅游"

系统执行:

python 复制代码
query_vec = bi_encoder.encode([query], normalize_embeddings=True)  
scores, ids = index.search(query_vec, k=3)

返回:

复制代码
东京旅行攻略
上海咖啡馆
健身入门

🎉 恭喜,你已经上线了第一个推荐系统。

虽然它现在叫:

语义召回系统

但很多公司第一版推荐就是这样上线的。


2️⃣ 第二版:加入用户行为(真正的推荐开始)

第一版的问题非常明显:

它只知道用户"现在想搜什么"

不知道用户"平时喜欢什么"

于是推荐系统第一次发生质变:

引入用户向量(User Embedding)


2.1 用户行为数据开始出现

最重要的数据登场:

text 复制代码
曝光 impression  
点击 click  
点赞 like  
收藏 favorite  
停留时长 dwell time

示例:

text 复制代码
用户A:  
点击:东京旅行、上海咖啡馆  
点赞:东京旅行  
长停留:健身入门

推荐系统第一次开始"理解用户"。


2.2 用行为生成用户向量

最简单但极其有效的方法:

用户向量 = 看过内容向量的平均值

python 复制代码
def build_user_vector(history_ids):  
    vecs = [doc_vectors[i] for i in history_ids]  
    return np.mean(vecs, axis=0)

用户A向量 ≈

text 复制代码
旅行 + 咖啡 + 健身 的兴趣融合

这是推荐系统的第一个核心里程碑


前面我们写了一个最简单版本:

python 复制代码
user_vec = mean(clicked_items)

但真实系统不会把所有行为看成一样重要。

因为在推荐系统里:

不同用户行为 = 不同强度的喜欢


1️⃣ 行为埋点是怎么收集的?

客户端会埋点上报用户行为,例如:

python 复制代码
user_behaviors = [
    {"user": "user_A", "content_id": 7,  "action": "click"},
    {"user": "user_A", "content_id": 10, "action": "like"},
    {"user": "user_A", "content_id": 13, "action": "like"},
    {"user": "user_A", "content_id": 15, "action": "collect"},
    {"user": "user_A", "content_id": 19, "action": "click"},
    {"user": "user_A", "content_id": 23, "action": "collect"},
]

这些数据每天都会源源不断进入数据仓库。

这一步叫:

text 复制代码
User Behavior Logging

推荐系统真正的燃料开始出现了。


2️⃣ 不同行为代表不同"喜欢程度"

直觉上:

行为 喜欢程度
曝光 很弱
点击 有兴趣
点赞 比较喜欢
收藏 非常喜欢
分享 超级喜欢

所以工程中第一件事就是:

给每种行为设定权重

一个典型的权重表:

python 复制代码
ACTION_WEIGHT = {
    "impression": 0.1,
    "click": 1,
    "like": 3,
    "collect": 5,
    "share": 8,
}

注意:

这些权重通常来自 经验 + A/B 实验


3️⃣ 加权用户向量(Weighted User Embedding)

现在我们不再简单平均,而是做加权求和:

python 复制代码
def build_user_vector(behaviors):
    vecs = []
    weights = []

    for b in behaviors:
        item_vec = doc_vectors[b["content_id"]]
        w = ACTION_WEIGHT[b["action"]]

        vecs.append(item_vec * w)
        weights.append(w)

    return np.sum(vecs, axis=0) / np.sum(weights)

用户向量现在变成:

text 复制代码
用户兴趣 = Σ(内容向量 × 行为权重) / 权重和

这一步非常关键:

用户向量开始包含"兴趣强度"。


4️⃣ 为什么"收藏"权重很高?

想象两个用户:

text 复制代码
用户A:点了10次旅游
用户B:收藏了1篇旅游

哪个更喜欢旅游?

大多数情况下:

收藏 > 多次点击

因为收藏意味着:

  • 愿意以后再看
  • 有长期价值
  • 兴趣更确定

这就是推荐系统里经典的一句话:

text 复制代码
显式反馈 > 隐式反馈
类型 行为
显式反馈 like / collect / share
隐式反馈 click / dwell

5️⃣ 加入时间衰减(非常重要)

再进一步,工程里几乎一定会加入:

时间衰减(Time Decay)

因为:

复制代码
三个月前喜欢 ≠ 今天喜欢

典型做法:

python 复制代码
import math

def time_decay(days):
    return math.exp(-days / 30)  # 半衰期≈30天

最终真实用户向量更像这样:

scss 复制代码
weight = ACTION_WEIGHT[action] * time_decay(days)

用户向量开始变得又懂强度,又懂时间


🔚 小总结

复制代码
用户向量 ≠ 点击平均值
用户向量 = 行为强度 × 时间权重 × 内容向量

从这一刻起,推荐系统真正开始"理解用户"。


2.3 首页推荐诞生

推荐逻辑发生关键变化:

v1 v2
query user_vector
搜索 首页推荐
python 复制代码
scores, ids = index.search(user_vec, k=20)

推荐系统真正诞生。


3️⃣ 第三版:召回不够了 → Rank 出现

问题来了:

向量搜索返回的 Top20
并不一定最适合展示

原因:

相似 ≠ 会点击

于是推荐系统进入经典架构:

text 复制代码
Recall → Rank

3.1 召回层(Recall)

目标只有一个:

从千万内容 → 找出 200 条候选

特点:

  • 只负责"可能喜欢"

常见召回方式:

召回方式 说明
向量召回 embedding 相似
热门召回 当前热门
关注召回 关注的人发的
协同过滤 相似用户喜欢

真实系统是 多路召回

text 复制代码
向量召回
热门召回
关注召回
协同过滤召回

最后:

text 复制代码
多路召回 → merge → 200条候选

3.2 排序层(Rank)

排序层才真正回答:

用户会不会点?

输入特征:

text 复制代码
用户特征:
  年龄 / 兴趣标签 / 活跃度

内容特征:
  类别 / 热度 / 发布时间

上下文特征:
  时间 / 设备 / 网络

输出:

python 复制代码
P(click)

第一代排序模型通常是:

text 复制代码
LR / GBDT / Wide&Deep
python 复制代码
score = rank_model.predict(user, item, context)

上线后 CTR 常见提升:

+30% ~ +80%


4️⃣ 第四版:用户兴趣是动态的

昨天你在看旅游

今天你在看 MacBook

兴趣不是固定的。

于是系统继续进化:

长期兴趣 + 短期兴趣


4.1 长期兴趣(Long-term)

来自:

text 复制代码
过去3个月行为

稳定。


4.2 短期兴趣(Session)

来自:

text 复制代码
最近10次点击
最近30分钟行为

非常重要。

工程中常见做法:

python 复制代码
user_vector = 0.7 * short_term + 0.3 * long_term

推荐开始变得"像读心术"。


5️⃣ 第五版:真正的生产级推荐系统

到这里,系统已经能长期运行并持续迭代。

真实推荐系统通常长这样:

复制代码
离线层:
  内容 embedding pipeline
  用户 embedding pipeline
  模型训练
  特征仓库

在线层:
  用户请求 →
  多路召回 →
  特征服务 →
  排序模型 →
  重排 →
  返回

继续进化的方向:

  • 探索 vs 利用(避免信息茧房)
  • 冷启动(新用户 / 新内容)
  • 多目标优化(点击 + 停留 + 转化)
  • A/B 实验平台

一个成熟推荐系统:

通常需要 1--2 年持续演进


结尾

推荐系统从来不是从复杂开始的。

它通常从一句非常朴素的话开始:

找相似内容 → 加入用户 → 加入排序 → 持续进化

从 embedding demo 到工业级系统,没有跳跃。

只有:

持续迭代。

下一步:AI 开始进入推荐系统

到这里,我们已经把推荐系统从一个 embedding demo,一步一步做成了一个可以长期运行的工程系统:

  • 有用户行为
  • 有用户向量
  • 有多路召回
  • 有排序模型

其实,很多公司的推荐系统第一阶段就停在这里,并持续优化很多年。

但当系统继续发展,会慢慢遇到新的问题:

  • 人工设计的特征越来越多,越来越难维护
  • 排序模型效果提升开始变慢
  • 召回策略越来越复杂

这时,一个新的阶段就会自然出现:

AI 开始逐渐进入推荐系统。

或许下一篇我们会聊:

《推荐系统是如何逐渐引入 AI 的》

从传统工程推荐,走向真正的 AI 推荐。

相关推荐
AI程序员2 小时前
Code Agent 的上下文压缩:不是 zip,而是工作记忆管理
人工智能
AI程序员2 小时前
OpenAI Frontier 到底是什么:企业 Agent 不只是需要一个更强的模型
人工智能
爱喝白开水a2 小时前
春节后普通程序员如何“丝滑”跨行AI:不啃算法,也能拿走AI
java·人工智能·算法·spring·ai·前端框架·大模型
掘根2 小时前
【微服务即时通讯】好友管理子服务2
微服务·云原生·架构
两万五千个小时2 小时前
解析 OpenClaw AgentSkills:AI Agent 如何通过「技能包」实现专业化
人工智能·程序员·代码规范
热点速递2 小时前
美团2025年“翻车”实录:从盈利王者到赤字领跑!
人工智能·业界资讯
ai产品老杨3 小时前
异构计算时代的架构突围:基于 Docker 的 AI 视频平台如何实现 X86/ARM 与 GPU/NPU 全兼容(源码交付)
人工智能·docker·架构
beyond阿亮3 小时前
OpenClaw在Windows上接入飞书完整指南
人工智能·windows·ai·openclaw
ybdesire3 小时前
通过训练代码来理解DLLM扩散语言模型
人工智能·语言模型·自然语言处理