资讯详情 Django + 协同过滤:从零搭建歌曲推荐系统
📅 2026/10/12 9:44:22
先交代一个场景我之前心血来潮给自己的音乐播放器加了“猜你喜欢”功能当时数据量不大用户就身边十几个朋友歌曲库也就几千首。一开始我打算用深度学习那套折腾了一个多星期召回效果反而不如一个最简单的协同过滤。后来我把推荐引擎整个推翻用Django做Web框架协同过滤算法做核心推荐逻辑重新搭了一套歌曲推荐系统效果立竿见影推荐列表从“被嘲笑”变成“真香”。这篇文章就把这套系统的完整思路、核心算法实现、Django后端细节和上线后踩过的坑一次性写清楚。项目本身不复杂但串联了数据采集、离线计算、在线服务三个层面非常适合想入门推荐系统的开发同学也适合有一两个Web项目经验、想尝试把算法落地的Django初学者。1. 项目整体设计与技术选型1.1 一句话说清系统核心逻辑整个系统做了一件什么事收集用户对歌曲的行为播放、收藏、跳过、循环把这些行为变成“用户对歌曲的评分”然后喂给协同过滤算法算出来“和用户之前喜欢的歌相似的其他歌”最终通过Django的Web接口把推荐结果展示给用户。流程上拆开就是行为采集 - 评分矩阵构建 - 相似度计算 - Top N 推荐 - 展示与反馈回收。这个链路看起来简单但是每一步都有可以展开抠的细节后面我会逐一讲。1.2 为什么选Django而不是Flask或Node我见过很多人做类似项目喜欢用Flask因为代码少、启动快但我最后还是选了Django。原因不是Flask不好而是这个系统里Django有几个决定性的优势首先Django自带ORM和Admin后台。推荐系统最麻烦的不是算法是数据。歌手、专辑、歌曲标签、用户评分、播放日志这些数据如果不用ORM光SQL就得写一大堆。更关键的是Admin后台我可以直接可视化地给歌曲打标签、查看用户评分分布、修正脏数据这在调算法的时候是救命用的。用Flask想做这个得自己搭后台界面时间成本完全不一样。其次Django的session、auth、CSRF、分页这些组件是开箱即用的。推荐系统最终是一个产品不是一段脚本。用户登录、推荐结果分页、防止跨站请求伪造这些直接用Django自带能力就能搞定不用去拼第三方库。第三Django的迁移机制对迭代非常友好。推荐系统的数据模型一定会频繁改动比如一开始只有评分表后来要加播放日志表Django的makemigrations和migrate能保证数据库结构平滑演进这个在后面加功能时帮了大忙。当然不是没有代价。Django框架重、学习曲线比Flask陡、请求生命周期里的中间件和信号机制需要额外理解成本。但如果你要做的系统包含用户体系、数据后台和算法接口Django的综合成本反而是最低的。1.3 为什么协同过滤能解决核心问题选择协同过滤我是做过对比试验才定的。当时我拿同样的用户行为数据分别跑了基于协同过滤和基于歌曲内容的简单分类匹配看离线评测指标准确率和召回率。结果协同过滤明显占优原因也好理解第一协同过滤不依赖歌曲的元数据。内容推荐需要给歌曲打标签比如“摇滚”“治愈”“民谣”这个标注成本极高而且风格这东西主观性太强。协同过滤只看用户行为一首歌说不清是什么风格但只要“喜欢A歌的人大多也喜欢B歌”这个关系就能被算法捕捉到这种隐含关联是内容画像很难做出来的。第二协同过滤能发现“意外之喜”。内容推荐很容易困在“同质化”里——你听民谣就一直推民谣。协同过滤不一样它通过用户群体的行为交叉能跨风格推荐。我实测就出现过用户之前只听国语流行结果被推荐了一首英文爵士并标注了喜欢。这种跨域发现是协同过滤的典型优势。第三落地成本低、可解释性强。深度学习模型要显卡、要调参、要理解Embedding协同过滤只需要一张评分矩阵加上简单的相似度计算就能跑出还不错的离线指标。而且“因为和你口味相似的用户也喜欢这首歌所以推荐给你”这种解释是用户能理解的做产品展示时很加分。当然协同过滤也有明显的弱点——冷启动和稀疏性。这个我会在2.4节单独说处理方案。1.4 系统模块划分整个系统我分了五个模块Django的app结构也照着这个划分来accounts用户注册、登录、个人偏好设置。music歌曲、歌手、专辑、歌单的模型和管理后台。interaction行为采集接口播放、收藏、跳过、循环的日志接收。recommender核心推荐引擎包含协同过滤算法的训练与预测。api对外提供RESTful接口给前端App或网页端调用。这样划分之后算法模块和业务模块严格解耦。推荐引擎只接收数据、产出结果具体的展示逻辑都在api层。后来的维护经历告诉我这种解耦做得越早后面换算法的时候就越从容。2. 协同过滤算法原理与代码实现2.1 UserCF和ItemCF音乐场景应该选哪个协同过滤有两大门派一个叫基于用户的协同过滤UserCF一个叫基于物品的协同过滤ItemCF。两者的核心思想都是“物以类聚”但聚的对象不一样维度UserCFItemCF基本思想找到和你口味相似的用户推荐他们喜欢的歌找到和你喜欢歌曲相似的歌推荐这些歌适用场景新闻、社交内容用户兴趣变化快电商、音乐、视频物品相对稳定计算开销用户数量大时计算量爆炸物品数量相对少易离线计算可解释性“和你相似的人也喜欢”“因为你喜欢某歌所以推荐这首歌”实时性用户行为变化后要即使更新相似用户可以离线算好相似物品表线上直接查我查了一下我们系统的数据分布歌曲只有几千首用户虽然也会涨但早期就是几十个人。理论上两个都能算但仔细一权衡还是选了ItemCF原因有两点。第一歌曲的数量增长速度远低于用户数量。用户会越来越多但曲库的增长是可控的。ItemCF只关心“歌曲之间的相似度”这个矩阵的大小是歌曲数乘以歌曲数几万首歌以内离线计算都没问题。UserCF如果用户量到了百万级实时算用户相似度矩阵基本不现实。虽然现在我的系统体量不大但选型时得给将来留余地。第二音乐的消费特点是“歌曲稳定、用户口味相对持久”。ItemCF抓住的是歌曲间的固定相似关系不随用户变化而漂移适合歌曲这种半衰期很长的内容。2.2 相似度计算的数学基础相似度计算是协同过滤的心脏。音乐场景我优先用余弦相似度公式是[ sim(i, j) \frac{\sum_{u \in U} r_{ui} \times r_{uj}}{\sqrt{\sum_{u \in U} r_{ui}^2} \times \sqrt{\sum_{u \in U} r_{uj}^2}} ]翻译成人话把每个用户对歌曲的评分看作一个多维向量的一维两首歌之间的相似度就是这两个向量夹角的余弦值。如果两首歌被同一群用户打了类似的分数夹角就小余弦值接近1说明相似度高。单独实现这个函数很简单Python代码也就十几行import math def cosine_similarity(vec_a, vec_b): vec_a, vec_b: dict, 形如 {user_id: score} common_keys set(vec_a.keys()) set(vec_b.keys()) if not common_keys: return 0.0 dot sum(vec_a[k] * vec_b[k] for k in common_keys) norm_a math.sqrt(sum(v * v for v in vec_a.values())) norm_b math.sqrt(sum(v * v for v in vec_b.values())) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)这里有个关键点用户在评分时因为性格差异有人天生喜欢打高分有人打分普遍偏低。如果不做处理余弦相似度会偏向“打分均值高”的用户。修正办法是调整余弦相似度Adjusted Cosine Similarity在计算前把每个用户的评分减去该用户对所有歌曲的评分均值import math from collections import defaultdict def adjusted_cosine_similarity(song_a_ratings, song_b_ratings): # song_a_ratings, song_b_ratings: dict, {user_id: rating} common_users set(song_a_ratings.keys()) set(song_b_ratings.keys()) if not common_users: return 0.0 # 计算每个用户的平均评分 user_ratings defaultdict(list) all_song_ratings [song_a_ratings, song_b_ratings] for song_ratings in all_song_ratings: for user_id, rating in song_ratings.items(): user_ratings[user_id].append(rating) user_mean {uid: sum(rs) / len(rs) for uid, rs in user_ratings.items()} dot 0.0 norm_a 0.0 norm_b 0.0 for uid in common_users: adj_a song_a_ratings[uid] - user_mean[uid] adj_b song_b_ratings[uid] - user_mean[uid] dot adj_a * adj_b norm_a adj_a * adj_a norm_b adj_b * adj_b if norm_a 0 or norm_b 0: return 0.0 return dot / (math.sqrt(norm_a) * math.sqrt(norm_b))我在离线评测里对比过用调整余弦相似度后推荐列表的准确率提升了好几个百分点原因就是它削掉了用户打分习惯带来的偏差。2.3 完整的ItemCF实现流程整个ItemCF的流程我按四步走构建评分矩阵 - 计算歌曲间相似度 - 选出每首歌的Top K相似歌曲 - 根据用户历史评分加权预测得分。第一步从数据库里把用户行为数据读出来构造成“歌曲 - 用户评分列表”的结构。在代码里我一般用字典嵌套# {song_id: {user_id: rating}} song_user_ratings defaultdict(dict) # 伪代码从数据库拉取 ratings 表 for record in Rating.objects.filter(rating__gt0).values(user_id, song_id, rating): song_user_ratings[record[song_id]][record[user_id]] record[rating]第二步用调整余弦相似度遍历计算每一对歌曲的相似度。这里有个优化不用真的算所有歌曲两两之间的相似度只有那些出现在同一个用户评分列表里的歌曲才有共同用户。构建一个“用户 - 歌曲列表”的倒排表可以省掉大量无效计算# 构建用户歌曲列表用于提速 user_song_ratings defaultdict(dict) for song_id, ratings in song_user_ratings.items(): for user_id, rating in ratings.items(): user_song_ratings[user_id][song_id] rating第三步对每一首目标歌曲取相似度最高的K首作为候选K我一般取20。这K首歌曲的相似度值会持久化到一张表里线上推荐的时候直接查这张表不用再现场跑算法。第四步也是预测评分的关键步骤。用户u对歌曲s的预测评分用下面这个加权公式[ \hat{r}{us} \frac{\sum{i \in N(u) \cap S} sim(i, s) \times r_{ui}}{\sum_{i \in N(u) \cap S} sim(i, s)} ]这里的含义是找出用户u评过分的且和目标歌曲s足够相似的歌曲集合用这些歌曲的评分按相似度加权平均。实现代码def predict_rating(user_id, target_song_id, similar_songs, user_ratings, k20): similar_songs: {song_id: similarity}, 目标歌曲的相似歌曲及相似度 user_ratings: {song_id: rating}, 该用户的历史评分 weighted_sum 0.0 sim_sum 0.0 for song_id, sim in similar_songs.items(): if song_id in user_ratings: weighted_sum sim * user_ratings[song_id] sim_sum sim if sim_sum 0: return 0.0 return weighted_sum / sim_sum最后对用户的候选歌曲通常是用户没听过的歌统一算一遍预测分按分数从高到低取前N首就是最终的推荐列表。整个流程跑下来离线实验里我对几百个用户的推荐效果做了抽样人工评估大概有七成以上的推荐结果是说得过去的。2.4 冷启动问题的应急策略协同过滤最怕冷启动。新用户一个行为都没有矩阵里没有他的向量算法直接罢工新歌曲没有任何人听过相似度全是0。我的处理方法是分层兜底不用一个算法扛所有场景新用户直接推荐全局热门歌曲榜按播放总量排序去掉用户已经明确不喜欢的类型。老用户用ItemCF生成个性化候选再混入一定比例的热门歌曲打散避免推荐结果太窄。新歌曲打上“新歌”标识通过“相似热门歌曲”拉新也就是利用歌曲风格、艺人、标签等元数据先做一层内容相似映射让新歌能有初始曝光。这套组合策略严格说已经不算是纯协同过滤了但推荐系统本来就是一个系统工程“算法策略”缺一不可。系统刚上线那阵子新用户流量主要靠热门榜兜底等行为攒够了再切入个性化推荐。3. Django后端实现模型设计、行为采集与API3.1 数据模型怎么设计才合理Django里模型设计得好不好直接决定后面写算法的效率。我最终落地的模型有四张核心表from django.contrib.auth.models import User from django.db import models class Song(models.Model): title models.CharField(max_length200) artist models.CharField(max_length200, db_indexTrue) album models.CharField(max_length200, blankTrue) genre models.CharField(max_length50, blankTrue, db_indexTrue) duration models.IntegerField(default0) # 单位秒 play_count models.BigIntegerField(default0) created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [ models.Index(fields[artist, title]), ] def __str__(self): return f{self.title} - {self.artist} class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) song models.ForeignKey(Song, on_deletemodels.CASCADE, related_nameratings) rating models.FloatField(default0) # 0~5分 created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, song) # 一个用户对一首歌只保留一条评分 indexes [ models.Index(fields[user, song]), ] class PlayLog(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameplay_logs) song models.ForeignKey(Song, on_deletemodels.CASCADE, related_nameplay_logs) action models.CharField(max_length20) # play / skip / favorite / repeat created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [ models.Index(fields[user, created_at]), ] class SimilarSong(models.Model): song models.ForeignKey(Song, on_deletemodels.CASCADE, related_namesimilar_items) similar_song models.ForeignKey(Song, on_deletemodels.CASCADE, related_namesimilar_from) similarity models.FloatField() updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [song, -similarity]几个设计细节我特别想强调第一Rating表用了唯一约束unique_together。这个约束保证用户对同一首歌的评分不会出现多条记录更新时用update_or_create即可省得在代码里做重复判断。第二PlayLog只存原始行为不直接参与相似度计算。评分是额外算出来的这样保留了灵活性。比如我想把“循环播放”加权重只需要在构造评分矩阵时调整转换公式改代码就能生效不需要改表结构。第三SimilarSong表是为了线上查询准备的。离线计算完相似度之后直接落库线上接口查这张表就行千万不能让生产环境每次请求都现场跑一遍协同过滤。3.2 用户行为怎么变成评分用户不会主动给歌打分他们只会播放、收藏、跳过。我得把这些隐式反馈转化成算法能用的评分。这是我早期踩过坑最多的地方因为转化规则直接决定数据质量。我制定的规则如下播放完成超过30秒基础分1分。播放完毕听到最后加1分。主动收藏直接加到5分。加入心愿单/歌单加2分。跳过播放不足10秒减1分。循环播放每次额外加0.5分上限5分。综合下来评分范围严格限制在0到5分之间低于0分按0分处理。这个转换逻辑封装成一个独立的函数因为它是连接“行为系统”和“算法系统”的桥梁必须单独测试from django.utils import timezone from datetime import timedelta def compute_rating_for_user_song(user_id, song_id): logs PlayLog.objects.filter(user_iduser_id, song_idsong_id) score 0.0 for log in logs: if log.action play and log.duration_played 30: score 1.0 if log.action complete: score 1.0 if log.action favorite: score 5.0 if log.action add_to_playlist: score 2.0 if log.action skip: score - 1.0 if log.action repeat: score 0.5 return max(0.0, min(5.0, score))注意PlayLog需要加一个duration_played字段记录实际听了多长时间这个是判断播放和跳过的关键。这个字段是我后来补上的最早设计表的时候没考虑结果没法区分“听完”和“随便点了下”数据质量低了一大截。3.3 推荐接口的在线与离线拆分线上推荐接口不能直接调用算法训练代码。我的做法是拆成两层离线层用Django的自定义管理命令触发跑一遍完整的ItemCF训练流程把相似歌曲写入SimilarSong表把推荐结果按用户粒度缓存到Redis或者数据库推荐结果表。流程大概是python manage.py compute_similarity python manage.py generate_recommendations --user-range 0:10000在线层则是一个DRF接口逻辑非常简单拿到当前用户的ID从缓存中取出推荐歌曲ID列表再查歌曲详情返回。核心代码长这样from django.http import JsonResponse from django.views.decorators.http import require_GET from django.core.cache import cache from music.models import Song require_GET def recommended_songs(request): user_id request.user.id if not user_id: return JsonResponse({error: unauthorized}, status401) # 优先从缓存读取 cache_key frecommend:{user_id}:top50 song_ids cache.get(cache_key) if song_ids is None: # 兜底策略返回热门歌曲 song_ids list( Song.objects.order_by(-play_count)[:30].values_list(id, flatTrue) ) else: # 过滤掉用户已经强烈不喜欢的歌 disliked_ids set( Rating.objects.filter(user_iduser_id, rating0).values_list(song_id, flatTrue) ) song_ids [sid for sid in song_ids if sid not in disliked_ids] songs Song.objects.filter(id__insong_ids) data [{id: s.id, title: s.title, artist: s.artist} for s in songs] return JsonResponse({recommendations: data})这样拆完以后整个系统的响应速度完全可控。算法训练再慢也不影响在线服务质量因为线上只查缓存和数据库不做矩阵运算。实际压测单接口QPS轻松过百对一个个人项目来说完全够用。3.4 定时离线计算怎么安排一开始我用的Crontab直接调管理命令后来发现日志没人看、出错没人知道就换成了Django的Channels定时任务。说实话项目初期上Celery有点重我用的是APScheduler集成到Django里简单够用# utils/scheduler.py 简化示例 from apscheduler.schedulers.background import BackgroundScheduler from django_apscheduler.jobstores import DjangoJobStore scheduler BackgroundScheduler() scheduler.add_jobstore(DjangoJobStore(), default) def start_scheduler(): scheduler.add_job( generate_all_recommendations, triggercron, hour3, minute0, idrecommend_daily, replace_existingTrue, ) scheduler.start() # 在 AppConfig.ready() 里调用 start_scheduler()定时任务选择在凌晨3点跑训练是因为这个时段线上流量最低训练任务即便占用较多资源也不会影响用户体验。跑完训练之后主动清掉所有用户的推荐缓存保证第二天用户拿到的数据是新的。4. 环境搭建与新手避坑指南4.1 从零到一搭建Django项目如果你是从头开始搭这套系统环境准备阶段有几个小细节值得注意。先确保Python版本我用的是Python 3.10Django用的LTS版本4.2这两个版本组合比较稳。建议在虚拟环境里安装别直接装到系统Python里python3 -m venv venv source venv/bin/activate pip install django4.2 pip install djangorestframework pip install django-cors-headers pip install redis创建项目和Appdjango-admin startproject music_recsys cd music_recsys python manage.py startapp music python manage.py startapp accounts python manage.py startapp recommender python manage.py startapp api这里有个非常多新手容易忽略的点创建完App之后必须到settings.py的INSTALLED_APPS里手动注册否则执行makemigrations时Django根本不会扫描新App的models.py数据表永远创建不出来。我当时因为没有注册recommender纠结了一晚上为什么迁移说“没有检测到任何改动”。4.2 在PyCharm中导入已有Django项目的正确姿势如果你的项目是在另一台机器上创建的要在PyCharm里打开不要直接双击打开文件夹就完事。正确做法是第一用PyCharm的Open功能选择项目根目录这个根目录应该是包含manage.py的那一层不是外层再套一层的目录。第二配置Python解释器在Settings - Project - Python Interpreter里选Existing环境指向你虚拟环境里的python比如venv/bin/python。这一步配错后边会一直报ModuleNotFoundError: No module named django。第三注册Django支持。在Settings - Languages Frameworks - Django里勾选Enable Django Support把manage.py路径填进去。不配置这个PyCharm的终端不会激活manage.py相关的快捷命令Django Console也跑不起来。第四如果Requirements里改了依赖记得在Terminal里跑pip install -r requirements.txt。常见的问题是依赖装了但PyCharm解释器没选对导致编辑器标红但实际能跑这个最容易被误导。4.3 Django查询与删除对象的常见误区热搜词里有一条“django执行查询-删除对象”这确实是新手重灾区。我在维护这套系统时也踩过几个get与filter不可混用。get返回的是单个模型对象查不到就抛DoesNotExist异常查到多条抛MultipleObjectsReturned。filter返回的是QuerySet即使只有一条也是一条QuerySet。很多人写a Song.objects.filter(id1)然后直接用a.title直接AttributeError。正确写法是a Song.objects.get(id1)。删除对象要小心级联。Django的外键on_deletemodels.CASCADE意味着删除一个用户会连带删除他所有的评分记录。有次我清理测试用户把几百个用户的评分全清光了影响力直接波及推荐算法训练数据。后来我把用户删除改成软删除或者先导出备份再删。代码里删除的正确写法# 删除单条 song Song.objects.get(id1) song.delete() # 批量删除 Rating.objects.filter(rating0).delete() # 注意delete() 返回的是元组 (删除总数, 按类型统计的字典) deleted_count, detail Rating.objects.filter(rating0).delete() print(deleted_count)QuerySet是惰性的。惰性意味着objects.filter()执行时不会立刻查数据库真正执行发生在迭代或切片时。这个特性在调试时容易造成困惑你可能看到日志显示SQL执行时间和你想象的对不上。4.4 数据量不大时也要注意的性能细节虽然是个人项目但很多细节早点注意后面不用返工一是给外键字段加db_index。歌曲表、评分表、播放日志表里查询条件通常落在user_id、song_id、created_at这些字段上不加索引的话联合查询会随数据量线性变慢。二是查询时用values_list代替对象化查询比如只需要歌曲ID时values_list(id, flatTrue)直接返回一个列表省掉ORM实例化开销。三是使用缓存用户推荐的Top N结果已经算好放在Redis不要再从数据库临时拼。四是避免N1查询。Django里你想获取每首歌的歌手名如果循环里每次执行song.artist就会产生大量SQL正确做法是用select_related或prefetch_related。5. 联调测试与常见问题排查实录5.1 依赖与版本冲突这套系统开发过程中我遇到过三个印象比较深的报错都是新手容易卡住的第一个是连接MySQL时报错django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.3 or newer is required。这个需要安装pymysql然后在项目的__init__.py里执行import pymysql pymysql.install_as_MySQLdb()第二个是静态文件404。Django生产模式默认不提供静态文件服务但你开发模式下如果settings.py的DEBUGFalse也一样找不到。建议开发阶段DEBUG保持True部署时用Whitenoise或者Nginx托管静态文件。第三个是Python版本兼容问题。协同时过滤计算用到了字典合并语法和f-string这些都需要Python 3.8以上如果你用老版本会直接SyntaxError。我踩过一次在服务器上默认是Python 3.6跑管理命令直接挂掉。5.2 推荐结果“永远不变”的排查系统上线后最常被问的问题是为什么用户点了很多歌推荐列表还是老样子这个问题的排查顺序很重要我总结了一个固定流程先看行为日志有没有写进来再看评分有没有更新然后看相似度表更新时间最后看缓存有没有失效。实际排查发现最常见的原因是行为日志确实写了但跑推荐结果时只读取了SimilarSong表而SimilarSong表是凌晨3点定时的任务刷的所以当天无论如何点歌推荐结果都不会变。这个不是bug是主动设计。但如果用户当天行为要立刻生效就得把“在线实时更新”做进去。我在实际项目里增加了“最近2小时行为增量重算”的任务低并发时跑一次也就几秒钟。还有一次缓存键写错了。缓存key用的是user_id但id在测试环境下从1开始A用户和B用户可能在不同环境里拿到了同一个key的数据导致互相污染。后来缓存key强制加上了用户标识前缀。5.3 矩阵稀疏导致推荐质量下降用户数少的时候评分矩阵稀疏到什么程度呢几十个用户、几千首歌有评分的网格连1%都不到。这时候协同过滤算出来的相似度很不可靠因为“共同评分过两首歌”的用户可能只有一个计算出来的共现关系偶然性很强。我的解决办法是三重降噪第一只保留播放次数达到一定阈值的用户行为过滤掉一次性用户第二调节余弦相似度时用IUFInverse User Frequency惩罚热门歌曲也就是喜欢冷门歌的用户给的评分权重更高因为这些行为更有区分度第三把评分离散化把0-5分的评分映射成“喜欢/不喜欢/无反馈”三档减少稀疏值带来的噪音。这个改进在离线评测中表现非常明显推荐列表的多样性指标上升了近两成。如果你发现候选歌曲总集中在那几首热门歌上大概率就是没有做IUF惩罚。5.4 数据库查询慢与连接数打满推荐系统上线后用户量上来了最直接的表现是Django日志里出现大量数据库连接超时。排查后发现两个原因一是每个接口都做了多次查询二是连接池配置默认太小。优化方法也很简单直接。第一推荐列表页用prefetch_related一次性把歌曲、歌手、评分数据全部取出来避免逐条查询。第二数据库连接池配置调大并把连接复用打开。第三把播放日志这种高频写操作丢进队列异步处理不能直接同步写数据库否则一个高并发瞬间就能把数据库拖垮。合作开发时可以遇到一个更隐蔽的问题推荐引擎训练时读取全部评分数据和线上接口的高并发查询同时进行导致数据库锁冲突。我把训练改成读从库或者限制训练任务在凌晨低峰期跑冲突就消失了。6. 实验数据与效果评估最后聊聊这套系统的效果验证。我自己的习惯是算法改不动手先跑离线评测用历史数据模拟“预测过去”。我按时间把数据集切成了训练集和测试集前80%的用户行为做训练后20%的行为做测试用准确率和召回率评估Top 10推荐效果。协同过滤加调整余弦相似度后准确率大概在0.23到0.28之间召回率在0.15左右。这个数值不高但对一个数据量稀少的个人项目来说已经不错了至少比随机推荐准确率不足0.01和热门榜推荐准确率不到0.05强了好几倍。真实上线后又跑了一段时间我统计了用户对推荐歌曲的点击率推荐结果中被点击播放的比例稳定在15%上下对比之前的纯热门榜推荐提升了接近一倍。有个挺有意思的细节是用户收藏推荐歌曲的比例比搜索歌单收藏的比例还高说明推荐结果确实有命中口味。顺便说一句评估不能只看准确率。一个只知道推周杰伦的系统准确率可能不低但用户体验会很差。我额外跟踪了两个指标推荐结果里不同歌手的覆盖数量多样性和用户对推荐歌曲的跳过率挫败感。协同过滤在多样性上天然有优势但需要把相似度权重微调别让头部热门歌曲占据太高比例。7. 写在最后的一点体会这套基于Django和协同过滤的歌曲推荐系统算法本身不算高深真正复杂的是工程链路从日志埋点到行为清洗从离线训练到线上缓存从冷启策略到效果评估每个环节都有坑。我个人的经验是第一次做推荐系统别一上来就上深度学习模型先用协同过滤把整个数据链路跑通。你会在这个过程中理解行为数据的重要性、评分机制的影响、离线与在线架构的拆分逻辑。这些经验在任何推荐系统里都是通用的。等数据量为足够大以后再把协同过滤升级成向量召回、多路召回加排序模型底子已经打好了。最后分享一个小技巧给推荐系统写日志的时候一定要记录“用户对推荐结果的响应”——点没点、听完没、收藏没。这套闭环反馈数据是你后续优化算法最重要的宝藏。我前期没记录导致想分析推荐效果时无从下手后来补上这个日志才真正开始用数据驱动优化。