Logo
Published on

从真心话大冒险到 HeartQuest:我为什么让 LLM 来当情侣游戏的"节奏师"

Authors

从真心话大冒险到 HeartQuest:我为什么让 LLM 来当情侣游戏的"节奏师"

一个关于"升温曲线"的产品思考,以及一段与 LLM 相爱相杀的开发记录。

项目仓库:github.com/5SRT7/HeartQuest

为什么做这个项目

市面上的情侣游戏,比如"情侣飞行棋",本质上都是随机出题:骰子一摇,翻一张卡,做什么全靠运气。

但亲密这件事,恰恰是最不能靠运气的。

两个人从害羞到放松,从试探到进入状态,是一条需要节奏感的曲线。随机出题带来的问题是:

  • 前戏还没做够,骰子直接扔出一个"大尺度"题目——尴尬
  • 气氛刚好升温,又抽到一张"对视 20 秒"——扫兴
  • 没有连贯性,没有记忆,每次翻牌都是孤立的

我想做的,是一个懂节奏的引导工具:它知道你们做过什么,知道当前到了哪个温度,然后推出下一步应该做的事。

LLM 天然适合这个角色——它不是摇骰子的机器,而是一个能感知上下文、维持节奏的"DJ"。

产品定位:不是游戏,是指令

最开始我也叫它"游戏",但很快想明白了:它不该有输赢,不该有终点

  • 情侣的主体是人,不是为了通关
  • 可能做到一半就"干柴烈火"了,这时候应该无缝进入现实
  • 如果满足了,直接关掉页面就好
  • 如果不满足,可以继续第二轮,在已有的基础上更进一步

所以它更像一个"共处脚本":在你们需要的时候推一把,不需要的时候就安静退出。

四阶段温度曲线

我把整个体验分成四个阶段:

阶段内容
破冰开始身体接触,隔着衣服抚摸、亲吻
试探逐渐脱掉外衣,抚摸敏感部位
升温脱光,口交等边缘性行为
高潮性交,指定体位和节奏

每个阶段内部再分任务,全部做完自动进入下一阶段。这就是整条"升温曲线"。

"审判官"式语气

UI 和指令都是冷峻的命令风格——没有"亲爱的你可以试试……"这种商量语气,只有直接的指令。

这个设计其实有心理学上的巧思:命令式语气给了双方一个免责框架。"不是我想这样做,是系统让我这样做的"——这种仪式感反而降低了尴尬和犹豫的门槛。

技术选型

  • Next.js 16(App Router):前后端一体,Vercel 部署零配置
  • Tailwind CSS v4 + shadcn/ui:快速搭出黑暗风格的 UI
  • API 提供商:DeepSeek(默认)、OpenAI、Ollama(本地)
  • 隐私优先:用户自带 API Key,只存在浏览器 sessionStorage,不经过任何服务器

架构演进:三版方案,一次比一次"笨"

这是我觉得最值得分享的部分——好的架构往往是被问题逼出来的

第一版:每点一次"完成",调一次 LLM

最直觉的方案:每次点击都请求 LLM 生成一条指令,附带完整的历史记录。

问题立刻暴露:

  1. LLM 会写言情小说。让它"给情侣下达指令",它输出的全是"指尖轻抚苏的眉眼,轻声说:'那就让时间停在这里……'"——这不是大冒险卡片,这是晋江文学城。
  2. 格式不稳定。今天输出 [1] [刘]:xxx,明天输出 阶段1 刘:xxx,后天把 32 条全挤在一行。
  3. 推理模型吃掉所有 token。DeepSeek v4 是推理模型,思考过程写在 reasoning_content 里,经常想完 8192 个 token,content 还是空的。
  4. 历史记录污染。只要历史里出现过一次言情风格,后面全部被带偏。

第二版:疯狂调 prompt

我开始跟 LLM 较劲,写了一大堆规则:

  • "指令必须简短"
  • "不要写对话"
  • "不要描写心理活动"
  • "不要用'了'字"
  • "每轮只输出一条"

结果呢?规则越多,效果越差。LLM 被规则框住之后,输出变得机械、僵硬,甚至开始"分析规则"而不是执行任务。

最后我把所有规则删掉,只留一句:

像大冒险卡片一样下达简短独立的命令。

反而好多了。

