前言
虾神最早接触计算机是在1995年,那时候还是一个青葱水嫩初中生,虾老爹是老家一家职业中专的老师,那个学校开设了电算会计课程,所以有了全县第一个计算机机房。那年暑假,虾神第一次接触了计算机,在那个没有互联网,甚至还没有图形化界面的时代,黑屏上那个闪烁着的光标,代表了一种科技的神秘......

我到今天还一直记得,使用WPS之前,先要敲ucdos命令,加载中文环境,才能够正常显示......

之后,几年,陆陆续续开始接触了更多的计算机知识,从命令行到图形界面,从记忆wps的都快捷键,到鼠标点点点......一直到大学,选择了当时超级爆火的计算机专业......
从第一次接触计算机,到今天,三十多年过去了,没想到兜兜转转,又回来了命令行。
什么是CLI
CLI(Command Line Interface)是命令行界面的缩写,它是一种通过计算机的命令行界面来与计算机交互的方式。
从使用上看,CLI实际上并不符合人类的使用习惯,你需要记忆指令、参数、选项等,然后通过键盘去输入指令,然后等待计算机执行。
在windows上大家可能用得少(废话,windows的意思就是窗口,主打图形化界面),但是在linux的使用上,绝大部分都是使用的CLI。例如我们要在某个目录中,列出符合某个条件的文件,就可以使用ls命令。例如:
ls -l

还可以通过参数,进行筛选,例如我们就看4月份的日志文件:

但是GUI则不一样了:GUI本身就是是给人看的,本质上是一个信息翻译工具,把人类的习惯,翻译成计算机能够理解的指令。一般情况下,没有人需要去专门学习GUI的操作方式,它就是依照人类的习惯,来设计的。
例如我们要把文本的字体设置为黑体加粗:

所以对比一下就可以发现,AI更适用于使用CLI而非GUI。
AI的泥头车就这样轰隆隆的开过来了,把所有的非遗古法编程都碾在车轮下。
从functioncall,到MCP,到Skills,再到OpenClaw,我们对于AI的应用一直在从对话转向行动,特别是MCP的出现,一度让大家认为AI的底层操作模式已经打通了。
但是为什么仿佛一夜之间,又回到了CLI?
从大模型的诞生,本身就是建立在海量代码和数据基础上,而且人类对于大模型的研究和期望,一直都是输出-输出,而非中间的运算过程,所以CLI 本质上就是 AI 的母语。 CLI 具备文本输入、结构化输出、报错清晰、易于组合以及方便自动化等一系列得天独厚的优势。先别说Anthropic 这种起始就一直坚持用cli的厂商,近期飞书、钉钉、企业微信、 Google 、 Stripe 等巨头,都快速上线和开源路自己的 CLI 产品。
本来MCP是想做成AI大模型的"标准工具箱"的,目的很简单,就是让大模型能通过它和外界互动,把活儿干完。但真到实际用起来才发现,MCP有三个大毛病,根本绕不开,也正因为这样,开发者们都纷纷放弃它,转而去用CLI了,具体毛病如下:
1. MCP专烧Token没上限,CLI却是精打细算
MCP最致命的问题,就是特别占上下文,即烧Token烧得特别厉害。它得把所有工具的名字、参数格式,还有怎么调用的例子,事无巨细全都一股脑、死板地塞进上下文里(你如果不塞完整了,说不得大模型调用的时候就给你整个:"啥啥啥?你说啥?俺不中捏"),多增加一个工具,Token的消耗就会暴涨。

但CLI就不一样了,好的cli都出厂自带"自我说明"的功能。Agent不用一下子学会所有命令,什么时候不会用了,输个-h(也就是help命令),就能随时查用法、学操作。这种"用到哪学到哪"的方式,既能大大减少Token的消耗,还能保证调用的时候不出错。
经过实际测试,用官方的CLI,不管处理什么任务,Token消耗都比MCP少好几倍。
2. MCP是让人头疼的"黑盒子",CLI却能让人机配合得很顺畅
MCP更像是专门给Agent量身做的"私有协议",对咱们人类开发者来说,它就跟个黑盒子似的------整个运行过程都在Agent内部,一旦出了错,咱们人类想在自己的电脑上重现这个错误、找出问题在哪,难上加难,根本没法调试。
所以MCP干成一件事,就像上香许愿,先别说最终成不成,但凡哪个步骤有问题,你都没法排查......

再看CLI,它对Agent和人类都很友好。要是Agent用CLI的时候出了错,咱们人类只要把那行出错的命令复制下来,在自己的终端里运行一遍,就能马上知道错在哪,还能帮AI把问题修好。
3. MCP调用起来又麻烦又笨拙,CLI的管道符能轻松搞定复杂活儿
任何一个工作,如果不能够一步完成,就涉及到了多个工具的组合调用,这种情况下,MCP就比较麻烦了。
首先他要对于每个工具的输入要进行计算,然后每个输出都要进行解析。不停的序列化和反序列化,那是妥妥的烧token如加州大火......

