记录一下「会成长的个人助手」这套系统的 MVP 架构:它不是一个聊天窗口,而是一台服务器上常驻、能自己收邮件、写博客、记资料,并且会把纠正记下来的 agent。
缘起
目标很直接:做一个持续为你工作的个人助手,具备五项基础能力——
- 个人数据库:索引、检索资料,随资料变化更新索引,回答时能定位到原文
- 持续关联 QQ 邮箱:获取新邮件、关联历史往来、拟回复草稿,你编辑确认后再发送,且重复检查不重复处理
- 编辑维护博客:按需编辑发布文章
- 手机端联动:从手机发起任务、看进度、编辑草稿、确认操作
- 能力组合:以上功能共享上下文,完成跨工具任务
此外还有一个更长期的要求:它得会成长——沟通方式、具体功能都能积累。成功流程和纠正要沉淀成可读可改的脚本、技能或记忆,并用版本控制管理。
本文介绍这套系统在 MVP 阶段定稿的架构。
一句话架构
以 pi agent 为核心的单进程插件化系统,服务器常驻:
core = pi(headless)跑 agent 循环 + 五个可插拔功能插件(extension 管代码逻辑 + skill 管知识流程),手机 Web 页和 cron 定时任务都通过网关入口触发 agent。
关键词有三个:单进程、插件化、数据与代码分离。
总体形态
[手机 Web 页] [cron 定时器] [本机终端 pi] \ | / \ | / ┌───────────────▼───────────────┐ │ 云服务器 │ │ ~/assistant/ (Git 仓库) │ │ ┌─────────────────────────┐ │ │ │ pi headless 进程 │ │ │ │ ├─ agent 循环 │ │ │ │ ├─ extension: gateway │ │ │ │ ├─ extension: memory-db │ │ │ │ ├─ extension: email │ │ │ │ ├─ extension: blog │ │ │ │ └─ extension: flows │ │ │ └───────────┬─────────────┘ │ │ ~/assistant-data/memory/ │ │ (真实数据, 不进 Git) │ └───────────────────────────────┘- 代码与数据分离:仓库只放代码、规则、技能;真实记忆数据放在
~/assistant-data/,不进 Git。服务器换个实例,数据目录照搬即可。 - 单进程:没有一个插件是独立服务,全部挂在同一个 pi 长会话里。
- 所有入口最终都是一次 agent 循环:不管任务从手机、cron 还是终端来,都是往同一台 agent 投一段文本。
三种触发方式
| 入口 | 机制 | 例子 |
|---|---|---|
| 定时 | cron 调网关的 HTTP 端点(带 token 鉴权) | 邮箱每 5 分钟轮询 |
| 消息 | 手机 Web 页把任务交给网关 | 「帮我拟一封回复」 |
| 本地 | SSH 到服务器手动跑 pi 命令 | 开发调试 |
agent 循环
整个系统的行为都收敛成一个循环:
触发 → 判断执行内容 → 调用插件工具 → 完成实现 → 总结流程并沉淀
最后一步是「会成长」的关键:一次跨工具任务做完,助手会把成功步骤写进流程文档、把纠正写进规则,下次换个场景也能复用。
插件体系:extension 与 skill 分工
所有功能插件化,不写独立服务。 载体分两类,分工很清晰:
| 载体 | 内容 | 位置 |
|---|---|---|
| extension(TypeScript) | 工具、定时逻辑、HTTP 端点 | 仓库 extensions/<name>/ |
| skill(.md 文档) | 知识与流程:怎么写草稿、怎么发博客 | 仓库 skills/<name>/SKILL.md,装机脚本安装到服务器 |
两条开发契约值得强调:
- 插件之间只通过工具接口通信,严禁触碰彼此实现。接口先定,代码后写。
- extension 不在工厂函数里起定时器 / 网络服务,从会话生命周期钩子启动——这样未配置的机器也能加载插件,只有真正调用工具时才失败。
五个插件
gateway:网关与任务状态机
单进程内的 HTTP 入口,是手机页与 cron 的统一通道。它同时管着「任务」这件事:$ASSISTANT_DATA/tasks/*.json 每个文件就是一台任务状态机。
状态长这样:
pending → delivered → awaiting_confirm → confirmed → executing → done ↘ ↘ ↘ └──────────────── failed ←────────┘- 状态写入只有一个入口:迁移要走
applyEvent,非法迁移直接报错,写不进去。 - 确认按「轮次」计:每次
request_confirm生成新的confirmId;手机页确认时必须回传同一个 id,对不上直接 409。多轮外发(同一任务发第二封)因此不会绑错轮次。 - 迁移保守:旧任务在读路径惰性归一化;历史上「已确认但未执行」的轮次永不自动投递,必须由用户重新确认一次,防不知情重发。
模型能主动调用的工具只有两个:
task_update(id, status: "delivered" | "done" | "failed", note?: string, draft?: string)request_confirm(id, what: string)其余是给手机页和 cron 的 HTTP 端点:提交任务、列任务、确认、取消、重试、触发。
memory-db:个人数据库
Markdown 目录树 + grep 检索,数据根由环境变量 ASSISTANT_DATA 指定。目录约定本身就承载了「记忆怎么组织」:
$ASSISTANT_DATA/memory/ profile/ # 个人资料(固定信息) notes/ # 资料、笔记(按主题建文件) rules/ # 纠正规则,每条一节,含触发场景 flows/ # 成功流程沉淀 index.md # 目录索引(插件自动维护)对外工具就三个:memory_search / memory_save / memory_read。契约只有两条但很硬:
- 检索必须返回原文位置(路径 + 行号),回答时能定位到原文。
- 资料变化时同步重建
index.md(文件名 → 主题 → 摘要)。
用 Markdown 而不是向量库,是刻意的:文件可以直接读、直接改,也更容易观察 agent 到底读了什么、写了什么、怎么组织记忆。
email:持续关联邮箱
QQ 邮箱经 IMAP + SMTP 接入(授权码登录)。三个工具:
email_check_new() // 拉取新邮件并归档,跳过已处理 IDemail_draft(id) // 取存档邮件全文(只读)email_send(id, body) // 收件人 / 主题由存档邮件推导,只传正文几个设计点:
- 去重:靠一份邮件 ID 清单,重复轮询不会重复处理或重复发送同一封邮件。
- 拟稿是被动模式:新邮件只归档、以任务条目呈现在手机页;用户主动说「帮我回这封」才结合记忆拟草稿。
- 收件人不由模型编造:
email_send的收件人优先取 Reply-To,主题固定为「Re: 原主题」。这是个约束也是安全边界——但代价是发不了「新收件人 + 自定义主题」的新邮件,只能回复已有来信。这条限制后来被老老实实记进了 notes。
blog:编辑维护博客
博客是 Astro 静态站(Mizuki 主题)。两个工具形成一条单向链:
blog_publish(title, content, description?, tags?) // 写入 posts/<slug>.md,draft: true,不上线blog_deploy(path) // 确认后:draft 改 false → 构建 → 部署- 未确认的文章绝不上线:
draft: true直接挡住渲染。 - 构建失败兜底:文章留在目录里但标回
draft: true,线上仍是上一版,任务报失败原因。 - 编辑已有文章 = 同名覆盖,不单独提供编辑工具,确认链不变。
- slug 有讲究:标题里带英文 / 数字 / 日期才有可读 URL;纯中文标题会回退成哈希名。这条也是踩出来的。
phone 与 flows
手机页就是网关的 GET /,覆盖三件事:发任务、看进度、编辑并确认草稿——MVP 不堆功能,按实际痛点迭代。
跨工具工作流不写代码,靠流程文档 + skill 驱动:成功流程沉淀到 memory/flows/<name>.md,格式固定为「触发场景 → 步骤 → 验收」。某个流程被验证为高频后,才考虑代码化。
一条贯穿所有外发动作的确认链
这是整个系统最重要的安全设计:发送邮件、发布博客这类对外可见的动作,必须先经用户在手机页确认;未收到确认前,绝不调用 email_send / blog_deploy。
流程是固定的一套:
- 拟好草稿,交到任务上(
task_update的draft字段) request_confirm请求确认- 手机页展示草稿,用户可以直接编辑
- 用户确认时回传该轮次的
confirmId与最终正文 - 仅当确认后,才用用户提交的最终版本执行外发
- 结果写回任务,收尾
工具本身不做确认检查——所以「绝不提前调用」这条纪律由 agent 侧守住。改稿后要重新 request_confirm,会换发新的 confirmId,旧轮次立即失效,保证发出去的一定是用户最后看到的那版。
成长机制
「会成长的助手」由三个互补机制构成:
- 纠正规则(硬约束) →
memory/rules/自由文本 Markdown。daemon 启动时把 rules 全部内容注入会话系统提示,每次任务都可见——所以像「总结时先列发件人和主题」「不要把报名截止时间当成活动开始时间」这类纠正,会在新会话、新场景里自动生效。 - 操作知识(怎么做某类事) → pi 原生 skills,
skills/<name>/SKILL.md。extension 管代码逻辑,skill 管「怎么做」。首批包括邮件回复、博客发布、资料入库、流程沉淀。 - 成功流程(跨工具任务沉淀) →
memory/flows/<name>.md,不强制,自然生长。
再加一层版本管理:memory 是独立 git 仓,agent 改完规则 / 流程后自动 commit + push 到服务器上的裸仓;daemon 启动跑一次同步脚本,长会话期间由 timer 周期调用。改动原因由任务系统留痕。这样「记忆」也能 review、能回滚。
设计与取舍
几个值得记下来的判断:
- 单进程 > 微服务:规模和负载都不需要拆分,单进程让上下文天然共享,跨工具任务才串得起来。
- Markdown > 数据库:可读、可 diff、可 grep、可手改,透明度优先。
- 被动拟稿 > 自动回复:发件人
no-reply、系统自动发送类邮件实际无法送达,自动回复风险大于收益。 - 确认链 > 权限系统:与其做复杂的授权模型,不如把所有外发动作都压到「人看一眼再点确认」这一个动作上。
当前进度
- 阶段 1:架构文档 + memory-db 插件跑通 ✅
- 阶段 2:gateway + 服务器部署 ✅
- 阶段 3:email(拉取 → 归档去重 → 按需拟稿 → 确认发送)✅
- 阶段 4:blog(写文 → 确认 → 构建部署)✅
- 阶段 5:成长机制 + 博客二期(进行中)
结语
这套架构没有多复杂的技术,真正的设计重心在「边界」上:哪些动作必须停下来问人、记忆怎么组织才可读、流程怎么沉淀才能复用。
助手会成长的前提,是它每次做完事都能把「这次为什么这么做」写下来——规则、技能、流程各管一层,再用 git 把整个过程管住。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时