ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+LLM实战:考研院校推荐系统设计与可视化大屏实现

Django+LLM实战:考研院校推荐系统设计与可视化大屏实现 每年考研季咨询我最多的问题不是“怎么复习”而是“老师我该报哪个学校”。这确实是个难题实力强的怕考不上实力弱的不甘心而关键信息——历年分数线、报录比、专业课难度、区域认可度全都散落在各个网站和文件里。后来在带毕业设计时我接到一个很有代表性的题目用 Django 搭一个考研院校推荐系统让 LLM 参与需求理解和择校匹配再用大数据可视化的方式把分数线、报录比、招生趋势整合成可交互的大屏。今天把整套系统的设计思路、核心模块的落地代码以及我在实际开发中踩过的坑都写下来给正在做类似题目的同学一个能直接参考的完整方案。这个项目适合三类人看一是正在做“Django LLM 推荐系统 可视化”方向毕业设计的本科生二是想入门大模型应用开发、但不想一上来就碰分布式架构的开发者三是准备做大数据方向作品集、需要一套能演示能讲解的完整系统的同学。1. 内容整体设计与思路拆解1.1 考研择校场景里的真实需求是什么我在设计这个系统之前先把“考研择校”这件事拆成了用户视角下的几个具体问题。第一是专业匹配本科专业能报什么方向跨考可行不可行这是用户最底层的约束。第二是院校梯度用户需要知道自己处于“冲、稳、保”哪个区间这决定了推荐结果的排序逻辑。第三是趋势判断各院校分数线每年波动很大光看一年数据没有意义必须看三到五年的变化曲线。第四是地域偏好很多人考研后要在当地就业学校所在城市的产业资源和认可度会影响选择。这些需求在传统搜索式页面里很难满足。原因在于用户在表达需求时用的是自然语言比如“我想去南方计算机专业强竞争不要太大”这种描述很难变成一条 SQL 查询。而 LLM 恰恰擅长把这类模糊的自然语言转化成结构化条件这也是我在这个项目里坚持引入大模型而不是只做规则筛选的核心原因。1.2 系统模块划分数据、推荐、预测、展示四层整个系统我按功能拆成四个核心模块每个模块独立开发最后通过 Django 的 MTV 架构整合到一起。数据管理模块负责院校信息、专业目录、历年分数线、报录比的录入和清洗。这个模块看起来不起眼但它决定了后面所有功能的上限。数据不干净推荐得再准也是错的。推荐引擎模块是系统的核心。我采用了“规则过滤 LLM 精排”的混合架构。第一层用确定的规则把范围缩小比如地域、学科门类、是否 985/211、考试科目是否匹配第二层把剩余院校连同用户背景一起交给 LLM让它按“冲刺、稳妥、保底”三档输出排序结果和推荐理由。分数线预测模块基于历年数据做趋势建模。我用的是时序思想加特征工程不依赖复杂的深度模型因为考研分数线样本量太小每年只有一次数据复杂模型反而容易过拟合。最终方案是“差分趋势 加权回归”在毕业设计这个体量下足够用了。可视化大屏模块负责把推荐结果、预测曲线、报录比分布、地图院校分布统一展示。技术上选 ECharts图表种类灵活地图资源和社区示例也丰富适合做课程展示和答辩演示。1.3 为什么技术栈选 Django 而不是 Flask 或 FastAPI这个项目我坚持用 Django有几个实际考量。第一推荐系统需要管理大量结构化数据Django 的 ORM 在处理关联查询、动态筛选和管理后台方面效率极高开发管理后台几乎是零成本。第二毕业设计通常要求“功能完整、文档齐全”Django 自带 Admin、认证、表单校验、分页这些能省下大量时间。第三后面我会用到 WebSocket 做后台数据主动推送Django Channels 虽然配置麻烦一点但和 Django 生态的整体集成度比在 Flask 里硬塞异步要舒服得多。FastAPI 在异步性能和自动生成 API 文档上确实更现代但它的生态偏轻量管理后台、认证、权限这些都要自己搭。如果目标是快速交付一个完整可演示的系统Django 更适合。2. 核心细节解析与实操要点2.1 数据库建模六张表把业务串起来我设计数据模型时遵循一个原则一张表只负责一类业务实体推荐和预测逻辑通过外键和查询集关联不搞大宽表。最终落地的核心表是这六张院校表存学校名称、所在省份、城市、院校层次985/211/双一流/普通、院校类型综合/理工/师范等、是否自划线。专业表存专业代码、专业名称、所属学科门类、考试科目组合。这个表决定了“跨考匹配”的可行性判断。分数线表核心业务表关联院校和专业存历年复试线、录取线、报考人数、录取人数、报录比、推免人数。学生画像表记录用户的本科院校层次、本科专业、目标专业、意向城市、英语/数学基础评分等。推荐结果表缓存每次推荐的院校列表、冲刺/稳妥/保底标签、LLM 生成的推荐理由。用户行为表记录用户对推荐院校的收藏、点击、对比行为为后续改进推荐算法积累数据。Django 模型里有一个细节要注意院校和专业的关联是多对多因为一所学校开多个专业同一个专业也多所学校开设。但如果把分数线直接挂在多对多关系上查询会非常绕。我的做法是让分数线表自己作为中间模型通过两个 ForeignKey 分别指向院校和专业再加一个UniqueConstraint保证同一院校同一专业在同一年度只有一条记录。这样既保留了多对多语义又让分数线查询变成对单一表的简单筛选。2.2 LLM 接入不是所有逻辑都该交给大模型很多同学拿到题目的第一反应是“所有推荐都让大模型生成”这个思路在实践中一定会出问题。首先是成本问题一次完整推荐要处理几十所院校的数据全部塞进 Prompt 会产生大量 Token 消耗。其次是准确性LLM 对数值型数据比如历年分数线的处理能力并不好它更擅长语义理解和文本生成。我在项目里对 LLM 的定位是三个具体职责需求解析用户输入“想去江苏浙江这边计算机相关专业竞争不要太激烈”由 LLM 提取出省份倾向、专业方向、竞争容忍度这三个结构化参数。这也是“LLM 的 token 三个点”在实际里的用法——用户的问题是 Query院校特征数据是 Key最终生成推荐理由时模型结合两者输出 Value。特征补全有些院校的专业实力在结构化的标签数据里看不出来比如“这个学校的计算机在业界口碑很好”。我把这类信息预先整理成文本描述作为应该检索到的 Key 存在数据库里Prompt 生成推荐理由时让模型引用这些信息避免它自己编造。推荐理由生成这是 LLM 最擅长也最不容易出错的部分。规则过滤和预测模型算出候选集和分数LLM 只需要对每个候选院校写两到三句推荐理由并给出“冲刺、稳妥、保底”的判断。所有数值决策由传统算法完成LLM 只做解释层这个分工让系统既稳定又省成本。2.3 推荐策略的实现两条腿走路才稳第一层规则过滤我用 Django ORM 做链式筛选参数来自用户画像和 LLM 解析出的结构化条件。地域过滤就是province__in target_provinces层次过滤就是level__in [985, 211]或反向排除专业过滤比较复杂要先判断用户的本科专业是否能报考目标专业这里用专业表中的考试科目匹配来解决。第二层是评分排序。我为每个候选院校计算一个综合指数加权项包括近三年复试线均值、分数波动方差、报录比、院校层次系数、城市发展指数。这些因子标准化到 0 到 1 后按权重相加。权重我调了很久最后定下的经验值是分数线稳定性占 0.3层次系数占 0.25报录比占 0.25城市指数占 0.2。然后按综合分从高到低排序结合预测模块给出的“录取概率”划分三档高分而且概率高的是“保底”中分中概率是“稳妥”低分高概率之外的定为“冲刺”。这里有个经验容易踩坑很多推荐系统只按分数高低推荐会推一堆用户根本考不上的名校。加了“录取概率”分档之后推荐结果才真正对用户有参考价值。实现上这个概率来自下一节说的分数线预测模型两套算法之间是数据联动的关系。2.4 分数线预测小样本场景下怎么做才有说服力分数线预测难处在于数据太少一年一个样本点五六年数据也就五六个点直接用神经网络没有意义。我的做法是把问题转成“相对趋势预测”而不是“绝对分数预测”先算目标院校专业近五年的分数线均值再预测下一年的变化量 Δ。变化量用加权一阶差分来估计近两年的差分值权重高更早的权重低相当于给了时间衰减。举例来说某专业近三年复试线分别是 350、358、364差分值是 8 和 6取权重 0.6 和 0.4预测下一年的变化量就是 8×0.6 6×0.4 7.2那么预测分数就是 364 7.2 ≈ 371。这个模型虽然简单但在答辩时很容易讲清楚原理而且能用一个具体的 Excel 表复现整个计算过程。我还在这个基准输出上加了一层修正把 985/211 院校的报考热度系数乘进去热门院校的预测增量会略上调反之略下调。这里必须强调预测模块的定位不是保证准确而是给出一个可解释的参考区间。在系统界面上我特意展示“预测值 ± 5 分的波动带”这个做法既诚实也体现了数学建模的严谨性。3. 实操过程与核心环节实现3.1 Django 项目初始化和 App 划分我用的是 Django 4.2 LTS 版本Python 3.10。创建项目后按业务边界划分了四个 Appschools管理院校和专业数据recommend负责推荐引擎和 LLM 接入forecast做分数线预测dashboard做可视化接口和数据聚合。这里特别建议读者从第一天就按这个方式拆分而不是把所有代码堆在一个app里。毕设写到最后最痛苦的就是改需求App 边界清楚改推荐逻辑时不会动到可视化代码调试定位问题也快得多。创建一个 App 的固定命令是python manage.py startapp schools python manage.py startapp recommend python manage.py startapp forecast python manage.py startapp dashboard然后在settings.py的INSTALLED_APPS里依次注册。这步做不做对运行没影响但后面执行makemigrations时会决定哪些模型会被识别注册漏掉一个数据库表就建不出来。跨 App 访问数据模型时用from schools.models import School这类导入没问题但要注意不要形成循环导入。我遇到过的情况是recommend里导入了schools的模型schools的一个信号处理器又反向导入了recommend的工具函数启动时直接报错。解决办法是把公共工具函数抽到项目根目录下的utils.py里任何 App 都只依赖utils不跨 App 互相依赖。3.2 推荐接口的完整实现推荐接口是系统的核心我直接展示一个精简后仍然能跑通的逻辑骨架方便对照自己的代码查漏补缺。# recommend/services.py from django.db.models import Q from schools.models import Major, SchoolScoreLine def rule_filter(user_profile, parsed_params): qs Major.objects.filter( school__province__inparsed_params[provinces], school__level__inparsed_params[levels], ) # 考试科目匹配本科专业与目标专业公共科目数 2 qs qs.filter( exam_subjects__overlapuser_profile[undergraduate_subjects] ) return qs.distinct() def score_calculation(candidates, weights): scored [] for major in candidates: records SchoolScoreLine.objects.filter( majormajor ).order_by(-year)[:3] avg_line sum(r.line for r in records) / len(records) volatility max(r.line for r in records) - min(r.line for r in records) score ( weights[stability] * (1 - volatility / 100) weights[level] * major.school.level_coeff weights[competition] * (1 - records[0].competition_ratio) weights[city] * major.school.city_index ) scored.append((score, major, avg_line)) return sorted(scored, keylambda x: x[0], reverseTrue)这个阶段有三个隐藏细节要特别留意。第一个是查询集的惰性和执行时机。rule_filter返回的qs直到被遍历时才真正查数据库。如果你想在得到候选结果之前就检查数量必须调用.count()或先list()一下否则后续又添加了.distinct()之类的操作SQL 会被组合在一起行为和你预期的不一样。第二个是.filter(exam_subjects__overlap...)只在 PostgreSQL 的 ArrayField 上才支持SQLite 会报错。我用的是 PostgreSQL 部署但如果读者本地用的是 SQLite就得改成先查出来再在 Python 里做集合交集。这个坑很容易踩建议提前确认自己的数据库类型。第三个是分数线记录不足三年的问题。有些新增专业只有一年数据len(records)作为分母会得出很离谱的分数。我在代码里做了一个兜底记录数少于两年的院校直接降权并在推荐理由里注明“历史数据较少仅供参考”。这样既保住了程序的稳定性也提高了推荐的诚实度。3.3 LLM 接入的封装细节超时、重试与降级这一节可能是整个项目里最容易失控的部分因为大模型接口的不确定性比传统代码高得多。我在项目里做了一层统一的LLMClient封装核心逻辑包括超时控制、重试机制和降级策略。# recommend/llm_client.py import json import time import requests class LLMClient: def __init__(self, api_key, base_url, timeout30): self.api_key api_key self.base_url base_url self.timeout timeout def chat(self, prompt, system_prompt, max_retries3): for attempt in range(max_retries): try: resp requests.post( f{self.base_url}/chat/completions, headers{Authorization: fBearer {self.api_key}}, json{ model: qwen-plus, messages: [ {role: system, content: system_prompt}, {role: user, content: prompt}, ], temperature: 0.3, }, timeoutself.timeout, ) resp.raise_for_status() return resp.json()[choices][0][message][content] except requests.exceptions.Timeout: # 超时重试但最后一次异常直接抛出 if attempt max_retries - 1: raise except requests.exceptions.RequestException as e: if attempt max_retries - 1: # 降级返回空字符串由上层走规则推荐 return time.sleep(2 ** attempt) return 这里有几个参数值得说明。temperature我设成 0.3原因是推荐理由生成需要稳定太高的随机性会让同一用户每次刷新得到完全不同的推荐结果这在演示现场是灾难但设为 0 又会让文本非常机械选 0.3 算是在一致性和自然度之间的平衡。超时配置 30 秒听起来很长但实际大模型首 token 返回可能就要 10 到 20 秒太短的超时在低峰期也会误伤。降级策略是系统设计里最重要的一环。我在推荐流程里做了判断如果LLMClient.chat()返回空字符串说明模型调用失败系统自动退回到“规则过滤 评分排序”的纯传统推荐模式保证用户在任何情况下都能拿到推荐结果只是推荐理由那一栏会显示“模型服务暂不可用”。答辩时这个问题一定会被问到你如果能说清楚降级逻辑反而是加分项。3.4 可视化大屏的实现数据接口先行可视化部分我在dashboardApp 里提供一组只读数据接口前端通过 AJAX 拉取 JSON 后交给 ECharts 渲染。大屏我设计了四个区域分数线预测趋势图横轴是年份纵轴是分数线每条线代表一个推荐院校的目标专业。我用的是折线图加平滑曲线在最后一年数据点后延伸出预测区间看起来直观而且答辩时好讲。报录比热力地图按省份展示报考热度。这个图我用 ECharts 的地图加visualMap组件数据来自分数线表里的报录比字段按省份聚合。推荐院校对比雷达图一个页面上同时展示三所推荐院校的五个维度得分分数线稳定性、层次系数、报录比、城市指数、学科热度。这个图在做“冲稳保”三档次对比时效果特别好评委一眼就能看出推荐逻辑。核心指标数字卡顶部放几个大数字显示本年度推荐总人数、平均预测分数线、推荐匹配率、院校覆盖率。数字卡虽然简单但大屏的整体视觉层次一下子就有了。ECharts 接口约定的 JSON 结构长这样{ school: 浙江大学, major: 计算机技术, years: [2020, 2021, 2022, 2023, 2024], lines: [355, 363, 358, 371, 366], prediction: { next_year: 2025, value: 371, lower: 366, upper: 376 } }前端渲染时预测区间我加了一个标记点区域用markArea组件把 366 到 376 的区域标出来视觉上很清楚。前端代码不复杂但有一个细节要注意ECharts 的异步载入场景下如果接口还没返回就执行setOption图表会空白。我用fetch拿到数据后再初始化图表而不是页面加载时立即初始化可以避免这个常见问题。3.5 后台主动推送数据Django Channels 的配置实录这部分的起因是指导老师在中期检查时提了一个需求希望后台数据更新后前端大屏能实时刷新而不是手动刷新页面。这就要用到 WebSocket。Django 里的解决方案是 Channels配合 Redis 作为 channel layer。我踩过的坑集中在三处。第一Channels 只支持 ASGI 模式所以项目里要加asgi.py并且runserver命令要换成daphne -b 0.0.0.0 -p 8000 project.asgi:application否则 WebSocket 连接根本建立不起来。第二channel_layer的 Redis 配置必须和视图中使用的 Redis 是同一个不然消息推送不到客户端。第三前端监听消息时要处理重连逻辑因为服务端重启或 Redis 闪断都会导致连接断开。一个典型的后端推送代码片段# dashboard/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class DashboardConsumer(AsyncWebsocketConsumer): async def connect(self): await self.channel_layer.group_add(dashboard, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(dashboard, self.channel_name) async def push_update(self, event): await self.send(text_datajson.dumps(event[data]))这个消费者本身不产生数据它只负责把频道里收到的消息转发给所有连接的前端页面。真正触发推送的地方在数据更新接口里更新完数据库后往dashboard组发一条事件。这样大屏上所有相关图表都会收到通知再去拉最新接口刷新数据。4. 常见问题与排查技巧实录4.1 ORM 查询一次比一次慢N1 问题开发前期我查院校列表时直接在模板里循环major_list然后每访问一个major.school.name就触发一次新的 SQL 查询。页面加载 200 条记录后台瞬间多了 200 条查询慢得离谱。这个问题的标准解法是select_related和prefetch_related前者用于 ForeignKey 和 OneToOne 关系后者用于多对多关系和反向查询。我的查询改成majors Major.objects.select_related(school).prefetch_related(scores)select_related(school)把院校表通过 JOIN 一次拉出来prefetch_related(scores)则把每个专业关联的分数线批量查出并缓存。这样查询次数从两百多次压到两次左右。排查这类问题有个实用工具在settings.py里暂时启用django.db.backends的connection.queries或者装django-debug-toolbar。我当时是在终端的shell_plus里手动统计 SQL 条数来定位的。答辩时提到自己用select_related解决 N1 查询这个点非常加分比单纯说“系统性能好”有说服力得多。4.2 LLM 接口超时导致整页卡住这是我最开始没处理好的地方。第一次跑通全流程时推荐接口里直接同步调用大模型接口高峰期一次请求要等 30 多秒才能返回页面长时间白屏。用户的体验极其糟糕哪怕有降级逻辑体验也不够好。后来我的做法是三步走。第一步把 LLM 调用从同步改成异步任务使用 Django 的Celery Redis 作为 broker推荐请求先返回“计算中”的状态大模型生成完后再通过 WebSocket 推送到前端。第二步给同一个用户的三小时内推荐结果加 Redis 缓存重复请求直接读缓存。第三步设置更短的超时15 秒超时后立刻进入降级模式不再无限等待。当时我的取舍是毕业设计场景下“快”比“准”更重要。评委现场演示时等十几秒出结果已经算缓慢等三十秒以上基本是一场灾难。宁可先出规则推荐结果再让 LLM 结果后补刷新也不要让用户干等。4.3 分数线预测数值震荡厉害怎么办初版预测直接对原始分数做线性回归结果预测值经常出现夸张的跳跃比如 360 分直接跳到 390 分。原因很简单原始分数序列里存在异常年份比如某年专业课特别难复试线突然降了 20 分线性回归对这种异常点非常敏感。我做的修正有两层。第一层是数据平滑用三年移动平均替代单年值作为建模目标。第二层是约束变化量预测的 Δ 不能超过历史最大差分的 1.5 倍这个约束避免了爆炸式预测。这里提醒一下这类约束条件在论文里一定要写清楚它属于建模层面的决策不是一个技术实现细节答辩时老师很可能就针对这个问。4.4 Redis 可视化工具和缓存管理的配合系统里有多个地方用了 Redis缓存推荐结果、存 WebSocket 连接信息、存短期访问计数。调试这些数据时命令行redis-cli查看键值非常麻烦我推荐用Another Redis Desktop Manager这个可视化客户端。连接上之后可以直接浏览键列表查看某个 key 的剩余过期时间手动删除脏数据比敲命令高效得多。调试过程里最容易出现的问题是缓存键冲突。比如两条不同专业的推荐结果如果键名只包含用户 ID那么不同专业会互相覆盖。我的键规范是recommend:{user_id}:{major_code}:{province_hash}一眼就能看出缓存属于哪个用户和什么条件。在可视化工具里排查乱掉的缓存时这种有结构的键名能让你迅速定位问题来源。5. 一个必须做的扩展把系统讲成一个故事这个项目做到尾声时我逐渐体会到一件事毕业设计不只是代码更是一个需要评委在十分钟内理解你思路的作品。单纯演示功能远远不够必须把整个系统串成一条线。我的答辩主线是用户输入自然语言需求 → LLM 解析成结构化条件 → 规则过滤缩小候选集 → 评分排序和预测模型输出三档推荐 → LLM 生成推荐理由 → 可视化大屏呈现完整数据逻辑。这条主线贯穿了市面上最受关注的 Djangdo 后端技术、LLM 应用、推荐算法、数据预测和可视化五块内容每一块都有实际代码支撑。最终交付时我建议做成三部分一份项目源码仓库、一篇包含技术选型论证和算法设计的论文、一个可以现场演示的大屏页面。PPT 的每一页对应系统的一个模块演示时按模块逐步点击让评委跟着你的思路走而不是一上来就甩出一个大屏让他们自己琢磨。我这个系统里最有价值的经验其实是混合架构LLM 负责理解和表达传统算法负责决策和计算。在大模型被热议的当下很多人容易走极端要么全用大模型要么完全不用。实际工程里让大模型做它擅长的语义和生成把数值决策交给确定性的算法系统稳定性、成本和效果才能同时兼顾。这个原则放在考研推荐场景里成立放在几乎所有业务系统里都成立。
返回列表