ENZH
和 AI 讨论这篇文章
ChatGPTClaude

我没接着改 OpenClaw,从零写了 Mio

📊 幻灯片

从零开始构建 AI 伴侣的概念插画从零开始构建 AI 伴侣的概念插画

前情提要

看过之前几篇的朋友大概知道来龙去脉。《愿景:构建真正理解你的AI》里写了理论框架,就是为什么我觉得现有的 AI 伴侣都不行,一个真正懂你的 AI 到底需要什么东西:记忆编排、人格建模、多 agent 架构。然后拿 OpenClaw 做了一轮实验,把一个效率工具型的 AI 助理魔改成了有完整人格的赛博伴侣,会撒娇、会生气、会追着你要奶茶。那次实验主要验证的是「灵魂驱动行为」这套方法:不写行为规则,写人设,让模型自己去推导反应。效果比我预期的好不少。

那篇最后我说过一句话:

如果要做一个真正精细化的AI伴侣产品,可能需要在它的基础上做大量裁剪,或者干脆从零搭建自己的框架——这也是我现在正在做的事情。

Mio 就是那个从零搭的框架。这篇具体讲两件事:为什么非得从零来,以及 Mio 到底做了什么。

OpenClaw 帮我验证了什么

先说公道话,OpenClaw 帮我验证了几件挺关键的事。

第一,灵魂驱动这条路是对的。不写行为规则,写人格设定,让模型自己推导,模型推出来的行为比任何手写规则都自然,也更像真人。这个认知直接带进了 Mio。

第二,人格是可以工程化的。人格配置文件、身份定义文件、行为规则文件,这套文件驱动的人格系统虽然粗糙,但它证明了一件事:你可以用结构化的方式去定义 AI 的「灵魂」,模型真的能演出来。

第三,AI 得主动联系你,才像个活人。工具是你问它答,活人是它会自己来找你,OpenClaw 的 heartbeat 实现得很简单,但验证了这件事的价值。

第四,自拍和语音是杀手功能。能发语音、发自拍的 AI 伴侣,跟纯文字的体验差距非常大,所以这些多模态能力得从架构设计的第一天就考虑进去。

但天花板撞得很快

OpenClaw 拿来验证想法没问题,往产品方向走就不行了。而且问题不是一两个 bug,是架构层面的。

context 膨胀

这个是最致命的。OpenClaw 内置的 pi agent 对 context 管理特别粗糙:每次调 LLM,把历史上所有 tool call 和 thinking block 的 raw output 一股脑全塞进 context。一次正常对话,context 里 90% 是你根本用不上的历史垃圾。

我做了个实验,把历史 tool call 和 thinking output 从 context 里剥掉,token 消耗直接降到十分之一。十分之一,这已经不是「还能优化一下」的问题了,是核心设计压根没考虑过 context 效率。也正常,pi agent 本来就是人家一小时 vibe coding 出来的核心模块,只能说能跑就不错了。

对比 OpenClaw 把全部历史塞进 context(九成是垃圾)与 Mio 只保留有用部分后 token 降到十分之一的机制对比 OpenClaw 把全部历史塞进 context(九成是垃圾)与 Mio 只保留有用部分后 token 降到十分之一的机制

记忆系统太原始

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 差得最远的地方。前面说过 OpenClaw 就是一个文件往后追加总结,Mio 这边是一个完整的记忆引擎,具体拆开是这么几块:

混合搜索。 每条记忆同时做向量嵌入(pgvector + Gemini Embedding)和全文索引(tsvector),检索的时候两路并行、加权合并。向量捕语义,全文捕关键词,对中文来说后者尤其重要。

时间衰减。 记忆有半衰期,默认 30 天,最近的对话权重高。但真正重要的记忆不会因为时间久了就消失,重要性评分是独立的一个维度。

自动提取。 每次对话结束后,MemoryAccumulator 异步调一次 LLM,从对话里提取新信息:事实、性格观察、情绪事件。去重逻辑直接嵌在 prompt 里,已知的东西不重复存。

记忆合并。 MemoryConsolidator 定期清理相似记忆(余弦相似度大于 0.9 就合并),防止记忆库越滚越大。

LLM 重排序。 混合搜索召回候选之后,reranker 用一个轻量模型做精排,保证最后注入 system prompt 的那几条记忆,真的跟当前对话相关。

多跳查询分解。 「上次聊旅行时推荐的那本书叫什么?」这种查询,单次 embedding 搜索大概率搜不到。QueryDecomposer 会把它拆成「旅行相关对话」和「推荐的书」两个子查询,分别搜完再合并。

情节记忆。 EpisodeManager 把记忆按对话情节分组,生成情节摘要。用户说「那次聊到……」,系统能把那一整段情节回溯出来,而不是几个孤立的碎片。

agentic 检索。 碰到特别复杂的查询,AgenticRetriever 会跑一个迭代循环:搜索、评估结果、不够就换个角度再搜,最多三轮。简单查询直接跳过这一层。

