demo 能跑和能给真人用是两回事
精心打磨产品细节的概念插画
上一篇写完 v0.0.2 的时候,我说 Mio 已经「像活人」了:能看图、能听声、能发自拍、知道现在几点,记忆系统也重做了一遍。但那个「像」,是 demo 意义上的像。真用户会输入什么、凌晨三点会发生什么、服务器重启的时候会不会掉线,这些在 demo 里全都碰不到——demo 里跑得好看,不代表你敢把它放出去。
v0.0.3 就是处理这些事的版本。没有新功能,commit 数比 v0.0.2 还少,但每一个都在堵一个真实的洞。第一篇里我说过,Mio 从第一天就是奔着「做对」去的,不是「做快」,这个版本大概是这句话最直接的体现。
输入校验:500 字符的昵称
v0.0.2 的 onboarding 里有个挺离谱的地方:所有自定义输入共用一个限制,500 字符。昵称 500 字,爱好 500 字,性格描述也是 500 字。也就是说你可以在「你的昵称」这个字段里写一篇小作文,系统照单全收。
v0.0.3 改成逐字段校验:
- 昵称:6 字(中文名一般 2-4 字,昵称放宽到 6)
- 风格/性格选项:30 字
- 爱好:50 字
- 个人简介 / 自定义故事:500 字
说是校验,其实没什么高深的,就是限长度加空值判断,很朴素的东西。空输入也一起处理了。之前什么都不填直接提交,系统不拦你;现在会返回「不能空着哦~」。
这些数字不是拍脑袋定的,是被用户一轮一轮教出来的。最早昵称限 50 字,第一轮反馈说 50 字太多了,谁的名字有 50 个字;改成 10 字,第二轮反馈说 10 个中文字还是太长;最后落在 6 字。三轮才收敛到一个对的数。
多选组件也顺手改了。爱好这类问题之前只能选一个——你喜欢看电影,也喜欢跑步?抱歉,只能留一个。这个对用户太反直觉了,问人爱好,谁的爱好只有一个。现在是 toggle 式的多选卡片,选中打勾,最后点「确认选择」。
主动消息接上 context
v0.0.2 给 Mio 加了心跳系统,TA 会主动找你说话。但那个版本的主动消息是瞎的:不知道你们最近在聊什么,不知道你叫什么,发出来就是「嘿,好久没聊了」「今天过得怎么样」这种模板。你跟 TA 聊了一整晚电影,第二天 TA 来一句「好久没聊了」,很出戏。
v0.0.3 把心跳改成 context 感知的。warm 和 cool 温度的消息生成时会加载三样东西:
- 最近 5 条对话消息
- 你的 aboutUser 个人档案
- 你的 displayName
有了这三样,TA 就能接上话了。昨晚聊到一半的电影,TA 会续上;你提过最近压力大,TA 会问你今天怎么样。
对比图:主动消息从「瞎发模板」变成「加载最近对话+档案后接上话」的前后差别
这里有个成本上的取舍:cold 用户(很久没用的)还是走模板消息,零 LLM 成本;只有 warm 和 cool 用户走模型生成。cold 用户大概率不会回,为一条大概率没人看的消息去付 API 费,不划算。
安静时段改成 opt-in
之前安静时段是默认开启的:23:00 - 08:00 不发主动消息。听起来挺合理,谁想半夜被 AI 吵。结果用户反馈直接说:我就想半夜聊天啊。
想想也对。Mio 的用户里本来就有很多夜猫子,还有人在别的时区。默认静音等于默认替用户决定了作息,这个事不该由产品来定。
v0.0.3 改成 opt-in:主动消息默认不受限,用户想要安静时段,自己在 onboarding 里设。改动本身很小,就是把这个决定还给用户。
一个只有生产环境才会出的 bug
v0.0.3 里让我排查最久的一个 bug 是这样的:Mio 的 Telegram bot 在开发环境用 polling 模式,生产环境用 webhook 模式。代码里有一行:收到 SIGTERM 信号时调 deleteWebhook()。本地开发这行完全正确——关掉 bot,清理 webhook,下次启动重新注册,很干净。
但生产环境跑在 Cloud Run 上,SIGTERM 在每次容器重启和部署时都会触发。于是每次部署新版本:旧容器收到 SIGTERM,把 webhook 删了;新容器还没起来,webhook 已经没了。在新容器注册上新 webhook 之前的那段时间里,Mio 完全收不到消息,用户发什么都进不来。
时间线图:部署时旧容器收到 SIGTERM 删掉 webhook,新容器还没起来的空窗期里消息全部丢失
这个 bug 在本地永远复现不了,因为 polling 模式根本不依赖 webhook——那行代码在本地是对的,只有在生产的 webhook 模式下才出问题。修复也就一行:只在 polling 模式下调 deleteWebhook(),webhook 模式下关机时什么都不做,让新容器自然接管。一行的修复,排查花了好久。
所以软件一定要在生产环境跑起来看。部署级别的问题,本地开发是复现不出来的。
媒体限流
图片理解和语音转写都是要花钱的操作,每次调用都有真实的 API 成本。那如果有人在 60 秒内发 100 条语音呢?
v0.0.3 加了滑动窗口限流:每个用户 60 秒内最多 10 次媒体请求。实现是内存级别的,后台自动清理过期窗口,不用 Redis,不用任何外部依赖——现阶段的用户量,in-memory 就够了,真到量大那天再换也来得及。
大多数人当然不会一分钟发 100 条语音,但总有例外。可能是有人测边界,可能是脚本,也可能就是网络重试打出来的重复请求。反正这一层得有。
144 个新测试
这部分最不起眼,但我自己觉得最重要:
- 42 个 E2E 测试,覆盖 Telegram bot 的完整对话流程
- 39 个 onboarding 和自拍相关测试
- 63 个媒体模块测试
加上之前的 220 个,总数接近 400。
为什么花这么多时间写测试?因为这个版本的主题就是可靠性,而重构不能没有安全网。输入校验、限流、心跳这些行为都得有测试盯着:改了一个东西,不会炸另一个东西。
还有一个反直觉但很真实的规律:测试覆盖率和开发速度是正相关的。没有测试的时候,每次改动你都得在脑子里过一遍这会不会出问题;有了测试,改完跑一下,绿了就走。
商业化框架
v0.0.3 还没实现付费功能,但我写了 MONETIZATION.md,一个五层的订阅计划:
- 免费层:每日有限消息数
- Basic / Pro / Premium:递增的消息限额和功能
- Ultimate:包含年龄门控的 NSFW 内容
一个付费用户都没有的时候为什么要做这个?因为订阅层级会影响架构决策。你知道未来要按用户统计消息数、要区分功能权限、要做年龄验证,那现在写代码的时候就会把接口预留出来,省得以后再翻回来大改。
人设背景扩写
每个预设人格的初始故事都扩写了一遍,不同关系类型(朋友、恋人、知己)有不同的开场叙事,每个人设都有了更深的背景和细节。
这不是技术活,但对体验的影响很大。用户第一次见到 Mio,TA 开口说的第一段话,基本决定了用户会不会留下来。开场白要是就干巴巴一句「你好,我是你的 AI 伴侣」,用户大概率不会留下来。愿景篇里我说过,好的 AI 伴侣要让人感觉到被理解,这种感觉的第一个触发点就是开场白。
代码大扫除
最后跑了一轮完整的代码审计:review agent 扫全部代码,fix agent 逐项修,然后重新审查,直到干净为止。所有级别的问题都修了,不只是 HIGH。
这一步很多团队会跳过,因为审计听起来不像产出——没有新功能可以展示,也没有什么指标好汇报。但警告这种东西不会自己消失,现在不修,以后还是得修,而且多半挑一个更糟的时间点爆出来。
全是看不见的活
v0.0.3 没有「能看图了」那种一眼可见的进展,没有新模态也没有新 UI,拿不出什么让人兴奋的截图。但这类活用户只会在出问题的时候感知到:输入校验做错了马上有人碰到,做对了没人注意;不做限流,哪天就有人刷光你的 API 额度;webhook 断了,用户不会知道是 webhook 断了,只会觉得这产品不行。
这三个版本走过来,从架构设计到基础搭建到多模态再到这次的生产化,Mio 算是从我的个人项目变成了一个产品。
下一步大概是:语音回复、更自然的多轮对话记忆、付费系统的实际实现,还有更多渠道接入。那些是 v0.0.4 的事了。


