MCP 还是 CLI 问错了问题,该问的是状态存放在哪里

Tobi Lütke 认为,MCP 与 CLI 之争发生在错误的层面:只要状态能在步骤之间保留,两者都能用。我们同意。对于同步来说,这个状态不是 REPL 的堆,而是集成自身的记录,这正是 Tajo 的 MCP 工具不在会话中保存任何东西的原因。

MCP 和 CLI 对比

AI 代理应该用 MCP 还是 CLI?

Lütke 说得对,MCP 与 CLI 之争处在错误的层面:只要状态能在步骤之间保留,两者都能用,而 bash 之所以胜出,只是因为文件系统就是那个状态。但对于同步来说,状态不是 REPL 的堆,而是集成自身的记录:游标、待处理的提案、租约、计划。这就是 Tajo 的 MCP 工具不在会话中保存任何东西的原因:任何客户端中的任何代理,都可以从这些记录继续工作。Tajo 目前不运行任何客户代码,也没有 REPL,下文会明确说明这一点。

了解更多
在本页面

2026年9月21日 UTC 00:33,Tobi Lütke 发帖说:“对 LLM 来说,MCP 与 CLI 之争是错误的讨论。人们在错误的层面上争论。”随后在 10月23日,他提到了一个新仓库 dquickts。它是 QuickJS 的一个分支,能在 for 循环中途暂停程序,把整个堆写成字节,再在另一个进程中恢复运行。他写道:“出色的代码模式环境。”

在层面这一点上,他是对的。我想先用他自己的话表示赞同,然后把这个论点收窄,直到它能说出对同步有用的东西。

他说的那个层面

下面是他原话中的论证。如果你打算改造一个论证,这是唯一公平的引用方式:

只要它们通过一个能保留状态的类 REPL 环境运行,两者都能表现得非常好。CLI 有趣的地方在于,CLI 工具通常通过 BASH 调用,而 BASH 恰好是一个带有持久状态(文件系统)的 REPL,因此 CLI 的效果比 MCP 好得多。但这根本谈不上是 MCP 的固有属性。这只说明我们需要更好的运行框架。

这是正确的,它解释了许多人观察到却没能解释的现象。拿到 CLI 的代理会写一个文件,三步之后再读回来,用 grep 搜索,用管道串联。拿到一个裸 MCP 服务器的代理,每次调用只得到一个结果,其余一切都只能放在上下文窗口里随身携带。CLI 赢在的不是协议,而是 bash 自带了一个存放东西的地方。

他的押注由此而来:会有人造出一个可嵌入的、类似 SQLite 的引擎,把 bash、TypeScript 或工具调用变成一个计划,在运行前检查,并为持久化执行而设计。dquickts 是朝这个方向迈出的一步。它的 README 描述了一个“函数和变量在重启后依然存在”的 REPL,还描述了一种持久化的宿主调用:“脚本调用一个数小时后才有回应的宿主函数。进程保存脚本后退出。”

到这里为止,我完全赞同。下面开始收窄。

哪种状态,用于哪项工作

“保留状态”背后藏着一个问题:谁的状态,以什么形式?对于编程代理来说,状态就是代码仓库和 shell,一个重启后依然存在的 REPL 是个好答案。对于在两个 SaaS 系统之间构建同步的代理来说,它在步骤之间需要的状态已经由这项工作本身定义好了。承载这些状态的是四条记录,没有一条是脚本里的变量。

游标。 同步按窗口读取数据源,必须知道上一个窗口在哪里结束。在 Tajo 的同步引擎中,这是一个按同步和范围分别存储的游标,在每个没有失败记录的批次处理之后写入,下一次运行从那里继续。定时运行会在做任何其他事情之前先保存起点,所以即使首次运行失败,下一次运行也会从同一个位置读取。背后的原则是只发送增量、绝不发送整张表,详见 反向 ETL 讲解。关于游标,我就讲到这里。