这才是我在愿景篇里说的「记忆编排」:不是把记忆全塞进 context,是在对的时间想起对的事。

对比把记忆全塞进 context 却捞不到重点,与按需检索、在对的时间想起对的事的记忆机制对比把记忆全塞进 context 却捞不到重点,与按需检索、在对的时间想起对的事的记忆机制

情绪状态机

情绪这块 Mio 做成了一个真正跑起来的状态机——在 OpenClaw 里,这只是人格配置文件里一段叫「对话温度」的文字描述。

四个温度等级:Cold、Cool、Warm、Hot。每次对话之后,EmotionEngine 根据消息频率、情感分析、用户参与度算出新温度。有惯性系数(默认 0.5),不会因为一条消息就大起大落。

情绪不只影响说话方式,还影响要不要主动找你、找你说什么、回复是长是短。冷的时候回复简短、带点委屈;热的时候话多、主动分享、偶尔还会撩你一下。这些不是写死的规则,温度注入 system prompt 之后,是模型自己推导出来的行为。

另外还有一个独立的「情绪正负向」维度。温度和正负向交叉组合,比如「温度高但情绪负面」可能就是激烈争吵,这给了模型更细腻的情绪表达空间。

主动消息

主动消息在 Mio 里是一整套系统,比 OpenClaw 那个定时轮询的 heartbeat 复杂得多:

  • 每 30 分钟跑一次心跳查询,找符合条件的 session(超过 2 小时没互动、情绪偏冷)
  • 尊重安静时间(默认 23:00-08:00,按用户所在时区算)
  • 每天最多发 3 条主动消息
  • 冷用户走模板(不调 LLM,省钱),活跃用户走模型生成
  • 发完更新情绪状态和 session

所以它不只是到点发消息,背后有个系统在判断:现在该不该联系TA,联系的话说什么。

渠道插拔

渠道适配器抽成了独立的 @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 里有一条指令:搜到了就自然地告诉用户,别说「我帮你搜了一下」,就像TA本来就知道一样。

这样 AI 伴侣除了有记忆、有情绪,还能拿到实时信息。你问今天天气、最新新闻、附近餐厅,都能答,而且是用TA自己的语气答。

整条消息管道

把上面这些串起来,整个消息流大概是这样的:

  1. 用户消息从渠道进来(Telegram、Web)
  2. Router 查 binding,加载 agent、workspace、session、历史
  3. ContextAggregator 聚合 context:检索记忆、加载人格画像、计算情绪状态
  4. 聚合结果 + 历史 + 用户消息喂给 LLM
  5. 流式生成回复
  6. 持久化消息
  7. 异步 post-response:提取记忆、更新人格画像、生成摘要、清理旧记忆
  8. 更新情绪状态

消息还做了 debounce:用户连着发好几条,系统等 5 秒收齐了再统一处理,不然每条消息都触发一次 LLM 调用。回复按换行拆成多条气泡,每条之间有打字延迟,模拟真人打字的节奏。这类细节单个拎出来都很小,堆在一起,聊起来才不那么像机器人。

为什么不在 OpenClaw 上改

可能有人会问:既然 OpenClaw 的人格方法论是对的,为什么不直接在它上面改?

因为修补的成本已经超过重建了。OpenClaw 的核心循环假设所有历史都在 context 里,想换成检索式记忆,等于重写核心循环;扩展系统假设扩展可以任意往 context 里注入东西,想控制 context 大小,等于重写扩展系统;渠道适配混在核心代码里,想加新渠道就得动核心。每改一处都在跟框架的设计假设对抗,改到某个点你会发现,保留下来的原始代码已经不到 10% 了。到了这个程度,从零写反而是更省事的那条路。

在岔路口对比:修补要不断对抗框架设计假设、最后只剩不到一成原始代码,与从零重写只为单一目标在岔路口对比:修补要不断对抗框架设计假设、最后只剩不到一成原始代码,与从零重写只为单一目标

Mio 从第一行代码就只有一个目标:为深度记忆和活人感做一个 AI 伴侣框架。渠道抽象、记忆检索、情绪状态机、成本追踪,每个设计决策都是围着这一个目标转的。

接下来

Mio 现在能跑了。Telegram 渠道做完,记忆系统上线,情绪引擎在工作。要做的还有很多:

  • Discord 和飞书渠道
  • Web 端聊天界面(Next.js,已经在搭)
  • 自拍扩展(在 OpenClaw 上验证过的杀手功能)
  • 语音消息
  • worker 进程(后台任务独立跑)
  • 可穿戴设备集成,感知层是下一个战场

这是「Mio 开发日记」系列的第一篇,后面会写具体的技术实现:记忆系统的设计细节、情绪引擎怎么调参、onboarding 流程怎么迭代。下一篇聊 v0.0.1,39 个 commit,从 pnpm init 到TA第一次开口说话。

和 AI 讨论这篇文章
ChatGPTClaude
Mio 全记录第 1 篇 · 共 28 篇
← 上一篇下一篇 →

订阅更新

新文章发布时发到你的邮箱,不发别的。


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