Skill + CLI Considered Harmful:MCP vs Skills vs CLI
标题是在致敬Dijkstra 1968年的那篇《Go To Statement Considered Harmful》,我想说的事情也差不多。现在很多平台都用Skill加CLI的方式给Agent做集成,这个方式用起来很顺手,但是平台方迟早要为它付出代价。它最大的问题是版本控制,而这个问题,软件行业过去20年已经用web解决过一次了。
Skill + CLI 为什么会流行
自从openclaw火了之后,越来越多的平台开始提供skill 加上 cli的组合,来让用户的agent可以访问用户的平台资源。agent使用cli有着天然的优势:过去有足够的人类Posix训练语料让agent可以使用cli;cli本身的设计就自带描述和合适的错误信息;最后就是unix哲学里的“文本流大于二进制”,文本流不仅是人和agent直接可读的,而且可以随意通过管道和其他的cli组合达到处理数据的目的。用openclaw之父 Peter Steinberger的话来说:“当agent可以使用bash的时候,一切都结束了”。
Skill里面则提供对具体调用方式,流程,错误码的描述,作为一个cli的上下文补充,来帮助agent知道:1 什么时候调用这个cli,2 如何调用这个cli,3 如何把一系列的操作串起来。
顺便说一下,其实Agent + Skill + CLI直接解决的问题就是企业集成的问题(或者说,低代码平台)。企业集成流程的三大要素就是:Workflow, Connectors, and Data Transformation。前两者其实都不算是难事,现在分别对应Skill和CLI。而第三个部分是最麻烦的:如何用自然语言表示Data Transformation?不过这个问题被LLM解决了。这个话题是个大坑,我们可以以后再聊。
最大的问题:版本控制
这种使用方式是可用的:相比较最早的MCP模式,或者是直接让agent调用HTTP API,肯定是会更准确,而且流程控制也更好;相比较让agent直接browser use或者computer use来说,又减少了很多额外的token消耗,速度更快,而且也不要求模型有多模态能力。但是这个模式最大的问题就是版本控制:我的电脑上的lark cli已经很久没有升级了,更不要说我的lark的skill版本。要求普通的用户升级skill和cli的版本就更加不可能了。
这是一个经典的分布式问题:我们不能假设系统内的所有的节点都在一个统一的和谐的版本上。尤其是企业平台,我们必须尽可能保证用户的体验不会因为服务端的一次升级而直接坏掉。如果是cli的版本和服务端不兼容,比如破坏性的schema变更,可能问题还好一点。这种情况服务端往往会有比较明显的错误信息。如果是企业集成的workflow流程变化了,比如过去需要一次审批,现在不需要了,那么这种问题就是一个隐性的问题:难以察觉,甚至可能会诱导agent作出错误的判断。
开历史的倒车
为什么过去20年,web端发展这么快?核心原因就是这种开发模式的分发最为简单,维护成本最低:你不需要担心用户的本地版本是否兼容。服务端把绝大部分的控制权和不兼容的可能性都收敛到自己这边,留给客户端的是一层非常薄的webapp,以及他们自己的浏览器。而浏览器相对稳定,不太经常发生破坏性的变化(当然也有要支持IE浏览器这种骇人听闻的需求)。
经历过桌面客户端时代的人应该还记得之前是什么样子:企业软件要一台一台机器装客户端,每次服务端升级,IT就得挨个通知用户跟着升级,总有那么几台机器永远停留在三年前的版本上,然后在某个周一的早上准时报错。后来SaaS把这些装在本地的企业软件一个一个干掉,很大程度上就是因为用户刷新一下页面,拿到的就是最新版本,“版本”这个问题对用户来说直接消失了。
再回头看Skill加CLI:CLI就是那个装在用户本地的胖客户端,Skill则是随客户端附送的一本操作手册。而且这个组合比当年的桌面软件还要糟糕。桌面软件后来好歹都学会了自动更新,移动端的App有应用商店推送,实在不行还能强制升级,不升级不让用。CLI呢?要用户自己想起来去跑一次升级命令。Skill就更惨了,大多数时候它就是躺在某个目录里的几个markdown文件,很多连版本号都没有,更不用说升级机制。服务端的流程改了,这本手册不会有任何变化,agent还会继续认认真真地照着一本过期的手册办事。
我们花了20年,才把控制权和兼容性的问题从用户的电脑上收回到服务端。现在采用Skill加CLI的方式对接各种平台,等于是又把它们原样发了回去,显然是在开历史的倒车。
历史早就给过答案
我这里提出的idea并不新鲜,这个是一个历史经验和逻辑思考的结果:Jim Waldo 1999年在论文《The Jini Architecture for Network-Centric Computing》中明确指出,大型网络没办法要求所有节点同步升级(甚至没办法保证某些节点会做任何升级,笑)。Roy Fielding 2008年在“REST APIs Must Be Hypertext-Driven”里面指出:REST API除了入口和标准media type之外,应用层的状态应该由服务端提供选择来进行驱动,这样才能避免服务端的workflow知识泄漏到客户端,形成抽象泄漏。这些东西都表明,平台方提供一个非常厚重的CLI是个错误的选择,提供一个非常厚的Skill层是一个自杀性的选择。
最恐怖的是什么呢?是Google的Hyrum’s Law:只要API的用户足够多,所有可观察的行为(无论是否是bug或者是typo)最终都会被人依赖。这一点意味着平台方放出去的0.0.1版本的CLI,在服务端迭代1年之后,很可能还得纳入向后兼容的考虑范畴。不相信这一点可以看看HTTP的Referer header,然后查一下这个referer到底应该怎么拼写。
解法:薄客户端 + 服务自描述
解法在我看来从根本上来讲应该是这样的:Agent侧,也就是客户端,维护一个薄的,无语义的,通用的模块;所有的服务端schema描述,错误码,方法描述都应该做成类似“服务反射”的服务自描述;服务端还需要提供描述流程和额外知识的能力。更重要的是,这些能力都需要设计成渐进式加载的方式,避免犯MCP早期的错误。
具体的解决方案在我看来有这么两种。
- 现在的MCP server可以动态提供capability,包括向客户端发送server变化的信号。这些可以解决服务端和客户端的Schema Drift的问题。MCP现在也支持上了Skill Extension这个试验性功能,这种对接平台,然后永远都以平台上挂的Skill为可信源的方式,应该逐渐成为主流。
- “复活”超媒体驱动(HATEOAS)的API,服务端不仅要自己提供渐进式加载的schema进行服务自描述,还需要提供接口层面的hint来提示客户端接下来可以做的事情,以及具体原因。如果采用这种方式,agent的客户端不需要再安装skills,也不需要注册MCP,只需要授权Agent直接做HTTP API调用即可,不过这里的安全问题还是需要考量。
写在最后
如果你不相信我的话,你可以让你的agent看一下你本地的面向平台的skills和cli,你会惊讶于它们的数量,以及版本偏移有多么的严重。平台方继续提供这种Skill和CLI的组合,只会给他们的维护继续埋雷。