1. 家政服务推荐:为什么不能直接套用电商那套推荐逻辑
做家政服务系统最头疼的一件事,不是下单流程,而是用户打开首页之后,面对少则几百多则上千的服务人员列表,根本不知道选谁。家政不是标品,同样是"日常保洁",不同阿姨的做事风格、工具习惯、沟通方式差别很大,用户一旦选错一次,复购率基本归零。我在做这套Spring Boot 3 + Vue 3家政服务系统时,最核心的目标就是在用户和服务人员之间搭一座桥,让"对的阿姨"出现在"对的人"面前,这也是整个系统引入协同过滤推荐算法的原始动机。
1.1 家政订单和买手机完全是两回事
电商推荐里那套"看了又看""买了又买"的逻辑,放到家政场景下会处处碰壁。买手机是标品决策,参数、价格、品牌摆在那里,用户的决策链路偏理性,推荐系统要做的是帮用户缩小搜索范围。家政服务则完全是另一套逻辑:低频、高价、强体验、非标准化。一个家庭可能一年只叫十几次保洁,但每一次服务体验都直接关系到下个月还续不续费。用户对"服务人员"的信任成本远高于对"商品"的信任成本,这就决定了协同过滤算法不能只盯着"相似用户买了什么",还得把服务人员本身的可解释性、稳定性考虑进来。
我在设计推荐模块之前,先把家政订单的几个特殊约束列了出来:
- 地理约束:服务人员有固定服务区域,跨区推荐基本没有意义,用户也不会为了一个保洁阿姨跨半个城市下单。
- 时间约束:家政服务是预约制,阿姨的时间档期不同,推荐时必须排除已约满或近期不可用的服务人员。
- 技能与场景约束:日常保洁、深度保洁、收纳整理、月嫂、育儿嫂是不同工种,混着推荐会严重拉低体验。
- 关系约束:很多用户认准一个阿姨之后会长期复购,这时候再推荐"相似的其他人"反而打扰用户。
1.2 需求画像:家政场景的决策因子和推荐约束
接下来我把用户决策一个家政阿姨时最看重的因子做了拆解,大致分成四类:
| 决策因子 | 数据来源 | 权重倾向 |
|---|---|---|
| 服务技能标签 | 服务人员档案、订单服务项 | 硬性要求,不匹配必须过滤 |
| 历史评价 | 用户评价打分、文字评价 | 强影响因子 |
| 价格区间 | 用户历史订单均价、浏览均价 | 中影响因子 |
| 区域与时间匹配 | 定位信息、预约档期 | 硬性约束,必须满足 |
这个拆解直接影响推荐算法的输入设计:不能只拿"用户-服务人员"交互矩阵跑协同过滤,还得在召回之后做一层规则过滤,把硬约束先卡掉,再让算法在候选集里排序。这也是很多家政平台做推荐时容易忽略的一点,上来就堆算法,结果推荐结果里混着大量跨区、下班后才能到岗的阿姨,用户点了一次就再也不信推荐位了。
1.3 先把推荐目标定清楚:不是转化率,是匹配满意度
电商推荐考核点击率和转化率,家政推荐的考核指标不能照搬。一个用户一个月可能就下两三单,点击率再高,如果下单后觉得"这人不行",不仅不会复购,还可能直接流失。我给这套系统定的推荐目标很简单:让用户找到愿意长期复购的服务人员。所以在线指标上,我更关注"首次下单后7天内是否再次预约同一人",这个指标比点击率更能反映推荐质量。
有了这个目标,后面所有算法设计和工程实现才有抓手。协同过滤算法在这套系统里的定位,不是炫技,而是解决一个实际问题:新用户不知道选谁,老用户想换个阿姨试试但不知道换谁。
2. 协同过滤算法选型:从UserCF到ItemCF再加规则兜底
2.1 为什么最终选了基于物品的协同过滤
协同过滤分两大类:基于用户的UserCF和基于物品的ItemCF。我一开始直觉上觉得UserCF更合适,家政是熟人经济,相似的用户应该喜欢相似的阿姨。但仔细推演后发现,家政场景的数据分布和电商完全不同,UserCF有两个硬伤:
第一,用户下单频率太低,用户之间的共同下单记录极其稀疏。假设平台有1万个活跃用户,平均每人年下单10次,共同下单同一人的用户对可能连千分之一都不到,UserCF算出来的相似用户基本都是"弱连接",推荐效果会很差。
第二,用户群体变化快,新用户不断涌入,给每个新用户实时算"最相似的K个用户"成本很高。ItemCF则不同,家政服务人员的数量相对稳定,比如一个城市可能只有几百个活跃阿姨,物品相似度矩阵规模很小,可以离线算好、缓存起来,在线阶段只做查表,性能压力小很多。
所以我最后选了ItemCF为主:先把服务人员之间的相似度算出来,再根据用户历史上喜欢过的服务人员,去推荐与他们相似的其他服务人员。
提示:ItemCF的直觉是"用户喜欢A,而B与A相似,所以推荐B"。家政场景里,这个直觉成立的前提是"相似"要定义得准,光看"被同一个人下单"是不够的,还得结合评价分、服务标签做加权。
2.2 隐式反馈怎么变成评分:行为权重设计
协同过滤算法的输入是"用户-物品"评分矩阵,但家政系统里没有显式的星级评分可用(用户下单前通常不会先打分),只能从行为数据里构造隐式反馈。这是我花时间最多的地方,因为评分构造直接决定相似度计算的质量。
我采用的综合行为评分公式如下:
score = 下单行为 × 5 + 收藏行为 × 3 + 详情页深度浏览 × 1 + 评价文本正向分 × 2但这只是个基础框架,落地时我做了几个关键修正:
- 时间衰减:半年内有过正向反馈,权重按月的指数衰减;一年前的行为权重砍半。原因是用户口味会变,去年喜欢"做事麻利的阿姨",今年生了孩子之后可能更需要"能帮忙带娃的育儿嫂"。
- 评价分加权:如果用户给阿姨打了4星以上,这条行为权重额外提高;如果打2星以下,该行为不计入正向评分,甚至可以作为负样本处理,用于后续的"不喜欢"过滤。
- 频次归一化:有的用户一年下单20次,有的用户一年只下单3次,如果直接用累计行为次数比较,高频用户会把整个相似度矩阵带偏。归一化方式是每个用户的评分向量除以该用户的L2范数。
这些细节不做的话,算法跑出来的结果会非常"政治正确"但实际无效,因为用户之间行为量级差异太大,相似度计算会被高频用户主导。
2.3 离线计算与在线召回排序的切分
ItemCF在家政场景下的计算链路,我拆成了两个阶段:离线预计算和在线轻量计算。
离线阶段每天凌晨跑一次定时任务,从订单库、行为日志里拉取近90天的数据,算出服务人员两两之间的相似度矩阵,然后缓存到Redis。相似度计算用余弦相似度,公式是:
similarity(a, b) = Σ(w_a * w_b) / (||a|| * ||b||)其中w_a和w_b分别是服务人员a和b在用户评分向量上的分量。离线任务跑完之后,会生成一个"每个服务人员最相似的Top 50服务人员"倒排表,这个倒排表是在线推荐时的主要数据源。
在线阶段拿到用户请求后,流程是:先取出用户近90天有正向行为的服务人员列表,查倒排表得到候选集,然后做硬约束过滤(区域、时间、技能、档期),最后按"候选物品与用户历史物品的相似度加权求和"排序,取Top 20返回。整个在线链路不涉及复杂的矩阵运算,Redis里查表即可,单次推荐接口的RT可以稳定控制在30ms以内。
3. Spring Boot 3后端落地:数据模型、算法工程与接口设计
3.1 技术栈选择与数据表设计
后端我选了Spring Boot 3 + JDK 17 + MyBatis-Plus + Redis + MySQL这套组合。Spring Boot 3最大的感受是启动更快、GraalVM支持更友好,但真正影响推荐模块落地的,还是数据表怎么设计。这里直接给出几张核心表的结构思路:
用户行为表(behavior_log)
| 字段 | 说明 |
|---|---|
| user_id | 用户ID |
| worker_id | 服务人员ID |
| behavior_type | 下单/收藏/浏览/评价 |
| behavior_weight | 行为对应权重分 |
| created_at | 行为时间 |
服务人员相似度表(worker_similarity),离线计算后落库,同时写Redis:
| 字段 | 说明 |
|---|---|
| worker_id | 源服务人员ID |
| similar_worker_id | 相似服务人员ID |
| similarity_score | 相似度分值 |
| rank | Top N排序位次 |
用户行为表按user_id + created_at建联合索引,相似度表按worker_id + rank建索引。表结构看着简单,但有个坑:业务初期行为日志量小,直接查MySQL没问题,上线三个月后日志量上来了,定时任务扫描全表会越来越慢。我的处理方案是行为日志按天分表,或者直接走日志采集通道先入ClickHouse,离线计算时只从聚合表取数据,避免扫全量明细。
3.2 相似度矩阵计算与缓存更新
离线计算我用一个独立的Spring定时任务执行,用的是Spring自带的@Scheduled。核心步骤拆成四步:
- 从行为聚合表中加载用户-服务人员评分矩阵,数据结构为
Map<Long, Map<Long, Double>>,外层是用户ID,内层是该用户对每个服务人员的评分。 - 对每个用户的评分向量做L2归一化,消除用户行为频次差异。
- 遍历所有服务人员两两组合,计算余弦相似度。这里有个性能优化点:家政服务人员的总量是几千量级,两两组合是百万到千万级,直接跑双重循环在测试环境能跑完,但生产环境数据量翻倍后会很吃力。我后来改成了先按"共同下单用户数量"做粗筛,再对候选对精确计算相似度,计算量直接降了一个数量级。
- 对每个服务人员取Top 50相似结果,写入MySQL和Redis。Redis里的Key设计成
recommend:similar:{workerId},Value用JSON字符串存相似列表,有效期设24小时。
定时任务跑完要加一个监控项:相似度矩阵覆盖率,也就是有相似结果的服务人员数量占总服务人员数量的比例。这个指标一旦下滑,说明行为数据太稀疏,推荐效果必然退化。
3.3 推荐接口的召回、过滤、排序链路
推荐接口对外暴露的是GET /api/recommend/services,传入userId、经纬度、需要的时间档期,返回推荐的服务人员列表。接口内部走四步:
召回:从Redis取出用户最近90天正向行为对应的服务人员,再查每个服务人员的Top 50相似表,合并成一个候选池。如果用户是纯新客,没有历史行为,直接跳过这一步,走热门兜底。
过滤:候选池里逐一卡硬性条件,包括服务区域是否覆盖用户定位、服务人员当天是否有空闲档期、技能标签是否匹配用户选择的工种。这块我用了一个内存过滤器链,每层过滤条件做成独立组件,方便后续扩展。
排序:按相似度加权得分降序排列。得分公式是:
finalScore = Σ(用户对历史服务人员i的评分 × 服务人员i与候选服务人员j的相似度)这一步权重可以加入业务调节因子,比如服务人员的响应速度、近30天完单率、被收藏次数,作为排序微调项。我建议不要让业务因子喧宾夺主,协同过滤的主体得分占比至少保持在70%以上,否则算法会被业务规则稀释成纯人工排序。
兜底:如果过滤完之后候选集不足10个,用热门服务人员补齐,热门定义是近30天完单量前20名,并且额外加权了"好评率大于4.7分"的条件。
4. Vue 3前端展示:推荐位布局与用户反馈闭环
4.1 推荐位怎么摆用户才愿意看
前端我用的是Vue 3 + Vite + Pinia + Element Plus。很多人觉得推荐模块后端才是重头戏,前端不过是"把接口数据渲染出来"。但实际上,推荐位的布局和交互方式直接决定用户愿不愿意点、点了之后会不会形成正向反馈,这个反馈质量反过来又决定推荐算法下一步的学习效果。
我在首页做了三个推荐区域,各有各的定位:
第一个是顶部Banner下方的"为你推荐"横滑卡片区,展示综合推荐结果,一般放12个左右阿姨卡片,包含头像、技能标签、服务区域、价格、好评率。这个区域的定位是"打开App第一眼看到什么"。
第二个是服务详情页底部的"相似服务人员"推荐区。用户点进某个阿姨的详情页,说明已经产生了初步兴趣,这时候推荐"和她相似的其他阿姨"转化率最高,因为用户大概率是在对比和备选。
第三个是下单成功页的"再看看其他人"区域,表面上是推荐,实际上是做长尾分发,把用户没有接触过的服务人员推给用户,扩展用户的服务人员池。
三个推荐位对应不同的推荐策略,不能都用同一个接口。我的做法是后端额外传一个scene参数,推荐服务根据场景调整排序权重。比如详情页的相似推荐,相似度得分权重最高;首页综合推荐,业务因子权重相对高一些。
4.2 推荐理由解释:把"猜你喜欢"变成"为你推荐"
家政推荐有个天然优势:推荐结果的可解释性非常强。因为家政服务人员是有面孔、有名字、有真实服务记录的人,不是冷冰冰的商品。我在前端做了一个"推荐理由"胶囊标签,展示在阿姨卡片下方,有多种模板:
- 因为您预约过"张阿姨",为您推荐风格相近的"李阿姨"
- 这个时间段还有空位的持证育儿嫂
- 您附近的五星好评保洁员
这个设计很小,但对用户信任感的提升非常大。协同过滤算法本质上是黑盒,用户不知道"为什么推荐这些人",给出明确的推荐理由之后,即使推荐结果不完全匹配,用户也更愿意点进去看看,而不是直接划过。我在内测阶段统计过,加了推荐理由标签之后,推荐位的点击率提升了将近40%,这个数据让我意识到,推荐系统的用户体验一半在算法,一半在表达。
4.3 埋点采集与主动反馈,数据闭环怎么转起来
推荐模块如果只做展示不做反馈回收,算法就等于在裸奔。前端埋点我分了四类,每一类对应算法侧的一个信号:
- 曝光埋点:推荐位卡片进入可视区域时上报,用于计算曝光到点击的转化率。
- 点击埋点:用户点击卡片跳转详情页时上报,记录的是用户对推荐结果的兴趣信号。
- 下单埋点:用户从推荐位进入并完成下单时上报,这是最强正反馈。
- 评价埋点:服务完成后的评价分和评价文本,回传后参与评分权重计算。
埋点数据通过一个统一的事件上报接口打点,先落到消息队列,再异步写入行为存储。这套链路从一开始就要搭好,不要等到算法上线了才补埋点,否则冷启动阶段完全没有反馈数据,算法迭代寸步难行。
另外我还做了一个主动反馈入口:用户可以在推荐卡片上点"不感兴趣",这个信号会进入一个负样本表,算法侧在召回阶段直接过滤掉该服务人员及其高相似人群。这个功能对体验的保护作用很明显,它能快速纠偏,同时暗示用户"这个推荐位听你的",心理感受上会更友好。
5. 上线后的评估与踩坑清单
5.1 推荐效果怎么量化:离线指标和线上指标
推荐模块上线前,先离线评估再上线实验。离线评估我用的是Hit Rate@10和Recall@10,具体做法是把用户近30天的真实下单记录切成训练集和测试集,用训练集跑推荐,看测试集里的真实下单服务人员是否出现在推荐Top 10里。
但离线指标只能做初筛,真正说服业务方的还是线上AB实验。我只改推荐算法,其他不变,实验组和对照组各切一部分流量,观察两个指标:
- 推荐位点击率:验证推荐结果是否吸引人。
- 首次下单后7天内复购率:验证推荐质量是否真的稳住用户。
这里有个经验:不要一上来就全量放开推荐模块。家政平台的用户基数小、订单频次低,数据量不够时AB实验需要跑很久才能看到显著差异。我的做法是先用灰度发布影响10%的用户跑两周,确认指标没有明显负向再逐步放量。
5.2 踩坑一:热门服务人员霸榜,相似度被"伪高分"带偏
第一个大坑出现在离线相似度计算上线后的一周。我观察推荐结果,发现无论什么用户进来,推荐列表里总是那二三十个热门高级阿姨。排查下来,问题出在热门阿姨被大量用户产生行为,导致她们之间的相似度普遍偏高,倒排表里Top 50候选几乎全是热门,长尾服务人员根本没有曝光机会。
这个问题的本质是物品流行度偏差。解决方式我用了两个手段:一是引入惩罚因子,参考的是"热门物品降权"的思路,让越热门的人物在相似度计算时权重适当调低;二是在召回阶段限定"每个用户行为列表里最多贡献10个候选服务人员",避免个别高频用户的行为淹没其他用户。
修正之后,推荐列表的长尾覆盖率明显改善,冷门但好评率高的服务人员开始进入推荐位,用户点击率反而提升了,说明之前的推荐确实"千篇一律"了。
5.3 踩坑二:冷启动用户拿到空推荐列表,排查全过程
第二个坑更隐蔽。某天线上反馈群里有人截图,说新注册用户打开首页,推荐位一片空白。我一开始想当然地认为是前端渲染问题,毕竟后端接口调用我们没有报错日志。但查了一圈前端日志,接口返回200,数据是空的,问题确实在后端。
定位过程是这样的:我先查了Redis里热门兜底列表,有数据;再查推荐接口日志,发现请求全被卡在过滤阶段,过滤完之后候选集空了。继续深挖,发现新用户进入时,我传的定位是默认的"全市范围",而热门服务人员的服务区域配置只覆盖了部分行政区,在默认范围下反而匹配不上。问题不在推荐算法,而在过滤条件的默认参数设计不合理。
修复方案是:当用户没有开启定位或定位范围过大时,过滤阶段直接按"全城"匹配,同时把热门兜底的查询条件放宽到"全市好评率前50的阿姨",不再限制小区域。这个坑给我的教训是:推荐系统出问题,80%的概率在工程边界,而不是算法本身。排查时不要一上来就调算法参数,先看数据链路和参数配置。
5.4 踩坑三:定时任务内存溢出,推荐服务直接雪崩
第三个坑是定时任务引起的。离线计算任务和在线推荐服务部署在同一个应用实例里,前期数据量小没啥感觉。上线两个月后,某天凌晨定时任务一跑,内存直接溢出,OOM把整个应用实例打挂了,推荐接口全部不可用。
排查后发现,行为聚合数据全量加载进内存时,有几个高频用户的评分记录非常多,Map<Long, Map<Long, Double>>加载了几百万条记录,堆内存根本装不下。修复方案是双管齐下:一是把离线计算任务拆分成独立部署的服务,不跟在线API共用JVM堆内存;二是将聚合过程改成分批加载,按用户ID分段拉取数据,算完一段释放一段,避免一次性全量入内存。
这里我想强调的是,推荐算法的工程化程度决定了系统能走多远。算法效果再好,如果离线任务能把在线服务拖垮,那一切都是零。我的建议是,离线计算服务和在线API服务从一开始就物理隔离,这是最省心的方案。
5.5 还能往哪个方向演进
协同过滤只是这套系统推荐模块的第一版,不是终点。我在实际使用中逐渐感觉到,家政推荐的下一步可以考虑两个方向:
一是引入更多上下文特征,比如季节、天气、节假日。夏天的空调清洗需求、春节前的全屋大扫除需求,都是天然的高价值推荐场景。协同过滤算的是"人相似",但要结合场景做推荐,"时相似"可能更有用。
二是把服务人员档案的结构化信息融合进算法,比如用技能标签构建知识图谱,或者用简单的SVD/矩阵分解做embedding。尤其是新上户的阿姨没有任何历史行为,纯粹靠协同过滤完全推不出去,这时候基于内容的信息就能派上用场,可以把新人推荐给"偏好同类技能标签"的老用户。
最后分享一个小技巧:我在做推荐结果的最终排序前,加了一个"行为新鲜度"微调权重。用户最近7天浏览过但未下单的服务人员,得分微涨;用户明确点过"不感兴趣"的,直接过滤。这个微调不复杂,但对体验提升非常明显,代价几乎为零,强烈建议在做更复杂模型之前先做掉。