每到毕业设计开题季,“旅游路线推荐系统”这类题目就会重新火起来。今年因为Django、LLM大模型和“大数据”这几个词被放到同一个标题里,关注度一路走高。我花了两周时间把这套系统完整搭了一遍,从用户偏好采集、经典协同过滤召回,到大模型生成推荐理由,再到基于大数据的可视化分析面板,整条链路都实际跑过。这篇就当是给想选这个方向的同学的一份实操笔记,里面每一段都是我自己在编码、调试、部署过程中遇到过的东西,不是那种只讲概念的空文。
1. 毕设选题的真实动机:Django和LLM为什么会出现在同一个系统里
1.1 选题前的三个误区,我帮你先排掉
做毕设最怕的不是技术难,而是题目看起来热闹、做起来不知道从哪里下手。这套系统名字很长,但大部分人拿到题目时其实是糊涂的。我先说三个最常见的错误想法,避免你走弯路。
第一个误区:以为LLM就是用来直接生成路线的。很多人觉得,大模型这么强,我把用户需求丢给它,让它输出一套旅行路线不就行了吗?实际试过就明白,完全不可行。大模型确实能生成看起来合理的路线,但它不知道真实的地理位置、景点营业时间、景区之间的交通耗时,更关键的是它会一本正经地编造不存在的景点和距离。让大模型直接当推荐引擎,结果就是答辩现场翻车。
第二个误区:以为大数据模块就是爬一堆数据堆在页面上。很多同学做的所谓大数据分析,就是从某个旅游网站抓了几万条数据,然后画几个饼图。这只能叫数据展示,不叫数据分析。一个合格的毕设,至少需要让数据在业务里流动起来:用数据做推荐依据、用数据支撑路线排序、用数据发现用户偏好。
第三个误区:以为推荐系统必须从零写神经网络才算有深度。实际上,对于本科毕设来说,经典的协同过滤加上合理的业务规则,配合LLM做增强和解释生成,已经能形成完整的故事。老师看的不是算法多前沿,而是你能不能把“为什么要这样设计”讲清楚,能不能把链路走通。
1.2 我最终敲定的功能清单和系统定位
在这个项目里,我把整个系统的定位确定为:基于用户行为数据和自然语言需求的智能旅游路线推荐与规划平台。说白了,用户登录后填一下偏好,系统给出几条路线,用户还能要求大模型解释一下“为什么推荐这条路线”。
最终交付的功能分四块:
- 用户端:注册登录、完善兴趣标签(如自然风光、人文历史、美食购物)、浏览景点库、查看推荐路线、路线详情页、收藏和评论。
- 推荐端:基于协同过滤召回用户可能感兴趣的景点,再根据时间、季节、区域等规则排序,最后交给LLM生成个性化的推荐说明。
- 路线规划端:用户在选定多个景点后,系统自动规划合理的游玩顺序,估算时间与交通距离。
- 大数据分析端:统计景点热度、用户行为趋势、客流预测、地区分析,用可视化图表展示,并反哺推荐引擎的权重参数。
这套功能既有技术含量,又不至于做不完。核心思路是用Django构建完整的Web业务体系,用经典推荐算法保证基础效果,用LLM提升交互体验和“个性化”的观感,用数据分析模块把项目整体抬高一个档位,让它配得上“大数据毕业设计”这个标签。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计:数据层、推荐引擎与LLM的角色划分
2.1 核心数据怎么建模:三张表决定了整个系统的上限
不管推荐算法多花哨,底层的数据模型必须稳。我设计了一个以“用户—景点—行为”为核心的数据结构。最开始做原型时我建了七八张表,后来精简成下面这些关键表:
| 表名 | 关键字段 | 作用 |
|---|---|---|
| User | id, username, password, age, city, interests | 存储用户基本信息和兴趣偏好 |
| ScenicSpot | id, name, city, category, price, longitude, latitude, popularity, score | 存储景点基础信息与热度评分 |
| UserBehavior | id, user_id, spot_id, behavior_type, timestamp | 记录浏览、收藏、评分、搜索等行为 |
| Route | id, user_id, spots, title, description, create_time | 保存最终的推荐或自定义路线 |
| Comment | id, user_id, route_id, content, rating | 用户对路线的评价反馈 |
这里要先说明一个容易被忽略的设计决策:用户兴趣标签,我是直接以JSON字段存在User表里,还是单独建表?我实际对比过,毕设阶段直接用JSONField存储即可。原因很简单,兴趣标签是低频更新数据,单独建表会引入大量冗余的关联查询,而且Django的JSONField在后续过滤和统计时完全够用。
景点数据从哪来?这是每个做旅游项目的人都会遇到的问题。我推荐走两条路:一是使用高校公开的旅游数据集,比如UCI上的景区点评数据,或国内一些开源数据平台的POI数据;二是自己编写爬虫获取公开展示的景点信息。但考虑到毕业设计的时间成本和法律风险,我建议优先使用公开数据集,再手动补充100~200条本地景点数据用于演示。演示时数据量不需要太大,关键是把“从数据到推荐”的链路打通。
2.2 推荐流程分三步:为什么不能全部交给LLM
很多同学在论文里写“基于大模型的推荐系统”,但实际架构却是把大模型API当成了一个黑盒,输入输出全靠提示词。这种设计在答辩时几乎必被追问:“如果API返回错误怎么办?延迟如何控制?成本如何控制?”我在设计时就明确划分了职责,推荐链路分三步走:
第一步,召回阶段:用协同过滤和兴趣标签,从景点库中筛选出用户可能感兴趣的50~100个候选景点。这一步追求的是速度快、覆盖广,不需要太精确。
第二步,排序阶段:基于规则模型进行精细化排序,考虑因素包括景点的综合评分、热度、用户所在城市、当前季节、游玩时间窗口、景点之间的地理距离。这一步产出一个带分数的有序候选列表,是最后的兜底推荐结果。换句话说,即使LLM完全失效,这个系统依然可以正常推荐。
第三步,LLM增强阶段:把用户的需求文本和候选路线列表一起丢给大模型,让它做两件事。一是对候选路线做一次“可读性排序”,比如根据用户“带父母”“亲子游”“一个人穷游”等表述微调优先级;二是生成推荐理由,用自然语言解释为什么推荐这条路线。这一步是大模型真正产生价值的环节,但它承担的是“增强”职责,而不是“核心计算”职责。
我实测下来,这种分工方式有几个明显好处:即使大模型API超时,也不会导致整个推荐流程失败,可以用排序阶段的兜底结果返回;大模型的Token消耗可控,因为每次只传10~20条候选路线而不是全量数据;答辩时可以自信地讲清楚每一模块的职责边界,而不是一句“AI生成”糊弄过去。
2.3 大数据分析模块在本项目中的真实角色
大数据这里并不是“伪需求”,而是可以直接为推荐系统服务的。我在项目里实现了三个分析场景:
- 景点热度预测:基于过去90天的用户行为数据,统计每个景点的日访问量变化,用简单的时间序列滑动平均法预测未来7天的热度趋势。这个热度值会直接进入推荐排序算法,作为权重之一。
- 用户画像分析:从用户的年龄、地域、兴趣标签、行为类型中提取特征,统计不同用户群体的偏好差异。比如“18~25岁用户更喜欢自然风光类景点”这个结论,可以反向指导协同过滤的相似度计算。
- 热门路线实时排行:基于Route表的收藏与评论数据,计算路线热度分,在首页做成排行榜。
这些分析结果全部通过可视化图表呈现,前端用ECharts,数据由Django的API接口输出JSON。数据量不用太大,几千条行为记录就能画出像样的趋势图。关键是让老师看到“数据—分析—业务决策”是闭环的,而不是各模块各玩各的。
3. 个性化推荐的落地代码:召回、排序到LLM重排的完整管线
3.1 基于协同过滤的召回是怎么写的
这一节我会把推荐引擎的关键代码贴出来。我用的是基于用户的协同过滤算法,核心思路是:找到与当前用户行为最相似的一批用户,再把这些用户喜欢过的、当前用户没看过的景点推荐出来。
python复制import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
def build_user_spot_matrix():
# 从 Django ORM 读取用户行为数据,构建 用户 x 景点 评分矩阵
behaviors = UserBehavior.objects.values("user_id", "spot_id", "behavior_type")
records = []
for b in behaviors:
score = {"view": 1, "collect": 3, "comment": 2, "share": 4}.get(b["behavior_type"], 1)
records.append({"user_id": b["user_id"], "spot_id": b["spot_id"], "score": score})
df = pd.DataFrame(records)
matrix = df.pivot_table(index="user_id", columns="spot_id", values="score", fill_value=0)
return matrix
def recommend_by_collaborative_filtering(user_id, top_n=10):
matrix = build_user_spot_matrix()
if user_id not in matrix.index:
return []
# 计算当前用户与其他所有用户的余弦相似度
user_vec = matrix.loc[user_id].values.reshape(1, -1)
sims = cosine_similarity(user_vec, matrix.values).flatten()
sims_df = pd.Series(sims, index=matrix.index)
# 取相似度最高的前10个用户
top_similar_users = sims_df.drop(index=user_id).sort_values(ascending=False).head(10).index
# 收集这些用户访问过的景点,加权去重
spot_score = {}
for similar_user in top_similar_users:
user_row = matrix.loc[similar_user]
for spot_id, score in user_row.items():
if score > 0 and matrix.loc[user_id, spot_id] == 0:
spot_score[spot_id] = spot_score.get(spot_id, 0) + score
# 按加权分排序
sorted_spots = sorted(spot_score.items(), key=lambda x: x[1], reverse=True)
return [spot_id for spot_id, _ in sorted_spots[:top_n]]
这里有个非常重要的注意点:评分矩阵在真实系统中会非常稀疏,如果用户行为太少,协同过滤的效果会很差。我的解决方案是做一个“冷启动兜底”:如果当前用户的行为记录少于5条,直接跳过协同过滤,改用兴趣标签匹配和热度排序召回路。这个细节在论文里可以作为“冷启动问题的解决方案”写进去,面试时也是加分项。
3.2 排序规则的权重设计
召回阶段拿到50~100个候选景点后,要排出一个真正符合用户需求的列表。我用一个简单的加权评分函数完成排序:
python复制def rank_spots(spots, user, candidate_spot_ids):
scored_spots = []
for spot in candidate_spot_ids:
scenic = ScenicSpot.objects.get(id=spot)
score = 0.0
# 基础分:景点评分与热度
score += scenic.score * 0.4
score += min(scenic.popularity / 1000, 5) * 0.3
# 兴趣标签匹配:如果景点分类与用户兴趣匹配,加分
if scenic.category in user.interests:
score += 2.0
# 距离因素:越近分越高(实际项目中可以接入地图API计算距离)
if scenic.city == user.city:
score += 1.5
# 季节因素:夏季偏向自然山水,冬季偏向室内文史
score += season_bonus(scenic)
scored_spots.append((scenic, score))
scored_spots.sort(key=lambda x: x[1], reverse=True)
return [item[0] for item in scored_spots[:20]]
权重是手工调的。我推荐你先按一个默认权重跑通流程,再用后面的标注数据测试不同权重组合下的推荐结果。不需要做太复杂的调参,因为论文里可以写“基于人工经验的权重设定”,重点在于解释每个权重的业务含义。
3.3 LLM重排与推荐理由生成:提示词是核心壁垒
把排序后的20条候选路线交给大模型时,提示词设计得好不好,直接决定了推荐理由看起来“智商在线”还是“废话连篇”。我调试了很多版提示词,最稳定的版本长这样:
python复制import openai
def generate_recommendation_with_llm(user_pref, candidate_routes):
prompt = f"""
你是旅游路线规划专家。请根据用户的旅行偏好,从给定的候选路线中选出最优的三条路线。
用户偏好:{user_pref}
候选路线:
{candidate_routes}
要求:
1. 按推荐优先级输出三条路线,用编号、路线名、推荐理由的格式输出。
2. 推荐理由必须结合用户偏好的具体描述,不要泛泛而谈。
3. 只参考给定的候选路线,不要自行编造景点。
4. 对每条推荐理由给出一个0-100的匹配度分数。
"""
resp = openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=800
)
return parse_response(resp.choices[0].message.content)
这里要留意提示词中的硬约束:“只参考给定的候选路线,不要自行编造景点”。不加这句,模型经常会加入候选列表之外的“私货”,导致推荐结果和数据库对不上。我做过多次对比实验,加了这条约束之后,推荐结果的可信度大幅提升。
另一个值得说的点是temperature参数。我一开始用的是默认值1.0,结果同样输入条件下,每次推荐理由风格差异巨大。后来调到0.3,发现既能保持内容多样性,又不会跑得太远。
至于接入哪个大模型,这类系统的路由和请求方式都差不多。我建议选择支持函数调用和JSON输出的API,这样 parse_response 部分会简单很多。
3.4 从路线到完整推荐结果的组合
推荐结果不只是一串景点ID,而是需要组装成一条包含“路线名称、景点列表、游玩顺序、推荐理由、匹配度”的完整数据。你可以在Django视图里这样组合:
python复制def get_recommendations(request):
user = request.user
user_pref = build_user_preference_text(user)
# 1. 召回
candidate_ids = recommend_by_collaborative_filtering(user.id, top_n=50)
# 2. 排序
ranked_spots = rank_spots(ScenicSpot.objects.all(), user, candidate_ids)
# 3. 组装成候选路线
candidate_routes = build_candidate_routes(ranked_spots[:20])
# 4. LLM增强
llm_result = generate_recommendation_with_llm(user_pref, candidate_routes)
return JsonResponse({"recommendations": llm_result, "fallback": candidate_routes[:3]})
fallback字段很关键。当LLM调用异常时,前端拿到这个兜底结果直接展示,用户感知不到系统故障。这个设计在答辩时一定要讲出来,老师会觉得你考虑问题很周全。
4. Django后端开发实测:模型设计、缓存策略与异步化改造
4.1 从零初始化Django项目时容易犯的错
这部分是我们每天都在做的事情,但细节仍然值得指出。我建议使用虚拟环境,并把依赖锁定到requirements.txt:
bash复制python -m venv venv
source venv/bin/activate
pip install django djangorestframework celery redis pandas scikit-learn openai
django-admin startproject travel_project
cd travel_project
python manage.py startapp recommend
python manage.py startapp analysis
设置里最容易被忽略的几项配置:
python复制# settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"corsheaders",
"recommend",
"analysis",
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_TZ = True
# MySQL 配置示例,开发阶段也可以用 sqlite3
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "travel_db",
"USER": "root",
"PASSWORD": "your_password",
"HOST": "127.0.0.1",
"PORT": "3306",
}
}
# Redis 缓存配置
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
corsheaders是我特意加上的,因为如果你后面打算用Vue做前后端分离,跨域问题早晚要处理。与其等踩坑了再补,不如一开始就配上。
4.2 核心模型的长这样:直接贴可用的代码
下面这些模型是被我实际验证过的,字段没有多余也不需要补:
python复制from django.db import models
from django.contrib.auth.models import User
class ScenicSpot(models.Model):
name = models.CharField(max_length=100, verbose_name="景点名称")
city = models.CharField(max_length=50, verbose_name="所在城市")
category = models.CharField(max_length=30, verbose_name="景点分类")
price = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="门票价格")
longitude = models.FloatField(verbose_name="经度")
latitude = models.FloatField(verbose_name="纬度")
popularity = models.IntegerField(default=0, verbose_name="热度值")
score = models.FloatField(default=4.5, verbose_name="评分")
description = models.TextField(blank=True, verbose_name="简介")
class Meta:
db_table = "scenic_spot"
class UserBehavior(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")
spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE, verbose_name="景点")
behavior_type = models.CharField(max_length=20, verbose_name="行为类型")
timestamp = models.DateTimeField(auto_now_add=True, verbose_name="发生时间")
class Meta:
db_table = "user_behavior"
indexes = [models.Index(fields=["user", "behavior_type"])]
class Route(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")
title = models.CharField(max_length=100, verbose_name="路线名称")
spots = models.ManyToManyField(ScenicSpot, through="RouteSpot", verbose_name="包含景点")
description = models.TextField(blank=True, verbose_name="路线描述")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
db_table = "route"
class RouteSpot(models.Model):
route = models.ForeignKey(Route, on_delete=models.CASCADE)
spot = models.ForeignKey(ScenicSpot, on_delete=models.CASCADE)
order = models.IntegerField(default=0, verbose_name="游玩顺序")
class Meta:
db_table = "route_spot"
这里有个设计细节:Route和ScenicSpot之间用中间表RouteSpot建立多对多关联,并且记录了order字段。为什么要额外加中间表?因为路线的核心属性就是“顺序”,如果不记录顺序,这条路线就只是一组景点ID,无法体现“规划”的价值。Django默认的ManyToManyField不带排序字段,所以必须自己建中间表。
4.3 异步化改造:LLM请求不能阻塞Web主流程
如果直接在最普通的Django视图函数里调用大模型API,你会发现请求耗时经常在5~10秒,有时候甚至更久。用户等不起,开发服务器也会因为并发请求堆积而假死。我的解决方案是用Celery做异步任务,把LLM调用放到后台执行,执行完再推送到前端。
python复制# tasks.py
from celery import shared_task
from .llm import generate_recommendation_with_llm
@shared_task(bind=True, max_retries=3, default_retry_delay=5)
def async_generate_recommendation(self, user_id, user_pref, candidate_routes):
try:
result = generate_recommendation_with_llm(user_pref, candidate_routes)
cache.set(f"recommend_{user_id}", result, timeout=3600)
return result
except Exception as exc:
raise self.retry(exc=exc)
在视图里,判断缓存是否有结果;没有就触发异步任务,先返回一个“推荐生成中”的状态,等前端轮询拿到结果。这样用户不会干等,体验好了很多。
实际上,我在本地开发时遇到一个很典型的坑:Celery默认的broker是内存,一旦重启就会丢失所有未完成任务。切到Redis之后要记得启动三个东西:Django开发服务器、Celery worker、Celery beat(如果你需要定时任务)。我第一次只启动了Django并在调用任务时直接卡了半小时,最后才发现worker根本没开。
5. 大数据分析模块:从数据清洗到可视化面板的实现思路
5.1 数据分析到底分析什么数据
这个模块需要一个“数据加工流水线”。我用pandas在Django的管理命令中独立跑了一套ETL流程,而不是每次请求时动态算,因为动态计算对数据库压力实在太大。步骤如下:
- 从
UserBehavior表中导出最近90天的行为记录到CSV; - 用pandas清洗数据,包括去掉缺失值、剔除重复记录、归一化时间戳;
- 按景点维度聚合,计算每日访问量、热度趋势、7日移动平均;
- 按用户维度聚合,计算年龄/城市/兴趣分布;
- 将聚合结果写入独立的分析表,供前端API快速查询。
我提供一段清洗流程的核心代码:
python复制import pandas as pd
def clean_behavior_data(raw_csv_path):
df = pd.read_csv(raw_csv_path)
df.dropna(subset=["user_id", "spot_id"], inplace=True)
df.drop_duplicates(subset=["user_id", "spot_id", "timestamp"], inplace=True)
df["timestamp"] = pd.to_datetime(df["timestamp"])
df["date"] = df["timestamp"].dt.date
daily_stats = df.groupby(["spot_id", "date"]).size().reset_index(name="visit_count")
# 7日移动平均
daily_stats["ma7"] = daily_stats.groupby("spot_id")["visit_count"].transform(
lambda x: x.rolling(7, min_periods=1).mean()
)
return daily_stats
这段代码跑出来的结果会直接为热度排行和趋势图提供数据。
5.2 可视化面板:Django模板还是Vue
我的建议是:如果时间紧迫,直接用Django模板加ECharts的CDN就足够了。前端页面用Bootstrap搭一个后台管理布局,左侧是导航,右侧嵌入各类图表。ECharts本身只需要一个div容器加一段JavaScript配置,不需要引入整个Vue全家桶。
html复制<div id="trendChart" style="width: 100%; height: 400px;"></div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5"></script>
<script>
fetch("/api/analysis/trend/?spot_id=1")
.then(res => res.json())
.then(data => {
const chart = echarts.init(document.getElementById("trendChart"));
chart.setOption({
title: { text: "景点热度趋势" },
xAxis: { type: "category", data: data.dates },
yAxis: { type: "value" },
series: [{ name: "访问量", type: "line", data: data.visits }]
});
});
</script>
如果你本来就熟悉Vue,那用Vue做前后端分离也行。只是要清楚这会增加不少工作量,需要处理接口跨域、打包部署等额外问题。毕设阶段,优先保证核心链路完整,Vue不是必须的。
5.3 用户画像分析结果怎么反哺推荐系统
这部分是最容易被忽视、却是论文里最有亮点的内容。我在analysis模块里统计了用户的年龄分布、兴趣偏好和对不同景点的评分均值,把这些数据存成了一张UserProfileSummary表。推荐引擎启动时读取这张表,做两件事:
- 根据年龄段调整排序权重。比如统计发现35岁以上用户更喜欢历史文化类景点,就在排序阶段对这类用户的该类别景点额外加权重。
- 根据热门城市组合生成“周边游”候选池,弥补协同过滤在冷启动时召回不足的问题。
这个闭环让整个项目的逻辑完整了:数据从业务中来,经过分析,再回到推荐服务,形成数据飞轮。答辩时这段描述远比“我用了协同过滤”更有说服力。
6. 部署、答辩与避坑:本地跑通到上台演示的关键经验
6.1 部署方案:我为什么最终选了宝塔
部署这一块,考虑到毕业设计需要稳定演示、且服务器成本不能太高,我选择了宝塔面板加Django的方式。相比Docker Compose,宝塔对新手更友好,能直接在网页上配置Nginx、MySQL和Redis,不需要写太多配置文件。
大致步骤是:
- 买一台2核4G的云服务器,安装宝塔面板;
- 在宝塔中安装Python 3.10、MySQL 8.0、Redis 7、Nginx;
- 将项目代码上传到
/www/wwwroot,创建虚拟环境并安装依赖; - 配置Gunicorn作为Django的WSGI服务,绑定到
127.0.0.1:8000; - 在Nginx中配置反向代理,把域名或IP的80端口转发到8000;
- 用supervisor或systemd守护Gunicorn进程,保证崩溃后自动重启。
有一点必须提醒:项目的settings.py里如果开了DEBUG = True,部署到线上会有严重的安全风险,比如暴露数据库密码和本地文件路径。部署到服务器后记得把DEBUG关掉,同时把ALLOWED_HOSTS改成你的服务器IP或域名。
6.2 我踩过的几个坑,踩完你就别再踩了
排在最前面的坑是LLM请求超时。我在实际演示时遇到过模型接口响应超过10秒的情况,页面一直转圈,很尴尬。解决思路前面提过,用异步任务加Redis缓存,同一用户的推荐结果在一小时内直接读缓存,不重复调用模型。
第二个坑是Django连接MySQL时的字符集问题。创建数据库时如果没有指定UTF-8,中文字段写入时会报Incorrect string value错误。建议建库时直接执行:
sql复制CREATE DATABASE travel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
第三个坑是静态文件不显示。使用Nginx部署时,Django自带静态文件服务在生产环境是失效的,需要执行python manage.py collectstatic把静态文件收集到指定目录,然后在Nginx中配置location /static/指向那个目录。我因为忘了这一步,白白排查了一下午。
第四个坑和虚拟环境有关。宝塔面板自带的Python版本可能和你本地的不同,直接运行项目经常会因为依赖版本冲突报错。处理方式很直接:在项目目录下用python -m venv venv创建新的虚拟环境,然后用venv/bin/pip install -r requirements.txt安装依赖,不要用系统自带的Python环境。
6.3 答辩高频问题,我提前帮你把答案备好
根据我参与过的毕设答辩和评审经验,老师大概率会问这几个问题,这里给出一个答题方向:
| 老师可能会问 | 推荐回答思路 |
|---|---|
| 为什么用大模型做推荐,而不是传统推荐算法? | 大模型不是替代推荐算法,而是增强推荐结果的解释性和个性化表达能力。核心召回和排序仍然由协同过滤和规则模型完成,大模型负责生成自然语言的推荐理由和二次微调排序。 |
| 如果大模型推荐了不存在的景点怎么办? | 在提示词中明确限制了候选集合,并且系统只展示候选路线中的景点;同时加入兜底逻辑,大模型异常时直接返回规则排序结果。 |
| 你的大数据分析体现在哪里? | 对用户行为数据进行ETL清洗、景点热度趋势预测、用户画像聚类,分析结果反哺推荐权重,形成闭环。 |
| 数据量这么小,效果能保证吗? | 毕设重点是业务逻辑闭环和方案可行性验证。系统支持横向扩展,实际场景中接入更多行为数据后,推荐效果会随数据量增长持续优化。 |
| 系统安全方面做了什么? | 用户密码加密存储、接口权限校验、SQL注入防护、部署时关闭DEBUG、限制不安全请求方式。 |
6.4 做完这套系统之后,还能怎样延伸
如果做完基础版本还有富余时间,我建议优先考虑两个扩展方向。
第一个方向是接入地图API。目前路线规划里的“距离”是靠城市字段计算的,非常粗糙。接入真实地图API后,可以根据经纬度计算景点间的实际驾车或公共交通距离,甚至调用路线规划服务自动生成景点顺序。这个改进会立刻让系统的“规划”属性更真实。
第二个方向是对话式交互。现在LLM只能生成一次性的推荐理由,你可以继续做成多轮对话:用户说“我不喜欢这个景点”,系统根据反馈结果进行下一轮推荐,动态更新路线。这种感觉很接近真正的AI助手,也是目前业界比较热门的方向。
我在实际使用中最深的一点体会是:毕业设计并不需要把每个技术都做到极致,但必须让每个技术都出现在它该出现的位置,并且能讲清楚它和核心业务的关联。这套Django+LLM+大数据分析的组合,本质上是在做一个“以用户为中心的智能旅游决策系统”,LLM也好、协同过滤也好、可视化也好,都只是实现这个目标的手段。把这条主线想明白了,后面的开发、论文、答辩都会顺很多。
