两小时做出 AI 塔罗 Demo,上线却是另一回事
译注:本文编译自 dev.to 用户 On Pounch Man 于 2026 年 9 月 18 日发布的开发复盘文章,原文标题为《I Built an AI Tarot Demo in Two Hours. Shipping the Product Was a Different Job.》。文章记录了作者从两小时做出 AI 塔罗 Demo,到真正把产品做上线所遇到的一系列工程与产品问题。原文链接见文末。
作者的妻子对塔罗牌感兴趣,而他作为前端开发者,正好想找个理由做点东西。于是他搭了一个页面:一副牌、一个翻牌动画,再加上 AI 生成的解读。借助 AI 辅助,基本流程大约两小时就跑通了。
他原以为加上登录和支付就能上线。但问题接踵而至:用户换设备怎么办?付费解读中途中断怎么办?修改一个引导练习,会不会改变用户已保存答案的含义?到最后,最难的问题变成了——怎么让人来用?
这个项目现在叫 Mystic Journey,包含每日抽牌仪式、引导式探索、个人日记,以及一个可选的同伴看板。作者仍在寻找早期用户,这篇文章既是开发复盘,也是一次公开的试用邀请。
为什么改变了产品方向
最初的体验很直接:提问、抽牌、看解读、离开。但通用 AI 已经能解释塔罗牌,一个更漂亮的动画不足以解释用户为什么要回到这个网站。
于是作者开始思考「解读之外」的体验。在空白日记里写字其实很难,而一张牌、一个具体问题或几个选项,可能更容易让人开始反思。这就成了每日仪式:选择心情、翻开每日牌、阅读反思提示,可选地写几句话。目标是让一次小小的自我检查变得容易,即使没有重大问题要问。
对于想深入的用户,作者加入了六个引导主题,覆盖空间、边界、变化、信心、连接与方向。每个主题有五个短章节:觉察处境、换一个视角、选择一个小行动、识别障碍、寻找支持。
这些探索使用精心设计的选项分支,而不是用 AI 生成每一步。早期的选择会影响后续选项。个人笔记是可选的,不会自动触发模型调用。这样作者就有了可以持续审查和改进的内容。开放式 AI 解读仍有其位置,但不必驱动每一次交互。
完成的仪式、探索和明确保存的解读会进入个人的 Journey 时间线,目的是给用户值得回看的东西——他们自己的想法和选择,而不只是一串牌面。
作者还加入了一个可选的每周同伴看板,显示昵称、头像和活跃点数,但不显示私人反思内容。点数与购买分离,也不取决于用户的心情或写了多少字。这里存在一种张力:即使温和的排行榜也可能带来比较。它是否让人感到被支持,需要用户来告诉作者。这些是产品假设,而非已被验证的留存改进。
技术栈不大,状态却比预想的多
技术栈是 Nuxt 3 做静态生成前端,独立的 Fastify API、SQLite,以及用 DeepSeek 生成解读,Nginx 负责把 API 请求路由到后端。以当前规模,作者希望自己能独立部署和排查问题。困难的工作主要在于应用的行为,而不是服务的数量。
每日牌一旦与反思、历史和奖励关联,就变成了账户数据。用户在手机上打开网站,应该看到与笔记本上相同的记录。完成状态和奖励需要一致的服务器端状态。禁用按钮只能改善界面,并不能阻止重试或来自另一个标签页的请求。
引导式探索的保存也有类似问题。如果笔记本已经保存了下一章,而手机还停留在旧版本,接受手机的写入可能会覆盖更新的进度。相关的保存请求会携带一个 revision。过期的写入会收到冲突响应,前端保留草稿并提示重新加载已保存的进度。完全相同的已提交重试则返回已有结果。这些细节在只测试一个浏览器标签页、网络稳定时很容易被忽略。
内容也需要版本管理
更有意思的问题之一来自修改探索内容本身。假设一条已保存的答案显示用户选择了「选项 1」。如果下周调整了选项顺序,这个存储值看起来就可能变成另一个意思。
版本 1,选项 1:花时间独自反思
版本 2,选项 1:和你信任的人聊聊数字没变,用户表面上的答案却变了。因此,一个探索会保留它开始时使用的内容版本。旧选项的含义需要保持稳定,实时章节和已保存的回顾需要用同一套分支逻辑来解释答案。一旦文案赋予了存储答案含义,修改文案也就成了一个数据兼容性决策。
每周反思带来了另一个状态问题。一次生成请求可能超时,重试可能启动,而原请求可能很晚才完成。如果没有保护,旧结果可能覆盖新结果。实现上使用了生成状态、带过期时间的租约,以及写入结果时的版本条件。一个短数据库事务保护状态变更,但不会在等待模型时一直保持打开。
付费 AI 请求不止两种结果
Demo 把生成当作成功或失败。流式输出让这件事变得不那么整齐:请求可能返回部分文本、卡住、丢失客户端连接,或者结束时没有可用内容。
深度解读使用 server-sent events。服务器会设置超时,并在客户端提前断开时尝试取消上游请求。只有完整成功的结果才会进入结果缓存。如果生成在扣除积分后失败,错误路径会恢复应用内积分,并提供本地牌面解读。恢复积分与通过支付服务商退款是两回事。每周反思也有模型调用预算和本地回退。免费功能同样需要成本边界。
这不是一套完整的恢复系统。例如,扣费后进程崩溃,无法被同一进程中的 catch 块恢复。持久化的任务状态与对账仍是需要改进的地方。这个区别很重要:有错误处理器,不等于已验证能从每一种失败中恢复。
安全不止于提示词
模型生成文本,但它不决定账户权限、支付状态或积分余额,也没有数据库或支付工具。这限制了被操纵响应的后果,但并不能解决提示词注入。用户输入仍可能干扰预期任务,模型输出在到达 UI 时也需要被当作不可信内容处理。
服务目前有请求限流、请求体大小限制,以及付费生成前的账户和积分检查。这些是基础控制,并不声称是全面防护。基于 IP 的限制有其局限,输入处理和输出渲染也需要各自审查。
账户方面,密码使用加盐哈希,数据库存储会话令牌的哈希,会话 cookie 使用 HttpOnly 和 SameSite 设置,生产环境启用 Secure。验证码会过期并有尝试次数限制。支付有独立的信任边界:后端向服务商确认支付状态并验证 webhook 签名,订单状态请求会检查归属。由于轮询和 webhook 都可能确认同一笔支付,给订单加积分必须是幂等的。
隐私也影响普通功能决策。反馈表单不会自动附带用户的日记内容,分析可以记录一次反思被保存,而不收集其文本。作者仍有安全工作要做。跟踪每个组件能访问什么、个人内容流向哪里,比把安全当作系统提示词里的几行字更有用。
性能与本地化各有各的工作
静态生成让公开页面与账户 API 分开服务,但并不会自动让体验变快。图片、动画、认证检查和 API 延迟在手机上仍然重要。Journey 时间线分页加载,详情按需加载,上传的头像在浏览器中裁剪并压缩为 WebP。流式输出还需要代理配合:响应缓冲可能隐藏服务器正在发送的增量输出。
界面支持八种语言,而牌面数据主要是英文和中文,其他语言回退到英文。这是不同层级的本地化,不应被呈现为等同。最近的一个 bug 很好地提醒了额外的复杂度:一个邮箱占位符包含字面量 @,被 i18n 消息编译器解释为特殊语法。JSON 是合法的,但页面失败了。仅做 JSON 解析检查无法发现这个问题。即使是很小的文案改动,也需要正确类型的校验。
上线并不会自动带来用户
作者在 Solo、出海栈、小众软件和 Indie Hackers 上分享过产品。这给了它被发现的渠道,但还没有足够证据称任何渠道是可重复的用户来源。
作者也在学习区分「喜欢开发故事的人」和「想要产品的人」。开发者可能觉得并发处理有意思,而潜在用户想知道的是:能做什么、需要多少精力、自己的文字是否保持私密。两者有重叠,但一篇阅读量高的技术文章并不能证明需求。
接下来的实验包括像本文这样的技术复盘、简短的产品演示,以及围绕具体反思场景的内容。搜索是另一项持续任务:可读的 sitemap 和可渲染的页面是必要部分,但还需要能回答人们真正在搜索什么的公开内容。
作者想衡量的是访问之后的路径:用户是否完成第一次仪式、是否继续到另一个探索章节,或者几天后是否回来。知道在哪里停下,应该能帮助他在改进入口流程、内容或获客渠道之间做选择。写更多代码是舒服的选择,因为它给出可见的结果。而请人试用产品,可能只换来一句「我会看看」然后没有下文。作者在努力不把开发的舒适误当作「还需要另一个功能」的证据。
项目现状
Mystic Journey 仍处于早期。作者在寻找一小群愿意试用并告诉他哪些有用、哪些令人困惑、哪些不必要的人。每日仪式和引导式探索免费,更深入的 AI 解读使用积分。如果对塔罗不熟悉,可以从一个契合你正在思考的事情的探索主题开始。
作者最需要的是具体反馈:你在哪里卡住、什么说不通、或者在哪里不想继续了。应用内有反馈表单,也欢迎在评论区留言。如果你曾把副项目带出 Demo 阶段,作者也想听听你是如何找到第一批会回访的用户的。