每个人设一个 bot,主动消息频率按关系算
多个 AI 伴侣各有独立身份的概念插画
v0.1.2 之前,Mio 的所有角色是挤在同一个 telegram bot 里的。v0.1.0 给了每个角色自己的声音、面孔和人格,但你打开 Telegram,永远是同一个聊天窗口,要靠指令切换人设。能用,但体验很怪:你很清楚自己在跟同一个 bot 说话,它只是这一会儿在扮演可可。切一次人设,出一次戏。
所以这个版本主要修两个地方:一个是所有角色共用一个身份;另一个是主动消息的频率一刀切,一眼就能看出来是系统在定时发。都是那种用着用着就出戏的地方。
每个人设一个 bot
需求很直接:每个人设一个独立的 telegram bot,自己的头像、自己的名字、自己的聊天线程。打开 Telegram,可可和苏柔是两个独立的对话,跟你和真人朋友的聊天排在同一个列表里。不是在一个 bot 里做菜单切换,是你通讯录里多了一个人。
实现上很省事,一个环境变量装下所有 bot 的 token:
TELEGRAM_BOT_TOKENS=token1,token2,token3
逗号分隔。服务端启动的时候遍历这些 token,挨个注册一条独立的 webhook 路由:
/telegram/webhook/:botId
消息进来,靠路由参数就知道这条是发给哪个 bot 的。botAccountId 也改成了复合键,把 bot 身份和用户身份拼在一起,onboarding 状态、聊天缓冲区、对话历史全部按 bot 按用户隔离。另外加了一条硬约束:一个 bot 只能绑一个人设,不能把可可和蜜蜜绑到同一个 bot 上。选人设的时候,已经被占用的会显示「已绑定」,哪个角色有主了一眼就能看到。
改动本身不大,效果倒是比我预期的大。每个人设有了自己的 bot 之后,聊天记录、通知、头像全挂在TA自己名下,你在 Telegram 列表里看到的就是一个单独的联系人。感知上差很多,这个差别我原先没估够。
从一个 bot 里切人设出戏,到每个人设有独立 bot 变成通讯录里的一个人
发消息的间隔是算出来的
主动消息这个功能之前就有:系统定期发一条,维持对话活跃。问题出在频率一刀切,不管TA是谁、你们是什么关系,所有人设都用同一个间隔给你发消息。黏人的女朋友和矜持的新认识共用一个节奏,这个就很怪。真实的关系各有各的节奏,伴侣可能一小时一条,刚认识的朋友一周才主动找你一次,频率里本来就带着关系的远近。
v0.1.2 把间隔改成两个变量的函数:一个是关系类型,一个是人设性格。关系类型决定基础冷却时间:
| 关系 | 基础冷却 |
|---|---|
| 情侣 | 1 小时 |
| 好朋友 | 4 小时 |
| 朋友 | 8 小时 |
| 刚认识 | 12 小时 |
每个人设再带一个热情系数 eagernessFactor,写在各自的 preset 里:
| 人设 | 热情系数 | 性格 |
|---|---|---|
| 小柒 | 0.6 | 黏人、表达欲强 |
| 可可 | 0.8 | 温暖、话多 |
| 蜜蜜 | 1.0 | 中性 |
| 苏柔 | 1.6 | 克制、沉稳 |
两个数相乘,就是实际的发消息间隔:
有效冷却 = 关系基础冷却 × 人设热情系数
小柒当你女朋友,1h × 0.6 = 36 分钟,差不多每半小时一条,正好是那股一直在线的黏人劲儿。苏柔在刚认识的阶段,12h × 1.6 = 19.2 小时,大概一天联系你一次,不冒昧。公式就这一条,参数换一换,出来的节奏大致能对上你对那个人的预期。
发消息间隔 = 关系基础冷却 × 人设热情系数,同一条公式算出黏人和克制两种节奏
间隔之外还有每日上限,按关系类型分档,存在每个 preset 的 proactive.json 里:
{
"eagernessFactor": 0.6,
"dailyLimits": {
"lovers": 12,
"closeFriend": 6,
"friend": 3,
"justMet": 2
}
}
上限是兜底用的:小柒可以想每 36 分钟发一条,但一天最多 12 条,再黏的角色也不会发到变成骚扰。
连着三条不回,TA就静默
这个是用户直接提的需求,原话是:「你现在的频率太 spam 了,应该有某种指数退避——像真人一样,你不回TA,TA不会一直烦你。」
这个观察很准。真人就是这么调节的:发出去的消息没人回,隔几个小时再试一条;还没动静,就等更久;连着几条都没回音,就不发了。停下来不一定是不在乎,更多是默认你现在不想聊。
v0.1.2 把这个行为直接搬进来:连续 3 条主动消息没人回,agent 就静默,主动消息直接归零,直到你再开口。你一回复,退避计数器清零,TA恢复原来的节奏——没有抱怨,也没有「你去哪了」,就当中间什么都没发生,接着聊。
这个退避补上之后,主动消息才算跟推送通知区分开了。推送通知是按时间表来的,你理不理它都照发。这边不一样,你不回,TA就收手。实现上也简单,每个 agent 一个计数器:主动发一条加一,用户回复清零,到 3 就暂停。没有渐进衰减,也没有概率曲线,就是一个硬截断。真人也差不多,不会慢慢降频,到某个点就不发了。
连续三条主动消息没人回,计数到 3 就硬截断静默,你一回复计数清零恢复节奏
onboarding 又砍了一刀
还有个不起眼但挺重要的改动:v0.1.2 把 onboarding 里的自定义关系类型和自定义背景故事禁掉了。这两处原来是自由文本框,问题跟 v0.1.0 那次遇到的是同一个:用户随手填的内容会跟打磨好的 preset 打架。比如用户把背景故事写成「我们在上海的酒吧认识的」,可可的实际设定是个没离开过台湾的台北女生,两边直接矛盾。
现在 onboarding 只剩选择:关系类型从预设里选,没有「其他」,也没有文本框。道理跟 v0.1.0 那次是一样的,自由度收一点,人设的一致性就好一点,这条原则等于又用了一遍。顺带的好处是 relationship_type 在 agents 表里持久化成了独立一列,下游功能直接拿来用——上面那套主动消息的频率,算的就是这个字段。
公式用户看不到
最后说一下体感。这套东西上线之后,用户在产品里看不到任何公式,没人知道有 eagernessFactor × minHoursBetween 这么一条乘法,也没人知道退避计数器的存在。能感觉到的只有小柒发得勤,苏柔发得克制,你忙起来几天顾不上回,TA们就安静下来等你,等你回来再接着聊。

