ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python驱动的新闻推荐系统:从数据采集到排序模型实战

Python驱动的新闻推荐系统:从数据采集到排序模型实战 1. 项目概述与整体思路拆解1.1 这个项目到底在解决什么问题先说说我做这个项目的初衷。做新闻聚合类产品有个非常典型的痛点用户每天能看到的新闻数量是有限的但信息总量是无限的如果只是按照时间线把所有新闻推给用户用户大概率会迷茫也留不住留存率、点击率都很难看。其实很多内容类产品死掉不是因为没有优质内容而是分发效率太低了。新闻推荐系统要解决的就是一件事情把用户当下最可能感兴趣的新闻在用户最有可能点击的那个时间点推到用户面前。这件事听起来简单做起来牵扯的东西非常多包括用户行为数据的采集、新闻内容的清洗与打标、用户兴趣画像的构建、召回与排序策略的选型、冷启动处理、以及一套能支撑全流程的大数据处理链路。我选择用 Python 来落地这个系统是因为 Python 在数据采集、数据处理、算法原型验证、后端接口开发这几个环节里几乎都能用同一套语言栈贯穿下来。你不需要在爬虫里用 Python、在数据处理里切 Scala、在算法里再切 Java虽然大数据生态里 Java 系组件绕不开但 Python 作为“胶水语言”去做调度和原型效率真的高很多。1.2 系统整体架构设计整个系统的架构我从一开始就按“分层解耦”的思路来拆没有把功能揉在一个大工程里。因为推荐系统天然就分“离线的数据加工”和“在线的实时服务”两条线硬揉在一起后面任何一个环节要调优都会牵一发动全身。我最后定下来的模块划分是这样几块数据采集层负责新闻内容的抓取、清洗、结构化以及用户行为日志的接收。数据仓库层负责对原始日志和新闻内容做 ETL生成用户特征宽表、新闻特征宽表以及各类中间结果表。推荐计算层包含候选集召回召回策略集合、特征工程、排序模型、以及最终的推荐结果生成。服务接口层对外提供 REST 接口App 端或 Web 端通过接口获取推荐结果同时把用户实时行为回传上来。效果分析层记录曝光、点击、停留时长等反馈数据产出推荐效果的离线报表。这套分层的好处是可以随时替换某一层的实现。比如今天我用规则召回明天换成向量召回只需要在推荐计算层做替换上下层接口不变就不用动。实际开发中这种“可替换性”极其重要因为推荐系统一定是迭代出来的不是一步到位的。2. 新闻数据采集与处理链路2.1 新闻内容抓取的几个关键决策新闻数据是所有推荐逻辑的基础这个环节做不好后面算法再漂亮也是白搭。在抓取新闻内容的时候我踩过不少坑总结下来有几个关键决策值得说。第一个决策是抓什么。不要只抓正文标题和发布时间这些东西只是最基础字段。我最后设计的新闻表结构里除了标题、正文、发布时间、来源、作者之外还增加了栏目分类、关键词列表、摘要、正文长度、图片数量、发布时间的时间戳。为什么需要这些字段因为后面做特征工程的时候新闻热度需要发布时间、内容丰富度需要正文长度和图片数、话题相似度需要关键词列表。你在采集阶段省掉一个字段后面就要花十倍时间补数据。第二个决策是怎么抓。我用的是 Scrapy 框架配合多级爬虫管道。先说一个很重要的原则爬虫要按“列表页 — 详情页”两级来设计不要试图在列表页直接解析出正文。列表页能拿到的是标题、链接、发布时间摘要正文必须进详情页去抓。这个设计看起来笨但可靠性最高因为很多站点的列表页和详情页的 DOM 结构是完全不同的混在一个解析逻辑里后期维护会非常痛苦。第三个决策是关于去重。新闻转载现象特别严重同一个事件往往有几十个来源发相似内容。我在系统里做了一层基于标题的 SimHash 去重。SimHash 的算法思想是把标题分词后给每个词一个哈希值再按词频加权最后得到一个 64 位的指纹。两篇文章的指纹汉明距离小于等于 3就认为是重复内容保留原始发布时间最早的那篇。这个方案实测下来效果不错能过滤掉七成以上的重复转载不是说内容完全一样而是事件撞车。2.2 正文清洗与结构化处理爬下来的 HTML 页面里正文内容往往夹杂着页头导航、页脚信息、广告区块、相关推荐等噪音必须做正文抽取。我在这块没有用太过复杂的方案用的是一种叫TextRank 与标签密度结合的策略。具体逻辑是先解析 DOM 树把文本节点按块划分然后计算每个文本块的“有效文本密度”也就是该块去掉标签后纯文本字符数占整体 HTML 代码长度的比例。正文区域通常文本密度高、噪声标签少聚类之后就能定位到正文主体。再对抽取出来的正文做一轮清洗去掉特殊字符、去掉多余空白、去掉“责任编辑”、“扫码阅读”这类导航性字句。做完清洗之后还需要给每篇新闻打标签。标签抽取我主要用了两个来源一是TF-IDF 提取关键词基于新闻语料库计算每个词的逆文档频率把 IDF 值高的词筛出来作为这篇文章的代表词二是分词后的词性过滤只保留名词、动词、专有名词形容词和副词基本没有区分价值。这里要特别提醒一点分词库的选择很关键。我对比过 jieba、pkuseg、HanLP 这几个主流方案最终在项目里用的是 jieba 加自定义词典。新闻领域有大量人名、地名、机构名比如“特朗普”“美联储”“新能源”这些词如果不加到自定义词典里会被切成没有意义的碎片后面做相似度计算的时候特征完全丢失。所以建一个持续维护的领域词典是新闻推荐系统里一个不起眼但极其重要的工作。2.3 用户行为数据的采集与埋点设计新闻推荐系统区别于传统门户最核心的一点就是能利用用户行为数据来动态调整推荐结果。所以行为采集是系统的生命线。我在前后端约定的埋点上报协议包含这几个核心事件曝光事件用户在信息流中看到某条新闻卡片即使没有点击也上报。曝光数据非常重要它是计算点击率的分子分母里的分母。点击事件用户点开了某条新闻。停留时长事件用户在新闻详情页停留了多长时间这个数据比点击更能反映真实兴趣如果用户点进去两秒就退出说明标题党或者内容不匹配。分享、收藏、评论事件这些深层互动行为的权重应该远高于点击。埋点的核心原则是事件驱动、异步上报前端不能因为上报日志阻塞正常浏览流程。后端接收日志我用的方式是走一个独立的日志采集接口数据先落到消息队列里不直接写数据库。因为用户行为日志的量级是新闻内容表的几十倍甚至上百倍高并发的时候直接怼数据库会把业务库打挂必须用消息队列做削峰填谷。日志的字段设计上除了用户名、新闻 ID、行为类型、行为时间这四个基本字段外我强烈建议加上场景上下文字段比如当前所在的推荐场景首页推荐还是分类频道、当前推荐的排序位置、设备类型、网络环境。这些上下文特征对训练排序模型非常有帮助比如同一个用户在工作时间刷手机推荐和晚上躺床刷行为模式差异是很大的。3. 用户画像与新闻画像的构建3.1 用户兴趣画像怎么建才有效用户画像是推荐系统里最容易做“虚”的一个环节很多新手喜欢把用户画像做成一堆标签的堆叠什么“科技达人”“体育迷”“美食爱好者”然后按标签去匹配新闻效果往往很差。我的经验是用户画像的标签必须可量化、可衰减、可直接参与计算否则就是摆设。具体来说我给每个用户维护这样几类特征短期兴趣向量近 24 小时的点击、搜索、浏览行为反映出的兴趣按小时做时间衰减。这一层解决的是“用户此刻想看什么”比如用户早上看了三条科技新闻下午可能还在关注科技。长期兴趣向量近 30 天的累积兴趣分布衰减周期更长代表用户稳定的偏好。这一层解决的是“用户一般而言喜欢什么”。行为权重特征不同行为类型给不同的权重分数。我的权重设计是点击得 1 分停留超过 30 秒加 0.5 分收藏得 3 分评论得 4 分分享得 5 分。这个权重比例需要结合自己的业务反复调没有标准答案但方向是明确的——浅层行为权重低、深层行为权重高。内容偏好细分比如用户点击的新闻里有 60% 是“科技”分类再往下细分“人工智能”话题占比 30%、“手机评测”占比 20%、“互联网公司动态”占比 10%这层细粒度数据才是排序模型真正用的特征。然后要强调一个词时间衰减。用户的兴趣是会漂移的一个人上个月疯狂看球赛新闻这个月可能完全没兴趣了。所以不能把历史上的行为数据简单累加必须对每个行为按时间做指数衰减。衰减公式我使用的是score base_score * exp(-lambda * (now - event_time))其中 lambda 是衰减系数短期兴趣取 0.05长期兴趣取 0.01单位是小时。这个参数可以按自己产品的用户黏性调整黏性高的产品衰减慢一点黏性低的衰减快一点。3.2 新闻画像不止是分类标签新闻画像是另一块基础数据。很多初版系统只给新闻打了分类标签然后用分类匹配用户兴趣这种做法的颗粒度太粗了推荐出来的内容千篇一律。新闻内容本身有很强的时效性特征这是新闻推荐和电商推荐、电影推荐最大的不同。电商商品可以卖一年新闻的生命周期往往只有三到五天所以新闻画像里必须要引入时效因子。我维护了一个叫做“新闻新鲜度”的衰减因子新闻发布后热度权重随时间衰减衰减公式类似hot_score base_hot * exp(-beta * age_hours)beta 我取的是 0.02 左右一条新闻差不多两三天后热度权重就降到一半以下五天以上基本不再进入推荐候选。这样能避免系统整天推荐“旧闻”用户在信息流里看到的永远是最新鲜的资讯。新闻画像还包括内容质量分基于正文长度、图片数量、来源权威性、是否有标题党特征比如大量使用感叹号、夸张词汇综合打分。低质量内容会直接降权。实体识别结果识别新闻中的人名、地名、机构名、产品名。比如一条新闻里出现了“苹果”“iPhone 15”“库克”这些实体就是后续做相似新闻召回的关键线索。相似新闻关系通过基于实体的相似度计算构建新闻与新闻之间的关联关系让系统可以“看过 A 的人还能看 B”。3.3 特征宽表的构建思路无论是做规则推荐还是后续引入机器学习排序模型都需要把用户特征和新闻特征拼成一张可训练、可查询的宽表。我在大数据链路里用 Hive 做离线特征宽表的计算核心是两张大表用户特征宽表以用户 ID 为主键一行包含该用户所有的画像特征、历史行为聚合指标、近 N 天各类新闻的点击分布等。字段数大概在几十个左右。新闻特征宽表以新闻 ID 为主键一行包含该新闻的文本特征向量、分类、关键词、热度分、质量分、发布时间戳等。这两张宽表在推荐服务启动时会被加载到内存或者 Redis 缓存里在线推理的时候直接查特征不需要在线做复杂的 SQL 聚合。离线算特征、在线查特征的架构是推荐系统的标准玩法因为在线的 QPS 要求根本不允许你做动态长计算。4. 推荐算法选型与排序逻辑拆解4.1 召回层多路召回策略组合推荐系统里召回和排序是两件不同的事情。召回的目标是从十万级别的新闻池子里快速捞出几百篇候选排序的目标是在这几百篇里选出最优的二十篇。我觉得做新闻推荐召回阶段至少需要做这几路召回兴趣标签召回根据用户短期、长期兴趣向量里权重最高的标签去新闻库中检索对应分类和关键词的近期新闻。这是最基础的一路保证推荐结果和用户显性兴趣相关。热门召回把当前时间段内全站热度最高的新闻拿出来做补充。热门召回解决的是“大家都想看什么”的问题对冷启动用户和兴趣不明确的用户尤其重要。协同过滤召回基于用户行为相似度。如果用户 A 和用户 B 在历史点击上有很高的重合度那么用户 A 近期点击的新闻可以被推荐给用户 B。这一路召回能发现用户自己都没意识到的兴趣点。相似新闻召回基于用户在近期点击过的新闻通过新闻实体相似度查找相关新闻。比如用户刚看完一条关于芯片出口管制的新闻这一路就会把关于同类话题的后续报道、深度解读捞出来。实时行为召回用户刚点击了一类新闻立刻用该新闻的分类和关键词做一次实时检索把刚出炉的同类新闻顶上来。多路召回的好处是单个策略的失败不会导致整体效果崩溃。某一路召回结果不理想还有别的路兜底。每一路召回都需要设定一个召回数量上限我一般每路控制在 100 到 200 条去重合并后进入排序阶段的候选集合控制在 300 条左右。4.2 排序层从规则加权到机器学习模型排序是决定用户体验的最关键一环。早期版本我用的是规则加权排序公式大概是final_score alpha1 * interest_score alpha2 * hot_score alpha3 * freshness_score alpha4 * quality_score这种方案的优点是解释性强、上线快但缺点是权重很难调优而且不同用户群体对各项指标的敏感度不一样。比如新用户更依赖热门内容深度用户更依赖兴趣匹配单一权重无法适配所有人。所以我在此基础上引入了LR逻辑回归排序模型。LR 在推荐排序里扮演的角色是对所有特征做非线性加权组合输出一个 0 到 1 之间的点击概率。虽然 LR 本身是线性模型但可以通过特征交叉、分桶离散化来引入非线性能力在新闻推荐这个场景下训练数据量大、特征维度高LR 的效果还相当不错关键是训练和线上预测的成本都很低。特征工程里我实际使用的核心特征可以分几组用户 - 新闻匹配特征用户兴趣向量和新闻标签向量的余弦相似度、用户对新闻所属分类的历史点击率、用户对新闻来源的偏好度。新闻自身特征热度分、新鲜度衰减因子、内容质量分、曝光量、点击率。上下文特征当前时间段早中晚、用户所在场景、新闻在列表中的排序位置。交叉特征比如“科技类新闻在晚上的点击率”这类条件概率特征。训练样本的构造是排序模型效果的基石。我用的是曝光日志 点击行为来构造正负样本。曝光了而且被点击的样本为正样本曝光了但没被点击的为负样本。这里有一个非常隐蔽的坑曝光日志里的大部分样本其实是模型已经筛选过的结果会造成一种叫做“选择偏差”的问题。比如模型之前把体育新闻排在很后面用户根本没看到那么体育新闻就很少出现在曝光日志里模型就永远学不到用户对体育新闻的真实偏好。缓解方案是给推荐结果保留一部分“探索流量”随机展示一些非最优结果的新闻卡片这样长期积累下来曝光分布会越来越接近真实兴趣分布。这个过程叫Explore Exploit做推荐系统必须接受这个理念这是被很多初版方案忽略的关键细节。4.3 冷启动问题怎么处理冷启动是所有推荐系统绕不开的难题新闻推荐尤其严重因为新闻的更新速度太快新用户进来时系统对他一片空白新新闻发布时系统对它也一片空白。新用户冷启动我采用了多层策略第一层默认给新用户推全站热门新闻保证内容质量的下限。第二层引导新用户选择一个初始兴趣标签至少选三个系统根据这些标签做一次带强先验的召回。第三层利用用户的设备型号、地区、网络环境等粗略画像做冷启动预估。比如来自某个城市的用户优先推本地新闻虽然粗糙但实测点击率比纯热门高出不少。第四层用户产生前几个点击行为后立刻用实时行为召回顶上结合短时兴趣衰减模型让推荐结果快速响应用户的初步反馈。新新闻冷启动也有讲究。系统不可能对所有新新闻都一视同仁。我有专门的新新闻探索通道每天会从最新的新闻池里选一小部分给少量用户做曝光测试收集点击反馈。如果点击率超过系统平均值就立刻扩大曝光范围如果点击率很低就维持小流量靠热门策略自然淘汰。这其实就是前面说的 Explore Exploit 策略在新内容上的落地。开始时因为新新闻没有历史数据系统会低估其热度导致很多优质新闻被埋没所以必须主动制造曝光机会。5. 大数据技术栈的选型与落地5.1 离线和在线架构怎么配合这个项目的标题里有“大数据技术”不是蹭热度而是新闻推荐的数据链路天然和 Hadoop 生态强绑定。我这里的离线计算链路是Flume 采集用户行为日志 → Kafka 消息队列 → HDFS 落地存储 → Hive 做 ETL → Spark 做特征计算 → HBase/MySQL 导出结果 → 推荐服务加载。这条链路每个环节都有存在的理由。Flume 负责日志的可靠采集Kafka 做数据缓冲和解耦Hive 做批处理分析Spark 处理需要复杂计算的用户画像更新和特征宽表生成。有人会问直接用 Python 连接数据库做分析不行吗单机跑一跑确实可以但两个原因让 Hadoop 生态成为必需品。第一个是数据量用户行为日志一天下来少说几千万条单机关系型数据库无论是存储还是计算都撑不住。第二个是特征计算逻辑的复杂性而且这些计算每天都要重复跑必须做成可调度的离线任务Hive 里的 SQL 化处理比 Python 脚本更稳定、更可维护。在线服务部分用的是Redis 缓存实时特征 Python Flask 服务提供 API。用户点击一条新闻后行为日志异步送进 Kafka同时在线服务直接更新 Redis 里该用户的短期兴趣计数器做到“秒级反馈”。用户再次刷新推荐列表时实时兴趣特征已经参与排序计算。这种离线批处理加在线实时计算的架构术语叫Lambda 架构。刚开始不用把架构搞得太复杂但离线和在线数据通道的分离是一定要做的否则每次推荐请求都去重算用户画像服务性能根本扛不住。5.2 用 Hive 做用户行为 ETL 的实践用户行为日志的 ETL 是整个数据流里最脏最累的活但它的产出质量决定了上层所有计算的准确性。日志原始数据长什么样举个实际例子从 Nginx 收到的行为日志原始格式是一长串 JSON里面可能包含脏字段、用户 ID 为空、新闻 ID 不存在、时间戳不合法等情况。Hive 里的第一步就是把这些脏数据洗掉这个过程叫“数据清洗”。我实际用的 Hive 处理脚本思路大概是CREATE TABLE dwd_user_behavior_clean AS SELECT user_id, news_id, behavior_type, from_unixtime(behavior_time) AS behavior_time, scene_id, position_no FROM ods_user_behavior_raw WHERE user_id IS NOT NULL AND user_id ! AND news_id IS NOT NULL AND behavior_time IS NOT NULL AND behavior_type IN (expose, click, stay, collect, share, comment);这种大宽表查询虽然逻辑简单但每次跑全量的代价非常大。所以我在实际任务调度里做了分区策略按日期字段做分区每次 ETL 只跑当天新增的日志分区历史分区不会重复处理。大数据场景下增量处理是性能和成本的生死线全量跑一次几千万条数据要几十分钟增量跑一次只需要几分钟。Hive 里计算用户特征宽表的另一个实用技巧是用 UNION ALL 合并多个维度的聚合结果。比如点击次数、曝光次数、阅读时长总和、各分类的点击分布这些指标来源不同、粒度不同可以先在子查询里分别聚合再通过 JOIN 到同一张用户宽表。写 SQL 的时候注意控制 JOIN 的数据量避免出现数据倾斜。用户行为数据天然就是长尾分布少数热门新闻被大量用户点击JOIN 的时候这些 key 会造成 reducer 负载严重不均。解决数据倾斜的办法包括加随机前缀打散、用 map join 优化小表关联、或者先过滤掉超高热度的行再计算这些在实际生产里都是必修课。5.3 推荐结果的存储与接口设计推荐结果算好之后不能直接丢给前端需要一个存储层和标准的服务接口。我的设计是离线任务每天定时算出每个用户的候选推荐列表写进 HBase。HBase 的 rowkey 设计为user_id列族里存一个 JSON 字符串内容是按排序分数排列的新闻 ID 列表。在线服务收到推荐请求时先从 HBase 读取用户的基础推荐列表然后结合 Redis 里的实时特征做二次重排最后返回给前端。这样的好处很明显基础推荐列表是离线算好的响应速度快实时重排只做小规模的列表内重排计算量小。我实测的接口响应时间能做到 50 毫秒以内完全满足信息流产品的性能要求。接口返回的 JSON 结构我设计得比较轻量{ code: 0, data: { scene: homepage, has_more: true, news_list: [ { news_id: 12345, title: xxxx, summary: xxx, cover: http://xxx/xx.jpg, category: tech, reason: 与你关注的AI话题相关 } ] } }上面 JSON 里的reason字段是“推荐理由”这个字段值得强调。用户看到推荐结果时会好奇“为什么给我推这个”给出透明的推荐理由能显著提升用户对推荐系统的信任度。哪怕是简单的一句“与你关注的AI话题相关”也比冷冰冰的新闻卡片要好得多这是很多推荐系统会被忽略的小细节。6. 推荐服务的性能优化与展示层适配6.1 接口性能优化与缓存策略新闻推荐系统的服务端性能目标就一句话单次推荐请求的响应时间不能超过 100 毫秒最好在 50 毫秒以内。这直接关系到用户体验信息流的加载如果转圈太久用户早就划走退出去了。我从几个层面做性能优化首先是结果缓存。对于热门的推荐结果比如首页的通用热门推荐列表直接在 Redis 里缓存 30 秒到 1 分钟同一时间段大量用户请求打过来时绝大部分直接在缓存层命中不需要回源计算。其次是并行化特征读取。排序计算需要读取用户特征、新闻特征、上下文特征这些特征分散在不同的存储里。我使用 Python 的协程或者多线程并发去读取将网络 IO 耗时重叠起来。实测下来原来串行读特征耗时约 200 毫秒改为并发读取之后降到 50 毫秒以内。然后是结果集精简。排序模型输出的候选新闻可能是 300 条但真正返回给前端的只要 20 条。计算最终排序分时可以先按轻量分数做一次初筛把分数有明显差距的低质量候选淘汰掉只对剩余的高质量候选做全量特征计算。这种粗排加精排的两阶段策略在大规模推荐系统里是标配即使在日均百万流量的中型系统里也非常值得做。6.2 大数据量展示层的优化方案热搜词里反复出现了“qt 表格大数据卡顿优化 tablewidget 到 qtableview 自定义 model”和“qt 表格大数据qtableview 自定义 qabstracttablemodel 视图只显示几十行”这说明很多人在做数据展示的时候遇到了表格渲染大数据量卡顿的问题。我在这个项目里也碰到了类似情况因为系统需要给运营或者管理端展示大量用户行为统计或者推荐效果数据。这里把经验也说透。Qt 中 QTableWidget 是“一次性把所有数据塞进表格控件”的思路每加一行都会创建对应的 item 对象。当数据量到几万行时创建和销毁 item 对象的内存开销、UI 刷新开销都非常大界面卡顿是必然的。解决方案就是换成QTableView 自定义 QAbstractTableModel。核心原理是QTableView 是视图层QAbstractTableModel 是数据层视图按需从 model 里取数据。表格滚动时视图只会请求当前可见区域的那几十行数据而不是一次性处理全部数据。实现上主要是继承 QAbstractTableModel重写几个核心方法rowCount()返回总行数模型知道总量但不会提前创建全部行。columnCount()返回总列数。data()根据传入的行列索引实时返回对应单元格的显示数据。这个方法只在视图需要显示某个单元格时才会被调用。headerData()返回表头名称。简单来说数据本身可以是内存中的一个 Python 列表或者 numpy 数组data() 方法只做索引映射和格式化。这样操作下来即使是几十万行的数据表滚动也相当流畅。还有一个性能细节可以在data()里把每列的数据类型统一比如全是数值的列不做字符串转换显示时再格式化能进一步减少不必要的开销。另外配合setUniformRowHeights(True)告诉视图所有行高一致Qt 就能跳过行高计算渲染速度会有肉眼可见的提升。6.3 大数据管理后台的可视化实践推荐系统不能只交付接口还需要一个管理后台让运营人员能查看推荐效果、调整策略参数、排查用户反馈。我这里做了一个数据大屏模块展示的核心指标包括当日新闻总曝光量、总点击量、整体点击率。各推荐场景的转化漏斗曝光 → 点击 → 阅读完成 → 互动。各频道的热度排行、点击率排行。推荐系统的实时反馈数据比如近一小时的曝光点击曲线。可视化部分我用的是 Python 后端提供聚合数据接口前端接 ECharts 做图表渲染。这里的难点不在图表组件而在数据接口的聚合效率。大屏要求刷新频率高每次刷新都实时跑 SQL 聚合肯定不行。我的做法是离线任务每五分钟计算一次指标数据写入 Redis大屏接口直接读取 Redis 结果。这是典型的“预聚合 展示”模式数据新鲜度能接受性能开销又小。7. 常见问题与排查技巧实录7.1 冷启动用户推荐效果差的排查思路这是个高频问题新用户进来推荐结果非常不精准点击率极低。先别急着调模型按下面的链路排查一下第一查日志新用户是否有行为日志上报。很多时候不是推荐算法的问题而是前端埋点漏了或者日志接口异常导致系统根本无法感知这个用户的行为。第二查画像查看该用户在用户画像表里是否生成记录。如果没有检查 ETL 任务是不是没有覆盖新用户的数据流转。第三查召回手动模拟一次新用户的召回请求看召回结果里热门占比是否合理。我遇到过热门召回被错误过滤掉的情况导致新用户拿到的全是质量分不高的小众内容。第四查排序看排序阶段是否施加了不合适的过滤规则。比如把用户已经不感兴趣的分类强行过滤掉了导致候选集过少。这个排查顺序的核心思路是从数据采集到画像构建再到召回和排序一层一层往下走每层都能单独测试而不是一上来就质疑排序模型。7.2 推荐结果“越来越窄”的反馈闭环处理系统上线一段时间后用户和运营都会反馈“推荐的内容越来越窄”翻来覆去就那么几类内容。这个问题专业术语叫“信息茧房”效应本质是推荐系统过度拟合了用户的既有兴趣导致探索性降低。原因通常有两个第一个是兴趣召回里长期兴趣权重太高。用户的长期兴趣基于历史行为累积一旦用户早期点击集中在某个分类这个标签的权重会越来越高新兴趣很难冒头。解决方法是在召回阶段对候选来源做比例控制强制保留一定比例的其他分类内容。第二个是排序模型里用户兴趣匹配特征权重过高。模型发现只要与用户兴趣匹配点击概率就高于是疯狂给这类内容加权。要解决这个问题需要在训练样本构造阶段对负样本做更精细的采样同时在排序结果上做打散策略。我在系统里实现了一个简单的打散规则连续 5 条推荐中保证至少 1 条来自不同的分类这样能在不牺牲太多点击率的情况下大幅改善用户的视野开阔度。7.3 大数据任务跑批延迟的排查案例推荐系统的离线任务链路较长任何一环卡住都会导致当天推荐结果无法按时生成。我遇到过的一个典型案例是 Hive ETL 过程中出现数据倾斜。用户点击日志的数据分布极度不均匀某一天有一条超级热门新闻几百万用户产生了行为记录。按照用户 ID 聚合时这个用户的 reducer 要处理的数据量是其他用户的几百倍整个任务因此卡住数个小时无法完成。排查过程是先看 YARN 的任务监控页面发现某个 reduce 任务的处理时长异常高同时磁盘和内存指标爆表然后定位到具体的 SQL 语句在聚合前加了一个过滤条件把超热点用户的数据单独剥离出来处理最后把该 key 的数据加随机前缀分散到多个 reducer 里再聚合问题就解决了。这类问题在大数据链路里几乎必然会出现所以从一开始设计任务时就应该考虑到热点 key 的倾斜情况。实用的经验是写聚合 SQL 之前先看一眼数据分布如果明显是长尾分布在写 SQL 的时候就要提前做热点探测和分桶处理不要等任务跑挂了再排查那时候成本就高很多了。7.4 服务接口偶发超时的定位思路在线推荐接口偶尔会出现超时这种情况最不容易复现也最难排查。我的排查路径一般是这样先看监控曲线确认超时集中发生在什么时间段。如果集中在整点或半点大概率是缓存过期的涌现效应大量请求同时回源导致存储被打满。解决方法是给热点缓存设置随机过期时间避免雪崩式失效。再看日志里有没有 Redis 连接异常。我遇到过 Redis 连接池配置过小、高并发下连接等待导致超时的情况。调整连接池大小和等待超时时间后问题解决。最后看排序计算耗时。如果某个用户的候选集特别大比如热门召回和协同召回结果大量重复去重后依然有几百条每条特征都要从存储里读计算累积时间就会超时。做一次候选集上限裁剪超时现象明显减少。8. 效果评估与持续优化的经验8.1 推荐效果的指标体系怎么定推荐系统上线后不能只凭感觉“好像效果变好了”必须有指标体系来度量。新闻推荐我主要关注这几个指标整体点击率CTR总点击量除以总曝光量是衡量推荐内容吸引力的最基础指标。人均阅读时长比点击率更深入反映内容质量和用户满意度。人均点击篇数衡量用户是否持续在信息流里“刷”下去。次日留存率最硬核的指标用户今天用了明天还愿不愿意来。探索率推荐结果里非用户历史偏好分类的内容占比保证系统不是只顾短期点击也有长期探索能力。这里想多说一句点击率高不代表推荐就是好的。如果推荐的全是猎奇类、标题党内容点击率会很高但用户停留时长和留存率反而会下降。所以我的原则是核心指标看留存辅助指标看点击短期优化点击率长期守护留存率。8.2 线上做 A/B 测试的正确姿势推荐策略的每次变更都应该通过 A/B 测试来验证不能直接全量上线。我的 A/B 测试设计是将用户按 user_id 哈希值分桶比如 hash 后模 1000 到 9 的用户进入实验组10 到 19 的用户进入对照组。两组用户同时使用线上流量实验组使用新策略对照组使用旧策略或者随机推荐观察指标差异。做 A/B 测试有几点经验值得分享第一实验流量不能太小否则数据波动会影响结论。我做流量分配时至少要保证实验组每天有几千次的曝光量如果曝光量太小指标差异从统计上完全不显著容易做出错误判断。第二实验周期不能太短至少要跑满三天。因为用户行为有明显的星期效应周一到周五和周末的用户行为差异非常大。只跑一天很容易被当天的偶然事件干扰。第三一定要控制变量。每次只测一个策略维度的变化不要同时改召回和排序。我踩过这个坑同时调整了兴趣召回和协同过滤的权重结果发现点击率上升了但完全说不清楚是哪个改动起了作用后面要优化就很被动。8.3 推荐模型迭代的实际经验基于这个项目的经验我的建议是别一上来就挑战复杂的深度学习模型。很多人看到“基于大数据技术”这个前缀就认为必须上深度模型才够高级。但在我实际开发过程中逻辑回归模型 丰富特征 多路召回已经能解决新闻推荐的大部分问题。深度模型比如 DeepFM、DIN、BERT 向量的双塔召回都是在数据规模和基础设施达到一定水平之后才有边际收益。对于一个中小型系统上线一套高可用的 LR 模型配上一套扎实的大数据链路效果已经可以超过市面上多数粗放的推荐实现。推荐系统的迭代风格应该是“小步快跑”。每两周尝试一个策略调整每次调整有明确的指标目标和对照组稳定后沉淀为系统默认策略。我在做这个项目的过程中逐步增加的策略有实时行为召回、Explore 流量、对相似新闻的关联召回、打散策略、本地新闻优先。每一步调整都有数据支撑否则就回滚。这种可控的迭代方式让系统的推荐质量稳步提升也避免了“重构一时爽线上火葬场”的惨剧。9. 写在最后的个人体会9.1 我在实际开发中踩过的几个坑这个项目从零到一、从一到再优化我踩过不少坑挑几个印象最深的写下来。第一个坑是特征工程和训练样本的构造比模型本身重要得多。早期我把大量精力放在尝试不同的模型上从逻辑回归到 GBDT 再到 FM换来换去效果波动很小。后来发现瓶颈根本不在模型而在负样本的采样方式不合理和特征缺失。把训练样本的重构做好、把用户实时行为特征加入之后同样一个 LR 模型的效果直接提升了一个档次。这让我深刻理解了那句圈内老话特征决定了效果的上限模型只是逼近这个上限。第二个坑是用户行为日志的数据质量必须从第一天就盯紧。有一段时间系统点击率突然下降排查了很久才发现是前端做了改版某个事件的上报字段名变了导致三分之一的点击行为没有被记录。如果日志采集环节的数据质量不把关后面所有基于行为数据的推荐策略都会失真。第三个坑是别低估 Nginx 和网关层的性能瓶颈。在线推荐服务本身计算很快但外层有日志上报、鉴权、限流、安全过滤等多个环节任何一个环节慢了都会拖慢整体响应。我最后是用了 OpenResty 做网关层把静态请求、日志上报、业务 API 分流处理性能才算稳定下来。9.2 后续还可以怎么扩展这个项目目前的架构已经能稳定支撑业务运转但继续深化的方向也很明确。一是引入基于向量检索的语义召回。用预训练模型把新闻文本和用户兴趣映射为向量通过向量相似度做召回。传统的标签匹配只能理解表面关键词向量召回能理解语义比如用户看了“新能源汽车销量创新高”的新闻系统能召回“比亚迪发布新款车型”的新闻而不需要两者包含相同的词。Python 生态里有很多成熟方案可以做这个事情向量召回能力会和当前的标签召回、协同召回形成补充。二是强化实时计算链路。目前用户的实时行为主要靠 Redis 计数据响应粒度能做到分钟级。如果接入 Flink 做实时流处理就能做到秒级甚至毫秒级的画像更新和热门新闻热度计算。这个升级的技术复杂度会上一个台阶但带来的效果提升也会非常明显。三是引入多样性重排策略。目前打散策略只是简单的分类轮换后面可以做基于 MMR最大边际相关性的多样性控制在保持相关性的同时提升推荐列表的整体信息丰富度。我研究过 MMR 的实现思路其实核心就是一个贪心选择过程每次从未选集合里挑一个“和用户相关度高同时和已选集合里最相似的新闻相似度低”的候选加入结果集。实现起来不复杂但对用户体验的提升很直观。这个系统做到现在对我最深的启发是推荐系统本质上是一个反馈闭环数据的质量、策略的迭代速度、效果评估的准确度三者缺一不可。永远不要指望模型本身能包打天下最好的优化路径永远是数据越攒越厚、特征越做越细、实验越跑越准整个系统就会像滚雪球一样越来越懂用户。希望这篇总结对正在做类似系统的朋友有帮助。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进