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