---
title: "MCP 还是 CLI 问错了问题，该问的是状态存放在哪里"
description: "Tobi Lütke 认为，MCP 与 CLI 之争发生在错误的层面：只要状态能在步骤之间保留，两者都能用。我们同意。对于同步来说，这个状态不是 REPL 的堆，而是集成自身的记录，这正是 Tajo 的 MCP 工具不在会话中保存任何东西的原因。"
url: https://tajo.io/zh-cn/blog/mcp-or-cli-is-the-wrong-question/
markdown: https://tajo.io/zh-cn/blog/mcp-or-cli-is-the-wrong-question.md
lang: zh-CN
author: "Set Noa"
author_url: https://tajo.io/zh-cn/set-noa/
published: 2026-11-04
updated: 2026-11-05
alternates: {en-US: /blog/mcp-or-cli-is-the-wrong-question.md, bg-BG: /bg/blog/mcp-or-cli-is-the-wrong-question.md, cs-CZ: /cz/blog/mcp-or-cli-is-the-wrong-question.md, de-DE: /de/blog/mcp-or-cli-is-the-wrong-question.md, da-DK: /dk/blog/mcp-or-cli-is-the-wrong-question.md, es-ES: /es/blog/mcp-or-cli-is-the-wrong-question.md, fr-FR: /fr/blog/mcp-or-cli-is-the-wrong-question.md, el-GR: /gr/blog/mcp-or-cli-is-the-wrong-question.md, hi-IN: /hi/blog/mcp-or-cli-is-the-wrong-question.md, id-ID: /in/blog/mcp-or-cli-is-the-wrong-question.md, hr-HR: /hr/blog/mcp-or-cli-is-the-wrong-question.md, hu-HU: /hu/blog/mcp-or-cli-is-the-wrong-question.md, it-IT: /it/blog/mcp-or-cli-is-the-wrong-question.md, ja-JP: /jp/blog/mcp-or-cli-is-the-wrong-question.md, ko-KR: /kr/blog/mcp-or-cli-is-the-wrong-question.md, ar-SA: /ar/blog/mcp-or-cli-is-the-wrong-question.md, nl-NL: /nl/blog/mcp-or-cli-is-the-wrong-question.md, no-NO: /no/blog/mcp-or-cli-is-the-wrong-question.md, pl-PL: /pl/blog/mcp-or-cli-is-the-wrong-question.md, pt-BR: /pt/blog/mcp-or-cli-is-the-wrong-question.md, ro-RO: /ro/blog/mcp-or-cli-is-the-wrong-question.md, ru-RU: /ru/blog/mcp-or-cli-is-the-wrong-question.md, sv-SE: /se/blog/mcp-or-cli-is-the-wrong-question.md, sl-SI: /sl/blog/mcp-or-cli-is-the-wrong-question.md, sr-Latn-RS: /sr/blog/mcp-or-cli-is-the-wrong-question.md, th-TH: /th/blog/mcp-or-cli-is-the-wrong-question.md, tr-TR: /tr/blog/mcp-or-cli-is-the-wrong-question.md, ur-PK: /ur/blog/mcp-or-cli-is-the-wrong-question.md, vi-VN: /vi/blog/mcp-or-cli-is-the-wrong-question.md, sk-SK: /sk/blog/mcp-or-cli-is-the-wrong-question.md}
---

[AI 智能体](https://tajo.io/zh-cn/blog/?category=ai-agents)

# 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，下文会明确说明这一点。

[了解更多](https://tajo.io/zh-cn/blog/mcp-or-cli-is-the-wrong-question/#%E4%BB%96%E8%AF%B4%E7%9A%84%E9%82%A3%E4%B8%AA%E5%B1%82%E9%9D%A2)

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

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

## 他说的那个层面

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

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

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

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

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

## 哪种状态，用于哪项工作

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

**游标。** 同步按窗口读取数据源，必须知道上一个窗口在哪里结束。在 Tajo 的同步引擎中，这是一个按同步和范围分别[存储的游标](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#cursor)，在每个没有失败记录的批次处理之后写入，下一次运行从那里继续。定时运行会在做任何其他事情之前先保存起点，所以即使首次运行失败，下一次运行也会从同一个位置读取。背后的原则是只发送增量、绝不发送整张表，详见 [反向 ETL 讲解](https://tajo.io/zh-cn/blog/what-is-reverse-etl/)。关于游标，我就讲到这里。

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

**租约。** 同步的一次运行在其整个生命周期内持有一个[独占租约](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#lease)，带有所有者令牌、默认十五分钟的时限以及续期机制。过期的所有者无法释放较新的租约。如果租约存储无法访问，运行会拒绝启动，而不是去猜测。哪个 worker 持有本次运行，是租约中的一个事实，而不在任何进程的内存里。

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

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

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

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

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

对集成工作来说，真正重要的结果是这样的。假设 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 上，从快照热启动，以及让脚本停在一个数小时后才有回应的宿主调用上。这些都是真实的用途。但同步仍然不应依赖它：快照只能加载到写入它的那个引擎构建中，对审阅者来说是不透明的，并且保存着脚本读取过的所有客户数据。

## 相关文章

### [答案正确，路径却错了](https://tajo.io/zh-cn/blog/the-answer-is-right-the-path-is-wrong/)

2026年10月8日 · 3 分钟阅读

3 个章节

### [每次 MCP 工具调用应该记录什么](https://tajo.io/zh-cn/blog/what-to-log-for-every-mcp-tool-call/)

2026年11月6日 · 7 分钟阅读

6 个章节

### [什么是 MCP 服务器？2026 年 MCP 与 MCP UI 如何运作](https://tajo.io/zh-cn/blog/what-is-an-mcp-server/)

2026年10月3日 · 12 分钟阅读

第 1 步 → 第 6 步
