- Published on
从一部电影到下一部:CineMind 的 AI 电影发现平台开发记
- Authors

- Name
- Tails Azimuth
从一部电影到下一部:CineMind 的 AI 电影发现平台开发记
项目仓库:5SRT7/CineMind
为什么想做这个项目
我一直想要一个真正的"电影推荐系统",不是那种"你看过《黑天鹅》,所以推荐《黑天鹅》的演员还演过什么"的简单逻辑,而是当我说:
我想看一部心理恐怖电影,主角逐渐精神崩溃,结局最好比较压抑。
系统真的能理解这种描述,并从庞大的电影库里找到合适的那一部。
所以我开始做 CineMind。最初定位是一个类似 IMDb / 豆瓣的电影搜索网站,但核心不是关键词搜索,而是 AI 理解、语义检索和推荐解释。
一开始的设想
第一版设计里,我希望用户直接用自然语言搜索:
找一些类似《燃烧》的电影,最好是韩国或者日本的,节奏慢一点,压抑、有悬疑感,不要商业大片。
整个流程大概是这样:
用户输入
-> LLM 理解需求
-> 提取结构化条件
-> TMDB 获取候选
-> 获取电影详情
-> 构建电影语义文档
-> 生成 Embedding
-> pgvector 语义检索
-> 混合排序
-> 生成推荐解释
当时我给自己定了几条原则:
- TMDB 是电影事实数据源,不自己编剧情。
- 不要同步整个 TMDB 到数据库。
- PostgreSQL + pgvector 只做 AI 搜索的临时语义索引。
- 成本尽量为 0,部署尽量简单。
- 不要过度工程化。
技术选型
最终选择了下面这套组合:
| 部分 | 选择 |
|---|---|
| 前端 | Next.js 16 + TypeScript + Tailwind CSS |
| 流程编排 | LangGraph |
| 电影数据 | TMDB API |
| 向量检索 | Neon PostgreSQL + pgvector |
| LLM / Embedding | OpenAI 或硅基流动 |
| 图标 | lucide-react |
Next.js 的好处是前后端在一个项目里,API Route 可以直接承担服务端逻辑,不需要为了第一阶段单独拆 FastAPI。
核心架构:把电影变成"可检索的语义文档"
传统电影推荐往往依赖类型标签。比如用户说"心理恐怖",系统就去找 Horror + Thriller 标签,结果可能全是新上映的商业恐怖片。
我在 CineMind 里做了一件事:把电影转成语义文档。
Title: The Machinist
Overview:
A machinist suffering from severe insomnia...
Genres:
Drama, Thriller
Keywords:
insomnia, paranoia, guilt, psychological
Director:
Brad Anderson
Cast:
Christian Bale, Jennifer Jason Leigh
AI Tags:
Psychological, Sleepless, Paranoid, Slow Burn
这段文本同时包含剧情、情绪、主题、人物和导演信息,再交给 Embedding 模型生成向量。这样用户说"主角长期失眠、逐渐精神崩溃",系统就能通过语义相似度找到《机械师》,而不是只靠类型硬匹配。
LangGraph:把 AI 搜索变成一条有状态的流水线
项目里用 LangGraph 管理 AI 搜索流程。它不是一个 Agent 到处乱跑,而是一条明确的状态图:
START
-> parse_query
-> retrieve_candidates
-> fetch_movie_details
-> build_documents
-> embedding
-> vector_search
-> rerank
-> generate_explanation
-> END
每个节点职责单一:
parse_query:解析自然语言或选片信息,生成结构化需求。retrieve_candidates:调用 TMDB 获取候选电影。fetch_movie_details:补齐剧情、关键词、演职员和分级。build_documents:构造语义文档。embedding:生成向量并写入临时索引。vector_search:在 pgvector 里做语义检索。rerank:结合结构化条件和口碑重新排序。generate_explanation:为每部电影生成推荐理由。
用 LangGraph 不是为了炫技,而是让每一步都能单独调试、单独测试,也方便以后扩展。
混合排序:不只看语义相似度
只用 Embedding 相似度会遇到一个问题:语义很像但口碑很差的电影可能排得很靠前。
所以我做了混合评分:
final_score =
semantic_similarity * 0.50
+ structured_match * 0.20
+ preference_match * 0.20
+ quality_score * 0.10
- 语义分来自 pgvector 的向量相似度。
- 结构化分来自类型、地区、语言、年份等条件。
- 偏好分来自情绪、风格和主题关键词。
- 口碑分来自 TMDB 评分和投票数。
这也是我理解中"Hybrid Search"的意义:向量负责理解语义,结构化条件负责约束候选,口碑负责兜底质量。
产品演化:从"让用户提问"到"让用户选电影"
第一版我做了自然语言 AI 搜索。后来用户反馈说:
我不想输入一段话,我就是看完一部电影,想再看类似的。
这个反馈很关键。它让我重新思考:普通用户不会像 Prompt Engineer 一样组织语言,但他们很清楚地知道自己喜欢哪几部电影。
于是我把 AI 搜索改成了"选电影找相似":
- 用户搜索并选择 1-5 部电影。
- 系统分析这些电影的共同类型、关键词、氛围和地区。
- 通过 TMDB 的
/similar和/recommendations获取候选。 - 构建包含共同点的语义查询。
- 在已 Embedding 的候选中排序,并解释推荐原因。
后来又加入了可选筛选:
- 类型
- 年份范围
- 语言
- 分级
- 最低评分
- 备注
备注不会被当作硬性条件,而是写进语义查询文本,让 AI 在排序时更理解用户偏好。
踩坑记录
这个项目最大的收获不是"功能做出来了",而是遇到并解决了一堆真实问题。
1. TMDB 域名在不同网络环境下不可达
开发过程中,api.themoviedb.org 在本地网络一直连接超时,但硅基流动可以正常访问。
排查后发现不是 Key 的问题,而是网络对 TMDB 域名的连通性不稳定。
解决方案:
- 支持配置
TMDB_API_BASE_URL切换镜像。 - 支持
TMDB_PROXY/HTTPS_PROXY走代理。 - 自动尝试备用域名
api.tmdb.org。 - 请求超时后自动降级到演示数据。
2. cast 是 PostgreSQL 保留字
建表时我把演员列表字段命名为 cast,结果 pgvector 建表一直失败:
syntax error at or near "cast"
AI 推荐看起来还能用,因为它自动降级到了内存缓存。但这意味着 Neon + pgvector 从来没真正启用。
最后把字段名改成 cast_names,pgvector 才真正生效。
3. TMDB 的 certification 参数不可靠
筛选分级时,TMDB Discover 的 certification 参数表现很不稳定:
R可能直接返回空。NR会混入大量 R 级片。
我最后改成"候选先拿回来,再逐部读取 /release_dates 的真实分级,然后过滤"。虽然多了请求,但结果准确。
4. 分页不是简单的 page + 1
TMDB 每页固定 20 条,而我想让产品每页显示 25 条。加上分级过滤后,一页可能只剩几条。
最后我在应用层重新分页:
- 普通浏览:抓取 2 个 TMDB 页,再切成 25 条。
- 分级筛选:持续扫描候选页,直到补满 25 条符合条件的电影。
- 前端提供页码选择、上一页、下一页,并自动回到页面顶部。
5. React Hydration 与 sessionStorage
AI 推荐结果保存在页面内存里,用户点进电影详情再返回,推荐就丢了。
我改用 sessionStorage 保存选片、筛选、备注和推荐结果。结果第一次实现时产生了 Hydration Mismatch,因为服务端渲染时没有 sessionStorage,客户端却用已保存状态初始化。
正确做法是:首屏永远用空状态渲染,页面挂载后再从 sessionStorage 恢复。
待看列表:用户数据要跟 AI 索引分开
详情页加入了"想看"按钮,数据保存在 Neon 的 user_watchlist 表。
这里我坚持了一个边界:
movie_search_cache是 AI 搜索专用的临时语义索引。user_watchlist是用户数据。- AI 的 Embedding 流程不读取待看列表。
因为待看列表代表的是用户偏好,不应该污染电影本身的语义索引。以后如果要做"根据待看列表推荐",再单独加一个入口,把列表 ID 喂给现有的电影推荐流水线。
为什么先不做登录?
待看列表目前是单用户模式,user_key 默认为 default。以后如果公开部署,可以把 user_key 换成用户 ID,不需要改表结构。
UI 设计:红黑电影感
整个界面采用深红 + 黑色主题,背景接近纯黑,主色是深红色 #B91C1C,配合金色评分。
电影卡片做了类似"抽卡"的悬浮效果:
- 鼠标移上去卡片向上浮起。
- 轻微放大并旋转。
- 深红色投影和斜向高光。
项目 Logo 也用统一的深红色重新调整,保持视觉一致。
一些取舍
为什么没有同步整个 TMDB?
同步整个 TMDB 成本太高,也不符合第一阶段目标。我只需要在 AI 搜索时缓存当前使用的候选电影,24 小时后过期。
为什么不用 Redis?
当前阶段只有一个简单的 TTL 需求,pgvector 表本身就可以承担,不需要引入额外组件。
现在做到什么程度
当前项目已经完成:
- 首页电影浏览
- 普通搜索
- 电影详情页
- 筛选浏览 + 分页
- 选电影找相似
- AI 推荐解释
- 待看列表
- OpenAI / 硅基流动双 Provider
- Neon pgvector 临时语义索引
- 演示数据降级
以后想做什么
- 用户登录和真正的多用户待看列表。
- "看过"状态管理。
- 基于待看列表的显式推荐入口。
- 长期电影语义索引。
- 更细粒度的推荐解释,比如按情绪、叙事节奏、导演风格拆分。
最后
CineMind 是我做过的完整度最高、也最贴近真实产品的一个 AI / RAG 项目。
它让我真正理解了几件事:
- 向量检索不是银弹,要和结构化条件、质量分一起用。
- 产品交互会改变技术设计。用户不想写 Prompt,于是推荐流程从"自然语言理解"变成了"电影共同点分析"。
- 真实项目里大量时间花在网络环境、数据库连接、保留字、Hydration 这些"不性感"的问题上。
- 个人项目也要控制边界,MVP 永远比完美架构更有价值。
如果你也在做 AI 应用,我的建议是:先让核心闭环跑通,再根据真实使用反馈迭代。功能可以少,但每一个功能都要能解释"为什么存在"。
