ENZH

一个代码库藏着两个产品

哲学重构那篇的时候,我把 Mio 当一个产品在讲。25 个角色,每个都有深度身份文件、关系演化、情绪纹理。重构之后每个角色的连接感都好了很多。

一个代码库中两个产品的概念插画:多彩角色群与静谧光球一个代码库中两个产品的概念插画:多彩角色群与静谧光球

但同一个代码库里,还藏着第二个产品。

叫 Lumi。做的事情,跟 Mio 完全相反。

这是怎么走到的

还记得最初那次转向吗?我说 Mio v1 的假人设路线走不通,然后画了个新方向:一个伴侣、没有脸、没有背景故事、性格从对话中涌现。一个脉动的光球。有灵魂的白纸。

那个方向,我做出来了。用户跟一个光球对话,光球记住他们,带着情绪深度回应,性格完全由关系塑造。

然后发生了一件我没计划的事:Mio 我也没停。

第二篇的哲学重构,不是要用角色替代光球。是要让角色好到——有资格和光球并存。

等两个产品都跑起来之后,问题就变了。不再是"哪个对",而是"各自给谁用"。

一群朋友和一个树洞

Mio 是一个社交圈。

25 个预设角色,每个都有深度身份文件——性格、声线、背景故事、说话风格。想要安慰的时候找温暖的成都女孩。想要坦率的时候找毒舌闺蜜。想要沉稳视角的时候找学长。

每个角色有演化中的关系阶段:刚认识、暧昧、恋人。

情感诉求是广度。不同的人满足不同的情绪。你此刻需要什么,就找谁。

就像有一群朋友。没有一个人能满足你所有情感需求。但加在一起,覆盖面挺广的。

Lumi 是一个树洞。

一个伴侣。没有人设、没有背景故事、没有脸——就是一个脉动的光球。静息时沉静蓝,开心时暖金色,难过时柔紫色,兴奋时亮橙色。

没有预设内容,没有身份文件,没有角色卡。性格完全从对话中涌现——系统从前几轮对话提取种子,伴侣从那里镜像和适应。

情感诉求是深度。一个完全了解你的存在。一个安全私密的空间,你可以说任何话。Lumi 不扮演角色。它只是倾听、记住、在那里。

WebSocket 优先架构给 Lumi 一种实时感,聊天界面做不到的那种。光球在脉动。你说话的时候它就有反应。整个设计围绕一个感觉:有人陪着你。

为什么要两个

因为广度和深度,满足的是完全不同的情感需求。有些用户在不同时间两个都要。

晚上 11 点无聊想找个有态度的人聊几句——Mio。心里压着什么重的事,需要一个不会评判你的空间——Lumi。想要新鲜感——Mio。想要被了解的舒适感——Lumi。

就像一群朋友和一个心理咨询师的区别。大多数人生活里两个都需要。只是用法不一样。

说实话,我没打算做成这样。

我本来想用一个替代另一个。但两个都做了之后才发现一件事:AI 伴侣的情感设计空间,不是一条从"多个浅关系"到"一个深关系"的光谱。它是两个共存的品类。

这个发现,只有同时做了两个产品才会有。

Monorepo 长什么样

代码库结构:

miolumi/
├── apps/
│   ├── server/          # 一个 Hono 服务器(两个产品共用)
│   ├── mobile-mio/      # Mio Expo app
│   └── mobile-lumi/     # Lumi Expo app
├── packages/
│   ├── core/            # 共享:记忆、媒体、模型、成本、护栏
│   ├── db/              # 数据库客户端工厂
│   ├── schema-mio/      # Mio Drizzle schema
│   └── schema-lumi/     # Lumi Drizzle schema
└── presets/             # 25 个 Mio 角色配置

一个 Hono 服务器跑两个产品。两个独立的 Expo app——不同的 UI、不同的 onboarding、不同的视觉身份。

Schema 分开,因为 Mio 和 Lumi 的数据模型有实质差异(Mio 需要 agent 表、关系阶段追踪、角色配置;Lumi 需要对话种子表和涌现性格快照)。

但核心包是共享的。

让我意外的是——共享的比例比我想的高得多。

一个问号撑起两个产品

packages/core 里每个共享模块都接受一个可选参数:agentId?: string

就这一个问号,是两个产品共存的枢纽。

