本地优先知识产品的架构演进
# 本地优先知识产品的架构演进
结论
从本地单机 App 走向多端成熟产品,关键不是「要不要服务器」,而是把边界划成 云端加工厂 + 本地主仓库;并且从第一天就把 AI 执行层抽成可替换的接口, 产品形态改在前端,数据模型改在后端。
两条配套边界:
apps是「可独立运行或发布的产品单元」,不都是服务器进程; 文档里写的目标架构 ≠ 当前运行事实(判断现状要看有没有部署配置)。
一、Agent 先变成可执行入口,再动数据模型
原状态:侧边栏四个功能入口(订阅 / 笔记 / 剪藏 / 沉淀),AIPanel 已有对话, 但工具只能读(读笔记、读剪藏、搜索),不能创建和整理。
演进顺序:
| 阶段 | 内容 | 是否动数据模型 |
|---|---|---|
| P0 | 把 createNote / saveClip / addSubscription 暴露给 AI tools,让对话能创建笔记、存剪藏、加订阅、搜索后整理成新笔记 | 否(改 AIPanel 与 tools 的接线) |
| P1 | 侧边栏信息架构改成 Agent / Inbox / 知识库 / 订阅 | 否(前端结构) |
| P2 | 剪藏、RSS 新文章、临时笔记统一进 Inbox | 先不做底层合并 |
| P3 | RSS 每篇总结 | 是(给 feed_entries 加字段) |
两个关键取舍:
- Inbox 第一版做「虚拟 Inbox」:前端把 clips / feed entries / 未整理 notes 混合展示,
每个 item 标来源类型;点「整理到知识库」时生成或更新一条 note。
统一字段(
inbox_status、output_note_id、ai_tags、processed_at)等跑通后再补。 理由是直接合成一张大表会牵动 Rust、迁移、搜索、列表、阅读器,成本远大于收益。 - RSS 总结不要一刷新就全量跑:慢、贵、失败难处理。先做「打开文章时可生成」,
并给每个 AI 任务留状态字段(
summary_status/summarized_at)。
判据:用户能体验到的能力先上线,迁移成本后置;能让前端解决的就不要先动表结构。
二、Agent 是侧边栏主入口,不是独立窗口
用户问「是否要单独一个 agent 窗口」,收敛结论是不要。
- 独立 Agent 窗口很容易变成「又一个 ChatGPT 窗口」,和知识库本体割裂, 用户会困惑:我到底是在用知识库,还是在和一个机器人聊天。
- 正确形态是贴着用户正在看的内容工作:
- 侧边栏
Agent= 工作台 / 起点(主区域首页 + 大输入框 + 当前可执行动作 + 最近的 Inbox / 待处理) - 右侧 AI Panel = 当前内容的上下文助手(看文章时总结/整理/解释名词,写笔记时润色/扩写/找关联)
- 侧边栏
- 整体结构:左侧选工作区,中间放知识内容与任务,右侧浮层是上下文 AI,Agent 作为第一入口而不是独立 app。
判据同族:新增一个独立入口,必须回答「去掉它用户会损失什么能力」(见 容器要有超出文件夹的意义才值得独立入口)。
三、AI 调用要抽成可替换的执行层
什么时候才真的需要独立 AI server —— 不是「AI 更耗性能」,而是这些需求出现时:
- 用户关闭 App 后仍要自动刷新 RSS、自动总结、发日报
- 不想让用户自己填 API key,由你统一提供 AI 能力
- 邮件推送、通知推送、定时日报
- 多设备同步
- 统一的成本控制、限流、缓存、失败重试
- 更复杂的 Agent 任务队列(每天处理大量订阅源)
- 账号体系、会员、用量统计
当前该怎么做:本地跑 AI,但按「未来可替换为 server」的方式写 ——
ai/summarizeFeedEntry()
ai/generateNote()
ai/classifyContent()
现在内部调本地 API,将来只把实现换成 call('/api/ai/summarize-feed-entry'),
UI 和业务流程不用大改。要点:不要把 OpenAI 调用散落在 React 组件里,
prompt 与 JSON schema 独立成模块,总结结果落 SQLite 并给每个 AI 任务留状态字段。
分层边界:
| 层 | 职责 |
|---|---|
| UI 组件 | 展示、按钮、状态 |
| 业务服务层 | summarizeEntry / createNoteFromEntry / addSubscription |
| Tauri/Rust | SQLite、RSS fetch、本地文件、系统能力 |
| AI Client | 模型调用、prompt、JSON schema、错误处理 |
四、云端加工厂 + 本地主仓库
用户提出的方案:本地把订阅源列表给云端 → 云端抓 RSS → 云端 AI 总结 → 结果回本地 → 本地保存。
这比「纯本地抓 RSS」更适合多端:
- 移动端后台任务限制多,本地 App 不一定能稳定定时抓;云端不受设备限制
- 多端体验一致,不会出现某台设备有新文章、另一台没有
- AI pipeline 更自然:云端抓到就总结,客户端只拿「标题 + 正文/摘要 + 总结 + 标签 + 名词解释」
- 日报天然适合云端生成
代价是隐私边界改变:云端至少会接触用户订阅了哪些源、文章标题/链接、正文或摘要、总结结果。 产品定义要跟着改成:
本地保存知识库主数据,云端负责订阅抓取和 AI 加工,可短期缓存处理结果。
要配的产品承诺:云端只存订阅任务与近期处理结果、本地才是长期知识库、用户可清除云端缓存、 可选「本地抓取 / 云端抓取」。云端的角色是待同步结果池,不是永久知识库。
结果怎么回本地:
| 方式 | 特点 |
|---|---|
| 本地主动拉取(App 打开时问「有没有新处理好的」→ 下载 → 写入 → 回执) | 第一版选它:最简单稳定;代价是不打开 App 本地就没有最新数据 |
| 云端推送(push / websocket) | 体验更好但复杂,Mac/Win/iOS/Android 推送体系各不相同 |
五、多端共享的是服务,不是 UI
目标若是 Mac / Windows / iOS / Android / iPad 的成熟产品,架构必须改: 数据不能只存在某一台电脑,RSS 也不能依赖用户打开某个设备才刷新。
客户端(只负责展示、编辑、缓存)
→ 统一服务器
→ 账号 / 数据同步 / RSS 抓取 / AI 总结 / 日报 / 知识库数据
- 4G 内存服务器够第一版(只要不在服务器跑本地大模型);真正的成本是 AI API 费用、并发与数据库设计
- 客户端选型:Mac/Windows 继续 Tauri,Web 保留一套,移动端可评估 Tauri mobile,但更稳的路线通常是 RN/Expo
- 原 Tauri 代码不废:本地 SQLite 从「唯一数据库」降级为「缓存」,RSS 抓取迁服务器,前端 AI 调用改成请求自己的服务器
判据:不要追求「一份 UI 跑所有端」,要追求「一套后端数据和 AI 能力,多端共用同一套服务」。
六、云端版先做骨架,Agent 面板先留位
从本地 Tauri App 迈向云端产品时,第一步不是「把 Agent 接上」,而是先把云端骨架搭出来:
- 新开独立云端项目(如
mewmo-cloud),不要塞进本地 Tauri App —— 它是新云端产品,不是「本地知识库 2.0」 - 技术选型偏云端能力:Next.js + TypeScript + Tailwind,后续 Postgres、cron/队列
- 首页只表达三件事:这是什么产品、它能做什么、怎么开始;主按钮进静态 demo dashboard
- 右侧 Agent 面板先留空占位(「将在这里读取当前页面上下文并对话」),不接模型、不接数据库、不接登录
理由:用户此时还不熟 Web Agent 的交互与技术架构,先验证「云端 mewmo 应该长什么样」的页面结构是否成立。Agent 后续分三层接:前端聊天 UI → 网站后端 API(读云端数据再调模型)→ Agent 工具层(search_notes / read_note / write_note / list_clips)。编排层选择见 Agent编排层薄自建优先于重型框架。
完整蓝图(Web / Mac / iOS / iPad / 扩展 / Agent / Admin / 同步 / 支付)要保留全量,但按阶段落地:见 完整蓝图不等于同阶段交付、数据权威模型要先于同步与迁移确定。
七、侧边栏是「当前内容的 AI 面板」,拆成总结与 agent 两个 tab
用户提出的结构(替代「侧边栏就是 chat」):
Article Intelligence
├─ Summary tab:后台 worker 产出的、已落库的 AI 结果(摘要 / 要点 / 标签 / 相关内容)
└─ Agent tab:围绕当前文章继续交互的会话
关键判断:
- 总结不是聊天记录,是文章的结构化 AI 元数据:打开任何一篇文章都应立刻读到已经算好的结果。
- 两个 tab 不等于两个 agent:总结是后台 worker 产物,agent 是前台交互能力;共享同一套 AI 能力,但生命周期不同。
- 链路:
后台:文章入库 → 自动总结 / 打标签 → 存 DB;前台:打开文章 → Summary 直读 DB → Agent 基于 Summary 继续行动。 - 首版范围收敛:总结 tab 只对
clip/feed_entry生效,不含 notes。
判据:「已算好的结果」和「继续交互」是两种不同生命周期的东西,要在 UI 上分开,而不是塞进同一条聊天流。
八、手机看电脑 Agent:bridge-owned app-server 是唯一真源
目标从「手机发起自己的 Codex turn」收敛到「手机实时同步电脑端 Codex」。判断:官方 remote-control 会被「第三方 key 不能用」卡住,所以另起独立 app-server 是对的;但独立 app-server 要能实时同步,前提是它成为唯一真源:
电脑端输入/界面 ─┐
├─→ 同一个 owned app-server ─→ 事件广播给所有客户端
手机端输入/界面 ─┘
路线:bridge 启动长期存活的 codex app-server(daemon 级);所有线程以 app-server 的 threadId 为主键;电脑端入口也改走 bridge(本地 Web UI / menubar companion / CLI wrapper);bridge 统一发 turn/start 并监听 notification 广播给所有客户端。
硬边界:若电脑端仍用官方 Desktop 且不连这个 owned app-server,就看不到它的实时流。 这一步(电脑端入口改走 bridge)是整套方案里最关键、也最容易漏的一环。
另一个反模式是「懒加载」:点进会话才 read_session_history、只在「手机发起的 turn」才推 stream_chunk,会让电脑端独立生成时手机既不更新窗口也不触发通知——这与「实时镜像」目标直接冲突。
九、apps 是「可独立运行或发布的产品单元」,不是「服务器进程」
判据:app 能独立启动 / 部署 / 持续运行;package 是被别人调用的共享能力,本身不能启动。
AI 能力放在 packages/ai 而不是 apps/ai,是有意分层:
apps/web ──────┐
├── packages/ai ──→ OpenAI / Anthropic / 自定义模型
apps/worker ───┘
packages/ai 只负责选 provider、拼请求、加载 prompt、清洗输入、解析返回,
暴露 summarizeContent() / generateAgentReply();它没有端口、没有页面、没有常驻进程。
即时对话由 Web 调用(用户要立刻拿到回复),异步摘要由 Worker 调用(可排队可重试)。
只有出现这些需求时才值得拆出独立 apps/ai-service:AI 请求量大需要独立扩容、自部署模型/GPU、
Web 与 Apple 与扩展之外还有多个系统要统一调用、需要集中限流/审计/计费/模型路由、AI 服务改用 Python。
现在拆只会多一次网络请求、一套服务鉴权、一份部署费用和一个故障点。
同理,apps 里也有跑在用户设备上的:apps/apple、apps/extension 不是服务器进程。
十、代管平台与自有服务器的边界
Vercel(请求驱动)、Railway(常驻进程)、Neon(托管 Postgres)、Upstash(托管 Redis) 本质都是替你提供服务器,只是把机器管理隐藏起来。
判据:需要 24 小时常驻的进程不能用 serverless 平台替代。 Web 的 Node runtime 里那个 scheduler 只能算兜底 —— 它不是队列消费者、 不能消费剪藏抓取与 AI 摘要队列,Vercel 的实例也不是可靠的 24 小时进程; 生产环境缺常驻 Worker 时,任务会进 Redis 但没人消费,表现为「已保存但一直抓取中」。
自有服务器可以完全替代 Railway:同一台机器上分别跑 worker 与 agent 两个容器,
不需要公开域名,只要能主动访问 Neon / Upstash / Vercel / AI API。
代价是自己承担进程守护、崩溃重启、安全更新与防火墙、日志监控与磁盘、部署与回滚、密钥管理 ——
Railway 卖的不是特殊技术,就是这些运维工作。
部署上的一条硬经验:构建镜像与运行容器要分离。
monorepo 构建(pnpm install + 生成 Prisma Client)瞬时吃 CPU/内存/磁盘,
在 4GB 无 swap、还跑着别的项目的服务器上本机构建,是整条部署链路里风险最高的一步。
因此:GitHub Actions 构建镜像 → 推 GHCR → 服务器只拉取运行;
好处是构建失败不影响线上、环境一致、按 commit 回滚、服务器不留构建缓存。
十一、AI 能力要分两类:产品 + AI 与 Agent
两类需求的架构归属不同:
- 产品 + AI:确定性、短时、单步能力(文章总结、标题生成、自动打标)→ 仍由 Web API / Worker 调用
packages/ai,本质是产品能力。 - AI Agent:目标导向、可调工具、多步执行、可暂停确认、失败恢复 → 独立部署的
agent-server,前端只负责发起任务、展示过程和处理确认。
packages/ai 不必删,但降级为「模型适配 + 通用 AI 原语」;Agent 的编排、状态与工具不能再放进 packages。运行状态真相源留在自己的 PostgreSQL(runs / steps / approvals / events),可观测平台只做 trace 与成本分析(见 观测平台不能当运行状态真相源)。
架构决定里最影响后续的一个问题是:首版 Agent 是否支持跨分钟甚至跨小时暂停后恢复。若已经独立建 agent-server,最好一开始就把持久化运行状态设计正确。
十二、Cron 是同一个 app 的第二个入口,不是新 app
RSS 定时更新没新增 apps/cron,而是在 apps/worker 里加第二个启动入口(cron:feeds,跑完即退出),部署上复用同一个镜像:常驻容器只消费 AI 队列,feed-cron 服务每分钟启动一次。
判据:两者共享数据库连接、抓取逻辑、AI package、环境变量、Dockerfile 与部署流程 —— 新建 app 只会重复 package、环境配置、Dockerfile 和部署流程。 但要在命名上区分清楚:「每分钟启动一次、做完就退出」与「24 小时常驻监听队列」是两种运行形态,即使放在同一个 app 里也不该混为一个概念。
相关
- 后台任务队列选型 —— 常驻 Worker 消费什么、用什么队列
- 共享历史不等于共享活会话 —— 三个层级的区分
- 终端画面同步Agent状态不稳定 —— tmux 这条替代路线
- Agent编排层薄自建优先于重型框架 —— 编排层怎么选
- 完整蓝图不等于同阶段交付 —— 全量架构与分阶段交付
- 容器要有超出文件夹的意义才值得独立入口 —— 独立入口的判据
- 一次循环只做一个可体验的v1能力 —— 范围与优先级
- 修复范围分保守彻底与最小闭环 —— 虚拟 Inbox 是「最小闭环」档
- 用户可感知的价值与差异化表达
- 差异要用使用模式说而不是功能清单 —— 同族:先造对照,再谈价值
- Agent读语义文件App读结构化存储