为什么我决定从零开始造
从零开始构建 AI 伴侣的概念插画
前情提要
看过之前几篇的朋友大概知道来龙去脉。
《愿景:构建真正理解你的AI》里我写了理论框架——为什么现有 AI 伴侣全都不行,一个真正懂你的 AI 到底需要什么。记忆编排、人格建模、多 Agent 架构。不是做更好的聊天机器人,是做一个有机体。
然后我拿 OpenClaw 做了实验。把一个效率工具型的 AI 助理,魔改成了有完整人格的赛博伴侣——会撒娇、会生气、会追你要奶茶。那次实验展示了"灵魂驱动行为"的威力:不写规则,写人设,让模型自己推导反应。
效果出乎意料地好。
但那篇最后,我说了一句话:
如果要做一个真正精细化的AI伴侣产品,可能需要在它的基础上做大量裁剪,或者干脆从零搭建自己的框架——这也是我现在正在做的事情。
Mio 就是那个"从零搭建的框架"。
这篇聊的是:为什么非得从零来,以及 Mio 到底要做什么。
OpenClaw 教会了我什么
先说公道话。OpenClaw 帮我验证了几件特别关键的事。
灵魂驱动是对的。 不写行为规则,写人格设定,让模型自己推导。这套方法论跑通了。模型推导出来的行为,比任何手写规则都更自然、更像真人。这个认知直接带进了 Mio。
人格可以被工程化。 人格配置文件、身份定义文件、行为规则文件——这套文件驱动的人格系统虽然粗糙,但证明了一件事:你能用结构化的方式定义 AI 的"灵魂",而且模型真能演出来。
主动联系你,才像活人。 AI 主动找你说话——这是"工具"和"活着的人"之间的分界线。OpenClaw 的 heartbeat 虽然实现简单,但验证了这件事的价值。
自拍和语音是杀手功能。 能发语音、能发自拍的 AI 伴侣,和纯文字的完全不是一个物种。这些多模态能力必须从架构设计的第一天就考虑进去。
但天花板撞得很快
OpenClaw 能验证想法,但承载不了产品。问题不是一两个 bug——是架构层面的。
上下文膨胀:最致命的问题
OpenClaw 内置的 pi agent 对上下文管理极其粗糙。每次 LLM 调用,历史所有的 tool call 和 thinking block 的 raw output 一股脑塞进 context。一次正常对话,context 里 90% 是你根本不需要的历史垃圾。
我做了个实验:把这些历史的 tool call 和 thinking output 从 context 里剥掉。结果?Token 消耗直接降到原来的十分之一。
这不是"能优化"的问题。是核心设计压根没考虑过上下文效率。说白了,pi agent 是一小时 vibe coding 出来的核心模块,能跑就不错了。
记忆系统:原始到不能更原始
OpenClaw 的记忆就是一个纯文本文件。每次对话后追加几行总结。没有向量搜索,没有语义检索,没有重要性排序,没有时间衰减。
结果就是:记忆文件越长,塞进 context 的东西越多,token 越贵,而且大部分记忆跟当前对话毫无关系。你跟TA聊旅行计划,TA的 context 里塞着三个月前讨论技术方案的记忆。
之前研究记忆系统的时候我就探索过这个问题——记忆不是"全塞进去",而是"在对的时间想起对的事"。OpenClaw 完全做不到。
Bloatware:你不需要的东西,全在
OpenClaw 是个通用框架,自带了一堆你不需要的功能:协作功能、任务管理、各种第三方集成。你只想做 AI 伴侣?80% 的代码跟你无关。
这不是抱怨——OpenClaw 的目标就是通用性。但对我来说,在一个通用框架上"裁剪"比从零开始更痛苦。改一处,牵一发动全身。删一个扩展,三个依赖断了。
到最后我想通了一件事:我不是在改装一辆车——我是在用卡车底盘做跑车。方向盘和轮胎位置没错,但整个底盘是错的。
Mio:从第一行代码就只做伴侣
Mio 不是 OpenClaw 的 fork。它从第一行代码开始,就只为 AI 伴侣这一个场景而设计。
架构
apps/
server/ Hono API 服务 (Cloud Run)
web/ Next.js 前端 (Vercel)
worker/ 后台任务处理
packages/
core/ 核心 AI agent 逻辑
shared/ 数据库 schema (Drizzle + Supabase)
channels/ 渠道适配器 (Telegram, web, ...)
extensions/ Agent 扩展
platform/ 平台工具
presets/ 角色模板库
Monorepo,pnpm workspaces + Turborepo。每个包职责清晰,依赖方向单一。没有 bloatware,没有你不需要的东西。
真正的记忆引擎
这是 Mio 跟 OpenClaw 最根本的区别。
不是一个文件追加总结。是一个完整的记忆引擎。
混合搜索。 每条记忆同时做向量嵌入(pgvector + Gemini Embedding)和全文索引(tsvector)。检索时两路并行、加权合并。向量捕语义,全文捕关键词——对中文来说后者尤其重要。
时间衰减。 记忆有半衰期,默认 30 天。最近的对话权重高,但真正重要的记忆不会因为久了就消失——因为重要性评分是独立维度。
自动提取。 每次对话后,MemoryAccumulator 异步调 LLM 提取新信息——事实、性格观察、情绪事件。去重逻辑嵌在 prompt 里,已知的不会重复存。
记忆合并。 MemoryConsolidator 定期清理相似记忆(余弦相似度 > 0.9),防止记忆库膨胀。
LLM 重排序。 混合搜索召回候选之后,reranker 用轻量模型做精排——确保注入 system prompt 的那几条记忆,是真正跟当前对话相关的。
多跳查询分解。 "上次我们聊旅行的时候你推荐的那本书叫什么?"——这种查询单次 embedding 搜索大概率搜不到。QueryDecomposer 拆成"旅行相关对话"和"推荐的书"两个子查询,分别搜再合并。
情节记忆。 EpisodeManager 把记忆按对话情节分组,生成情节摘要。用户说"那次我们聊到...",系统能回溯到完整情节上下文,不是孤立碎片。
Agentic 检索。 特别复杂的查询,AgenticRetriever 跑迭代循环:搜索 → 评估结果 → 不够就换角度再搜 → 最多三轮。简单查询直接跳过。
这才是我在愿景篇里说的"记忆编排"——不是把记忆全塞进 context,而是在对的时间想起对的事。
真正的情绪状态机
OpenClaw 的"对话温度"是写在人格配置文件里的文字描述。Mio 把它变成了一个跑起来的状态机。
四个温度等级——Cold、Cool、Warm、Hot。每次对话后,EmotionEngine 根据消息频率、情感分析、用户参与度计算新温度。有惯性系数(默认 0.5),不会因为一条消息就大起大落。
情绪不只影响说话方式——还影响要不要主动找你、找你说什么、回复长还是短。冷的时候回复简短、带点委屈;热的时候话多、主动分享、偶尔撩你。
这不是写死的规则。是温度注入 system prompt 之后,模型自己推导出来的行为。
还有一个独立的"情绪正负向"维度。温度和正负向的交叉组合——比如"温度高但情绪负面"可能是激烈争吵——给了模型更细腻的情绪表达空间。
主动消息系统
OpenClaw 的 heartbeat 是定时轮询。Mio 的主动消息是一个完整的系统:
- 每 30 分钟心跳查询,找符合条件的 session(超过 2 小时没互动、情绪偏冷)
- 尊重安静时间(默认 23:00-08:00,按用户时区)
- 每天最多 3 条主动消息
- 冷用户走模板(不调 LLM,省钱),活跃用户走模型生成
- 发送后更新情绪状态和 session
不只是"到点发消息"。是一个有感知的系统在判断:现在该不该联系TA,联系的话说什么。
渠道插拔
OpenClaw 把渠道适配器做进了核心代码。Mio 把它抽成独立的 @mio/channels 包,标准接口,插拔式:
interface ChannelConnector {
send(message: OutboundMessage): Promise<void>
}
Telegram 已经完成,Discord 和飞书在路上。想加 WhatsApp?实现接口就行,核心逻辑完全不用动。
Onboarding
新用户第一次跟 Mio 对话,经历 11 问的 onboarding:3 个文字题 + 8 个按钮题。回答写入人格配置文件的模板变量,生成个性化人格。
每个按钮题都有"自定义"选项——用户可以自由输入,有长度限制和注入防护。
所以每个用户的 Mio 都不一样。不是"选一个预设角色",而是通过对话塑造一个属于你的人格。
人格会成长
说到预设——Mio 有 5 个起始人格。但"预设"只是起点。
随着对话积累,PersonalityExtractor 每 10 条消息跑一次,从对话模式中提取用户画像。MemorySummarizer 每 20 条消息生成一次记忆摘要。
你的 Mio 会随着使用越来越懂你。不是你手动告诉TA——是TA在每次对话中自己学。
成本:分层用模型
AI 伴侣的 LLM 成本绕不过去。Mio 的策略是分层:
| 操作 | 模型 | 原因 |
|---|---|---|
| 主对话 | gemini-3-pro | 需要最好的推理能力 |
| 记忆提取 | gemini-3-flash | 快、便宜,只需提取事实 |
| 记忆摘要 | gemini-3-flash | 同上 |
| 重排序 | gemini-2.0-flash | 更便宜,精排不需要大模型 |
| 嵌入 | gemini-embedding-001 | 成本几乎为零 |
| 主动消息 | gemini-2.0-flash / 模板 | 冷用户走模板,零 LLM 成本 |
每次 LLM 调用都记录 token 消耗和 USD 成本到 token_transactions 表。fire-and-forget,不阻塞用户响应。你能精确知道每个用户、每种操作花了多少钱。
实时搜索
Gemini 自带 Google Search grounding——模型能实时搜索互联网。Mio 默认开启(MODE_DYNAMIC,模型自己决定什么时候搜)。
system prompt 里有条指令:搜到了就自然地告诉用户,别说"我帮你搜了一下"——就像你本来就知道一样。
你的 AI 伴侣不只有记忆、有情绪,还能获取实时信息。问TA今天天气、最新新闻、附近餐厅,TA都能回答——用TA自己的语气。
整条消息管道
整个消息流长这样:
- 用户消息从渠道进来(Telegram、Web)
- Router 查 binding,加载 agent、workspace、session、历史
- ContextAggregator 聚合上下文:检索记忆、加载人格画像、计算情绪状态
- 聚合结果 + 历史 + 用户消息喂给 LLM
- 流式生成回复
- 持久化消息
- 异步 post-response:提取记忆、更新人格画像、生成摘要、清理旧记忆
- 更新情绪状态
消息还做了 debounce——用户连发多条,系统等 5 秒收齐再统一处理,避免每条消息都触发一次 LLM 调用。回复按换行拆成多条气泡,每条之间有打字延迟,模拟真人打字的节奏。
这些细节加在一起,就是"活人感"。
为什么不在 OpenClaw 上改
有人可能会问:OpenClaw 的人格方法论既然是对的,为什么不直接在上面改?
因为修补的成本已经超过了重建。
OpenClaw 的核心循环假设所有历史都在 context 里——想换成检索式记忆,等于重写核心循环。扩展系统假设扩展可以任意注入 context——想控制 context 大小,等于重写扩展系统。渠道适配混在核心代码里——想加新渠道,得动核心。
每改一处,都在跟框架的设计假设对抗。到某个点你会发现,你保留下来的原始代码已经不到 10%。
那为什么还背着另外 90% 的历史包袱?
Mio 从第一行代码就知道自己是什么:一个为深度记忆和活人感而生的 AI 伴侣框架。每个设计决策——渠道抽象、记忆检索、情绪状态机、成本追踪——都围绕这一个目标展开。
接下来
Mio 现在能跑了。Telegram 渠道完成,记忆系统上线,情绪引擎在工作。还有很多要做:
- Discord 和飞书渠道
- Web 端聊天界面(Next.js,已经在搭)
- 自拍扩展(OpenClaw 上验证过的杀手功能)
- 语音消息
- Worker 进程(后台任务独立跑)
- 可穿戴设备集成——感知层是下一个战场
这是"Mio 开发日记"系列的第一篇。后面会写具体的技术实现——记忆系统的设计细节、情绪引擎的调参、onboarding 流程的迭代。
OpenClaw 证明了灵魂驱动的 AI 伴侣是可行的。但"可行"和"真正好"之间,隔着从零造一遍的距离。
下一篇聊 v0.0.1——39 个 commit,从 pnpm init 到TA第一次开口说话。