每年毕业设计的选题库里,“旅游网站”几乎是大热门,但说实话,十个里面至少有八个做成了标准的增删改查演示——用户注册登录、景点列表、后台维护,顶多再加一个关键字搜索。答辩的时候老师问一句“技术亮点在哪里”,很多人当场就卡壳。这次我想分享的是一个完全不同的做法:用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 + Redis | LLM 调用限流、热门数据缓存、离线计算任务 |
这个组合的好处是每一个模块都可以独立成为答辩的一个亮点。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_length=128) # 景点名称 city = models.CharField(max_length=64) # 城市 tags = models.ManyToManyField("Tag") # 景点标签 rating = models.FloatField(default=0.0) # 平均评分 heat_index = models.FloatField(default=0.0) # 热度指数 avg_play_hours = models.FloatField(default=2.0) # 平均游玩时长 season_factor = models.JSONField(default=dict) # 各季节的游玩适宜度 lat_lng = models.CharField(max_length=64) # 经纬度 description = models.TextField(blank=True) # 简介 class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) preferences = models.JSONField(default=list) # 用户偏好的标签列表 class RoutePlan(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) params = models.JSONField(default=dict) # 规划参数:城市、天数、偏好 result = models.JSONField(default=dict) # 结构化路线结果 created_at = models.DateTimeField(auto_now_add=True) class BehaviorLog(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) attraction = models.ForeignKey(Attraction, on_delete=models.CASCADE) action = models.CharField(max_length=16) # view/collect/rating score = models.FloatField(default=0.0) created_at = models.DateTimeField(auto_now_add=True)这里最值得设计的其实是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, size=100) for _ in range(200): fitness = [cal_fitness(ind, preferences) for ind in population] selected = tournament_selection(population, fitness, k=5) offspring = crossover(selected, rate=0.8) offspring = mutate(offspring, rate=0.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_ascii=False)} 要求: 1. 每天 3-5 个景点,给出推荐游玩顺序。 2. 补充每个景点的推荐理由,说明为什么排在当前时段。 3. 给出每日餐饮建议和整体提醒。 4. 只输出 JSON,不要输出多余文字。 """ response = llm_client.chat(prompt, temperature=0.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(r"```json(.*?)```", 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 指标:
- Precision@10:推荐列表前 10 个中,有实际交互的比例。
- Recall@10:用户实际交互的景点中,有多少出现在推荐前 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[i+1:], i+1): 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 个用户行为数据跑了一轮对比:
| 推荐策略 | Precision@10 | Recall@10 | 覆盖率 |
|---|---|---|---|
| 热门推荐 | 0.18 | 0.12 | 5.2% |
| 单纯 User-CF | 0.31 | 0.24 | 11.4% |
| 混合推荐 | 0.42 | 0.31 | 16.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 缓存后的在线服务 |
| 推荐 Precision@10 | 0.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 中如实说明来源。公开数据用公开数据,模拟行为数据就写明“该数据仅用于验证算法流程”,不要为了好看把模拟数据包装成真实数据。踏实这一点,反而会让评审老师印象更好。
最后别忽视工程细节,比如接口异常处理、缓存降级、数据清洗这类“没什么技术含量”的工作。毕设从“写完”到“经得起问”,差距往往就体现在这些地方。