但是CLI则不然,它有个绝活,就是管道符操作(也就是|这个符号)。简单的来说,就是它能把好几个命令拼起来,做成一条"流水线",上一个命令的输出,能直接当成下一个命令的输入,不但不用额外操作,而且在计算机内部还是实现的是零拷贝,几乎没有任何开销。

举个例子,比如我们要从dem中把等值线提取出来并且渲染成图,那么首先可以有一个命令是读取数据的,然后解析空间信息,之后提取等值线,最后渲染并且导出成图片,用CLI的话,就只要"一条命令",像接水管一样拼起来,就能轻松搞定这些复杂需求。 如下所示:
伪码示例
read_tif --input dem | contour --output contour.shp | renderer --output image.png
这一行一共调用了三个命令,分别是read_tif、contour、renderer。
-
read_tif:读取dem文件,输出空间信息。
-
contour:提取等值线,输出等值线文件。
-
renderer:渲染等值线文件,输出图片。
每一个命令,都有自己的输入和输出,但是它们之间的关系是"流水线",即上一个命令的输出,能直接当成下一个命令的输入,不用额外操作。
回过头来,既然讲到了CLI,那么有人可能会问:为什么不用API呢?API不也能直接实现么?而且两个API之间的交互甚至比CLI还要方便。
答案很简单,API的调用是要写代码的,既然要写代码,就涉及到编程环境、编译器、运行环境等一系列的东西。拿最简单的Python或者node.js这种语音语言,都要安装对应的环境,然后写代码,最后运行。
当然,有人说,我这安装也就是一次性的事情,这当然也不是不能接受,例如目前最火的openclaw,就是这样干的,替你把环境都配置好,AI直接生成代码就可以开跑了。
既然说到写代码......又带来新的问题......写代码不用Token么?
下图是著名AI公司ScaleKit的官方测试结果:
MCP对比CLI,token的消耗最高几乎可以多几十近百倍。

AI时代,软件的底层操作模式
软件业,归根到底是服务业,几十年以来,所有的软件都是为人设计的,所以从打孔纸带到编译器,然后是命令行,一直到GUI。
这是因为只有人才能使用软件,但是AI时代来了,未来越来越多的软件,第一使用者就变成了AI。
所以,软件的底层思维就变了,未来的软件,首先服务的对象是AI,准确来说,交互逻辑发生了改变。
软件还有没有必要存在?答案当然是有。因为AI要做好一件事,它也需要使用各种工具,只是这些个工具有可能AI自己去创造。
题外话:自举
就像新入行的同学,特别是学习C/C++或者Rust这种系统级语言的同学,特别喜欢问的一个问题:编程语言的编译器是哪里来的?(软件界的先有鸡还是先有蛋)
编程语言要有编译器才能编译成计算机能执行的,但是第一个编译器又是哪里来的呢?
或者说,喜欢争论某某语言是最好的语言的同学,怎么去接受自己语言的编译器是其他语言编写问题?
在计算机领域中,这种情况有一个专用术语,叫做"自举",即一个东西要自己去实现自己。例如C语言的cc编译器,或者CPP的clang/GCC编译器,或者 Rust 编译器 rustc 就是用该语言自身编写的,不再依赖其他语言的编译器。
当然,追根问底,第一个版本的编译器,肯定是用其他语言编写的。但是从第二个版本开始,就用自己写自己的编译器了。
工具的使用是人类与动物的区别
劳动资料的使用和创造,虽然就其萌芽状态来说已为某几种动物所固有,但是这毕竟是人类劳动过程独有的特征,所以富兰克林给人下的定义是「a toolmaking animal」,制造工具的动物。
------马克思《资本论》第一卷
从软件领域来看,AI能不能自己给自己造工具呢?当然没问题,但是并没有那么简单,起码目前的AI还做不到。特别是一些复杂的软件工具,如果完全要AI自己去做,还有不少的差距。
所以,软件还有没有必要存在?答案当然是有。因为AI要做一件事,它也需要使用各种工具,只是从未来发展来看,这些个工具有可能AI自己去创造。
正如我以前说的:利用AI进行地图制图,地图制图里面有很多个技术细节,例如边境线的实相交、label沿线标注、要素图斑里面填充符号的边界判断与角度......这些东西,都是在多年的发展中,一步步被总结和完善出来的规则。
这些细节,AI能做么?当然没问题,但是做得最好的永远是GIS软件,AI要做的,并不是全面提掉GIS的功能,而是把GIS工具里面的这些成熟的功能工具,都原子化和agent化,变成AI的手和脚。

