先说三个数字:二进制 6.12MiB,冷启动 10 微秒,常驻内存个位数 MB。这是一个编码代理,fx,Zig 写的。不是 hello world,是能接模型、能读仓库、能挂插件的那种。
我拿到新工具的第一件事是 ls -lh:
$ ls -lh fx
6.1M fx
6.1M。我见过的一些“轻量级”agent,装完先占两百 MB,就不点名了,都懂。
10 微秒这个数,人的手指感知不到。你分辨不出 10 微秒和 100 毫秒,瓶颈在键盘、终端和你自己。那这个数字凭什么写进 README?
因为它是一张收据。意思是:在你敲下第一个键之前,fx 不做任何额外的工作,不做任何 I/O。不读多余的配置,不起后台进程,不跟谁的服务器报个到。
现在能做到这条的工具不多。跑个 --version,抓包能看见一个遥测请求;跑个 --help,它顺手把更新检查做了。每一个都是“合理的顺手”,单看都无害。加起来就是按秒计的启动,和几十上百 MB 的常驻。
常见启动路径:解析配置 -> 检查更新 -> 起守护进程 -> 预热 -> 等你输入
fx:等你输入
我本来想说 10 微秒是炫技。写到这发现说错了,炫技从来都是加法,没人靠做减法炫技。这个数字是克制的副产品。
这些数字从哪来的。fx 把“研究和可嵌入”写成设计目标,要能作为更大系统的一部分跑。可嵌入是个狠约束:你的程序不能假设这台机器是自己的。守护进程赖不得,全局状态占不得,启动时连网都碰不得。10 微秒和个位数 MB 不是目标,是这些约束结账时打出来的小票。
体积也一样。一个工具的大小下限,是它运行时的大小。Node 系的工具再怎么瘦身,地板就是运行时本身;Zig 没有这块地板。同一个工具链还能直接交叉出 WebAssembly 版本,支持 JSPI 的浏览器(Safari 27+、Chrome)里完整功能都能跑。JSPI 就是给同步 WASM 配的那套栈切换,让“等一个网络往返”可以挂起,而不是硬开线程去等。
能编译进一个标签页,说明核心从头就没假设过机器是自己的。这件事的后果比听上去大:至少存在一种形态,界面和执行都在你手里,中间不需要一台别人的服务器替你转发。一整类中间生意的前提,就是你的代码必须出门。做研究的那半个目标也跟着受益,想给别人试的东西,发个链接比发一份安装文档便宜。
内存个位数 MB 还有个朴素的好处:一台机器能同时跑一打实例。一个任务一个 agent,一个分支一个,跑完就退。进程小而多、用完即走,这是 Unix 的老路数,比一个大而全的守护进程好组合得多。fx 的界面也顺这个方向:类 shell,输出克制,滚动历史默认留着。给人看的和给下一个程序读的,它优先照顾后者。
说句题外话。一个 agent 出来说“我就是个命令行”,很多人第一反应是不完整——不带面板、不开端口、不驻后台,好像就不算产品。工具做小,会被当成没做完。这个反应本身,比工具更值得研究。扯回来。
“启动前零 I/O”这条纪律我自己也在练。手头那个玩具存储的客户端,连接是懒的:
/* 第一条命令之前,一个字节都不发 */
static peer *p;
int cmd_get(const char *key, sds *out) {
if (p == NULL && (p = peer_connect()) == NULL)
return -1;
return do_get(p, key, out);
}
这行 if 我改过三遍。最早写在 main 里,进程序先拨号,“省得后面判断”;后来做嵌入测试,测试进程一跑就碰网络,全红 😅。懒初始化不是技巧,是边界:还没用到的东西,就不该存在。
扩展那块,fx 走的是技能、插件加 MCP,核心保持小。我的判断标准只有一条:谁拥有主循环。扩展以数据和外部形式待在边上,核心不认识任何一个具体插件,这叫扩展;核心 import 一堆插件、替它们管生命周期,那玩意儿叫框架,抽象税从第一行就开始收。fx 声称是前者。这层代码我没读透,v0.0.6 也还在动,先不替它打包票。
模型那边倒干脆:本地模型、网关、直连 API、订阅,都能接。一个 6MiB 的工具没有商业模式要防守,自然不需要把你锁在哪一家。锁定通常是商业需求,不是技术需求。
版本号也老实。v0.0.6,自己标实验性,装法就一行 curl,从 fx.sh 拉 setup.sh。我见过太多 1.0,功能没它多,层比它厚五倍。实验性写在明面上,比“生产就绪”印在 README 可信。
所以我的筛法就一条:凡是 --version 都要联网的工具,别信它任何关于“本地”的承诺。启动路径藏不住,所有没砍掉的依赖,都会那几毫秒或者那几秒里排队自首。
10 微秒不是性能优化。是没东西需要优化。