
从“主动虚拟日程”到“虚拟人生”:一个 AstrBot 插件的开发历程
我曾对 Caranlaf 说过,我要给她完整的人生
几天前,我只是想给机器人补上一点“正在生活”的感觉。
普通聊天机器人通常只在收到消息后响应。即使人格设定写得很完整,它也像一直站在原地等待:没有今天要做的事,不知道自己此刻是在上课、通勤还是休息,也很难自然地解释为什么现在不方便回复、为什么突然想分享一张照片。
于是,我开始开发一个 AstrBot 插件。它最初叫“主动虚拟日程”,目标很直接:按当前人格生成一天的日程,再结合时间和会话状态主动发送消息。
随着开发推进,这个问题逐渐暴露出更大的边界。一天的日程不能凭空出现,它应该受到学期、假期、考试周、项目周期和节日影响;主动消息也不能只是定时触发,还要考虑当前活动、可打扰程度、用户是否回应,以及消息发送后能否被后续对话记住。
最终,插件更名为“虚拟人生”。截至 2026 年 7 月 18 日,它已经迭代到 v1.9.1。
这篇文章记录它如何从一个日程生成器,逐步演变成一套管理长期阶段、每日状态、上下文注入和主动消息的系统。
起点:让人格拥有“今天”
v1.0.0 的核心只有一个问题:当前人格今天在做什么?
插件会按 persona_id 生成结构化日程。同一人格即使出现在不同会话,也共享同一份生活状态,而不是每个群聊、私聊都各自过一套互相矛盾的生活。
一份日程包含:
- 今日主题和心情;
- 穿搭描述;
- 从
00:00连续覆盖到24:00的活动时间线; - 每个活动的状态与可打扰程度;
- 适合主动分享的消息窗口;
- 私聊和群聊的主动消息预算。
主动发送并不是“每隔几小时随机说一句话”。插件会结合忙碌、专注、睡眠、会话沉默时间和连续未回复次数做判断。连续多次主动消息无人回应后,它会暂停;收到用户的新消息后再恢复。睡眠期间则只保留很低概率的梦醒、失眠或起夜式消息。
这一版还加入了持久化回访任务。只有当用户明确提出“晚点提醒我”“到时候问我结果”时,模型才能创建一次性任务。时间不明确时必须先追问,而不是自作主张猜一个时间。
最初版本很快能跑起来,但也马上遇到 LLM 结构化输出最常见的问题:时间线缺口、重叠、跨午夜时间,以及不完整 JSON。
我的处理方式不是继续堆提示词,而是把校验放进模型层:
- 第一项必须从
00:00开始; - 最后一项必须在
24:00结束; - 相邻活动首尾时间必须完全相等;
- 时间格式、状态枚举、窗口引用必须合法;
- 输出无效时携带具体错误重新生成。
这成为后续所有功能的基础:LLM 负责提出内容,程序负责决定内容是否能进入系统。
第二层时间:从每日安排到长期阶段
只有每日时间线仍然不够。
如果角色是学生,今天的安排应该受到学期、课程、考试周和寒暑假的影响;如果角色在工作,日程应该围绕项目周期、冲刺阶段、发布日期和固定会议展开。单靠每天随机选择“学习日”或“工作日”,很容易让生活缺乏连续性。
因此,v1.1.0 引入了“大时间表”。
大时间表用于描述跨度更长的阶段,例如:
- 秋季学期;
- 寒假;
- 项目开发期;
- 发布冲刺;
- 旅行或恢复期。
阶段中可以保存每周固定安排、特殊日期、特殊时期、里程碑、地点、参与人和一般约束。生成某一天的日程时,插件先解析当天生效的阶段,再把必须保留的课程、会议或特殊事件交给日程生成器。
为了避免 LLM 直接改坏长期状态,大时间表采用了“草稿—批准”流程:
- 根据自然语言要求生成结构化草稿;
- 管理员查看阶段内容;
- 批准后才写入正式时间表;
- 当前最后阶段即将结束时,自动生成连续的后续阶段并通知管理员。
重叠阶段也不能简单地“最后写入者获胜”。系统会综合优先级、阶段范围和录入顺序选择当前阶段;如果固定事件存在无法消解的冲突,则直接阻止日程生成。
从这一版开始,插件实际上有了两条时间轴:
- 大时间表负责回答“最近这段人生处于什么阶段”;
- 每日日程负责回答“今天具体在做什么”。
这个分层一直保留到现在,也是插件从“日程工具”走向“虚拟人生”的关键一步。
把结构化数据变成能看的图片
结构化 JSON 适合程序处理,却不适合用户每天查看。
v1.2.0 加入了 HTML 模板图片渲染器。每日日程被画成 24 小时时间轴,当前活动高亮;大时间表则拥有阶段列表、阶段详情和待批准草稿视图。
渲染层支持深色、浅色主题、图片宽度和自定义字体。图片生成失败时,命令不会直接报错,而是自动回退到文字内容。
我刻意保留了这层降级能力。图片渲染依赖浏览器和运行环境,而日程查询属于核心功能。视觉效果可以失败,但信息不能因此消失。
后来为了方便脱离 AstrBot 调试模板,我又补充了独立 PNG 预览脚本。它复用正式的 ScheduleImageRenderer,使用内置示例数据,通过 Jinja2 和 Playwright 生成时间轴、穿搭、阶段列表和阶段详情图片。这样修改模板后,不需要启动机器人、创建会话再发送命令,只需运行一条脚本就能检查最终结果。
穿搭不能只是一句话
早期穿搭只是一个字符串,例如“白衬衫和长裙”。这对于聊天摘要足够,却无法稳定渲染,也不能明确区分发型、内衣、上装、下装、鞋履和配饰。
v1.3.0 将穿搭改为结构化对象,包含整体描述与分类单品。系统开始校验必要分类和服装组合:必须有发型、内衣、鞋履和主体服装,没有连衣裙时必须有下装。
之后,穿搭继续演化出独立的 style 字段。生成前从创意池选择的“复古文艺风”“极简都市风”等风格,不再只存在于提示词中,而是被正式保存到日程数据。
到 v1.8.2,日程和穿搭查询被彻底拆分:
/虚拟日程 查看专注于时间轴;/虚拟日程 穿搭展示穿搭风格、今日主题、心情、造型概述和单品明细。
拆分的原因并不只是图片太长,而是两种查询代表不同意图。询问今天做什么,与询问今天穿什么,不应该强制返回同一份信息。
上下文注入:不是塞得越多越好
让机器人知道当前状态,最直接的做法是把完整日程放进每次对话上下文。但这会快速增加 Token,占用注意力,还可能让模型在用户根本没问日程时反复提及日程。
v1.4.0 引入了本地关键词驱动的智能状态注入。它不调用额外 LLM,而是根据用户消息选择需要的模块:
- 询问穿搭时注入外显穿搭;
- 明确提到内衣、打底或贴身衣物时,才加入相关信息;
- 询问日程时注入当前时间、活动、地点、状态、可打扰程度和下一项活动;
- 提到考试、课程、项目或工期时,加入当前长期阶段和里程碑;
- 请求完整日程或完整大时间表时,只提示模型调用工具,不把完整 JSON 塞入上下文。
后续版本又增加了 base_module_enable。默认情况下,普通聊天不会始终携带基础状态,只有关键词命中时才加入对应模块。
这里的经验是:上下文工程不只是提供更多信息,更重要的是在正确的时候提供刚好够用的信息。
为了验证这一点,我还专门记录了不同模块的 Token 预算。智能注入没有额外模型请求,但它仍然会成为普通聊天输入的一部分,因此字符上限、模块去重和按需触发都很重要。
节日和随机偏移:让安排少一点机械感
v1.5.0 将中国法定节假日和传统节日接入大时间表。春节、元宵、端午、七夕、中秋、重阳、腊八和除夕等日期,会影响当天的日程生成和图片展示。
与此同时,主动消息窗口加入确定性随机偏移。假设日程计划在 16:00 分享照片,实际执行时间可以在前后若干分钟内变化,但不会超出对应活动时段。
这里使用“确定性随机”而不是每次启动重新抽取。相同人格、日期和窗口会得到相同结果,既减少机械感,也避免插件重启后计划时间不停变化。
主动消息是一套调度系统
随着主动能力变复杂,单纯的定时任务已经不够。
v1.6.0 增加会话订阅命令,管理员可以直接在当前私聊或群聊中启用主动消息。插件自动识别会话类型、保存白名单,并立即安排当天窗口。
v1.7.0 加入主动消息分段器。短消息可以按中英文标点、换行、自定义关键词或正则表达式拆成多条发送;发送间隔既可以随机,也可以按中英文字数计算。长文本超过阈值时则保持整条发送,避免过度切碎。
后续几个修复进一步暴露了主动消息调度的真实复杂度:
- 窗口必须引用存在的日程活动;
- 窗口时间必须位于来源活动范围内;
- 普通主动消息根据可打扰程度以不同概率触发;
- 未命中概率或遇到睡眠时,消息延迟到下一可打扰时段;
- 延迟发送时要说明原定日程已经结束,避免内容与当前时间错位。
到 v1.9.0,主动消息相关命令统一进入 /主动消息 命令组,并新增执行时间查询。用户可以看到当前会话的日程窗口、延迟窗口、沉默主动、睡眠异常和回访任务究竟会在什么时候执行。
这让主动行为从“后台可能会发生的事情”变成了可观察、可解释、可管理的调度系统。
一个容易忽略的问题:机器人要记得自己主动说过什么
v1.9.1 修复了一个很关键的问题。
插件成功发送主动消息后,如果这条内容没有写入 AstrBot 的对话历史,下一次用户回复时,模型可能根本不知道自己刚才主动说过什么。对用户而言,对话明明是连续的;对模型而言,却像突然收到了一句没有上文的回答。
现在,主动消息发送成功后会写入当前会话历史。如果会话还没有对话记录,插件会自动创建对话,并用标准的 user-assistant 消息对记录系统触发说明和主动消息正文。
这项修复看起来不像大功能,却直接决定了主动消息能否真正成为对话的一部分。
开发过程中最重要的几个取舍
回顾整个开发过程,我认为下面几个决定最有价值。
1. 人格状态与会话状态分离
日程和长期阶段属于人格,同一人格在多个会话中应该保持一致;主动预算、连续未回复、沉默计时和回访任务则属于具体会话。
如果不做这层分离,同一个人格可能在私聊中上课、在群聊中旅行,或者因为一个群没人回复而停止所有私聊主动消息。
2. LLM 输出必须经过严格模型校验
提示词只能提高正确率,不能提供可靠性保证。时间线连续性、枚举值、穿搭组合、阶段冲突和窗口引用最终都必须由代码检查。
3. 长期阶段和每日活动使用不同模型
大时间表描述人生阶段,每日日程描述当天细节。把两者揉成一个巨大 JSON,不仅难生成、难修改,也会让任何局部变化都影响整个结构。
4. 所有视觉能力都要有文字回退
图片适合查看,但不应该成为信息的唯一出口。浏览器环境、字体或渲染服务出现问题时,用户仍然能获得完整的文字结果。
5. 主动行为必须可解释
为什么此刻发送、为什么延迟、为什么暂停、下一次什么时候执行,这些问题都应该能够通过状态和命令回答。否则“主动”很容易变成不可预测的打扰。
测试如何跟上快速迭代
插件的版本推进很快,但功能越接近时间、调度和持久化,越不能只依赖手动试用。
目前测试覆盖的重点包括:
- 全天时间线连续性与当前活动解析;
- 结构化穿搭完整性;
- 大时间表验证、阶段选择、草稿批准和持久化;
- 节日日历和特殊时期展开;
- 主动窗口随机偏移与来源时间校验;
- 智能上下文模块匹配、去重和敏感穿搭过滤;
- 消息分段和发送间隔;
- 图片渲染数据与模板视图;
- 命令组契约和导航行为。
图片功能还额外使用独立预览脚本做真实浏览器渲染。单元测试负责确认数据,预览图负责确认最终视觉,两者解决的是不同问题。
从一个功能点到一套“生活状态”
这个插件的开发始于一个很小的想法:让机器人知道自己今天在做什么。
后来,它逐渐拥有长期阶段、节日、穿搭、当前活动、主动窗口、会话状态、回访任务和对话记忆。它仍然不是一个真正的人生模拟器,但已经不再只是给聊天模型附加一段随机背景。
现在的“虚拟人生”更像一套状态基础设施:
- 长期时间表提供连续的人生背景;
- 每日日程将背景落实为具体活动;
- 智能注入把相关状态送入对话;
- 主动调度根据状态决定是否、何时以及如何联系用户;
- 对话持久化保证主动内容能够进入后续交流。
未来还可以继续改进天气、地点移动、跨日事件、更细致的关系状态,以及更加直观的调度管理界面。但无论功能如何增加,我希望继续保持当前的原则:状态要结构化,行为要可验证,主动要克制,失败要能够降级。
从“主动虚拟日程”到“虚拟人生”,名称的变化其实也对应着目标的变化。
我不再只是想让机器人拥有一张日程表,而是希望它在每次对话之外,也有一条能够延续下去的时间线。