1. 从“CLI-Anything”说起:为什么命令行正在被重新定义
第一次看到“CLI-Anything”这个说法,我脑子里蹦出来的不是某个具体工具,而是一种趋势判断:命令行界面正在从“人敲命令”变成“Agent 调命令”。过去我们讲 CLI,默认主语是人——人手敲ls、git commit、docker run。但现在越来越多的场景里,敲命令的主体变成了 AI Agent,人只负责给目标、做审核。这个转变听起来只是换了个操作者,实际上把 CLI 的设计约束整个掀翻了。
传统 CLI 是给人用的,所以讲究短、好记、有缩写、有交互提示、有彩色输出。Agent 不需要这些。Agent 需要的是:输出结构化、退出码语义明确、幂等可重试、无交互阻塞、错误信息可解析。你会发现这两套需求几乎是冲突的。一个为人类优化的 CLI,往往对 Agent 极不友好——比如它会弹一个Are you sure? (y/n),人类觉得贴心,Agent 直接卡死。
“CLI-Anything”这个标题,我理解它想表达的核心是:任何软件、任何服务、任何能力,都应该能被包装成一个 Agent 可调用的 CLI。这背后对应的是热词里反复出现的 CLI-Hub、Agent-Native、AI Agents 这几个概念。CLI-Hub 是分发层,Agent-Native 是设计理念,AI Agents 是消费方。三者串起来,就是一条完整的链路:把能力做成 CLI,注册到 Hub,让 Agent 按需调用。
这篇文章适合谁看?如果你是把 AI Agent 接进自己工作流的开发者,或者是想把自己写的工具暴露给 Agent 用的独立开发者,再或者你只是好奇“为什么最近大家都在聊 codex cli、claude cli 这类东西”,都能从下面找到能直接抄的东西。我会从设计思路讲到实操落地,包括参数怎么定、错误怎么设计、怎么避免 Agent 调用时踩坑,尽量把我知道的坑都摊开讲。
2. 整体设计思路:Agent-Native CLI 到底该怎么设计
2.1 人类 CLI 和 Agent CLI 的根本差异
先把差异摆清楚,不然后面所有设计都是空中楼阁。我做过一个对比表,是我自己在把几个内部工具改造成 Agent 可调用版本时总结的:
| 维度 | 人类 CLI | Agent-Native CLI |
|---|---|---|
| 输出格式 | 彩色、对齐、可读优先 | JSON/JSONL,字段稳定 |
| 交互 | 提示、确认、分页 | 零交互,全参数化 |
| 错误 | 自然语言描述 | 结构化错误码 + 可解析消息 |
| 幂等性 | 不强制 | 必须支持重复执行 |
| 退出码 | 0/1 为主 | 细分语义,便于分支判断 |
| 帮助信息 | 给人看的 | 给 Agent 看的 schema |
| 默认行为 | 尽量智能 | 尽量保守,显式优先 |
这张表里最关键的一行是“默认行为”。人类 CLI 喜欢猜你的意图,比如git push没设 upstream 会帮你设。Agent CLI 不能猜,因为 Agent 没有“常识兜底”,它只会按 schema 调用。你一旦猜错,Agent 会基于错误结果继续往下走,错误会级联放大。
2.2 为什么是 CLI,而不是 API 或 SDK
有人会问:既然要给 Agent 用,为什么不直接给 HTTP API?为什么要绕一层 CLI?这个问题我在实际项目里反复被问,我的答案有三点。
第一,CLI 是天然的进程隔离边界。Agent 调用一个 CLI,本质是 fork 一个子进程,权限、环境变量、工作目录都是隔离的。API 调用是网络请求,你得处理鉴权、限流、超时、重试,复杂度高一个量级。对于本地能力(文件操作、编译、格式转换),CLI 是最短的路径。
第二,CLI 的发现成本极低。Agent 只要知道命令名和--help,就能自己摸索出用法。你不需要维护一份 OpenAPI schema,--help本身就是 schema。这也是为什么 codex cli、claude cli 这类工具都选择 CLI 形态——它们本身就是 Agent 的入口。
第三,CLI 可组合。管道、重定向、xargs,这些几十年的基础设施直接复用。Agent 可以把一个 CLI 的输出喂给另一个 CLI,形成流水线。API 要做到这点,得自己写编排逻辑。
当然 CLI 也有代价:启动开销、跨平台差异、参数解析的边界情况。这些后面会讲怎么处理。
2.3 CLI-Hub 的定位:分发与发现
CLI-Hub 这个概念,我理解它是一个“命令的注册中心”。Agent 面对的问题是:我知道我要做什么,但我不知道有哪些命令可用。CLI-Hub 解决的就是发现问题。
一个合格的 CLI-Hub 至少要提供三样东西:命令清单(含描述和分类)、每个命令的 schema(参数、输出格式)、以及版本信息。Agent 拿到这些,才能决定调哪个、怎么调。这跟 npm、pip 这类包管理器的角色类似,但服务对象从人变成了 Agent。
我在设计内部 CLI-Hub 时,用的是最土的办法:一个 JSON 清单文件,每个命令一条记录,字段包括name、description、args_schema、output_format、examples。Agent 启动时读这个文件,就能建立自己的能力地图。不需要复杂的服务,一个静态文件就够。这也是我推荐的做法——先跑通,再优化。
3. 核心细节解析:参数、输出、错误三件套
3.1 参数设计:显式、扁平、可校验
Agent 调用 CLI 时,参数是通过命令行传的。这里有几个硬性要求。
第一,所有参数必须显式。不要依赖环境变量或配置文件里的隐式默认值。Agent 不知道你的配置文件在哪,也不知道里面写了什么。所有影响行为的输入,都要能从命令行参数推导出来。我见过一个工具,行为受~/.toolrc影响,Agent 调用时结果和预期完全不符,排查了半天才发现是配置文件的问题。
第二,参数尽量扁平。嵌套的 JSON 参数对 Agent 不友好,因为 Agent 生成嵌套结构容易出错。能用--key value就别用--config '{"a":{"b":1}}'。如果参数确实复杂,提供一个--config-file让 Agent 写文件再传路径,比直接传 JSON 字符串稳。
第三,参数要可校验。每个参数的类型、取值范围、是否必填,都要在--help里写清楚,并且运行时严格校验。Agent 传错参数时,你要返回明确的错误,告诉它哪个参数错了、期望什么格式。不要静默忽略错误参数,那会让 Agent 以为调用成功了。
举个我实际用的参数定义示例:
mytool convert \ --input /path/to/in.txt \ --output /path/to/out.json \ --format json \ --encoding utf-8 \ --strict每个参数都是扁平的、显式的、有明确取值的。Agent 生成这样的命令几乎没有歧义。
3.2 输出设计:结构化优先,人类可读为辅
输出是 Agent CLI 最容易翻车的地方。人类喜欢看彩色表格,Agent 需要的是稳定字段。我的做法是:默认输出 JSON,人类可读格式通过--pretty开启。这样 Agent 拿到的永远是结构化数据,人想看的时候加个参数就行。
JSON 输出有几个细节要注意。字段名要稳定,不能这次叫file_path下次叫path。字段类型要稳定,不能这次是字符串下次是数字。数组和对象的嵌套层级要浅,Agent 解析深嵌套容易出错。时间、路径这类字段,格式要统一,别一会儿 ISO 一会儿时间戳。
如果输出量很大,用 JSONL(每行一个 JSON 对象)。这样 Agent 可以流式处理,不用等全部输出完。日志类、记录类输出特别适合 JSONL。
提示:输出里不要混入任何非结构化内容。我见过工具在 JSON 前面打印一行 "Starting...",Agent 解析直接失败。所有日志走 stderr,stdout 只放结构化结果。
3.3 错误设计:退出码 + 结构化错误体
错误处理是 Agent CLI 和人类 CLI 差距最大的地方。人类看到 "Error: file not found" 就知道怎么回事。Agent 需要的是可编程判断的错误。
我的做法是双轨制:退出码给粗粒度分类,stderr 给细粒度结构化错误。退出码约定如下:
0:成功1:通用错误2:参数错误(Agent 可以修正参数重试)3:资源不存在(Agent 可以换路径)4:权限不足(Agent 不该重试)5:外部依赖失败(Agent 可以稍后重试)
stderr 里输出 JSON 格式的错误体:
{ "error": { "code": "FILE_NOT_FOUND", "message": "Input file does not exist", "path": "/path/to/in.txt", "retryable": false } }retryable字段特别重要。Agent 拿到这个字段,就知道该不该重试。没有这个字段,Agent 要么盲目重试(浪费资源),要么直接放弃(错失可恢复的错误)。
4. 实操过程:从零把一个工具改造成 Agent-Native CLI
4.1 第一步:梳理能力边界
改造之前,先想清楚这个工具到底提供哪些能力。我习惯列一个能力清单,每条能力对应一个子命令。比如一个文件处理工具,能力清单可能是:读取、转换、校验、统计。每个能力再拆参数。
这一步的关键是一个子命令只做一件事。不要设计mytool process --do-everything这种万能命令。Agent 需要的是原子能力,组合逻辑交给 Agent 自己编排。万能命令的参数会爆炸,Agent 也难理解。
4.2 第二步:定义 schema 并生成 help
能力清单确定后,为每个子命令定义参数 schema。我用的是 YAML 描述,然后写个脚本生成--help文本和校验逻辑。这样 schema 是单一事实来源,help 和校验不会脱节。
command: convert description: Convert input file to target format args: - name: input type: path required: true description: Path to input file - name: output type: path required: true description: Path to output file - name: format type: enum values: [json, yaml, csv] default: json生成 help 时,把 schema 渲染成人类可读的文本;校验时,用同一份 schema 做类型和取值检查。这样 Agent 读 help 和实际校验行为永远一致。
4.3 第三步:实现幂等与重试安全
幂等性是 Agent 场景的刚需。Agent 可能因为超时、网络抖动、自身逻辑重试等原因重复调用同一个命令。如果命令不幂等,重复调用会产生副作用。
实现幂等的常见手法:写操作前先检查目标状态,已达成则直接返回成功。比如convert命令,如果输出文件已存在且内容匹配,直接返回成功,不重复转换。删除类操作,删除不存在的资源返回成功而非报错。
对于确实无法幂等的操作(比如追加日志),提供一个--idempotency-key参数,Agent 传一个唯一 key,服务端记录已处理的 key,重复请求直接返回上次结果。
4.4 第四步:接入 CLI-Hub
工具改造完,注册到 CLI-Hub。我用的清单格式前面提过,这里给个完整示例:
{ "name": "mytool", "version": "1.2.0", "description": "File processing toolkit", "commands": [ { "name": "convert", "description": "Convert file format", "args_schema": "schemas/convert.json", "output_format": "json", "examples": [ "mytool convert --input a.txt --output a.json --format json" ] } ] }Agent 读这个清单,就知道mytool有哪些能力、怎么调、输出什么格式。examples 字段特别有用,Agent 可以照着例子生成调用。
4.5 第五步:实测与调优
改造完别急着上线,先让 Agent 跑一批真实任务。我一般准备 20 到 30 个典型任务,覆盖正常路径和各种边界。观察 Agent 调用时的行为:参数传错的比例、错误恢复的成功率、输出解析的失败率。
实测下来,最常见的三个问题是:Agent 不知道某个参数的存在(help 没写清楚)、Agent 传了非法值(校验太宽松)、Agent 解析输出失败(字段不稳定)。针对这三点逐个修,通常两三轮就能稳定。
5. 常见问题与排查技巧实录
5.1 Agent 调用卡住不动
这是最高频的问题。原因几乎都是 CLI 在等交互输入。Agent 不会回答(y/n),进程就挂在那里。排查方法:检查代码里所有input()、readline()、confirm()调用,全部改成参数控制。默认行为要保守,需要确认的操作通过--yes显式开启。
注意:有些第三方库会在内部弹交互,比如某些认证流程。这种要提前 mock 掉或者用非交互模式。
5.2 输出解析失败
Agent 解析输出失败,通常是输出里混了非结构化内容。排查步骤:先看 stdout 里有没有日志、进度条、警告信息。这些全部挪到 stderr。再看 JSON 是否合法,用jq验证一遍。最后看字段是否稳定,跑两次对比字段名和类型。
我踩过的一个坑:工具在输出 JSON 后打印了一行 "Done in 1.2s",Agent 解析时把这行也当 JSON 解析,直接报错。后来把所有计时信息挪到 stderr 才解决。
5.3 跨平台兼容问题
热词里出现了不少 Windows 相关的报错,比如node_modules\@opencode\cli\bin\opencode.exe 与你运行的 windows 版本不兼容。这类问题的根源是路径分隔符、可执行文件格式、环境变量差异。Agent 场景下,跨平台问题会被放大,因为 Agent 可能在不同平台上调用同一个命令。
我的做法是:路径统一用正斜杠,让运行时自己处理;可执行文件用脚本包装,屏蔽平台差异;环境变量读取集中在一处,方便排查。测试时至少在 Linux 和 Windows 上各跑一遍。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决 |
|---|---|---|---|
| 进程卡住 | 等待交互输入 | 检查 input/confirm | 改参数控制 |
| 解析失败 | 输出混入非结构化内容 | 检查 stdout | 日志挪 stderr |
| 重复执行副作用 | 不幂等 | 检查写操作 | 加状态检查 |
| 参数被忽略 | 校验太宽松 | 检查 schema | 严格校验 |
| 跨平台报错 | 路径/格式差异 | 检查平台相关代码 | 统一抽象 |
| 退出码无意义 | 未细分 | 检查 exit 调用 | 按语义分类 |
5.5 几个我踩过的坑
第一个坑:默认值陷阱。我给一个参数设了默认值,人类用着很舒服,但 Agent 不知道默认值的存在,以为必须传。后来我把所有默认值都写进 help,并且允许 Agent 显式传--param=default来覆盖。
第二个坑:错误信息太笼统。早期我的错误信息都是 "operation failed",Agent 拿到后完全不知道怎么办。后来改成结构化错误,带上code、retryable、hint字段,Agent 的恢复成功率明显提升。
第三个坑:版本漂移。CLI 升级后参数变了,但 CLI-Hub 里的 schema 没更新,Agent 按旧 schema 调用直接失败。后来我加了版本校验,CLI 启动时检查自己的版本和 Hub 里记录的是否一致,不一致就警告。
6. 工具选型与生态观察
6.1 为什么 codex cli、claude cli 这类工具值得研究
热词里 codex cli、claude cli、trae cli、zcode cli 反复出现,说明这类“Agent 入口型 CLI”正在成为标配。它们的共同特点是:把一个大模型能力包装成一个命令行工具,接受自然语言或结构化输入,输出结构化结果。研究它们的参数设计、输出格式、错误处理,能直接借鉴到自己的工具上。
我实测过几个,发现它们在输出结构化上做得都不错,但在幂等性和错误细分上还有提升空间。这也说明 Agent-Native CLI 这个领域还在早期,规范没定型,谁先做好谁有优势。
6.2 CLI-Hub 的几种实现路径
目前我见过的 CLI-Hub 实现有三类。第一类是静态清单,一个 JSON 文件,简单可靠,适合小规模。第二类是动态注册,CLI 启动时向 Hub 注册自己,适合频繁变更的场景。第三类是包管理器式,像 npm 一样有版本、依赖、发布流程,适合生态化。
我的建议是从静态清单起步。别一上来就搞动态注册,复杂度高,收益不明显。等命令数量超过几十个,再考虑升级。
6.3 Agent 调用 CLI 的编排模式
Agent 调用 CLI 不是单次调用,而是编排。常见模式有三种。串行编排:一个命令的输出喂给下一个。并行编排:多个独立命令同时跑,结果汇总。条件编排:根据前一个命令的退出码决定下一步。
支持这些编排,CLI 需要做到:输出可被下一个命令消费(结构化)、退出码可判断(语义化)、执行可重试(幂等)。这三点前面都讲过,是编排的基础。
7. 我个人的一些实践体会
把工具改造成 Agent-Native CLI,最大的感受是:约束比自由更重要。人类 CLI 可以有很多“智能”行为,Agent CLI 必须把每个行为都显式化。一开始会觉得啰嗦,但跑起来之后,稳定性提升是肉眼可见的。
另一个体会是:测试要面向 Agent,不是面向人。我现在的测试用例,很多是模拟 Agent 的调用方式——传各种边界参数、检查输出结构、验证退出码。这些测试人类不会写,但对 Agent 场景至关重要。
最后分享一个小技巧:给每个 CLI 加一个--self-check子命令,输出自己的版本、依赖状态、schema 摘要。Agent 在调用前先跑一次 self-check,能提前发现环境问题,避免调用到一半失败。这个命令实现成本很低,但排查问题时特别有用。
这个方向后续还能扩展的地方很多,比如把 CLI 的 schema 自动导出成 Agent 能直接用的 function calling 格式,或者做一个 CLI 调用的可观测性面板,记录每次调用的参数、耗时、结果。这些我都还在摸索,有进展再分享。