
1. 为什么“CLI-Anything”值得单独拿出来聊命令行工具这几年经历了一轮很明显的回潮。早些年大家觉得 GUI 才是效率的终点什么都要拖拽、点按、可视化结果绕了一大圈做开发、做运维、做数据、做自动化的人又回到了终端里。原因不复杂终端里的东西可组合、可脚本化、可复现而且天然适合被 Agent 调用。你让一个智能体去点网页按钮它得截图、识别、定位、点击每一步都可能翻车你让它执行一条命令它只要拼对参数就行。“CLI-Anything”这个标题我理解成一种思路而不是某个具体产品把任何能力都包装成命令行接口让 Agent 能像人一样在终端里干活。热搜词里 CLI、Agent、CLI-Hub 三个词凑在一起其实已经把方向说透了——CLI 是执行层Agent 是调度层CLI-Hub 是分发层。这三层搭起来才是一个能跑通的闭环。我最早接触这类玩法是因为手头有一堆零散的脚本有的用 Python 写的有的用 Node 写的还有几个是 shell 拼出来的。每次想让 Agent 帮忙跑个流程都得先告诉它“这个脚本在哪、怎么调、参数是什么”说一遍还行说十遍就烦了。后来我把这些脚本统一包成带--help的命令注册到一个统一的入口里Agent 只要知道命令名就能自己查用法、自己拼参数。那一刻我才真正体会到“CLI-Anything”的价值不是让 CLI 变得多花哨而是让所有能力都变成 Agent 能理解、能调用的标准件。这篇文章适合几类人看。第一类是正在做 Agent 开发、被工具调用折腾得够呛的工程师第二类是手里有一堆脚本、想让它们被自动化调度的运维或数据同学第三类是想入门 Agent 但不知道从哪下手的新手CLI 其实是最好的切入点因为它足够简单、足够透明。我会从整体设计思路讲到具体实现再到踩过的坑尽量把每一步的“为什么”说清楚让你看完能直接动手。2. 整体设计思路把能力拆成三层2.1 CLI 层为什么命令比函数更适合 Agent很多人做 Agent 工具调用第一反应是写一堆函数然后注册成 tool。这个做法在工具少的时候没问题工具一多就乱每个函数的参数结构不一样返回格式不一样错误处理不一样Agent 每次都要重新理解。而 CLI 有一个天然优势——它有一套几十年沉淀下来的约定--help看用法--version看版本参数用--key value或--flag退出码 0 表示成功、非 0 表示失败标准输出给结果、标准错误给日志。这套约定意味着什么意味着 Agent 不需要你额外教它怎么用。它只要会执行xxx --help就能自己搞清楚这个命令能干什么、要什么参数。我实测下来让 Agent 自己读--help输出再拼命令成功率比让它读一堆 JSON schema 高不少因为--help是给人看的语言模型对自然语言说明的理解本来就强。所以 CLI 层的设计原则就一条每个能力都是一个独立命令命令要自解释。所谓自解释就是--help里把用途、参数、示例都写清楚。别偷懒写“参数 a字符串”要写“参数 a目标文件路径支持相对路径和绝对路径例如 ./data/input.csv”。你多写这一句Agent 就少犯一次错。2.2 Agent 层调度逻辑该放在哪Agent 层要解决的核心问题是给定一个用户意图怎么决定调哪个命令、传什么参数、拿到结果之后下一步干什么。这里有个常见的误区就是把所有逻辑都塞进一个大 prompt 里让模型一次性想清楚。工具少的时候能跑工具一多模型就开始胡编参数。我的做法是把调度拆成两步先选命令再填参数。第一步让模型看命令列表命令名 一句话描述选出最相关的那个第二步把选中命令的--help全文喂给模型让它根据用户意图填参数。这样每一步的上下文都很小模型不容易分心。实测下来两步走的准确率比一步到位高出一大截尤其是命令超过二十个之后。还有一个细节命令的返回值要结构化。别让命令直接吐一大段人类可读的文本最好同时支持--json输出。Agent 解析 JSON 比解析自然语言稳得多。我一般会让命令默认输出人类可读格式加--json时输出机器可读格式Agent 调用时统一加--json。2.3 CLI-Hub 层为什么需要一个统一入口CLI-Hub 这个词很有意思它解决的是“命令散落各处”的问题。你可能有 Python 脚本在~/scriptsNode 工具在~/tools还有几个二进制在/usr/local/bin。Agent 要知道所有这些路径还得知道哪个用 python 跑、哪个直接执行。这太脆弱了。CLI-Hub 的思路是做一个统一的命令注册中心所有能力都注册进来对外暴露统一的命令名内部负责路由到真正的实现。Agent 只需要知道 Hub 的入口剩下的交给 Hub。这有点像包管理器你不需要知道包装在哪只需要知道包名。实现上可以很简单一个目录放一堆可执行文件每个文件是个薄薄的 wrapper负责调用真正的实现。也可以复杂一点做一个配置文件驱动的路由表。我倾向于从简单开始因为 Hub 本身不该成为瓶颈。3. 核心细节解析命令该怎么设计3.1 参数设计位置参数还是选项参数这是个老生常谈的问题但在 Agent 场景下答案很明确能用选项参数就用选项参数。位置参数对人类友好对 Agent 不友好因为 Agent 得记住顺序。cp a b这种两个参数的还好参数一多就容易错位。选项参数--source a --target b虽然啰嗦但自解释Agent 不容易搞混。当然也不是绝对的。如果某个参数是命令的核心且只有一个用位置参数也行比如search 关键词。判断标准是这个参数是不是命令的“主语”。是主语就用位置参数是修饰就用选项参数。还有一点布尔标志要设计成正向的。别搞--no-cache这种否定式Agent 容易理解反。要缓存就--cache不要缓存就默认不缓存。如果默认要缓存那就提供--no-cache但要在--help里写清楚“默认启用缓存加此参数禁用”。我踩过这个坑Agent 看到--no-cache以为是启用缓存结果行为完全反了。3.2 输出设计人类可读与机器可读并存前面提了--json这里展开说。命令的输出其实有两类消费者人和 Agent。人对 JSON 不友好Agent 对花哨的表格不友好。所以最好的做法是默认输出人类可读格式加--json时输出 JSON。JSON 的结构也要讲究。我一般会包一层{ success: true, data: { ... }, error: null, meta: { duration_ms: 123, command: xxx } }success让 Agent 一眼知道成没成data放真正的结果error放错误信息meta放一些辅助信息。这样 Agent 处理起来很省心不用去猜“这个输出到底算成功还是失败”。错误信息也要结构化。别只给一个“执行失败”要给错误码和错误原因。比如{success: false, error: {code: FILE_NOT_FOUND, message: 文件 ./a.csv 不存在}}。Agent 看到FILE_NOT_FOUND就知道该去检查路径看到PERMISSION_DENIED就知道该去检查权限。3.3 退出码别小看这个数字退出码是 CLI 的古老约定0 成功、非 0 失败。但在 Agent 场景下退出码可以承载更多信息。我一般会约定退出码含义Agent 应对0成功继续下一步1通用错误看错误信息决定2参数错误重新检查参数3依赖缺失提示安装依赖4权限问题提示检查权限5超时考虑重试或延长超时这样 Agent 拿到退出码就能快速判断该重试、该改参数、还是该放弃。比让它读一大段错误日志再自己判断要快得多。4. 实操过程从零搭一个最小可用的 CLI-Hub4.1 目录结构设计先定目录结构这是地基。我的习惯是这样cli-hub/ ├── bin/ # 对外暴露的命令入口 │ ├── hub # 主入口 │ └── ... ├── commands/ # 各个命令的实现 │ ├── fetch/ │ │ ├── main.py │ │ └── command.json │ └── process/ │ ├── main.py │ └── command.json ├── registry.json # 命令注册表 └── README.mdbin/放对外入口commands/放实现registry.json是注册表。每个命令一个目录里面放实现和元数据。元数据command.json描述命令名、描述、参数、示例Hub 启动时读这些元数据生成统一的--help。为什么用目录而不是单文件因为命令多了之后单文件会膨胀得没法维护。一个命令一个目录改哪个命令就进哪个目录互不干扰。元数据和实现放一起也不会出现“改了实现忘了改元数据”的情况。4.2 注册表格式registry.json我设计成这样{ version: 1.0, commands: [ { name: fetch, description: 从指定 URL 抓取内容并保存到本地, entry: commands/fetch/main.py, runtime: python3, args: [ {name: url, type: string, required: true, description: 目标 URL}, {name: --output, type: string, required: false, default: ./output.txt, description: 保存路径} ], examples: [ hub fetch https://example.com --output ./page.html ] } ] }这个格式的好处是Hub 可以据此生成统一的--help也可以据此做参数校验。Agent 拿到这个 JSON就知道有哪些命令、每个命令要什么参数。examples字段特别重要Agent 看示例比看参数说明学得快。4.3 Hub 主入口实现Hub 主入口的逻辑很简单解析第一个参数作为命令名找到对应命令把剩余参数透传过去。用 Python 写大概是这样#!/usr/bin/env python3 import json import subprocess import sys from pathlib import Path HUB_ROOT Path(__file__).parent.parent REGISTRY json.loads((HUB_ROOT / registry.json).read_text()) def find_command(name): for cmd in REGISTRY[commands]: if cmd[name] name: return cmd return None def main(): if len(sys.argv) 2 or sys.argv[1] in (--help, -h): print_help() return 0 if sys.argv[1] --list: for cmd in REGISTRY[commands]: print(f{cmd[name]}\t{cmd[description]}) return 0 cmd find_command(sys.argv[1]) if not cmd: print(f未知命令: {sys.argv[1]}, filesys.stderr) return 2 entry HUB_ROOT / cmd[entry] runtime cmd.get(runtime, python3) result subprocess.run([runtime, str(entry)] sys.argv[2:]) return result.returncode def print_help(): print(用法: hub 命令 [参数...]) print( hub --list 列出所有命令) print( hub 命令 --help 查看命令用法) if __name__ __main__: sys.exit(main())这段代码不长但把核心逻辑都覆盖了。--list给 Agent 看命令列表命令 --help给 Agent 看具体用法其余情况透传给具体实现。退出码直接透传保持一致性。4.4 单个命令的实现模板每个命令的实现我建议遵循同一个模板这样 Agent 学一个就会所有#!/usr/bin/env python3 import argparse import json import sys def build_parser(): p argparse.ArgumentParser(description从指定 URL 抓取内容) p.add_argument(url, help目标 URL) p.add_argument(--output, default./output.txt, help保存路径) p.add_argument(--json, actionstore_true, help以 JSON 格式输出) return p def main(): args build_parser().parse_args() try: # 真正的业务逻辑 content do_fetch(args.url) with open(args.output, w) as f: f.write(content) if args.json: print(json.dumps({success: True, data: {path: args.output}, error: None})) else: print(f已保存到 {args.output}) return 0 except FileNotFoundError as e: if args.json: print(json.dumps({success: False, error: {code: FILE_NOT_FOUND, message: str(e)}})) else: print(f错误: {e}, filesys.stderr) return 3 except Exception as e: if args.json: print(json.dumps({success: False, error: {code: UNKNOWN, message: str(e)}})) else: print(f错误: {e}, filesys.stderr) return 1 if __name__ __main__: sys.exit(main())这个模板的关键点用argparse自动生成--help加--json开关错误分类返回不同退出码。你照着这个模板写每个命令都长一个样Agent 用起来就有预期。5. 让 Agent 真正用起来调度与提示词设计5.1 命令选择提示词命令选择这一步提示词要短、要聚焦。我一般这么写你是一个命令行调度助手。下面是可用命令列表 {command_list} 用户意图{user_intent} 请只输出最相关的命令名不要输出其他内容。如果没有相关命令输出 NONE。command_list就是hub --list的输出每行一个命令名加描述。让模型只输出命令名输出空间小准确率高。我试过让模型输出 JSON反而容易出错因为它会画蛇添足加一堆字段。5.2 参数填充提示词选中命令之后把该命令的--help全文喂进去你要执行命令 {command_name}。以下是它的用法说明 {help_text} 用户意图{user_intent} 请输出完整的命令行参数不含命令名本身每个参数一行。如果某个参数不确定用默认值。让模型一行一个参数比让它输出一整条命令好解析。拿到参数列表之后直接拼成[runtime, entry] args执行就行。5.3 结果处理与下一步决策命令执行完之后把结果JSON 格式喂回给模型让它决定下一步命令 {command_name} 执行结果 {result_json} 用户原始意图{user_intent} 请判断任务是否完成。如果完成输出 DONE如果需要继续输出下一步要执行的命令名。这样就形成了一个简单的循环选命令 → 填参数 → 执行 → 看结果 → 决定下一步。循环上限设个 10 次防止死循环。6. 常见问题与排查技巧实录6.1 命令找不到或路径错误这是最高频的问题。Agent 拼出来的命令名可能大小写不对或者 Hub 的路径没加到 PATH 里。排查思路先手动执行hub --list看命令在不在再执行hub 命令 --help看能不能找到。如果手动能跑、Agent 跑不了那就是 Agent 拼错了命令名检查提示词里的命令列表是不是最新的。提示Hub 的入口最好用绝对路径或者在提示词里明确告诉 Agent Hub 的完整路径。相对路径在不同工作目录下会失效。6.2 参数类型不匹配Agent 经常把数字参数写成字符串或者把布尔标志写成--flag true。解决办法是在--help里把类型写清楚比如“--count整数默认 10”。另外在命令实现里做一层类型转换和校验别直接信任输入。我一般会在argparse里用typeint强制转换转换失败就返回参数错误退出码。6.3 输出解析失败Agent 解析 JSON 失败通常是因为命令输出里混了日志。比如某个库往 stdout 打了一行 warningJSON 就不合法了。解决办法是把日志全部打到 stderrstdout 只放结果。Python 里用logging配置到 stderrprint只用于最终输出。问题现象可能原因排查方法解决方式命令找不到路径未加入 PATH手动执行hub --list用绝对路径或配置 PATH参数报错类型不匹配看--help类型说明实现里加类型校验JSON 解析失败stdout 混入日志检查输出是否纯 JSON日志改到 stderr退出码异常未捕获异常看 stderr 堆栈加 try/except 分类返回执行超时命令卡住加超时参数实现里加超时控制6.4 超时与重试有些命令会卡住比如网络请求。Agent 等太久会浪费 token。解决办法是给命令加超时参数默认 30 秒超时返回退出码 5。Agent 看到 5 就知道可以重试或者放弃。重试策略我一般设最多 2 次间隔 1 秒避免雪崩。6.5 权限与依赖问题命令执行失败有时候是权限不够或者依赖没装。这类问题最好在命令启动时就检查别等到执行到一半才报错。比如需要读某个目录启动时先os.access检查一下需要某个库启动时先 import 一下。早失败早报错Agent 也好处理。7. 进阶玩法让 CLI-Hub 自己长出命令7.1 命令模板生成既然命令都遵循同一个模板那就可以写个生成器给个命令名和描述自动生成目录、main.py骨架、command.json然后注册到registry.json。这样新增命令的成本从“写一堆样板”降到“填几个字段”。我实测下来新增一个命令从 15 分钟降到 3 分钟。7.2 命令自检Hub 可以加一个hub --doctor命令检查所有注册的命令入口文件在不在、runtime 能不能找到、--help能不能正常输出。这样在 Agent 用之前就能发现坏掉的命令避免运行时才报错。7.3 与 Agent 框架的对接CLI-Hub 本质上是一个工具提供方任何 Agent 框架都能对接。对接方式就是告诉框架工具列表用hub --list获取工具调用用hub 命令 参数执行。这样框架不需要知道具体命令的实现只需要知道 Hub 的入口。换框架的时候Hub 不用改只改对接层就行。8. 我踩过的几个坑第一个坑是命令名太抽象。我一开始用do、run、exec这种名字结果 Agent 经常选错。后来改成动词加名词比如fetch-url、process-csv、convert-image准确率明显提升。命令名要让人一眼知道干什么别玩文艺。第二个坑是**--help写得太简略**。我一开始觉得--help是给人看的随便写写就行。结果 Agent 全靠--help理解命令写简略了它就瞎猜。后来我把--help当文档写每个参数都带示例Agent 的准确率上了一个台阶。第三个坑是错误信息不分类。一开始所有错误都返回退出码 1Agent 分不清是参数错了还是网络错了只能盲目重试。后来加了退出码分类Agent 就能针对性地处理参数错了就改参数网络错了就重试权限错了就提示用户。第四个坑是没做超时。有个命令调外部接口偶尔会卡住Agent 就一直等token 哗哗地烧。后来加了超时超时返回退出码 5Agent 看到就放弃或者重试成本可控多了。第五个坑是JSON 输出没包一层。一开始直接输出业务数据Agent 得自己判断“这个输出算成功还是失败”。后来包了success/data/error三层Agent 处理起来清爽多了。9. 关于 CLI-Anything 的一点个人体会这套东西搭起来之后我最大的感受是CLI 是被低估的 Agent 接口。大家都在卷 function calling、卷 MCP、卷各种花哨的协议但 CLI 这套几十年前的东西反而因为简单、透明、自解释在 Agent 场景下焕发了第二春。Agent 不需要你教它怎么用 CLI它只要会执行--help就能自学。这种“自解释”的能力是很多复杂协议给不了的。另一个体会是别把 Hub 做太重。我见过有人把 Hub 做成一个庞大的框架又是插件系统又是依赖注入结果维护成本比命令本身还高。Hub 的职责就一个路由。找到命令、透传参数、返回结果完事。剩下的交给命令自己。保持 Hub 薄命令厚整个系统才好维护。最后分享一个小技巧给每个命令加一个--dry-run参数只打印将要执行的操作不真正执行。Agent 在不确定的时候可以先 dry-run 一下看看参数拼得对不对再真正执行。这个参数实现成本很低但能避免很多误操作。我在几个涉及文件删除、数据写入的命令上加了--dry-runAgent 的翻车率明显下降。这套 CLI-Hub 的思路后续还可以往几个方向扩展。比如加一个命令市场把常用命令打包分发比如加一个执行历史记录 Agent 调过哪些命令、结果如何方便复盘比如加一个权限层敏感命令需要额外确认。这些都是后话先把最小闭环跑通再慢慢加。