缘起
FutureOS 是一个开源的通用 AI Agent:一个跑在本地的 agent 后端,同时驱动终端 TUI、桌面应用(Tauri)、移动端、CLI 和 IM 机器人(飞书/钉钉)。它的核心组件——agent 后端、IM 渠道桥、loop 控制面、CLI、TUI——全部用 Rust 实现。
选 Rust 倒不是性能情怀,而是两个硬约束:
- Agent 是一个拿到用户文件系统和 shell 权限的常驻进程,内存安全在这个场景不是加分项,是底线;
- 一套代码要跑在 macOS / Linux / Windows(x86_64 + arm64)四处,单二进制分发,不能要求用户装运行时。
这篇不谈 AI,只谈工程。几个觉得值得分享的架构决策和踩坑。
Workspace 架构:一个 future 二进制装进所有组件
仓库是一个 Cargo workspace,成员按"谁拥有 ~/.future/ 下的哪片数据"划分:
agent/— gRPC 后端,唯一的核心tui/— 终端 UI,纯 gRPC 客户端channels/— 飞书/钉钉 IM 桥,也是客户端orchestration/loop/— 长任务控制面cli/— 把所有组件 embed 成统一的future二进制:future agent|tui|channel|loop分别启动对应组件packages/rpc/— protobuf 的唯一事实源
有意思的两个边界决策:
桌面端(Tauri)被刻意排除在 workspace 之外。 desktop/src-tauri 按自己的节奏构建(npm/tauri 驱动),如果拉进 workspace,每次根目录 cargo 命令都会把它拖下水。代价是要处理一个 CI 坑:tauri.conf.json 声明了 externalBin: ["binaries/future"],sidecar 文件不存在时 tauri-build 直接中止构建——CI 里的解法是生成空的占位 sidecar 文件。
proto 代码生成收口到单一 crate。 早期 agent / channels / desktop 各自持有一份生成代码,漂移是迟早的事。现在由 future-rpc 独占 codegen(opt-in 再生成、产物入 git、CI 检查陈旧度),所有 Rust 消费方只依赖这一个 crate。迁移期间服务端做"双写":typed payload 和旧的 JSON data 字段同时下发,等所有已发布客户端都迁移到 typed 解码后才下线 JSON——字段号只增不减,optional 标记区分 null 与缺省。
gRPC 作为内部边界,而不是内部实现细节
Agent 只监听 127.0.0.1:50051,所有客户端(TUI、桌面、移动端、IM 桥)都是薄 gRPC 客户端。这个决定的收益是:
- 运行时与界面解耦:新增一个客户端不需要动 agent 核心;IM 机器人这种"无界面客户端"和 TUI 地位完全平等
- 多客户端天然并发:桌面端跑着长任务,终端可以同时开另一个会话,共享同一个后端状态
- 崩溃隔离:TUI 挂了对 agent 毫无影响——对长任务场景这是刚需
成本当然也有:本机 IPC 走 gRPC 有固定开销,proto schema 演进需要纪律(就是上面那套双写机制)。但对"一个后端,N 种界面"的形态,这条边界划得值。
手写终端后端:为什么没用 crossterm / ratatui
TUI 没有依赖任何终端 UI 框架。终端后端是自己写的:POSIX 走 libc(termios、poll),Windows 走 windows-sys 的控制台 API(raw mode、VT 输入输出、窗口尺寸与输入等待),按 target 条件编译。Markdown 渲染用 pulldown-cmark,文本分段用 unicode-segmentation。
换来的东西:
- 依赖极小:整个 TUI 的外部依赖屈指可数,供应链面小——对一个默认能执行 shell 命令的工具,这不是小事
- Windows 控制台行为完全可控:Windows 上 VT 序列和控制台 API 的坑很多(输入法、窗口缩放、鼠标事件),框架的抽象在这些地方恰恰最容易漏
- 按需实现:输入框、滚动、弹层、markdown 渲染都只实现自己需要的部分,没有框架税
代价是要自己处理所有平台差异,工作量确实大。如果你的 TUI 需求标准,crossterm/ratatui 依然是正确默认;我们的场景是"终端是一等公民界面,且对依赖和权限敏感",所以选择自己扛。
24 小时长任务的控制面:事件溯源 + 确定性内核
AI Agent 跑长任务的真实问题不是模型不够聪明,而是工程上撑不住:跑到半夜进程崩了无人知晓、上下文膨胀后开始漂移、任务"做完了"没人验收。我们为此写了一个 loop 控制面(以 LoopX 为参考蓝本的 Rust 再实现,Apache-2.0),两个关键设计:
状态是事件溯源的。 目标、待办、门禁、监控的全部状态都从事件日志重放重建。Agent 进程半夜被 OOM 杀掉,重启后从日志恢复到断点继续跑——"崩溃"从致命事故变成可恢复的日常。
"该不该再跑一轮"由确定性内核决定。 should-run 判断是纯函数:目标状态 + 显式门禁(证据下限、验收契约、verify 闸门、租约活性)→ 是/否。不为了调度决策再多花一次 LLM 调用,也不让模型自己宣布"我做完了"。
这套东西跑起来的体验是:晚上扔一个深度调研目标进去,早上在飞书里收它的交付物和待确认项。
跨平台与 CI 的几条军规
rust-toolchain.toml钉死工具链,本地 lint 与 CI 用同一套 clippy flags- clippy 必须带
--all-targets——不带的话测试代码里的警告会漏掉,本地绿 CI 红 - 不要假设 POSIX:路径分隔符、
.exe后缀、大小写不敏感文件系统、Windows 上 shell 是 PowerShell(5.1 连&&都不支持)
现状与短板
项目还很早期:Windows/Linux 的系统级沙箱还没做(目前只有审批门控 + macOS Seatbelt),移动端比 TUI 新。仓库:https://github.com/futuregene/future-os(MIT + Apache-2.0),SECURITY.md 里写了完整的信任模型。
对架构有任何不同意见,欢迎来评论区拍——尤其是"内部边界该不该用 gRPC"和"手写终端后端值不值"这两个问题,想听听大家的判断。
评论区
写评论还没有评论