当然,AI算力到了一定的程度,它也可以重写重构传统GIS里面的这些工具和功能,不冲突,正所谓:
编程语言有自己的上限,但是编程思想没有。
所以,下一代AI软件化的趋势,才刚刚开始。
而CLI,就是这个开局的一个定式。
如何使用和编写CLI
要使用和编写CLI,首先要了解CLI的基本概念,这样才能写出好用的cli工具来。所以我们先来搞明白两个问题:
一、针对GUI而言,CLI有哪些特点呢?利用AI总结了一下,主要有如下特点:
-
纯文本交互,无图形界面
-
只用键盘输入命令、参数,没有按钮、窗口、图标
-
输出也是文本:日志、结果、报错、表格等
-
资源占用极低,适合服务器、无显示器环境
-
-
执行效率高、速度快
-
启动快、运行快,没有 GUI 渲染开销
-
适合批量、自动化、高性能任务(搜索、压缩、编译、数据处理)
-
-
易于自动化 & 脚本化
-
可以写脚本批量执行(bash、python、rust 等)
-
支持管道、重定向:a | b | c、> log.txt
-
天然适合 CI/CD、定时任务、批量运维
-
-
功能强大、操作精准
-
一条命令可以完成复杂操作,参数控制粒度极细
-
适合开发者、运维、工程师做专业操作
-
可远程通过 SSH 直接使用,GUI 通常做不到
-
-
跨平台、一致性强
-
Windows/Linux/macOS 都能运行 CLI 工具
-
行为相对统一,不像 GUI 每个系统差异巨大
-
易于打包成单文件分发,开箱即用
-
-
学习成本高,记忆门槛大
-
要记命令、参数、语法,新手不友好
-
没有提示引导,全靠文档和经验
-
出错时只返回文本错误,不如 GUI 直观
-
-
资源占用极低
-
几乎不占内存、CPU、显卡
-
低配机器、服务器、嵌入式设备都能流畅跑
-
二、为什么我会推荐要用Rust编写CLI?
(因为我不会C/CPP)
主流的cli都是使用C语言编写的,例如linux的bash、macOS的zsh、windows 的cmd,都是用C写的。还有像git这种使用特别广泛的码农工具,也是C语言写的。
之后一些新一代的cli工具,开始改用其他的一些语言,例如windows的powershell是用C#写的,还有ffmpeg、早期版本的yarn,是用CPP写的。
但是这几年,随着Rust语言的兴起,越来越多的cli工具开始用Rust编写。甚至以前的一些特别知名的软件,也开始用Rust重写,例如:
-
ffmpReg :用Rust重写ffmpeg。
-
uv : u是由 Astral 团队开发的新一代Python 包管理与环境管理工具,采用 Rust 编写,提供比 pip 快 10-100 倍的依赖解析和安装速度,可替代 pip、pip-tools、virtualenv、poetry 等传统工具。
-
deno : 安全的 Node.js 替代运行时,由Node.js 作者创建,采用 Rust 编写,提供安全、快速、跨平台的 Node.js 运行环境。
-
ruff : Python 代码检查 / 格式化工具,采用 Rust 编写,提供快速、准确的代码检查和格式化功能。
还有大量的linux命令,都在用Rust重写,例如:
-
fd:替代:find的简单好用的文件查找工具
-
bat:替代cat的,是一个带语法高亮、Git 集成的 cat
-
exa / eza:替代:ls的,是一个现代化 ls 工具,支持彩色、图标、树形展示、Git 状态标记 等等等等
为什么大家都开始喜欢用Rust编写CLI了?官方给出来如下理由:
-
性能与体验双优 Rust 编译为原生机器码,无 GC 开销,启动速度与运行效率接近 C/C++,远胜解释型语言。常见替代工具如 ripgrep(搜索)、fd(文件查找)、bat(带语法高亮的 cat)均由 Rust 编写,性能提升显著。
-
零内存安全隐患 Rust的借用检查器在编译期阻断野指针、缓冲区溢出等问题,无需垃圾回收即可保证内存安全,让 CLI 工具稳定不崩溃。
-
分发与部署极简单 二进制文件无依赖,跨平台分发轻松;用户无需配置环境,下载即可用,尤其适合 CI/CD、服务器自动化场景。
-
可靠的跨平台与错误处理 统一处理跨平台配置、路径与日志,Result/Option 类型让错误提前暴露,友好的编译器提示降低维护成本。
-
成熟生态加速开发
-
参数解析:clap 支持子命令、自动帮助文档、参数验证,是 Rust CLI 事实标准。
-
错误处理:thiserror+anyhow 统一错误类型与信息,调试更高效。
-
交互体验:indicatif(进度条)、dialoguer(交互式输入)提升用户体验。
-
工程化:cargo 一站式管理依赖、测试、构建,支持交叉编译,适配多平台。
-
不过对于我来说,最重要的是以下三点:
-
分发与部署极简单,Rust可以写出无依赖独立二进制文件,用户无需配置环境,下载即可用。
-
成熟的生态,Rust提供了丰富的库和工具,可以快速开发出功能强大的cli工具。
-
Rust的错误处理机制,要求你在写代码的时候,就需要考虑到各种可能的错误情况,而不是在运行时才去处理。这样不管是开发的时候,还是AI在调用的时候,都能够有很清晰的反馈。
下面我们给出一个简单的例子,来看看怎么用Rust写一个cli工具。我们写一个读取shapefile文件,并且显示统计数据和进行简单可视化的cli工具。
cli的命令行,主要通过输入参数来进行交互,在Rust里面,可以用过clap包来进行处理:
// 定义命令行参数结构
#[derive(Debug, Parser)]
#[command(author, version, about)]
structCli {
/// 输入的 .shp 文件路径
input: PathBuf,
/// 输出图片路径 (默认: output.png)
#[arg(short, long, default_value = "output.png")]
output: PathBuf,
/// 输出统计数据 JSON 文件
#[arg(short, long)]
stats: Option<PathBuf>,
/// 图片宽度
#[arg(long, default_value = "1200")]
width: i64,
/// 图片高度
#[arg(long, default_value = "800")]
height: i64,
}
这里面定义了输入路径、输出路径、统计数据路径、图片宽度、图片高度。其中输入shapefile的路径是必填项,其他路径为可选,但是预留了默认值。
定义统计信息:
// 定义统计信息结构
#[derive(Debug)]
structShpStats {
file_name: String,
feature_count: usize,
shape_type: String,
bounds: (f64, f64, f64, f64),
fields: Vec<String>,
}
然后是编写主函数,来处理命令行参数和业务逻辑:
fn main() -> Result<()> {
// 解析命令行参数
let cli = Cli::parse();
// 读取 shp
letmut reader = Reader::from_path(&cli.input)?;
letmut count = 0;
letmut fields = Vec::new();
letmut shp = Vec::new();
for shape_record_result in reader.iter_shapes_and_records() {
match shape_record_result {
Ok((shape, record)) => {
count += 1;
if count <= 1 {
for (name, _value) in record {
fields.push(name);
}
}
shp.push(shape);
}
Err(e) => {}
}
}
// 范围
let head = reader.header();
let bbox = head.bbox;
let stats = ShpStats {
file_name: cli.input.file_name().unwrap().to_string_lossy().into(),
feature_count: shp.len(),
shape_type: format!("{:?}", head.shape_type),
bounds: (bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y),
fields,
};
println!("✅ 要素数量: {}", stats.feature_count);
println!("✅ 类型: {}", stats.shape_type);
println!("✅ 范围: {:?}", stats.bounds);
println!("✅ 字段: {:?}", stats.fields);
// 输出统计
ifletSome(path) = &cli.stats {
let json = serde_json::to_string_pretty(&stats)?;
std::fs::write(path, json)?;
}
// 绘制图片
render(
&shp,
bbox.min.x,
bbox.min.y,
bbox.max.x,
bbox.max.y,
cli.width,
cli.height,
&cli.output,
)?;
println!("🎉 完成:{}", cli.output.display());
Ok(())
}
// 绘制地图
fnrender(
shapes: &Vec<Shape>, xmin: f64, ymin: f64,xmax: f64,
ymax: f64, w: i64, h: i64, output: &PathBuf,
) -> Result<()> {
// 详细实现见源码
}
执行结果如下:
先看点数据:

可视化效果:

这里渲染的时候我直接绘制了一个5*5像素的矩形,来表示每个POI点的位置,看起来效果不太明显。
在看看面要素:


面要素我仅绘制出来面的边线,没有做填充,如果大家有兴趣,后面有时间我们再专门讲一下绘制填充的实现。
这里都采用的默认参数,我们也可以使用我们自己定义的参数来实现,而clap包已经帮我们提供帮助信息:

例如我们现在要输出json格式的统计数据文件,我们可以这样写:

其他参数也是同理。
总结:
-
AI时代,对CLI以及相应的形式,需求会越来越多,与几十年来的GUI相对应,会成为新的趋势。
-
Rust 提供了编写CLI的全套框架,非常适合CLI的开发。特别是clap包这一类的库,可以用来处理命令行参数,非常方便,可以快速地定义命令行参数,并且可以自动解析参数。
后面我们会通过几篇完整,带代价完整的走一遍CLI编写和AI的交互,有兴趣的同学可以关注一下。