待处理的提案。 在 Tajo 的连接器 MCP 工具中,一次写入需要两次调用。brevo_propose 会根据声明的操作校验输入,并记录一个提案。不会发送任何东西。结果中包含提案 ID,当需要人来决定时,还包含批准该提案的页面。brevo_apply 按顺序执行该连接中已批准的写入,并在第一个仍在等待人工处理的写入处停下。两者之间的那条记录就是状态。它保存输入及其摘要,因此已批准的写入在决定与执行之间无法被修改。它不保存服务商返回的响应值,因为一条只追加的批准记录不能变成个人数据的存储库。

租约。 同步的一次运行在其整个生命周期内持有一个独占租约,带有所有者令牌、默认十五分钟的时限以及续期机制。过期的所有者无法释放较新的租约。如果租约存储无法访问,运行会拒绝启动,而不是去猜测。哪个 worker 持有本次运行,是租约中的一个事实,而不在任何进程的内存里。

计划。 syncs.dryRun 从游标读取下一个窗口,逐条记录判断是插入、更新、删除还是不变,然后只写入一行:计划,保留一小时。它不移动游标,也不向任何一个系统写入任何东西。syncs.apply 将这个计划写入一次。如果游标已经移动,或者在此期间变化的记录超过了容许的比例,它会拒绝执行。重复调用会返回第一次的结果,而不会再次写入。

这就是步骤之间的状态:四条带有 ID 的记录,任何有权查看的代理都能读取。

为什么 MCP 端不在会话中保存任何东西

一旦状态存放在记录中,会话就没有什么需要保存的了。所以 Tajo 的 MCP 端点不保持会话。每个请求都会得到一个全新的服务器和一个没有会话 ID 的传输层。读取每次调用对应一个请求,结果有上限,避免一个回答就塞满一轮对话。写入则是先提案、再执行,你随身携带的是提案的 ID。

协议也朝同一方向发展。MCP 规范的 2026-07-28 修订版 从 Streamable HTTP 传输中移除了协议层面的会话和 Mcp-Session-Id 请求头。需要连续性的服务器现在会自己生成一个句柄,并以普通参数的形式拿回来。提案 ID 正是这样的东西。什么是 MCP 服务器 介绍了这次修订,以及工具调用与持久同步的区别;本文只讨论状态放在哪里。

对集成工作来说,真正重要的结果是这样的。假设 Claude Code 中的一个代理提出了一次写入,然后它的会话结束了。一小时后,有人批准了它。另一个客户端中的代理,或者第二天的同一个代理,用这个 ID 调用 apply,写入就会生效,而且只生效一次。崩溃的代理失去的是它的上下文窗口,此外什么都不会失去。没有任何东西被挂起,所以也没有任何东西需要恢复。

我们没有什么,以及代价是什么

诚实的反对意见来自 REPL 一方,大致是这样:“好吧,你们的记录保存下来了。但每次调用都要重新读取上一次调用已经拿到的东西。一个脚本会一次性获取联系人,做关联、过滤,然后返回一个数字。你们的代理要调用五次,每次都要付出代价。”这个反对意见在机制上是对的,而我们还没有衡量这个代价。用来统计有无携带状态时每项任务调用次数的评估还没有建立。在它建成之前,“调用模式消耗更多 token”这一说法仍然没有得到回应。

还有三件事必须明确说明,因为读完本文的读者不应该以为它们存在:

  • 目前不运行任何客户编写的代码。 连接器的读取每次调用对应一个请求,路径中没有沙箱。我们的技术栈里确实有一个代码运行器,它是一个内部工具。在生产环境中,除非设置了两个独立的开关,否则它是关闭的,并且只响应管理员。每次调用都会得到一个全新的上下文、只允许读取的绑定,以及同步代码两秒的时间预算。它自己的文件头用大写字母写明,它是正确性边界,而不是安全边界。
  • 没有 REPL。 代理在一次调用中计算出的任何东西,除非作为带有 ID 的记录写入,否则下一次调用都无法使用。
  • 没有持久的代理会话。 客户端保存它的对话。Tajo 保存提案、计划、游标和租约。不存在第三种东西。

