最近技术圈里有个很有意思的动向,不是“MCP 好不好用”的争论,而是越来越多做 Agent 基建的团队开始往回走。前两年 Model Context Protocol 火起来的时候,几乎所有人都觉得这是 AI 连接外部工具的标准答案。可到了落地阶段,一批头部厂商的工程团队反而把内部工具链从 MCP 迁回了 CLI,有些甚至直接定了一条规矩:新工具一律 CLI-first,MCP 只作为兼容层保留。这篇文章想聊的就是这件事——为什么会出现这种“倒退”,以及我们自己做技术选型时,到底该怎么权衡。
这不是说 MCP 不行,而是它被放到了不合适的位置上。我见过不少团队,花了两三周接 MCP Server,结果调试工具调用时被 JSON-RPC 的报错绕得晕头转向。换成 CLI 之后,问题突然简单了:命令就是命令,输出就是文本,模型能跑通的东西人也能跑通。这篇文章会拆解 MCP 在真实工程环境里的隐性成本,分析 CLI 回归背后的朴素逻辑,再给出一套具体的迁移路径和适用性判断标准。无论你是正在做 Agent 工具层选型,还是单纯想理解这两条技术路线差异的开发者,应该都能在这里找到可以参考的答案。
1. MCP 当初解决的真问题,以及它被捧高之后暴露的麻烦
1.1 MCP 的核心价值:连接与标准化
先客观说几句 MCP 的好话。它刚出来的时候,解决的是一个非常真实的集成痛点:每个模型厂商都要对接不同的数据源和工具,每个工具提供商也要为不同的模型适配接口,N 个模型乘以 M 个工具的复杂度完全是指数级的。MCP 的思路是把这层集成标准化,模型侧通过 MCP Client 去发现和调用工具,工具侧只要实现 MCP Server 的协议,就能被所有兼容的模型使用。
这套设计解决了“接口爆炸”的问题,所以它迅速铺开是有道理的。尤其在第三方生态互操作场景里,比如让模型访问本地文件、数据库、设计软件,MCP 的价值非常明显。一个软件只要实现了 MCP Server,就能直接接入主流模型客户端,不需要为每家模型单独写适配器。这是它真正成功的地方,也是它在某些场景里依然不可替代的原因。
1.2 藏在“标准化”背后的协议成本
问题出在“标准化”这三个字被理解成了“银弹”。MCP 本质上是一个基于 JSON-RPC 2.0 的远程调用协议,完整的调用链路是这样的:模型生成意图 → 编排器解析 → MCP Client 发起请求 → MCP Server 接收 → 工具执行 → 结果回传。每一步都有序列化和协议开销,而 Python 和 JavaScript 的 SDK 虽然屏蔽了很多细节,却也让开发者很难直观感知这层开销有多大。
实际走一遍就清楚了。MCP Server 启动时要做初始化握手,然后客户端要拉取完整的工具列表。如果你接的是一个有几十个工具的内部系统,tools/list 返回的 JSON 可能有几百 KB。这对本地开发还能忍,部署到远端走 HTTP transport 之后,每次会话建立都要付出一次不小的握手成本,线上故障排查时更是痛苦。
我记得有个团队接过一个开源项目管理系统,注册了大概 80 个工具。结果跑起来发现,光是把这些工具的描述、参数 schema、示例塞给模型,就占掉了两三万 token。现在的模型上下文窗口虽然大了,但两三万 token 意味着模型在做选择时的“注意带宽”被稀释了。更麻烦的是,很多工具描述本身就写得含糊,模型经常选错工具或者填错参数。后来他们把调用方式改成封装好的 CLI 命令,把工具描述精简成几行命令帮助文本,上下文的压力一下就下来了。
1.3 上下文吞噬与工具“幻觉”
这里其实藏着一个反直觉的点:给模型更多工具,不等于让它更强。心理学上有个“选择的悖论”,放到模型工具调用上一样成立。当可调用工具从十几个变成上百个,模型需要从巨大的候选集合里挑一个最合适的,这个挑选动作本身就会消耗推理资源,也会引入更多随机性。实际表现就是:工具少的时候,模型几乎每次都能选对;工具多了,它反而开始出现张冠李戴式的调用错误。
MCP 的自动发现机制加剧了这个问题。因为协议鼓励“全量暴露、按需调用”,Server 为了不影响模型发挥,倾向于一股脑把所有工具都注册上去。而 CLI 天然是“按需探查”的:模型可以看命令列表,不够就看 --help,再不够就看文档。它把上下文的使用变成了梯度加载,而不是一次全量加载。这个差别,在长链路、多轮工具调用的场景里被放得特别大。
2. CLI 的回归:不是复古,是对确定性的重新追求
2.1 确定性才是 Agent 工具层的第一需求
为什么大家转了一圈又回到了 CLI?说白了,Agent 工具层最需要的不是“灵活”,而是“确定”。CLI 有一个 MCP 几乎无法比拟的优势:它的输入输出是可预测的。命令跑完,要么有 stdout,要么有 stderr,要么有非零退出码。人能用这套规则判断成功失败,脚本能用这套规则做流程控制,模型同样也能。
MCP 给你的是“一堆可调用的函数”,而 CLI 给你的是“一条可执行的命令”。函数可以干任何事,但它的边界、输出格式、错误语义都藏在文档里,模型只能靠 tool description 去猜。命令不一样,命令的参数必须显式声明,输出被强大的 Unix 约定固定成了文本流。有一种“跑一下就知道”的确定性。对于要在生产环境里稳定跑的 Agent,这种确定性比花哨的协议重要得多。
另一个确定性的体现是审计。内部工具一旦出了事故,你总得回答“刚才系统到底执行了什么”。CLI 可以靠 shell 历史、服务端日志、命令包装器把完整调用记录下来。MCP 的调用信息散落在 client 和 server 两边,每次工具调用的参数和结果需要专门埋点才能串起来,审计成本高不少。我见过某大型团队出过一次线上事故,排查时发现 MCP Server 的日志里根本没有记录具体参数,只留了一个工具名和耗时,真实参数全得靠模型侧回放才知道。这种黑盒感在 CLI 里几乎不存在。
2.2 管道式思维:让工具协作回归 Unix 哲学
CLI 大潮回归的另一个底气,是它天然拥抱 Unix 哲学:每个命令只做一件事,做好它,然后用管道把命令组合起来。这种组合能力在 Agent 场景里意味着什么?意味着你可以让模型先跑一个查询命令,把结果缓存成文件,再让下一个命令基于文件处理,最后用一个脚本统一校验输出。每一步都可以人为介入,每一步都可以单独调试。
对比之下,MCP 的调用是会话式的。Agent 发起一个工具调用,Server 返回结果,整个过程像一个黑盒管道,中间你很难插进去“看一眼”。如果某个中间环节出了问题,你得翻日志、打断点、重新发起整条链路。而 CLI 的中间产物随时可见,随时可验证,出问题时定位路径短得多。
举个例子。我以前帮一个团队做过日志分析的 Agent,一开始用 MCP 定义了一个“分析日志”工具。每次模型分析,都是 Server 内部把日志读进来、做过滤、统计、返回结果。听起来没什么问题,但用户一旦怀疑统计数据,整个链路只能从头跑。后来我们把逻辑拆成了三个命令:log_fetch、log_filter、log_stat,每个命令只做一件事,模型拿着管道符号自己组合。结果模型反而跑得更顺,统计出问题的时候,只要把中间文件翻出来看一眼就知道哪一步错了。这个体验一旦习惯,很难回去。
2.3 上下文省俭:CLI 让 token 花在刀刃上
前文已经提到,MCP 倾向全量暴露工具描述。CLI 的按需加载策略,意味着模型只需要为“当前任务真正需要的命令”付 token。一开始给模型看一个 10 行的命令清单,它对某个命令不确定时,再让它执行 command --help 获取详细说明。这种渐进式探索跟人用命令行一模一样,非常省 token,也让模型把注意力集中在真正要处理的数据上。
这里说个实测数据。我们把一个内部工单系统的工具接口从 MCP 迁到 CLI 包装之后,单次多轮对话的平均 token 消耗降了差不多 40%。主要是因为原来每次启动都要加载全部工具 schema,现在只需要加载命令清单,执行时再按需补充帮助信息。省下来的 token,模型都花在了实际的数据和判断上,回答质量反而上升了。这不是什么高深的技术,就是单纯地少让模型做无用功。
3. 从 MCP 迁移到 CLI 的实操观察与对比
3.1 一个真实场景的对比:让 AI 操作内部系统
拿一个很常见的场景来对比:给 Agent 接一个内部运维工单系统,让它能查服务状态、看日志、执行诊断命令。用 MCP 实现,标准流程是这样:
- 写一个 MCP Server,用 SDK 定义工具;
- 每个工具要写 description、inputSchema、处理函数;
- 处理函数里写业务逻辑,比如连接运维 API、解析返回、格式化输出。
Server 跑起来之后,还要配置 transport 和权限,然后才能接入 Agent 客户端去测试调用。工具多一点的话,每个工具的描述怎么写、参数怎么设计,都是心智负担。
用 CLI 实现,简单直接:在系统里装一个 ops 命令,内部用脚本封装现有的 ssh、systemctl、journalctl、curl 这些工具。模型需要做什么操作时,直接通过 shell 执行 ops 相关命令,解析 stdout 就完事。
# 一个简单的 ops 命令封装示例 ops status service-name ops logs service-name --tail 50 ops diagnostic --check memory --check disk每条命令对应一个明确的脚本函数,内部再调用系统工具,输出统一格式化为纯文本。模型不需要理解你的内部 API 结构,只需要知道三条命令的名字和参数就够了。
3.2 从 MCP 迁到 CLI 通常怎么进行
迁移并不是把 MCP 一脚踢开,通常有三种渐进式做法,我按推荐程度排列:
第一,新工具一律 CLI-first。内部新增工具时,先实现命令行接口,任何业务逻辑都能被命令唯一表达。需要给 Agent 用时,直接让模型走 shell,不额外套协议。
第二,旧 MCP Server 缩成“薄适配层”。保留 MCP Server 作为兼容协议入口,但里面的工具实现不再写业务逻辑,改成调用新的 CLI 命令。这样模型客户端那边不用改,接口行为却变得可预测了,调试也简单很多。
// 伪代码:MCP 工具变成 CLI 的薄壳 async function handleCall(request) { const { incident_id, status } = request.params.arguments; const { stdout } = await exec(`incident update ${incident_id} --status ${status}`); return { content: [{ type: "text", text: stdout }] }; }第三,Agent 编排层加“CLI 优先”约束。在系统提示里明确:凡是内部操作,优先使用 shell 命令,只有跨平台集成场景才走 MCP 协议。让模型自己形成“先试命令、再试协议”的调用习惯。
迁移时最需要处理的是历史工具的参数兼容性。MCP 工具的参数通常是命名参数,CLI 则习惯用位置参数加选项。建议给 CLI 加一个 --json 输出选项,方便机器解析,也方便对接旧逻辑。同时给常用场景做几个“套餐命令”,避免模型在组合命令时天马行空。
3.3 适用性判断:什么项目该留在 MCP,什么该转向 CLI
先说结论:两者不是替代关系,而是分层关系。MCP 适合做协议层,CLI 适合做实现层。下面这张表是我在实际咨询里总结出来的判断维度。
| 维度 | 适合 MCP | 适合 CLI |
|---|---|---|
| 接入对象 | 第三方生态、跨模型互操作 | 内部工具、自有系统 |
| 工具数量 | 少而稳定(10 个以内) | 任意,推荐按命令组织 |
| 调试要求 | 可接受黑盒调用 | 需要逐步可验证 |
| 输出格式 | JSON schema 约定 | stdout/文件/退出码 |
| 权限模型 | 模型侧授权,粒度粗 | 系统权限可控,粒度细 |
| 上下文开销 | 全量暴露,开销大 | 按需加载,省 token |
| 审计成本 | 需额外埋点 | 天然日志 |
具体来说,如果你做的是面向外部开发者的开放平台,想让别人的模型也能调用你的服务,那 MCP Server 值得做,因为它是生态标准。如果你是给自己内部的 Agent 接十几个系统,那先写 CLI 再考虑套协议,基本不会错。
我见过一个大企业内部搞了个平台,所有工具都实现了 MCP Server,结果真正高频调用的就那么五六个,剩下的全是上线之后再没人碰的僵尸工具。后来他们把高频工具改成 CLI 命令,MCP Server 只做转发,维护成本一下子降了非常多。这个案例很能说明问题:协议解决的是“能不能接”的问题,CLI 解决的是“好不好用”的问题。
4. 常见问题与避坑实录
4.1 MCP 报错难排查:问题出在链路太长
跟 MCP 打交道,最常见的挫败感来源就是排错。MCP Server 本身跑得好好的,但接到 Agent 上就是调不通。报错信息经常是通用的 JSON-RPC 错误,你根本不知道是握手失败、参数校验失败、还是工具内部抛了异常。我踩过的坑包括:
- stdio transport 的 Server 进程因为工作目录不对,启动即崩溃,客户端还一直显示“waiting for init”;
- tools/list 返回超时,原因是工具描述里有序列化不了的对象;
- 某些 SDK 版本的参数命名不一致,同一个参数在工具定义里叫 id,在调用里传成 incidentId,直接校验失败。
排查这些问题的思路,我建议按这个顺序来:先在独立的终端里手动启动 Server,看能不能正常完成初始化;再用调试器或协yi客户端直接发 tools/list 和 tools/call,绕过 Agent 那层;最后在 Agent 侧打开完整日志,看请求和响应的原文。如果你没做过,别跳步骤,链路越短越容易定位。
CLI 方式就省心多了。命令可以在终端直接跑,输出就在眼前。模型调用出错时,把 stderr 拿出来看,基本上看一眼就知道是命令拼错了还是权限不够。这种直接反馈本身就是一种高效的排错机制。
4.2 CLI 方式的安全边界如何建立
很多人不敢把 CLI 暴露给模型,怕模型乱用命令造成事故。这个担心是合理的,但解法不是退回 MCP。你完全可以用一层薄薄的包装器来约束模型能做什么。
建议至少做好这四件事:命令白名单、超时控制、工作目录隔离、日志审计。白名单确保模型只能调用预设命令,超时防止模型在某个命令上卡死,工作目录隔离防止它读写了不该碰的文件,日志审计保证事后可追溯。
#!/bin/bash # 一个带白名单和超时的 shell wrapper 示例 ALLOWED=("ops" "status" "log_check") cmd="$1"; shift if [[ ! " ${ALLOWED[@]} " =~ " ${cmd} " ]]; then echo "command not allowed: $cmd" >&2 exit 1 fi timeout 30 "$cmd" "$@" 2>&1 | tee -a /var/log/agent-cmd.log exit ${PIPESTATUS[0]}这套架构下,模型本质上还是一个受控的 Shell 用户,但权限颗粒度比 MCP 细得多。MCP 的授权是“工具级”的,一个工具内部做什么完全取决于实现;CLI 的授权是“命令级”的,白名单本身就是最小的权限单位。对于需要严格合规的场景,CLI 反而更容易证明“模型不可能做白名单之外的事”。
4.3 混合架构是否可行:以谁为准才是关键
最后一个大家常问的问题:能不能两者都要?答案是能,但必须有一个主心骨。我的建议是:以 CLI 为工具事实标准,MCP 只做协议适配层。所有工具先有 CLI 实现,MCP 只是把它包一层暴露给外部生态。这样,内部效率、稳定性、审计都按最优路径走,外部兼容性也保留住了。
有朋友问过:那我是不是可以把 Agent 接到一个既有 MCP 又有 CLI 的系统里,让它自己选?我个人不太建议让模型自由选择,因为模型会倾向于走那条描述更详细的路径,往往是 MCP,而这恰恰是你想让它避开的黑盒路径。与其让模型选,不如在编排层直接固定好调用方式,内部业务走 CLI,外部生态才走 MCP。决策放在代码里,不要留给模型临场发挥。
如果你真的决定要迁,也可以从一个小范围开始。挑一个最常用、最早熟的场景,先把它的 MCP 工具改成 CLI 封装,跑通一个完整闭环,再做推广。别想着一次把几十个工具全部迁完,那会导致过渡期两头各写一套逻辑,维护成本翻倍。
我个人实际做下来,最有用的一个判断标准是:当一个工具被调用出错时,你希望多久能定位到问题。如果你想的是“看一眼命令输出就知道”,那这工具就应该走 CLI;如果你想的是“调协yi日志、复现请求、开 Debug”,那它留在 MCP 里也不冤。技术选型没有绝对的对错,只有是否匹配你团队的调试能力和容错偏好。最后再分享一个小经验:很多团队说“MCP 是未来”,但未来一定不是一套协议吃掉所有东西。把工具做成好用的命令,协议做成兼容的皮,这才是兼顾当下和长远的路子。