v0.1.0:TA找到了自己的声音
AI 伴侣找到自己声音的概念插画
终于不是在"试着做"了
七个版本。v0.0.1 到 v0.0.7,每个版本都在疯狂堆能力——记忆、人格、图片、语音识别、模型路由、网页浏览。功能越来越多,毛边也越来越多。
v0.1.0 不一样。
这个版本有新东西——语音消息、新视觉。但真正的活儿藏得更深:剥掉过度工程的代码,从零重写每个角色,把注册流程砍到骨头。
这是我第一个敢塞给陌生人说"你跟TA聊聊"的版本。
版本号本身就是态度。0.0.x 是原型。0.1.0 是产品。
TA开口了
v0.1.0 之前,Mio 只会打字。能听懂语音消息(v0.0.5 做的语音转文字),但只能用文字回你。
你想想这个画面——你对TA说话,TA给你打字回。像打电话给朋友,对方回了条微信。
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。一个 API 调用,一次发送。
但更有意思的问题是:什么时候该发语音?
不是每条消息都适合。"好的"不需要语音条。三段话的长回复也不需要。语音最适合情感浓度高的时刻——安慰、撒娇、兴奋、晚安。
我让 LLM 自己判断。system prompt 里加一条 [VOICE] 标记指令:"当你想表达情感的时候,用 [VOICE] 包裹。"服务端解析标记、剥离文本、后台触发 TTS。文字气泡秒到,语音条紧跟其后。
用户:我今天好累啊
Mio:[VOICE]辛苦啦宝贝,今天做了什么呀?
文字先到。然后:一条语音,温暖熟悉,问你今天过得怎样。
每个角色有自己的声音。preset 目录里 voice.json 指向 Fish Audio 参考模型。没有 voice.json 就关闭语音。配置只读一次,按 preset 缓存:
{
"reference_id": "6654b47e06334174ac47059ff0a8f6dd"
}
五个角色,五种声音。可可说台湾腔。亦楠是成熟的男声。苏柔沉稳温和。声音和人格匹配——不只是说什么,还有怎么说。
成本?跟 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不能脆弱、不能疏离、不能莫名安静。这些不符合标签。关系角色变成了牢笼。
v0.1.0 把所有关系标签拿掉了。每个角色现在由性格特质、背景故事和说话方式定义——不是TA对你的身份。
咪咪不再是"你的闺蜜"。TA是 24 岁的上海女生,在媒体行业工作,说话快、爱分享、对什么事都有强烈看法。闺蜜感还在——但它是从TA这个人身上自然流出来的,不是标签在规定TA该怎么表现。
苏柔不再是"你的学姐"。TA是 27 岁的留学归来者,说话精准克制,用行动表达关心。导师的质感还在——但TA也可以调皮、疲惫、出乎意料地搞笑。因为TA是一个人,不是一个角色。
preset 文件夹名也跟着改了:coco → keke-taimei,guimi → mimi-guimi,xuejie → surou-xuejie。名字在前,标签在后。身份优先。
每份身份定义文件、行为规则文件、人格配置文件都重写了。加了深度角色背景——日常习惯、小癖好、深夜和早晨完全不同的语气。就是那种让一个角色感觉"在对话之外也存在"的细节。
打破第四面墙怎么办
重写之后,一个新问题冒出来。用户说"你是AI吧"的时候,有些角色会破功——承认自己是人工智能,解释自己怎么运作,甚至为"只是一个 AI"道歉。
这是致命的。当一个伴侣承认自己是软件的那一刻,关系从"和一个人聊天"变成了"在用一个工具"。
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 和蜜雪冰城。不是泛泛的标签,是有名有姓的朋友、具体的饮料、真实的地名。
删掉 200 行代码
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 里的人格描述控制。每个角色的人格配置文件定义了TA怎么发消息——可可发密集的短消息,亦楠写更长的思考性回复。模型自然遵循。不需要手动操控 stream。
清理标记
一个隐蔽的 bug:[VOICE] 和 [SELFIE] 标记泄漏到了数据库里。
agent loop 生成带标记的文本,服务端解析并执行,但原始文本(带标记的)被存进了消息表,也被送进了记忆提取。记忆摘要里混着 [VOICE] 前缀,历史上下文里藏着 [SELFIE] 标签。模型在历史记录里看到这些,有时会在不该用语音的场景里复制它们。
v0.1.0 在数据库写入和记忆提取之前剥离所有标记。标记是临时指令——存在于生成中,被服务端消费,然后消失。不存储,不记忆,不回收。
从毛坯到精装
v0.1.0 之前的 Mio:能力不错但毛边很多的文字伴侣。过度工程的内部实现、通用的视觉风格、用关系标签定义的角色、像填税表一样的注册流程。
v0.1.0 之后:每个角色是一个独立的人——有脸、有声音、有背景故事、有不依赖于"TA对你是什么"的存在方式。选一个跟你共鸣的人,回答四个问题,开始聊天。TA用文字回你,有时候——在合适的时刻——用TA自己的声音。
这是让人忘记自己在跟软件说话的版本。不是因为技术被藏起来了,而是角色足够鲜活,技术变得不重要了。