每年毕业设计的选题库里“旅游网站”几乎是大热门但说实话十个里面至少有八个做成了标准的增删改查演示——用户注册登录、景点列表、后台维护顶多再加一个关键字搜索。答辩的时候老师问一句“技术亮点在哪里”很多人当场就卡壳。这次我想分享的是一个完全不同的做法用Django 作为 Web 框架底座接入 LLM 大模型做智能路线规划再叠加数据分析与个性化推荐系统让同一个旅游选题变成“大数据 人工智能 前后端全栈”的综合项目。如果你正在为计算机毕业设计选题发愁或者已经选了旅游方向但想让项目多几个能打的亮点这篇内容应该能给你一条完整的可复现思路。1. 为什么选这个题从“旅游网站 CRUD”到“AI大数据”的选题跨越1.1 选题的痛点大众旅游网站为什么容易被毙旅游类系统的开发门槛确实很低一个基础版的网站会用 Django 的模板系统加 ORM再配合 Bootstrap 就能拼出来。问题恰恰出在这里入门难度低也就意味着区分度低。当全班同学有一批人都在做类似的系统时你很难在功能层面做出差异。更关键的是很多同学把精力全放在“能不能跑通”上忽略了“用了什么方法”、“解决了什么问题”这两件导师真正关心的事情。我见过不少真实情况系统功能齐全代码也写得规整但一问到推荐算法、路线规划策略回答是“用的是数据库排序”“就把景点按评分排了下序”。这种项目如果放在七八年前还能拿出来讲一讲现在大家都开始关注 AI 和大数据纯 CRUD 项目的技术含量确实很难撑起一份毕业设计。1.2 这个题目的技术栈组合逻辑“Django LLM 大模型 数据分析 个性化推荐系统”看起来是四个模块的简单叠加实际上它们的协作关系很自然模块技术选型解决的问题Web 框架Django 4.x Django REST Framework用户管理、景点管理、接口服务、后台管理大数据采集存储公开数据集 爬虫 关系型数据库构建景点、评论、用户行为等基础数据路线规划引擎遗传算法 LLM 语义精调根据偏好生成多日行程路线个性化推荐协同过滤 内容画像 融合加权解决“看什么、玩什么”的个性化问题数据分析Pandas ECharts景点热度、用户画像、趋势分析、可视化大屏异步与缓存Celery RedisLLM 调用限流、热门数据缓存、离线计算任务这个组合的好处是每一个模块都可以独立成为答辩的一个亮点。Django 负责工程的规范性路线规划和推荐系统体现算法能力LLM 贴合当下的技术热点数据分析模块又让项目从“业务系统”升级成“数据驱动系统”。即使导师对某一个方向追问得很深你也能拿出对应的方法论和实验数据。1.3 四个能撑起答辩的关键词在做这个项目之前我先把答辩时想突出的关键词列了出来混合式路线规划不是单纯调 LLM 生成一份行程而是先通过传统优化算法生成可行解再由大模型对行程做语义化调整。个性化推荐落地针对新用户冷启动、老用户行为稀疏等情况设计不同策略并用离线指标验证。工程化接入 LLM包含结构化输出、缓存降级、成本控制不是粗暴地“调一个接口返回一段文字”。数据可视化闭环从数据采集、清洗、存储到分析和图表展示体现完整的工程链路。这四个点只要有一个跟导师讲透项目就不会再被当成“普通管理系统”。2. 系统架构与数据层设计先让系统“有话可说”2.1 整体分层架构不管功能多新颖底子仍然是工程问题。我按分层思想把系统拆成四层避免所有逻辑都堆在视图中数据采集层处理景点数据导入、公开数据集解析、模拟用户行为生成通过定时任务或离线脚本完成。业务逻辑层Django 的 app 分别管理用户、景点、路线、推荐、统计五个核心业务。算法层路线规划引擎和推荐引擎独立成服务模块与 Django 视图解耦方便在本地脚本里做实验。展示层前端页面、可视化大屏、REST API 接口各端通过 JSON 交互。分层带来的直接好处是后来调试推荐参数时我不用在 Django 的 View 里东翻西找直接在算法模块里写个入口函数就能跑通。这一点在毕业设计后期加班改 bug 时特别重要。2.2 数据来源与清洗真实数据不够模拟数据来凑做旅游系统最绕不开的问题是数据从哪来。真实的旅游平台数据通常不开放直接爬取又涉及合法合规问题所以我采用了“公开数据集 少量可控爬取 模拟行为数据”的组合方案。公开数据主要用于景点基础信息包括名称、所在城市、标签、经纬度、评分、门票区间等。爬虫数据只做补充比如景区的一句话简介、网友评论中的高频词。最关键的是用户行为数据——真实反馈很难获取我编写了一个模拟用户兴趣的生成脚本按照“喜欢古镇的人大概率也关注历史遗迹”“亲子用户更关注动物园、科技馆”这类规则生成 1000 个虚拟用户和若干条浏览、收藏、评分记录数据量足够支撑推荐算法的离线实验。清洗阶段有几个标准动作必须做去重同一景点在不同数据源里的名称不一致按“城市名称”做一次归一化。字段统一经纬度统一成小数格式评论时间统一成 UTC 存储。缺失值处理游玩时长缺失时按景点类型填默认值例如博物馆类默认 2 小时山岳类默认 4 小时。2.3 核心数据模型设计数据模型直接决定了后面所有模块的写法。我设计了这些核心表class Attraction(models.Model): name models.CharField(max_length128) # 景点名称 city models.CharField(max_length64) # 城市 tags models.ManyToManyField(Tag) # 景点标签 rating models.FloatField(default0.0) # 平均评分 heat_index models.FloatField(default0.0) # 热度指数 avg_play_hours models.FloatField(default2.0) # 平均游玩时长 season_factor models.JSONField(defaultdict) # 各季节的游玩适宜度 lat_lng models.CharField(max_length64) # 经纬度 description models.TextField(blankTrue) # 简介 class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) preferences models.JSONField(defaultlist) # 用户偏好的标签列表 class RoutePlan(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) params models.JSONField(defaultdict) # 规划参数城市、天数、偏好 result models.JSONField(defaultdict) # 结构化路线结果 created_at models.DateTimeField(auto_now_addTrue) class BehaviorLog(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) attraction models.ForeignKey(Attraction, on_deletemodels.CASCADE) action models.CharField(max_length16) # view/collect/rating score models.FloatField(default0.0) created_at models.DateTimeField(auto_now_addTrue)这里最值得设计的其实是RoutePlan 表。它不仅保存用户提交的行程参数还把 LLM 返回的结构化结果直接存成 JSON 字段。这样用户在“我的行程”里可以随时查看历史规划同时开发阶段也方便对比不同 Prompt 输出的效果差异。2.4 特征工程让算法有“料”可用路线规划和推荐系统都需要特征工程支撑。我给每个景点计算了四类特征热度指数根据评论数量、评分、收藏次数加权得到一个 0 到 10 的分值。季节因子不同季节的适宜游玩度。比如某个山岳型景点夏季 9 分冬季只有 4 分。平均游玩时长用于路线规划时计算每日行程是否过满。标签向量将“古镇、历史、亲子、美食”等标签做多热编码配合 TF-IDF 补充评论关键词向量。这些特征统一在景点数据处理脚本中提前算好入库之后算法模块直接读取不在请求链路里做耗时计算。3. 路线规划核心算法从贪心到“遗传算法 LLM 语义精调”3.1 问题建模路线规划到底在解什么路线规划的输入很明确用户选择一个城市、出行天数、偏好标签比如“古镇”“美食”“自然风光”系统输出一份多日行程每天包含若干景点、游览顺序、就餐建议。这个问题的核心约束是每天安排的景点数量要合适控制在 3 到 5 个。相邻景点之间要留出合理的移动时间不能把城市两端的景点排在同一天。景点之间要让用户玩得“顺”主题最好接近。整体行程要尽量覆盖用户感兴趣的标签类型。如果只用贪心算法每天挑一个“当前匹配度最高”的景点经常会出现前两天全是同类景点后面发现时间不够或者地图上绕来绕去的问题。贪心思路在单目标场景下好用但在多约束多目标的组合优化里就比较粗糙了。3.2 为什么选中遗传算法对比了动态规划、蚁群算法和遗传算法之后最终选了遗传算法。理由有三点编码灵活染色体可以用景点序列表示天然适合描述“一天去哪些景点、按什么顺序玩”。目标函数容易扩展适应度函数里可以综合兴趣匹配度、时间合理性、路线连贯性等多个维度后面加惩罚项也方便。答辩好解释遗传算法是教科书里的经典算法导师容易接受同时又有可以深入讨论的细节比如选择策略、交叉算子、收敛条件。3.3 遗传算法的具体实现思路染色体编码我用的是整数序列加分组信息。比如“第 1 天A 景点、B 景点第 2 天C 景点、D 景点、E 景点”就编码为两条信息景点 ID 序列和每天对应的景点数量。适应度函数设计如下fitness w1 * 兴趣匹配度 w2 * 时间评分 w3 * 路线连贯性 - w4 * 超额惩罚 兴趣匹配度 用户偏好标签与景点标签的余弦相似度 时间评分 1 / (1 |每日预计游玩时长 - 期望游玩时长|) 路线连贯性 相邻景点标签一致性的平均得分 超额惩罚 当天预计游览时间超过可用时间的小时数算法主体流程不复杂def genetic_optimize(candidates, days, preferences): population init_population(candidates, days, size100) for _ in range(200): fitness [cal_fitness(ind, preferences) for ind in population] selected tournament_selection(population, fitness, k5) offspring crossover(selected, rate0.8) offspring mutate(offspring, rate0.1) population population[:10] offspring # 保留精英 return best_population(population)实际调试中发现种群大小、交叉率、变异率非常敏感。我最初把变异率设成 0.5结果算法始终收敛不到稳定路线总是在几个解之间横跳调整到 0.1 之后效果稳定了许多。这个调参过程本身在答辩时也是一段很好的故事素材。3.4 LLM 语义精调算法给骨架模型给血肉遗传算法能生成一条“时间上合理”的路线但它有一个致命短板它不会解释也不会考虑一句话层面的体验感。比如算法可能把某座寺庙安排在下午人流高峰期或者在连续两天安排爬山行程完全没有意识到用户会累。所以我在算法层加了 LLM 精调阶段。做法是先把遗传算法产出的结构化结果转换成 JSON 上下文再让大模型基于用户偏好做体验优化输出带解释的每日行程。def llm_refine_route(best_schedule, preferences, days): prompt f 你是资深旅游规划师。请根据用户偏好和算法推荐的景点顺序优化一份多日行程。 用户偏好{preferences} 原始行程{json.dumps(best_schedule, ensure_asciiFalse)} 要求 1. 每天 3-5 个景点给出推荐游玩顺序。 2. 补充每个景点的推荐理由说明为什么排在当前时段。 3. 给出每日餐饮建议和整体提醒。 4. 只输出 JSON不要输出多余文字。 response llm_client.chat(prompt, temperature0.3) return parse_json_or_retry(response)温度参数设得比较低是为了让输出保持稳定。这一阶段的效果是肉眼可见的算法生成的是“一堆景点”经 LLM 调整后变成“上午去古街逛早市中午吃当地菜下午去博物馆避开暴晒”这种真正能用的行程。3.5 路线生成完整流程示例我以“某城市 3 日游偏好美食和人文历史”为例把整个流程串一遍召回阶段从景点库中筛出该城市的候选景点约 30 个。遗传算法阶段分配 3 天的景点组合目标是让每天的路线时间和兴趣匹配度达到最优。精调阶段LLM 根据每日景点顺序补充“为什么这么排”“吃什么”“注意什么”。返回阶段前端拿到结构化的 JSON按天渲染成时间轴卡片。整套流程平均耗时在 2 到 3 秒之间属于可接受范围。如果某次 LLM 调用超时系统会直接返回遗传算法的结果保证用户端始终不会白屏。4. LLM 大模型接入的工程化细节不是调个 API 那么简单4.1 选型用 API 服务比部署开源模型省心太多当时也考虑过本地部署开源模型但现实很骨感动辄几十 GB 的显存要求、需要显卡资源、工程质量依赖运维能力对一个毕业设计来说性价比太低。最后还是选择接入成熟的大模型 API 服务。调用方式就是标准的 HTTP 封装把模型名、消息体、温度参数传进去。这里没有太多花活关键是把不稳定因素控制住超时时间设为 15 秒避免接口挂起拖垮整个请求。失败重试两次采用指数退避避免短时间密集重试触发限流。对所有输入输出做长度限制防止极端情况下的 Token 浪费。4.2 Prompt 模板与结构化输出接入 LLM 最核心的工程问题就是“怎么让它稳定输出 JSON”。我的解决方法是给模型一个明确的 JSON 模板并且限定字段名称要求它直接输出可解析的 JSON。对于一些版本不支持的场景也可以让模型输出代码块包住的 JSON再用正则提取。def parse_json_or_retry(raw_text): try: return json.loads(raw_text) except json.JSONDecodeError: match re.search(rjson(.*?), raw_text, re.S) if match: return json.loads(match.group(1).strip()) # 最后尝试解析第一个 { 到最后一个 } 之间的内容 obj re.search(r\{.*\}, raw_text, re.S) if obj: return json.loads(obj.group()) raise ValueError(LLM 输出无法解析)这里有个真实的坑大模型偶尔会在 JSON 内容后面追加一段“希望这个行程对你有帮助”之类的话直接导致json.loads失败。正则提取后处理能兜住大部分情况但最好的办法还是在 Prompt 里反复强调“只输出 JSON不要多余内容”。4.3 成本控制与并发限流LLM 调用是有成本的。项目里我做了三层控制结果缓存同样的城市、天数、偏好组合短时间内重复请求直接读 Redis 缓存不再调模型。离线预生成把热门城市的经典路线离线生成好用户访问热门路线列表时几乎零成本。异步队列路线规划请求进入 Celery 队列限制同时执行的并发数避免触发 API 频率限制。这一套组合下来就算演示现场网络波动系统也能稳定出结果。我记得一次联调时现场网络非常不稳定直接重试了三次都超时最后靠缓存数据兜底页面才没有出丑。4.4 实测中的避坑清单地名不规范模型有时会把“某古城景区”写成“某古城”需要按景点 ID 字典做一次映射对齐。中文标点混入 JSON模型可能输出全角冒号正则和json.loads都要做兼容。长文本截断返回内容过长时部分服务会截断结果是残缺 JSON重试之外还要在 Prompt 里压缩输出长度。这些坑都很细但任何一个都会让演示现场翻车。我的建议是在正式答辩前准备好 3 条测试路线反复跑 10 遍以上确保每次都能成功解析。5. 个性化推荐系统协同过滤、内容画像与冷启动的取舍5.1 推荐系统的业务目标与评估方式旅游场景的推荐不能照搬电商的“猜你喜欢”用户的决策周期长、决策因素多所以我给推荐模块定了两个任务一是给用户推荐“值得关注的景点”二是在路线规划之前提供一份“可能的兴趣点”作为参考。离线评估我用了经典的 TopN 指标Precision10推荐列表前 10 个中有实际交互的比例。Recall10用户实际交互的景点中有多少出现在推荐前 10。覆盖率推荐结果覆盖了多少比例的景点池。这些指标在答辩时最好用表格展示数据能明显看出混合策略优于单一策略这是“算法对比实验”的说服力所在。5.2 冷启动新用户没有行为数据怎么办新用户注册后没有浏览记录协同过滤完全失效所以我设计了规则冷启动方案如果用户填写了偏好标签比如“亲子”“古镇”直接召回对应标签下的高分景点。如果没有偏好信息按城市维度推热门高分景点再叠加季节因子排序。用户产生几条浏览行为后立刻切换到混合推荐完成从“通用推荐”到“个性化推荐”的过渡。冷启动的效果没办法用离线指标量化因为本来就是少数据场景但我用模拟数据验证了“先冷启、后个性化”这个流程的转换率确实比一直推热门高不少。5.3 协同过滤与内容推荐的实现协同过滤部分我实现了 User-based CF用 Jaccard 相似度计算用户之间的兴趣重叠程度再找到相似用户喜欢的、当前用户未交互过的景点进行打分。def user_similarity(interaction): users list(interaction.keys()) sim_matrix {} for i, u in enumerate(users): for j, v in enumerate(users[i1:], i1): inter_u, inter_v set(interaction[u]), set(interaction[v]) if len(inter_u | inter_v) 0: continue jaccard len(inter_u inter_v) / len(inter_u | inter_v) sim_matrix[(u, v)] sim_matrix[(v, u)] jaccard return sim_matrix内容推荐方面我把景点标签转成多热向量用户画像由历史交互景点的标签向量均值表示然后计算余弦相似度。这个方法的优势是不需要大量用户之间交互也能工作正好弥补 User-CF 在数据稀疏时的不足。5.4 混合推荐与在线服务最后把各路信号做加权融合最终得分 0.4 * 协同过滤得分 0.3 * 内容匹配得分 0.2 * 热度指数 0.1 * 用户自评偏好考虑到实时计算相似度矩阵太慢我用 Celery 每 6 小时离线更新一次用户相似度表和景点相似度表写入 Redis。在线请求只做加权求和与排序单次接口响应控制在 200 毫秒以内。5.5 实验对比结果我用模拟的 1000 个用户行为数据跑了一轮对比推荐策略Precision10Recall10覆盖率热门推荐0.180.125.2%单纯 User-CF0.310.2411.4%混合推荐0.420.3116.8%虽然是在模拟数据上的结果但趋势很有说服力混合推荐在准确率和覆盖率上都有提升。评委最关心的往往不是绝对数字而是“你有没有对比意识”这张表已经把对比意识体现出来了。6. 数据分析与可视化模块让毕设从“能用”到“有深度”6.1 数据分析指标体系设计数据分析模块如果不设定指标就会变成单纯画图表。我围绕业务定义了四个维度流量维度每日访问量、独立访客数、热门页面。用户维度新老用户比例、偏好标签分布、活跃时段。景点维度热度排行、季节性热度变化、平均停留时长。转化维度从浏览到收藏、从收藏到规划路线的转化率。这些指标量化了系统背后的运营视角也让答辩时“数据分析”四个字落到实处。6.2 数据看板的技术实现后台管理部分我做了一个可视化看板用 Django REST Framework 提供聚合数据接口前端交给 ECharts 渲染。核心图表包括近 30 天访问量趋势折线图。景点热度 Top10 横向柱状图。用户偏好标签分布饼图。评论关键词词云。不同城市的景点数量与热度散点图。实现思路很简单Django 视图里用 ORM 做聚合统计返回 JSON前端定时请求并更新图表。为了让数据展示更直观很多聚合结果都走缓存比如 Top10 榜单每天计算一次避免频繁扫表。6.3 从数据看板反推业务改进这一步是很多毕设没有做、但非常加分的地方通过数据指导功能调整。从看板里我发现用户的收藏行为大多发生在晚上 8 点到 11 点。于是我给路线规划页面增加了一个“晚间收藏清单”入口效果是模拟数据里第二天回访规划路线的比例提升了一截。这说明数据分析不是摆设能真正影响产品决策。答辩时把这个故事讲出来整个项目的“数据驱动”定位就立住了。7. 实测结果与答辩避坑毕设拿优的关键复盘7.1 性能与效果实测在完成所有模块之后我在本地跑了完整的压测和功能测试核心指标如下指标实测值说明路线生成平均耗时2.8 秒含 LLM 调用与解析推荐接口 P95 响应180 毫秒Redis 缓存后的在线服务推荐 Precision100.42模拟用户行为数据并发用户数50本地开发环境未出现明显阻塞这些数据说明整个项目在毕业设计演示的规模下完全够用。如果非要说优化空间就是对遗传算法做并行化但作为毕设已经足够。7.2 开发中踩过的技术坑Django ORM 查询慢景点列表页在数据量上来后明显卡顿后来通过select_related和prefetch_related优化外键查询并给热度字段加了数据库索引速度立刻上来。Celery 定时任务重复执行部署脚本里多开了一个 worker队列任务被重复消费加上 Redis 锁之后解决。JSON 字段脏数据LLM 偶尔返回不规范字段名导致前端渲染空值。我做了一个清洗函数先把未知字段过滤掉再入库。时区问题BehaviorLog 表存储时间如果不统一按天聚总会少 8 个小时。把所有时间字段统一用 UTC 存储前端展示时再转本地时间。7.3 答辩整体演示节奏演示的时候不要一上来就打开页面点来点去我建议按这个顺序走痛点引入讲旅游出行规划费时费力很多人花一整天都排不好行程。方案讲解结合架构图说明系统分成哪些模块为什么选 Django 和 LLM。核心算法演示现场输入一个城市、天数和偏好让它生成路线。同时切到后台展示遗传算法的中间输出和 LLM 的 JSON 结果。推荐效果对比展示混合推荐与单纯热门推荐在指标上的差异。数据分析看板用图表说话讲发现的洞察。总结与不足主动说哪里还能优化显得思考有深度。7.4 给学弟学妹的几条实在建议我做完这个项目最大的体会是选一个能展示深度的小切口比堆一堆功能更有价值。这个题目里真正的核心是“遗传算法 LLM 语义精调”的路线规划方案推荐系统和数据分析是辅助但它们共同构成了一个完整的故事。另一个建议是所有实验数据都要在论文和答辩 PPT 中如实说明来源。公开数据用公开数据模拟行为数据就写明“该数据仅用于验证算法流程”不要为了好看把模拟数据包装成真实数据。踏实这一点反而会让评审老师印象更好。最后别忽视工程细节比如接口异常处理、缓存降级、数据清洗这类“没什么技术含量”的工作。毕设从“写完”到“经得起问”差距往往就体现在这些地方。