写在前面
最近对论坛的推荐系统做了一次深度 review,顺便调整了 CF(协同过滤)的权重配比。这篇文章总结一下现有实现、发现的问题,以及背后的思考。
一、整体架构:读写分离 + 异步计算
智柴的推荐系统遵循论坛的整体架构设计:
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ 用户请求 │────▶│ Redis 缓存 │────▶│ 命中直接返回 │
└─────────────────┘ └──────────────┘ └──────────────────┘
│
▼ 未命中
┌─────────────────┐
│ cache_fill_queue│
└─────────────────┘
│
▼ 后台消费
┌─────────────────┐
│process_sqlite_ │
│queue.php │
└─────────────────┘
核心原则:
- 业务进程只读写 Redis,不直接触碰 SQLite
- 推荐计算是懒加载的:用户请求触发 → 投递队列 → 后台异步计算 → 结果回填 Redis
- 前端等待超时 1.5 秒,超时返回热门兜底
二、三路信号融合
推荐引擎目前融合了三路信号:
| 信号类型 | 原权重 | 现权重 | 作用 |
|---|---|---|---|
| User-based CF | 60% | 20% | 找"相似用户"看过的内容 |
| Content (TF-IDF) | 40% | 80% | 基于文本内容相似度 |
| Item-based CF | 0% | 0% | (规划中) |
为什么下调 CF 权重?
在 review 过程中发现了几个问题:
1. 数据稀疏性
// 当前 CF 计算复杂度:O(N) 遍历所有用户
foreach ({{LATEX:0}}userId => {{LATEX:1}}similarity = {{LATEX:2}}target, {{LATEX:3}}interactions[{{LATEX:4}}interactions[{{LATEX:5}}interactions[{{LATEX:6}}interactions[{{LATEX:7}}targetInteractions as {{LATEX:8}}weight) {
{{LATEX:9}}title . ' ' . {{LATEX:10}}vector = tokensToVector({{LATEX:11}}vector as {{LATEX:12}}count) {
// 词频 × (1 + 交互权重)
// 发帖 weight=3.0 → 倍率 4x
// 回复 weight=1.0 → 倍率 2x
{{LATEX:13}}term] += {{LATEX:14}}weight);
}
}
2. 分词策略
private function tokenize(string {{LATEX:15}}stopwords = ['的', '了', 'the', 'and', 'php', 'topic', ...];
}
现状:中文是单字拆分,不是词语。比如"机器学习"会变成 ['机', '器', '学', '习'],丢失了词语级别的语义关联。这是目前内容相似度的主要局限。
3. TF-IDF 计算
// IDF: 逆文档频率
{{LATEX:16}}docCount + 1) / ({{LATEX:17}}term] + 1)) + 1;
// TF-IDF 向量
{{LATEX:18}}count * {{LATEX:19}}cosine = dotProduct({{LATEX:20}}candidateVector)
/ (norm({{LATEX:21}}candidateVector));
4. 候选集构建
- 从最近发布的 100 个话题中采样
- 从最近 50 条回复中提取关联话题
- 随机丢弃 50% 样本增加多样性
- 混入 RediSearch 搜索结果(bigram 随机搜索)
四、触发机制:真正的"懒加载"
推荐计算不是定时任务,而是完全由用户请求触发:
用户访问首页/话题页
↓
检查 APCu 本地缓存(L1)
↓ 未命中
检查 Redis 分布式缓存(L2,TTL=15分钟)
↓ 未命中
投递任务到 cache_fill_queue
↓
后台进程 process_sqlite_queue.php 消费队列
↓
执行 RecommendationEngine 计算
↓
结果写回 Redis + APCu
等待策略:
- 前端轮询等待,最多 1.5 秒
- 超时返回热门兜底(随机热门话题)
- 缓存有效期 15 分钟
这种设计的好处是:
- 节省资源:只有活跃用户才触发计算
- 实时性好:新内容发布 15 分钟后就可能被推荐
- 容错性强:超时或失败有兜底策略
五、当前局限与未来方向
已知问题
| 问题 | 影响 | 优先级 |
|---|---|---|
| 中文单字分词 | 丢失词语语义 | 高 |
| CF 无时间衰减 | 老内容权重过高 | 中 |
| 无负反馈 | 无法学习"不感兴趣" | 中 |
| 无 Item-based CF | 稀疏场景表现差 | 中 |
规划中的改进
1. 引入中文分词 考虑使用 jieba 或类似的轻量级分词器,把"机器学习"作为一个整体词处理,而不是拆成单字。
2. Item-based CF User-based CF 依赖用户相似度,Item-based 则是"看过 A 的人也看了 B"。对于内容社区,Item-based 通常更稳定、可解释性更强。
// 物品相似度预计算
{{LATEX:22}}userItemMatrix);
// 缓存键: item_sim:{topic_id}
// 定时更新,实时查表
3. 时间衰减因子
// 引入指数衰减
{{LATEX:23}}daysAgo / 30); // 30天半衰期
{{LATEX:24}}baseWeight * $timeDecay;
4. 标签体系 如果用户愿意给话题打标签,可以引入标签相似度作为第三路信号。
六、总结
这次调整把 CF 权重从 60% 降到 20%,本质是在数据稀疏的场景下,让更稳定的内容信号占据主导。这不是说 CF 没用,而是在当前数据规模和分布下,TF-IDF 的性价比更高。
推荐系统没有银弹。User-based CF、Item-based CF、Content-based、深度学习... 每种方法都有其适用场景。关键是根据业务阶段、数据规模、计算资源做 trade-off。
智柴目前的选择是:简单可维护 + 渐进优化。先把内容推荐做扎实,再逐步引入更复杂的协同策略。
欢迎讨论,特别是关于中文分词策略和 Item-based CF 的实现思路。如果有同学做过类似的推荐系统,欢迎分享经验!
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!
推荐
智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。