第三版:让用户自己写任务,LLM 只做参考

治标不治本的 prompt 调教太累了。我让用户自己整理了一份 141 条的任务文档,分成四个阶段。

这时候思路转变了:不要让 LLM 从零创作,让它参考风格

但后来发现用户其实想要的是"举一反三"——既要有参考池子的风格,又要结合场景生成新任务。于是 prompt 变成:

参考以上任务的风格和尺度,结合当前阶段、两人关系和地点,设计一条合适的任务。

最终版:一次生成 32 条题库,游戏过程零 LLM 调用

这是最重要的一次重构。核心洞察是:

既然实时调用 LLM 有那么多不确定因素,为什么不把不确定性前置?

现在的流程变成:

  1. 用户填写基本信息(名字、关系、地点、API Key、每人每阶段题目数)
  2. 点一次"生成题库",LLM 一次性生成 24~40 条任务
  3. 游戏过程中完全不调用 LLM,只是逐条显示预生成的任务

优点:

  • 游戏过程零等待、零失败
  • 没有格式不稳定问题(解析失败只发生在生成阶段,可以重试)
  • 执行人由前端轮流分配,不再依赖 LLM
  • 任务数可自定义(默认每人每阶段 3 题,共 24 题)

踩过的坑,和我的解法

1. LLM 输出没有换行

生成题库时,LLM 把 32 条任务全部挤在一行,用空格分隔。split("\n") 直接废掉。

解法:不用换行解析,用正则匹配所有 [阶段号] [执行人]:内容 模式,无论有没有换行都能拆开。

2. LLM 输出格式变来变去

  • [1] [刘]:xxx(带方括号)
  • 1 刘:xxx(不带)
  • 阶段1 刘:xxx(中文格式)
  • 第一条任务甚至没有前缀

解法:预处理阶段在所有任务边界插入换行,逐行尝试多种解析模式。方括号全部做成可选。

3. 推理模型空响应

DeepSeek v4 是推理模型,思考过程占用大量 token。当 max_tokens 不够时,模型"想"完了但"说"不出来。

解法max_tokens 提到 16384,并在 prompt 里明确写"直接输出结果,不要输出任何分析过程或思考内容"。

4. 衣物状态不连贯

生成的题目经常出现"刚脱完上衣突然就全裸了"这种跳跃。

解法:在生成 prompt 里写明衣物状态的初始值和每阶段的进度约束,让 LLM 知道这是一条连续的物理状态线。

现在长什么样

HeartQuest/
├── app/
│   ├── page.tsx            # 配置页:填写信息、生成题库
│   └── session/page.tsx    # 游戏页:逐条显示任务、进度
├── lib/
│   ├── llm.ts              # LLM API 封装(DeepSeek/OpenAI/Ollama)
│   ├── phase.ts            # 阶段定义 + 类型
│   ├── prompt-builder.ts   # 系统提示词 + 批量生成提示词
│   └── tasks.ts            # 用户手写的 141 条参考任务
├── hooks/
│   └── use-session.ts      # 会话状态管理
└── components/ui/          # shadcn/ui 组件

最大的收获

1. 不要把不确定性放在关键路径上。

实时调用 LLM 意味着每个用户交互都依赖一个不确定的系统。把生成过程前置成"一次性批量生成",游戏体验的稳定性直接提升了一个数量级。

2. 对 LLM 的 prompt,少即是多。

规则越多,LLM 越容易钻牛角尖。给它一个风格参考(比如"大冒险卡片"),比给它十条禁令有效得多。

3. LLM 是生成器,不是状态机。

让 LLM 每次调用都保持一致的状态(交替执行人、阶段推进、衣物状态),非常不靠谱。正确做法是:LLM 只负责生成内容,状态由前端管理

4. 隐私可以成为产品特性。

自带 API Key + 本地模型支持,不只是技术选择,也是一个卖点:这个工具不需要账号、不需要服务器、数据不出你的设备。

下一步

  • 支持用户自定义参考任务池(前端编辑,而不是改代码)
  • 增加"再来一轮"的自动衔接
  • 尝试更多本地模型(Ollama)
  • 把生成失败时的重试和降级做完善

如果你也做过类似的项目,欢迎交流。如果你想直接体验,仓库在 github.com/5SRT7/HeartQuest