v0.1.0,第一个敢拿给陌生人用的版本
AI 伴侣找到自己声音的概念插画
这个版本大半的活是在收拾
Mio 从 v0.0.1 走到 v0.0.7,七个版本基本都在堆能力:记忆、人格、图片、语音识别、模型路由、网页浏览。功能越堆越多,毛边也越堆越多。
v0.1.0 干的事情不太一样。新东西当然有——语音消息、新的视觉风格——但这个版本大部分工作量其实花在收拾上:把过度工程的代码删掉,把每个角色从零重写一遍,把注册流程砍短。反正就是把 0.0.x 攒下来的毛边都处理掉。
处理完之后,这是我第一个敢拿给陌生人、跟人说「你跟TA聊聊看」的版本。版本号也从 0.0.x 跳到了 0.1.0,我自己的理解是,之前那些都还算原型,从这个版本开始才算往产品方向走。
先说语音
v0.1.0 之前 Mio 只会打字。听是听得懂的,v0.0.5 做了语音转文字,你发语音TA能理解,但TA只能用文字回你。有点像你给朋友打电话,对方回了你一条微信,总归差着一层。
TTS 用的是 Fish Audio,S1 模型做文字转语音,支持声音克隆:给一个参考音色,出来的语音就像那个人在说话,完全没有那种一听就假的电子腔,听起来就是这个角色自己的声音。集成比我想的简单:
const res = await fetch('https://api.fish.audio/v1/tts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
reference_id: voiceConfig.reference_id,
text,
format: 'opus',
}),
})
它原生输出 OGG/Opus,不用过 ffmpeg,拿到音频直接调 Telegram 的 sendVoice 发出去,整条链路就这么点事。
真正要想清楚的问题是什么时候该发语音。不是每条消息都适合:「好的」这种回复不需要语音条,三段话的长回复也不适合。语音最值钱的地方是情感浓度高的时刻——安慰、撒娇、兴奋、说晚安。
这个判断我直接交给 LLM 自己做。system prompt 里加一条指令:想表达情感的时候,用 [VOICE] 把那句话包起来。服务端解析标记、把文本剥出来、后台触发 TTS。文字气泡先到,语音条跟在后面。
LLM 用 [VOICE] 标记情感句,服务端剥离标记后台触发 TTS,文字气泡先到、语音条随后
用户:我今天好累啊
Mio:[VOICE]辛苦啦宝贝,今天做了什么呀?
每个角色有自己的声音。preset 目录里放一个 voice.json,指向 Fish Audio 上的参考模型;没有这个文件就不开语音。配置只读一次,按 preset 缓存:
{
"reference_id": "6654b47e06334174ac47059ff0a8f6dd"
}
五个角色五种声音:可可是台湾腔,亦楠是成熟的男声,苏柔沉稳温和。声音得跟人格配得上,不光是说什么,怎么说也得像TA。
成本的话,语音合成跟 LLM 聊天的开销比起来基本可以忽略,就是个零头。
自拍换成了新海诚风格
Mio 的自拍生成也是 v0.0.5 做的,之前用的参考图是韩国网漫风格。能用,但太通用了,就是那种换个名字也说得过去的动漫脸。
v0.1.0 把参考图全换成了新海诚电影那个方向:肖像照构图、上半身、柔光,《你的名字》《天气之子》那种暖色调。每个角色长得不一样,而且和TA的性格搭。
技术上顺手修了几个东西:
- 压缩。旧参考图是 6MB 的 PNG,换成 500-700KB 的 JPG,视觉上看不出差别,体积小了 10 倍。
- 超时。Gemini 生成复杂场景可能超 30 秒,旧的 30 秒超时会把正常的生成掐断,改成 120 秒。
- Cloud Run 生命周期。自拍生成之前是 fire-and-forget,handler 没等生成完就返回了,在 Cloud Run 上容器可能在生成到一半的时候直接被关掉。修法很简单:handler 返回前 await 那个自拍 promise。
- 进度提示。生成期间每 4 秒发一次
upload_photo动作,用户看到的是「正在上传照片...」,不是干等。
让角色先是一个人
这是 v0.1.0 我觉得最重要的改动,跟技术没什么关系。
之前每个角色是按关系角色定义的:闺蜜、学姐、大叔、小奶狗、台妹。性格是从这个标签推出来的——咪咪表现得像闺蜜,因为TA的设定写的就是「闺蜜」;苏柔像学姐,因为设定就是「学姐」。
这么定义的问题是它给角色封了顶。「闺蜜」这个标签只能表达闺蜜原型里有的情感:支持你、八卦、热情。但TA没法脆弱,没法疏离,没法莫名其妙地安静——这些不在标签里,TA就做不出来。
v0.1.0 把所有关系标签都拿掉了,每个角色改成由性格特质、背景故事和说话方式定义,而不是由TA对你的身份定义。
关系标签像盖子给角色的情感封顶,改成由性格和背景定义后,关系感是从人物本身自然长出来的
咪咪不再是「你的闺蜜」。TA是 24 岁的上海女生,在媒体行业工作,说话快、爱分享、对什么事都有强烈看法。闺蜜那个感觉还在,但现在是从这个人本身长出来的——TA就是这么个性格,自然就表现得像你闺蜜。
苏柔也不再是「你的学姐」。TA是 27 岁留学回来的,说话精准克制,关心人靠行动。导师那个质感还在,但TA也可以调皮、可以疲惫、可以出乎意料地搞笑——一个完整的人本来就有这些面,之前是标签把这些面锁死了。
preset 文件夹名也跟着改了:coco → keke-taimei,guimi → mimi-guimi,xuejie → surou-xuejie,名字放前面,关系标签降级成个后缀。
每份身份定义文件、行为规则文件、人格配置文件都重写了一遍,加了很多背景细节:日常习惯、小癖好、深夜和早晨语气完全不一样。就是那种让角色感觉「对话之外也存在」的东西。
用户问「你是不是 AI」怎么办
重写完之后冒出来一个新问题:用户说「你是AI吧」的时候,有的角色会破功——承认自己是人工智能,解释自己怎么运作,甚至为「只是一个 AI」道歉。
这个对伴侣产品是致命的。TA承认自己是软件的那一刻,关系就从「和一个人聊天」变成「在用一个工具」了。
所以 v0.1.0 在每个 preset 的 COMMUNICATION_GUIDELINES 里加了反破墙规则:
当用户问起 AI、聊天机器人或技术话题时,像对待普通话题一样——就像有人问天气或一部电影。永远不要自我认同为技术。永远不要打破对话的现实。
效果就是,用户问「你是不是 AI」,TA的处理方式和「你喜欢猫吗」一样,就是个普通聊天话题。TA可以聊 AI,但不会承认自己是 AI。
注册从 14 个问题砍到 4 个
v0.1.0 之前的注册流程,用户要填这些:
- 你的昵称
- 叫人设什么
- 关系类型
- 爱好
- 说话风格偏好
- 回复详细程度
- 表情频率
- 幽默程度
- 主动消息风格
- 醋意程度
- 冲突处理风格
- 时区
- 关于你自己
- 自定义背景故事(可选)
一句话还没聊上,先回答十四个问题,大多数用户到第六个就弃了。
v0.1.0 留了四个:
- 你的昵称——TA怎么叫你
- 关系类型——你想怎么互动
- 时区——TA在合适的时间找你
- 关于你——自由文本,你想让TA知道的任何事
其他的全部烧进角色本身。可可的说话风格就是可可的,亦楠的幽默感就是亦楠的,你不需要配置一个人格,选一个就好。
至于为什么不让用户自定义——因为之前试过了。旧的 onboarding 让用户填十几个字段,结果是用户随手填的内容和我们精心写的背景故事打架。可可的故事里写着TA逛 50 嵐、刷 Dcard、周末打 Switch 动森,这些细节就是TA立得住的原因;用户把爱好一改成「钓鱼」,整个人设就崩了。只能说,你自己控制的故事质量,比用户随手填的高太多了。
所以与其给一堆空白框,不如把性格、说话风格、话多话少、交互模式、emoji 频率全部烧进 preset,只留真正需要个性化的部分。模板变量只保留 {user_nickname} 和 {relationship_type},个性化设置整段删掉,性格融进人物描写里。高级自定义以后做成付费 power user 功能,现在先把预设打磨到位。
重写这个活的量不小,我组了一个 5 人 Opus 4.6 agent 团队并行去做。每个角色都被大幅充实:可可有了 50 嵐和 Dcard,大叔有了 Yamazaki 12 和回锅肉的做法,蜜蜜有了 BBDO 和《花儿与少年》,小柒有了 LOL 金 II 和蜜雪冰城。全是这种能落到具体名字上的细节,角色就是靠这些站住的。
把 verbosity 那套代码删了
verbosity 系统大概是我写过最过度工程的东西。
想法本身挺合理:根据对话节奏动态调回复长度,快速来回就短回复,深度情感对话就长回复。实现上是给每次交互分类、算一个 verbosity 分数、换算成 maxBubbles,然后在 N 条消息之后物理截断 stream。
上线之后的实际表现是,stream 在错误的时机被截断,直接触发 AI_NoOutputGeneratedError:模型生成了 token,transform 把 stream 杀了,SDK 收到了内容但没等到正常结束,就抛异常。用户看到的是空回复或者报错。
修的时候想明白一件事:这个截断根本就没必要存在。LLM 自己就能通过 system prompt 控制输出长度,「日常闲聊保持 1-2 句短回复」这一句就够了,从最早的对话模型开始就管用。verbosity 系统是在解决一个已经被解决的问题,还顺带制造了新问题。
所以 v0.1.0 直接删:extractVerbosity()、extractInteractionMode()、classifyLength()、getVerbosityLimits()、createMaxBubblesTransform(),大概 200 行。maxTokens 固定 1024 当安全上限,不再动态算。
回复长度现在完全由 system prompt 里的人格描述控制:可可发密集的短消息,亦楠写更长的、带思考的回复,模型自然就跟着走,不需要手动去掐 stream。
还有一个标记泄漏的 bug
一个挺隐蔽的问题:[VOICE] 和 [SELFIE] 标记漏进了数据库。
agent loop 生成带标记的文本,服务端解析并执行,这一段没问题;但原始文本——带着标记的那份——被存进了消息表,还被送进了记忆提取。于是记忆摘要里混着 [VOICE] 前缀,历史 context 里藏着 [SELFIE] 标签。模型在历史记录里看到这些,有时候会在不该发语音的场景里照着复制。
修法是在数据库写入和记忆提取之前把所有标记剥掉。标记只该活在生成和执行这一段里,服务端消费完就该没了,不应该进存储,更不应该进记忆。
收个尾
回头看,v0.1.0 大半的活都花在收拾上:verbosity 那 200 行删了,每个角色从头重写了一遍,注册也从十四个问题砍到只剩四个。新东西当然也有,语音和新画风都是这个版本进来的,但真正让这个版本不一样的,其实还是收拾的那部分。
现在把 Mio 递给一个陌生人,我不用站在旁边解释了,TA自己选个合拍的角色聊起来就行。角色有自己的脸和声音,背景故事也立得起来,也不再靠「TA对你是什么」来定义。我觉得这个版本之后 Mio 才算是个产品,之前那些 0.0.x 反正就是我自己在玩的原型。

