Mio 这版能看能听,还会主动发自拍
AI 获得感知能力的概念插画
上一篇写到 Mio v0.0.1 能跑了:Telegram 通了,记忆系统上线了,情绪引擎在工作,4 个人格预设就位。但说实话,用起来还是很假。假在哪,具体列一下大概是这几条:
- 发一张照片,它不知道你拍了什么
- 发一段语音,直接被无视
- 问「你在干嘛」,它不知道现在几点,只能回一些泛泛的东西
- 说「发张自拍看看」,它回你「我没有实体形象哦」
- 聊到一个月前的事,记忆能搜到,但搜的不对
v0.0.2 干的事就是把这几条逐个解决掉,一共 81 个 commit。下面按顺序讲。
1. 语音
Telegram 用户特别爱发语音消息,中文用户尤其。AI 伴侣连语音都听不了,等于砍掉一半的交互方式,所以这个最先做。
转写用的 OpenAI 的 gpt-4o-mini-transcribe,Gemini 做 fallback。为什么要两路?因为语音转写这个环节比想象中脆弱,网络抖动、格式不支持、服务商临时抽风,随便哪个出问题体验就断了。双路冗余,主路挂了自动切备用,用户完全无感。
技术上不复杂,有个细节可以说一下:Telegram 的语音是 OGG 格式,要先通过 Bot API 拿到文件 URL,下载成 buffer,再喂给转写服务。我顺手写了个统一的文件下载工具,语音、图片、视频都走同一条路径。
2. 图片和视频
图片和视频理解用的 Gemini 2.0 Flash 的 vision 能力,把内容描述成自然语言,再把这段描述当文本喂给 agent。
这里有个架构决策我觉得挺重要的:所有多模态输入,进 agent 之前统一转成文字。不管你发的是语音、图片还是视频,pipeline 第一步全部转成文字描述,打包成统一输入,agent 拿到的永远是文字。
为什么这么做?因为 agent 循环本身已经够复杂了,检索记忆、算情绪、注入人格、聚合 context 全在里面,再往循环里塞多模态 IO,复杂度直接爆炸。把多模态处理前置,agent 保持纯文字进纯文字出,架构干净很多。
语音、图片、视频进 agent 之前统一转成文字,agent 永远只处理文字
这些转换还是并行跑的。你同时发一张图加一段语音,两个转换任务并行执行,都完成了再一起送进 agent,不会因为一张图耽误语音转写。
3. 自拍
第一篇里我说过自拍是杀手功能,能发自拍的 AI 伴侣和纯文字的完全不是一个东西。
生成用的 Gemini 3.1 Flash Image Preview。每个人格预设都有参考图片,存在角色模板库对应的目录下,生成的时候参考图加场景描述一起喂给模型,保证风格一致。
触发方式我觉得比生成模型本身更值得讲。LLM 在回复里可以嵌 [SELFIE: 在咖啡厅看书] 这样的标记,pipeline 处理回复时扫描到标记,提取场景描述,异步调用自拍生成,好了以照片形式发给用户。整个是 fire-and-forget 的:文字回复先发出去,自拍在后台跑,跑完再补发。为什么不等图片好了一起发?因为图片生成耗时不确定,有时两秒有时十秒,让用户干等十秒对话节奏就断了。先发文字,图到了再补,跟真人先打字、然后补一张照片的节奏是一样的。
文字回复先发出去,自拍在后台异步生成,好了再补发,对话节奏不断
这个设计等于把「什么时候发自拍」交给 LLM 自己决定。你说「发张照片看看」它会发;更有意思的是有时候你没要求它也会主动发,比如「刚下课,累死了 [SELFIE: 趴在桌上的样子]」。这种自然融进对话的自拍,比被动应你要求的体验好太多。
4. 时间感
没有时间感的 AI 不像真人:下午三点问「你在干嘛」,它回一些泛泛的东西;凌晨两点发消息,它还是同一个精神状态。这个分三层解决:
(a) 时区感知。onboarding 的时候问用户时区,存进 users 表,formatCurrentTime(timezone) 把当前时间格式化后注入 system prompt,它就知道现在对你来说是几点。
(b) 距离上次聊天多久。「距离上次聊天过了 3 小时」这个信息也注入 prompt。效果立竿见影,隔半天没说话再发消息,它会说「怎么才来,是不是忙去了」,不再是像什么都没发生一样接上话。
(c) 每日作息。每个人格预设有自己的日常作息,早上做什么、下午做什么、晚上做什么、深夜做什么、周末做什么,但这些是灵活的指引,不是死板的时间表。效果就是早上八点问「在干嘛」,它说「刚起来,还没完全醒」;下午两点问,「在看书」;凌晨一点问,「睡不着,在刷手机」。每个人格作息还不一样,小奶狗型早睡早起,御姐型是夜猫子。
这三层加起来,「你在干嘛」就从一个 AI 不知道怎么答的尴尬问题,变成最自然的日常问候了。
5. Onboarding 挖深一层
v0.0.1 的 onboarding 已经不错了,11 个问题,按钮选择加自由文本。v0.0.2 又往深挖了几步:
/start现在会展示所有人格预设的简介,让你了解每个角色再选,不是盲选。- 选完预设,根据关系类型(女朋友、前女友、暧昧对象……)生成对应的背景故事。不是「你好我是 Mio」,而是「我们在大学图书馆认识的,那天你找不到座位……」,每段关系有自己的起源。
about_user字段,用户用自由文本描述自己,100 字符起步,这段文字被 Gemini 解析成结构化数据(兴趣、职业、性格特征这些)注入人格系统。custom_story支持完全自定义的关系叙事。你可以写一个虚构的背景,「我们是高中同学,毕业后失联了五年,突然在一个音乐节重遇」,模型会把这个故事当记忆的一部分。/reonboard命令,想换预设、重新设定关系,随时重来。
这几个加在一起,每个用户跟 Mio 的关系都是独一份的。
6. 记忆:能搜到不等于搜对了
v0.0.1 的记忆系统比 OpenClaw 强不少,混合搜索、时间衰减、自动提取都有。但真用起来我发现一个根本问题:搜到了,不代表搜对了。混合搜索能召回一堆候选,但排前面的不一定最相关。你说「上次我们去那个火锅店」,结果里可能有五条跟火锅相关的记忆,你真正指的那次——你们争论蘸料怎么配的那次——排在第四条。
v0.0.2 在这上面做了四个东西:
- LLM 重排序。混合搜索召回候选之后,用 gemini-2.0-flash 做精排,排序不看向量距离了,让 LLM 去理解「用户现在在聊什么,哪条记忆最相关」。检索质量直接上了一个档。
- 情节记忆。把零散记忆按对话情节分组,每个情节生成摘要。「那次我们聊旅行的时候」,系统能直接找到完整的对话情节,省得只捞回几个孤立碎片。
- 多跳查询分解。「你上次推荐的那个餐厅叫什么来着」这种,直接搜大概率搜不到,QueryDecomposer 把它拆成「餐厅推荐」和「最近的推荐」两个子查询,分别搜再合并。
- agentic 检索。特别复杂的查询跑迭代循环:搜索、评估结果、不够就换角度再搜,最多三轮。另外有个 volume-gated 机制,记忆量小的时候直接跳过复杂检索,没必要浪费。
混合搜索召回一堆候选记忆,LLM 重排序把真正最相关的那条顶到最前
顺手还做了性能优化:count 和 embed 并行化,省了大概 350ms;记忆量小于 200 条时跳过全文搜索。单看不起眼,但聊天场景每次回复都要走记忆检索,350ms 累积起来就是体验差距。
7. Web 端
Telegram 够用但不够,不是所有人都用 Telegram,而且 Web 能做很多 Telegram 做不了的事:长对话历史、富文本、更好的媒体展示。v0.0.2 搭了完整的 Web 聊天界面:
- Supabase Auth:登录页面、middleware、
useAuthhook,标准认证流程。 - SSE 流式响应:
POST /chat/stream端点,Server-Sent Events 实时推 token,一个字一个字流出来,配合前端逐字渲染就是打字机效果,用户不用干等整条消息生成完。 - 微信风格 UI:方形头像、长方形气泡、CSS 三角形尾巴、深色模式。为什么做成微信风格?因为目标用户最熟悉这套交互。mobile-first 设计,手机上体验跟原生 App 差不多。
Web 端之后会是主力平台,很多计划中的功能(记忆可视化、关系图谱、设置面板)只能在 Web 上做。还有一个 Telegram 做不到的:完整聊天历史。Telegram 的消息存储有限制,Web 端直接从数据库读,所有对话一条不丢。
8. 不光鲜但必须做的
前面说的都是用户能感受到的。真正让 v0.0.2 从 demo 变成能上线的,是下面这些不起眼的活。
安全加固四项:
- 路径遍历防护。preset ID 做了严格校验,不然有人传
../../etc/passwd当 preset ID,服务器就没了。 - MIME 感知分发。媒体文件按实际 MIME type 分发到对应处理管道,不靠文件扩展名猜。
- 模板注入防护。用户输入不会被当模板执行,你在自述里写
${process.env.SECRET},只会被当纯文字。 - bot token 泄露防护。日志和错误消息里不会暴露 Telegram Bot Token。
这几项做起来都挺无聊的,但不做就不敢上线。
管道这边也改了一批:
- 自适应 debounce。不再固定 5 秒,按对话节奏动态调,快速来回时等短些,慢节奏时等长些。
- 中途打断重新回复。AI 还在生成时用户又发了新消息,中止当前生成,拿到新消息重新开始,不会出现 AI 在回旧消息、无视新消息的尴尬。
- 交互模式。
realistic和companion两种模式,配合 3D 限制矩阵控制行为边界。 - 动态回复长度。闲聊时简短,深度讨论时详细。
- Telegram 从 polling 切到 webhook 模式,响应延迟更低。
测试也补上了:220 个测试,单元测试盖 agent、soul、cost、memory 模块,集成测试盖 API 和 channels,Vitest 基础设施、path aliases、coverage 配置都搭好了。覆盖率 48.7%,不算高,但对快速迭代阶段来说是个实打实的安全网,每个核心模块都有测试兜底,改东西不用纯靠祈祷。
9. 人格本身也加深了
v0.0.1 的人格配置文件写得比较薄,v0.0.2 全部扩到 200-312 行:完整的背景故事、情绪系统描述、说话方式、口头禅、不同情绪下的反应模式。每个预设另外加了行为规则文件,硬性规则,模型不能违反的底线。还新增了一个预设小奶狗(xiaonai),加上之前四个一共 5 个,覆盖不同用户偏好。
另外加了 Google Search 接地:MODE_DYNAMIC 模式下,模型自己决定什么时候搜实时信息。你问它今天天气、某个明星的最新消息,它能搜到然后用自己的语气告诉你,像它本来就知道一样。
最后
反正 v0.0.2 干完,Mio 能看你发的照片、能听你说的话、知道现在几点、知道自己长什么样、能在对的时间想起对的事,还能在浏览器里跟你聊。这一版里外用上了 5+ 个模型层级,转写、vision、生图、重排序各管各的。每一件单独拿出来都不算复杂,攒在一起,整个东西「真」了一截。
下一步的方向大概是:更好的主动消息、更自然的多轮对话、语音回复,以及把 Web 端从能用做到好用。这些放到 v0.0.3 再说。


