Streamlit + 多模态RAG电影推荐系统:从数据到答辩实战 又到毕设选题季后台私信里问得最集中的一类项目就是“大数据深度学习”方向的推荐系统。今年反复被提到的一个题目组合是Streamlit 多模态RAG的电影推荐系统。这个方向确实值得做它不像纯协同过滤那么老套也不像全链路大模型那样难以掌控难度适中、展示效果好、答辩时有得聊。这篇文章我会把项目完整拆一遍从数据准备、特征嵌入、向量检索、推荐融合到Streamlit界面和答辩现场的高频问题一条线讲清楚。这个项目之所以在一堆“XX推荐系统”里脱颖而出核心在于多模态RAG把传统推荐从“只看评分”变成了“能理解描述、能看懂海报”的语义检索系统。用户可以直接输入一句“一部色调温暖、发生在海边小镇的治愈系爱情片”系统返回匹配的电影也可以上传一张海报找到风格相近的作品。对有Python基础、想做推荐方向、又希望系统看起来“有点东西”的同学来说这是性价比很高的毕设方向。1. 项目整体设计与技术选型思路先聊方向选择。每年推荐系统毕设都有大量“撞车”大家不约而同做协同过滤、矩阵分解、或者是普通的内容推荐。这类方案成熟没错但答辩容易陷入“背书”模式老师很难问出有区分度的问题。Streamlit 多模态RAG的巧妙之处在于它把推荐系统和当下比较热的技术点结合了向量检索、多模态Embedding、大模型生成推荐理由。技术栈不虚工作量真实演示效果又很直观。整个系统我拆成四层数据层、特征层、检索融合层、展示交互层。数据层负责收集评分、元数据、海报图片特征层把文本和图像分别向量化检索融合层做多路召回和加权排序展示交互层用Streamlit承接用户在前端操作背后触发检索链路。分层清晰答辩画架构图也好讲每层都能单独展开不会一问就漏洞百出。1.1 为什么选Streamlit而不是Flask或Gradio很多同学一开始会纠结在Web框架选型上。我见过有人用Flask从零写HTML页面结果前后端联调了两周CSS样式没调明白最后答辩前一天还在改布局。毕业设计的时间有限Streamlit这类纯Python交互框架是更理性的选择。它不需要写HTML、CSS、JS只要写Python逻辑组件会自动渲染而且刷新机制、状态管理、缓存机制都内置了对数据类应用非常顺手。对比Gradio它更适合快速展示单个模型效果比如上传图片、返回分类结果这种单任务交互。但电影推荐系统需要多页签、多组件联动、结果列表、推荐理由展示这类偏应用层的交互Gradio的自由度和视觉风格会受限。Streamlit的布局能力更强表格、卡片、侧边栏、文件上传都能直接支持可塑性和演示效果都更好。还有一点容易被忽略Streamlit的st.cache_data和st.cache_resource在设计上就考虑了“模型加载一次、数据缓存多次”这类场景。推荐系统最大的性能瓶颈就是Embedding生成和向量检索用缓存把结果复用之后页面响应速度会快很多。Flask要自己搭缓存而Streamlit是开箱即用实际开发效率差距非常大。1.2 多模态RAG在电影推荐里到底解决什么问题先说RAG的基本套路用户输入一条查询系统不做硬编码匹配而是把查询转成向量在向量库里召回最相关的若干条内容再把召回结果交给后续模块生成回答或推荐理由。放到电影推荐场景里就是用户描述一句模糊需求系统在几千部电影里找到“最贴合的候选集”而不是只靠历史行为数据猜。多模态的部分更有意思。传统推荐系统只用用户-物品评分矩阵冷启动用户和冷门电影都难以处理。多模态RAG引入文本Embedding和图像Embedding之后系统能理解剧情简介、导演风格、海报视觉风格。用户没评过几部片但只要给出描述或者上传一张海报系统依然能给出高质量推荐。这对推荐系统的覆盖率和多样性是非常明显的补充。我实际做完后的感受是多模态RAG解决的不是“预测评分更准”的问题而是“用户表达更自由”的问题。以前用户只能得到“猜你喜欢”现在用户可以直接和推荐系统对话或者用图像说话。这个视角差异本身就是答辩时最能讲清楚创新点的地方。2. 数据准备与多模态特征工程数据是整个系统的天花板。很多同学上来就闷头跑模型结果做出来的推荐结果千奇百怪问题基本都出在数据处理上。这个项目需要三类数据用户评分数据、电影元数据、海报图片数据。三者要能通过电影ID对齐供后续特征拼接和向量索引使用。评分数据我建议用常用的公开电影评分数据集量级在10万到100万评分即可太多处理耗时太少说服力不足。电影元数据需要包含标题、类型、导演、演员、剧情简介、年代、评分信息。海报图片数据是另一条关键支线需要按电影ID逐一下载并做图片校验这一步耗时较长建议写脚本批量抓取失败重试最后把能成功获取的图片单独归档。2.1 数据处理的核心链路数据处理最耗时的不是“处理”而是“对齐”。评分数据里的电影ID和元数据API里的电影ID往往不是同一套体系需要自己建立映射表。映射对不上后面所有特征都白算。我的做法是先加载评分数据里的全部电影ID再去开放元数据接口逐条补齐信息然后以元数据表为主表逐个匹配海报图片最后生成统一的数据表。清洗时要关注几个细节年代、时长这些数值字段可能缺失或异常统一用中位数或众数填充类型字段往往是多值建议拆成标签列表后面做文本拼接时更方便简介文本可能包含很多无关标点和广告词要统一做一次清洗海报图片下载失败的比例比想象中高要记录缺失列表后续可手动补给一个参考我当时处理5000部电影元数据补齐率约96%海报能成功下载约87%。这已经足够完成推荐演示没必要追求100%。与其花时间补齐小比例图片不如把现有数据的质量提上来。2.2 文本Embedding怎么生成才不“跑偏”文本Embedding的目的是把电影的描述性信息转成向量让语义相近的电影在向量空间里靠近。关键不是选多大模型而是如何拼接文本。经验做法是把类型、导演、演员、简介、风格标签拼成一段固定格式的文本中间用换行或分隔符隔开。导演和演员建议放靠前位置因为它们是决定“这部电影像谁”的重要因素。直接简单拼接简介容易让模型抓不住重点尤其是长文本里大量“剧情转折”描述反而会稀释风格信息。具体Embedding生成可以用预训练模型。我用了句子向量模型输出384维向量。生成时要注意两点一是文本统一小写并清理多余空白二是向量要归一化。归一化之后内积等价于余弦相似度检索效率更高。批量生成时控制batch size实测下来文本平均长度200字符左右时5000条文本大约需要一到两分钟完全可接受。存储体积可以估算假设5000部电影每部384维用float32存储一个维度的体积是4字节总内存约5000乘384乘4约7.7MB。这在内存里就是小意思根本不需要上分布式存储答辩时如果老师问数据规模与存储可行性直接算这笔账就行。2.3 图像Embedding与多模态对齐的关键点图像Embedding我建议直接用多模态CLIP系列模型它最核心的特点是文本和图像在同一个向量空间里。这意味着你可以把“海报图像”和“电影文本描述”直接做向量相似度计算而不需要额外训练映射层。这是本项目多模态能力成立的基础。CLIP模型输入图像前要做预处理resize到224x224或336x336转成RGB归一化。实际使用中最好把预处理封装成一个函数因为用户上传的海报可能是任意尺寸不统一处理直接喂进模型会报错。输出向量维度通常比文本向量高比如512维。文本向量和图像向量维度不一样不影响各自检索但后续融合计算时要做维度统一或分开打分再加权。一个容易忽略的细节是图像Embedding的batch处理。如果你一次性对5000张图片跑特征显存不够会很尴尬。我在实践里按每批32到64张循环处理输出到numpy数组存下来。整个流程耗时和机器有关用带GPU的环境相当快纯CPU会慢很多建议提前规划好运行环境不要等到答辩前才去跑全量特征。3. 向量检索与混合推荐链路向量检索是系统从“存特征”走向“出结果”的临门一脚。这个模块的核心任务是把用户输入的文本或图像转换成查询向量然后在电影向量库中召回最相似的Top-K个候选。这里的技术方案直接决定系统的响应速度和推荐质量值得花时间调优。我采用的方式是把“向量检索”和“推荐理由生成”拆成两段。向量检索负责快而准地召回候选生成模块负责慢而详细地把候选解释给用户看。如果一上来就让大模型逐部生成推荐理由几十部候选会等很久先召回Top-5再生成效率和体验都能兼顾。3.1 向量库选型Chroma还是FAISS向量库我用过Chroma和FAISS各有优势。Chroma使用门槛极低支持Python原生、自动持久化到本地磁盘后端存储类似SQLite风格非常适合几千条数据量的场景。它自带元数据过滤可以直接存电影ID、标题、年代等字段查询时还能用where条件过滤。FAISS是更偏传统的向量检索库性能上限高支持大规模向量索引和GPU加速。但在几千条数据规模下它的性能优势不明显反而要手动管理索引的保存和加载。真正实践经验是小项目优先Chroma因为上手快、不容易出错如果演示时希望展示“工业级”技术栈可以把FAISS和Chroma结合向量用FAISS存索引元数据用Chroma或SQLite两条链路配合用。选型带来的答辩收益也值得一提。老师问“为什么选这个向量库”最怕听到“因为网上教程多”。能说出Chroma适合小规模持久化、FAISS适合大规模高性能召回再补一句“数据量不同选型策略不同”这个回答比背概念强很多。3.2 检索策略从查询向量到候选集标准RAG检索链路是用户输入文本 → 模型生成文本查询向量 → 在向量库中执行Top-K相似度检索 → 返回电影ID列表 → 读取元数据详情。这一步看似简单但有个很关键的细节是相似度阈值。我最初做召回时没有阈值控制结果用户随便输入“空调坏了”都能召回一堆热门电影因为热门电影在向量空间里天然处于密集区域相似度偏高。后来我加了一个“最低相似度”的判断比如低于0.25的查询结果直接提示用户“描述太模糊试试更具体的关键词”。这样既能避免无意义推荐也能在答辩演示时显得系统有判断力。在一句话查询之外我也加了个“关键实体提取”的轻量环节。查询先拆出几个关键词比如“温暖色调、海边小镇、治愈系爱情”然后分别对每个关键词做向量检索最后把多路召回结果合并去重。这种查询扩展策略对抗长句漂移效果明显也是正文向量直接检索做不到的细节。3.3 多路召回与融合排序我的推荐结果来自三路召回协同过滤、文本语义检索、图像相似检索。三路分数都不能直接比较所以要先归一化再加权融合。协同过滤分数基于用户历史评分语义分数基于自然语言/元数据相似度图像分数基于海报特征相似度。融合公式按场景动态调整。用户只输入描述时文本语义权重最高用户上传海报时图像权重拉高用户单纯想看历史偏好推荐时协同过滤权重占主导。实际代码里可以用一个简单的加权函数根据当前交互模式动态分配权重。你在答辩时可以把这张“权重分配表”直接摆出来向老师解释不同场景下推荐如何兼顾用户行为与语义理解。融合最大的作用是缓解冷启动。新电影没有评分协同过滤给不出分数但语义和视觉索引依然能靠内容特征补位。新用户没有历史行为协同过滤也无能为力此时自然语言或海报输入就是最佳入口。多路召回融合本质上是用多路信息互相兜底这个逻辑讲清楚推荐系统的鲁棒性问题就回答了一半。4. Streamlit应用层实现与性能优化Streamlit层的目标不是做出复杂的后台管理系统而是让用户在三个自然入口轻松获得推荐结果。我最终把系统设计成三个页签自然语言推荐、海报找片、我的片单。侧边栏统一放筛选条件比如年代范围、类型过滤、最低评分阈值这些条件在每次推荐时都会参与排序调权。自然语言推荐页签是重头戏。用户输入一句话系统先判断类型是“找电影”还是“随便看看”。找电影时走完整RAG链路生成推荐理由随便看看时输出随机Top结果配合当前日期和主题的轻量推荐。这个交互设计让系统显得“更聪明”实际上只是在入口处加了一个简单的意图分隔逻辑。4.1 页面结构与交互核心页面布局我建议不要堆砌组件。Streamlit虽然加组件很容易但过度堆砌会显得没有产品思维。核心交互围绕两个输入、一个输出两个输入分别是文本输入框和图片上传框一个输出是推荐结果卡片列表。每张卡片展示电影标题、年代、类型、余弦相似度分数以及大模型生成的推荐理由。推荐理由生成是展示环节的加分项。我接入了本地可运行的轻量大模型把召回的电影元数据拼成Prompt让模型输出两到三句推荐语。Prompt大概长这样基于以下电影信息用一句话推荐这部电影并点出它在视觉风格和剧情上与用户需求的契合点。注意生成必须做超时控制否则用户输入一条奇怪描述时页面会卡在原地。“我的片单”页签则是为了承接协同过滤。选一个用户ID展示他评分过最高的几部电影然后输出协同过滤的推荐结果。这里的交互重点是让评委一眼看出“系统确实在做个性化推荐”而不是每次输出都一样。配合侧边栏的筛选条件同一用户更换类型偏好后结果有明显变化演示效果就很直观。4.2 性能优化从卡顿到秒开Streamlit最容易翻车的地方是每次页面刷新都重新加载模型、重新计算特征、重新生成推荐结果。如果没做缓存一次交互要等几十秒答辩现场会非常尴尬。必须把缓存设计放到工程优先级第一位否则演示效果直接崩塌。第一层缓存是模型加载。用st.cache_resource缓存Embedding模型实例确保整个应用生命周期内模型只在第一次加载时初始化。第二层缓存是数据索引。电影特征向量、元数据表、向量库连接都是只读数据用st.cache_data缓存避免每次刷新重新读取。第三层缓存是推荐结果。同一个查询在短时间内重复点击直接复用上次生成好的推荐结果和理由。图像上传环节也要做压缩。用户上传的海报可能是几兆大小直接喂进模型会拖慢预处理。我先把图片统一压缩到最长边不超过512像素再走CLIP预处理。这样既能保证特征提取效果也能把上传图片的处理时间从几秒降到几百毫秒。另外要给用户视觉反馈。检索耗时较长时用st.spinner加上“正在召回最相关的电影...”之类的文案配合st.progress展示进度。很多毕设系统并不是不好用而是交互等待太干瘪。加上反馈之后整体体验会有非常明显的提升答辩评委的印象分也会更好。5. 答辩高频问题与排坑实录答辩环节的问题其实是可以预判的。评委不会为难你用多么高深的技术他们更在意你能否把“为什么这么做”讲清楚以及系统在不同场景下是否有真实思考。这里我把项目里最容易被追问的五类问题整理出来并给出我实际准备的回答思路。第一类问题是“为什么用RAG和传统推荐有什么区别”。我准备的回答是传统推荐基于用户行为或内容标签匹配RAG核心是把推荐变为带语义检索的生成链路。用户表达的需求经过向量化召回候选再交给大模型生成解释。区别在于系统不是“猜你会不会喜欢”而是“理解你为什么喜欢”。第二类问题是“多模态向量怎么对齐”。我坦诚说CLIP本身提供了共享表征空间所以文本和图像可以在同一度量下比较。但维度不同融合时采用“分开打分再加权”的方式不强行拼接。同时说明如果遇到更细粒度的跨模态对齐可以通过对比学习微调这是论文方向不是毕设必需。第三类问题是冷启动。我的答案是多路召回方案新用户没有评分就走文本和图像检索用户表达越具体结果越准。新电影没有评分就走内容和语义特征因此冷启动不再是死局。配合侧边栏筛选机制短时间内也能产出合理推荐。第四类问题是效果怎么评价。我的回答是分两层离线指标上用召回率和多样性辅助验证在线演示上强调“解释性”即不只给结果还给出推荐理由让用户理解推荐逻辑。同时强调在大规模在线评测中才用A/B测试毕设场景以案例验证为主。第五类问题是“数据量这么小结果能说明问题吗”。我建议这样回答小数据聚焦验证流程完整性如果你的框架在5000部电影中跑通迁移到更大数据只需替换数据源和扩容索引。关键系统设计不依赖数据量而依赖索引结构和融合策略。答辩时把这个格局打开比一直道歉有用得多。5.1 答辩现场老师最爱问的几个问题与回答思路答辩时最容易露出破绽的不是算法而是细节。老师可能会追问文本描述包含多个语言时Embedding模型是否支持回答思路是预训练模型对英语支持最好因此系统对英文元数据做标准化处理对中文元数据可以先用翻译或双语模型转换或者只在纯英文环境中展示完整效果。不要回避问题而是说明你意识到了这个限制并有预案。老师还可能好奇“推荐结果里面有重复电影吗”这其实是在考察多路召回后的去重逻辑。我实际实现了基于电影ID的集合去重并对同一电影的多个召回分数取最高分。这块代码很简短但被问到时能体现出工程严谨性。还有个高频追问是“如果用户的自然语言描述和电影简介完全不匹配怎么办”。我的做法是输入侧加了相似度阈值和提示词引导输出侧保持冷静给出“暂时没有找到高度匹配的电影”并展示相近结果。这比强行推荐一个有调性的结果更有可信度让评委看到你考虑过边界情况。5.2 调试中踩过的坑与规避方法调试过程里踩得最深的坑是嵌入向量“语言不一致”。我最初把中文简介和英文元数据混在一起生成特征结果语义检索几乎不可用热门中文电影和热门英文电影挤成一团完全没有区分度。统一语言之后检索效果立刻正常。这块的教训是特征工程的第一原则是标准化而不是用好模型。第二个常见问题是模型重复初始化。我把模型加载写进了Streamlit的回调函数里结果每点一次按钮都重新加载一次CLIP页面直接卡死。改成st.cache_resource装饰器后问题解决。要学会区分缓存资源和缓存数据前者针对模型、数据库连接后者针对计算结果。第三个问题是内存溢出。图像特征一次性全量提取时机器内存直接飙满。改成批次提取并写入本地文件后稳定了很多。这个问题的通用解法是任何大数据量级处理都要分批不要指望一次循环解决所有事。第四个问题是向量库持久化失败。Chroma默认持久化路径如果没有提前创建会经常写一半报错。解决办法是先初始化持久化目录再操作集合。我曾经因为路径问题重跑了三遍全量索引非常浪费时间。希望学弟学妹看到这条能少走弯路。最后一个建议是答辩前一定要把整个系统在干净环境里跑通三遍。Streamlit依赖版本兼容性很敏感同一个代码在不同机器上表现可能完全不同。提前做好虚拟环境锁定把模型权重和向量索引预下载到本地避免答辩现场临时下载文件。把这些细节准备到位你的演示基本就稳了。