ENZH
和 AI 讨论这篇文章
ChatGPTClaude

Mio 这版能看能听,还会主动发自拍

📊 幻灯片

AI 获得感知能力的概念插画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 之前统一转成文字,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 在这上面做了四个东西:

  1. LLM 重排序。混合搜索召回候选之后,用 gemini-2.0-flash 做精排,排序不看向量距离了,让 LLM 去理解「用户现在在聊什么,哪条记忆最相关」。检索质量直接上了一个档。
  2. 情节记忆。把零散记忆按对话情节分组,每个情节生成摘要。「那次我们聊旅行的时候」,系统能直接找到完整的对话情节,省得只捞回几个孤立碎片。
  3. 多跳查询分解。「你上次推荐的那个餐厅叫什么来着」这种,直接搜大概率搜不到,QueryDecomposer 把它拆成「餐厅推荐」和「最近的推荐」两个子查询,分别搜再合并。
  4. agentic 检索。特别复杂的查询跑迭代循环:搜索、评估结果、不够就换角度再搜,最多三轮。另外有个 volume-gated 机制,记忆量小的时候直接跳过复杂检索,没必要浪费。

混合搜索召回一堆候选记忆,LLM 重排序把真正最相关的那条顶到最前混合搜索召回一堆候选记忆,LLM 重排序把真正最相关的那条顶到最前

顺手还做了性能优化:count 和 embed 并行化,省了大概 350ms;记忆量小于 200 条时跳过全文搜索。单看不起眼,但聊天场景每次回复都要走记忆检索,350ms 累积起来就是体验差距。

7. Web 端

Telegram 够用但不够,不是所有人都用 Telegram,而且 Web 能做很多 Telegram 做不了的事:长对话历史、富文本、更好的媒体展示。v0.0.2 搭了完整的 Web 聊天界面:

  • Supabase Auth:登录页面、middleware、useAuth hook,标准认证流程。
  • 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 在回旧消息、无视新消息的尴尬。
  • 交互模式。realisticcompanion 两种模式,配合 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 再说。

和 AI 讨论这篇文章
ChatGPTClaude

订阅更新

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


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