持久化引擎适合做什么

以上这些都不是反对 dquickts 的理由。它的 README 列出了快照的用途,这份清单很好。让空闲脚本休眠并释放内存。在任何空闲的 worker 上分片运行长时间的计算。从快照热启动每个实例,而不是重复初始化。让脚本停在一个稍后才有回应的宿主调用上。对于编程代理的工作区,或者需要在重启后保留的笔记本,这是合适的工具。

但同步仍然不应依赖它,理由有三条,都来自 README 本身。第一:“会话文件只能加载到写入它的那个 qjs 构建中。”一个引擎版本写下的堆,升级之后就是一个废文件;而游标只是一个字符串。第二:“快照是受信任的输入,就像字节码一样”,并且“恶意快照不会被检测出来”。存储下来的堆是你必须保护的东西,而不是你能够审阅的东西;提案则是一行在批准前由人阅读的记录。第三,快照保存着脚本读取过的一切,包括客户记录。上面那些记录恰恰相反:计划存储的是摘要而不是值,提案记录从不存储服务商的响应。

我们遵循的原则之一是:确定性胜过巧妙。一个能在循环中途恢复的堆很巧妙。游标、提案、租约和计划则平淡无奇、可以检查,并且在任何引擎、任何客户端、任何一次崩溃之后都完全一样。

Lütke 提出了正确的问题:状态存放在哪里?对于同步来说,答案从来不会是在进程里。它注定在记录里,因为批准写入的那个人必须能在那里读到它。

常见问题

对 AI 代理来说,MCP 和 CLI 哪个更好?

单就其本身而言,都不是。Tobi Lütke 在 2026年9月21日提出的观点成立:只要调用通过某种能在步骤之间保留状态的东西运行,两者都能用。CLI 今天感觉更好用,是因为 bash 把状态保存在文件系统里。区别不在协议,而在运行框架。

在同步的各个步骤之间,AI 代理需要什么状态?

集成自身的记录:一个说明上次运行停在哪里的游标,一个等待人工批准的提案,一个说明哪个 worker 持有本次运行的租约,以及一个 apply 必须与之匹配的试运行计划。这些都不是脚本里的变量,而是带有 ID 的行。

为什么 Tajo 的 MCP 服务器不在会话中保存任何东西?

每个请求都会得到一个全新的服务器和一个没有会话 ID 的传输层。下一次调用所需的一切都是记录:提案 ID、计划 ID、游标。因此,另一个客户端中的第二个代理可以从第一个代理停下的地方继续,崩溃的代理除了自己的上下文窗口之外什么也不会丢失。

Tajo 会运行 AI 代理编写的代码吗?

不会为客户运行。连接器工具上的读取每次调用对应一个请求,没有沙箱。现有的代码运行器是一个内部工具,位于两个开关之后,只有管理员可以访问,使用只读绑定,每次调用都使用全新的上下文。它自己说明自己不是安全边界。没有 REPL,也没有持久会话。

像 dquickts 这样的持久化 JavaScript 引擎适合做什么?

它的 README 列出了用途:让空闲脚本休眠,把长时间计算切分到多个 worker 上,从快照热启动,以及让脚本停在一个数小时后才有回应的宿主调用上。这些都是真实的用途。但同步仍然不应依赖它:快照只能加载到写入它的那个引擎构建中,对审阅者来说是不透明的,并且保存着脚本读取过的所有客户数据。

申请抢先体验

请输入您的名字和电子邮箱地址。我们会发送确认 Tajo 访问申请的链接。

获取Brevo

本页的 Markdown: mcp-or-cli-is-the-wrong-question.md

正在加载 Markdown…