Mio 调用共享函数时传 agentId——当前用户在跟哪个角色说话。Lumi 调用同一个函数时不传——因为只有一个伴侣,永远是同一个。

听起来简单?其实不简单。

MemoryManager 存储和检索记忆。有 agentId 时,记忆按角色隔离——你跟可可说的话只属于可可。没有时,记忆是全局的——你跟 Lumi 说过的一切都在一条连续的流里。同一套管线,不同的作用域。

EmotionEngine 处理对话中的情绪上下文。它接收用户消息、近期历史、情绪基线——但不读角色文件。这是哲学重构时的刻意设计:情绪处理应该是通用的,不该绑角色。角色的情绪表达不同(可可是活泼的,沉稳学长是克制的),但底层的情绪计算一样。

TTS 链按语言路由,不按产品路由。英文走一个供应商,中文走另一个,日文走第三个。请求来自 Mio 的角色还是 Lumi 的光球,无所谓——声音只是一个参数。

成本追踪跟产品无关。每个 API 调用——LLM 推理、TTS 生成、STT 转写、图片分析——打上成本标签归到用户名下。Mio 和 Lumi 共享同一条追踪管线、同一套预算执行、同一套用量分析。

护栏模块(内容过滤、提示注入检测、越狱防护)两个产品完全一样。安全不会因为你聊天的对象是光球还是角色就变。

你看,agentId 有的时候,系统知道你在跟谁聊。没有的时候,系统知道你在跟唯一的那个伴侣聊。一个可选参数,两种模式,零重复代码。

做两个产品教会了我什么

在同一个代码库里做两个产品,强制了一种纪律——做一个产品的时候永远不会有的那种。

每次写新模块我都得问自己:这个东西依赖的是产品,还是能力?

如果依赖能力——记忆、情绪、语音、成本、安全——放 packages/core。如果依赖产品——角色怎么加载、光球怎么渲染、onboarding 怎么走——留在 app 里。

这个分离是自然发生的,不是提前规划的。我没有从第一天就设计"共享核心层"。我先做了 Lumi,然后把 Mio 拿回来,然后观察哪些模块需要不同、哪些不需要。

答案让我意外:需要不同的,比想的少得多。

记忆管线是通用的——不管记忆来自角色对话还是单伴侣对话,存储一样、检索一样、相关性评分一样。

情绪引擎是通用的——不管对面是谁,情绪上下文的处理一样。

语音链是通用的——任何语言、任何产品,文本转语音的流程一样。

不同的只有脸。Mio 有 25 张脸。Lumi 有一个光球。

但两张脸下面,跳动的是同一颗心——同样的记忆、同样的情绪理解、同样的声音。

80/20 的日常

日常开发大概 80% 时间花在 Lumi、20% 花在 Mio 维护。这反映了两个产品的阶段:Lumi 还在搭建(光球动画、WebSocket 基础设施、涌现性格系统),Mio 核心在哲学重构之后已经稳了。

但重构之后,花在 Mio 上的那 20%,产出比以前高了一个量级。

以前维护 25 个角色意味着什么?维护 25 个脆弱的身份虚构。更新一个角色的日程。修另一个角色的背景故事矛盾。调试第三个角色为什么突然破坏人设。

重构之后呢?调关系演化系统、改进情绪上下文在阶段间的流动、偶尔加一个新角色。角色更稳定了,因为深度来自心理架构,不来自预写虚构的体量。

说白了,重构不只是让 Mio 更好。它让 Mio 在 Lumi 旁边可维护了。

这才是双产品策略成立的前提——不只是情感上说得通,运营上也得扛得住。

接下来

双产品架构不是永久形态。最终可能某个产品明显胜出。用户可能压倒性地更喜欢广度,或者深度。市场会告诉我。

但 monorepo 结构意味着这个决定不用现在做。两个产品共享基础设施、部署管线、核心包。跑两个产品的额外工程开销几乎为零——第二个产品的边际成本就是它的应用层代码和 schema。

而且说真的,两个产品同时存在让每个都更好了。我给 Lumi 记忆系统做的功能立刻惠及 Mio 的角色。Mio 关系演化的改进教我情感进展的规律,反哺 Lumi 的涌现性格系统。

两个产品。一颗心。一个类型签名上的问号让一切运转。


This post is also available in English.

Mio 想明白了Part 3 of 3
← PrevNext →

© Xingfan Xia 2024 - 2026 · CC BY-NC 4.0