CLI-Anything:让大模型统一调度命令行工具的实践指南
发布时间:2026/9/28 14:55:10来源:尧图网络
先说一句大实话写代码的人电脑上基本都装了几十个CLI工具。git管代码docker管容器ffmpeg管音视频jq管JSONripgrep管搜索……每个单拎出来都是把好手但真要用的时候你会发现它们就是几十座孤岛各管各的互不相通。我捣鼓了几年自动化脚本一直在跟这种割裂感较劲直到把CLI-Anything这个原型跑通才算是找到了一个统一的解法让大模型当“总调度”把散落各处的命令行工具全部接到同一条流水线上用一句人话就能驱动它们干活。这篇文章就把我完整的搭建过程和踩坑记录整理出来给同样想“命令行万物”的朋友一条可以直接抄的路线。CLI-Anything是什么我自己的定义是一个面向LLM Agent的CLI工具统一执行框架核心就三层——工具发现层、执行引擎层、权限控制层。它能做的不是再发明一个命令而是把系统里已有的所有命令行工具变成标准化的“执行单元”让AI代理通过调用这些单元去完成真实任务。适合谁看两类人一类是做AI应用落地、想给Agent接上真实执行能力的开发者另一类是每天跟终端打交道、想把重复操作交给AI脚本处理的运维和效率控。下面我从设计思路、核心实现、实测记录到排坑经验按实际推进的顺序挨个说透。1. 这个项目到底在解决什么问题1.1 CLI工具的割裂现状先摆事实。我随手数了一下自己常用的命令行工具超过三十个。git一个语法体系docker一个体系ffmpeg又是另一套参数逻辑。更麻烦的是它们之间的数据流是断的git log的输出不能直接喂给dockerffmpeg的分析结果也没法天然被jq解析。每次要完成一个跨工具的任务比如“把这台机器上所有无用的Docker镜像清掉同时把对应的git分支历史整理干净”我都要开好几个终端窗口手动复制粘贴中间结果。这种割裂感带来两个直接后果。第一是记忆成本高每个工具的用法都要靠脑子硬记时间一长就忘第二是自动化门槛高写跨工具脚本要处理各种输出格式、退出码、异常情况代码很容易膨胀成一座屎山。我早期写过不少shell脚本最后基本都死在维护上——某个CLI升级后参数变了整个链路就瘫了。1.2 为什么是“Anything”而不是“一个全能工具”其实我一开始想过自己写一个超级工具把常用功能全部内置进去。但很快就否决了一来重复造轮子git、jq这些已经有几十年沉淀的工具我重写一遍完全没有意义二来内置功能注定是封闭的今天接三个工具明天想接第四个又要改源码。所以最终选了“Anything”这条路线——不重复造轮子而是做一个“调度层”。底层所有工具继续用社区维护的优秀CLI上层由一个执行引擎统一调用、统一解析输出、统一做权限管理。这样有一个非常大的好处生态是活的。任何一个新工具出现只要写一个50行的适配器立刻就能被整个系统使用某一个工具升级了也只需要改那一个适配器其他部分纹丝不动。1.3 能跑通的最小闭环是什么我的验收标准很简单能用一句自然语言让系统自动调用两三个CLI工具完成一个以前需要手动10分钟以内反复操作才能完成的任务并且全程留下了可审计的执行日志。我记得第一个跑通的场景是“把当前git仓库里改动的文件列出来统计行数变化再把结果输出成表格”——就这么一个看起来不起眼的任务背后涉及git status、git diff的解析、统计逻辑的编写在没有这套框架之前我得用shell脚本加awk写半天而现在只需要把这句话丢给CLI-Anything它自己翻译成工具调用序列然后逐个执行把结果拼装好还给我。2. 整体架构与核心设计思路2.1 三层模型的完整拆解CLI-Anything的整体架构我拆成三层来看。最底层叫“工具发现层”负责收集系统里所有可用的CLI工具和它们的能力描述。说得直白一点这层做的事就是让AI知道“你有什么工具可以用、每个工具大概能干嘛”。工具描述不是随便写两句就行我后来发现描述质量直接决定了大模型调用的准确率这一点后面实操章节细说。中间层是“执行引擎层”这是全系统的核心。它拿到大模型产出的“意图参数”结构首先要做合法性校验——命令在白名单里吗参数类型对吗有没有触碰危险操作规则校验通过后引擎用asyncio.create_subprocess_exec拉起真实的CLI进程并做好超时控制、输出截断、退出码判断。这一步最关键的设计是禁止命令经过shell去拼接执行而是要求所有参数以结构化数组的形式传给子进程从根上避免注入问题。再往上是“交互控制层”负责跟用户以及大模型对话。用户一句话进来先经过大模型解析成标准工具调用我用的JSON格式然后交给执行引擎。执行结果会再返回给大模型让它根据输出决定下一步动作或者把结果整理成人类能直接看懂的答复。这个“工具调用循环”是整个系统能处理复杂任务的关键。2.2 命令路由为什么不直接做字符串匹配第一版实现里我偷懒做过一段时间的正则匹配用关键词把用户的话映射到固定命令。结果惨不忍睹“帮我看看仓库里有什么改动”和“git status一下”这两句话表达的是一个意思但正则模板写起来完全是两条路。后来我意识到当底层工具一多基于字符串的“意图路由”根本没法维护。现在采用的是“白名单指令 LLM决策”的组合方式。具体来说工具的调用指令全部用结构化Schema定义出来大模型只能从这些Schema里选择要调用的工具和合法的参数取值不能自由发挥生成命令。这不是把决策权交出去而是反过来——给大模型划了一个坚实的活动范围。这条路线的优势在于模型选择工具偶尔会选错但永远不会产出白名单之外的命令风险边界是可控的。2.3 可插拔适配器设计适配器是我最得意的一块。每个CLI工具对应一个Python类类里定义三个东西工具名称、能力描述、执行方法。例如git适配器的描述会写清楚“它支持status、log、diff、branch、commit等操作”同时给每个操作标出参数。这套描述会随系统一起加载最终拼接成Prompt或者工具Schema给大模型。更关键的适配器里可以定义“结果解析逻辑”。因为很多CLI输出的是给人看的文本不是给机器读的JSON。比如docker images的输出第一行是表头后面才是数据而且不同docker版本格式还略有差异。适配器里写解析函数把这个差异消化掉上层拿到的永远是干净的结构化数据。所以整个系统才有“Anything”的底气——不管底层工具输出多么混乱适配器层把它转成统一格式再往上走就顺畅了。2.4 权限与审计的底线设计命令行工具能做的事情太可怕了rm -rf、强制删除数据库表、推送危险提交这都远比普通应用API的危险操作来得猛。所以CLI-Anything从第一版开始就做了三道防线。第一道是白名单机制任何不在注册列表里的可执行程序一律没有调用权限第二道是“高危操作确认机制”适配器层会标记出例如删除、覆盖、远程提交这类危险动作触发后执行引擎会暂停回传一条确认请求给用户必须人工点头才继续第三道是全量审计日志每次工具调用、每个参数值、每段输出摘要都记录到本地SQLite里方便事后追查。我见过太多AI自动化工具出事几乎都是因为少了这最后一层出了问题根本不知道它干了什么。3. 核心功能选型与依赖取舍3.1 编程语言和核心库的选择这个项目我用了Python 3.10 Typer asyncio。选Python没有任何悬念生态最全写适配器门槛最低读者里会写的人也多。Typer是用来搭CLI入口的好处是参数定义和帮助文档一次搞定后面管理子命令很方便。异步这块我吃过大亏第一版用同步subprocess调用当一个耗时命令比如docker pull跑起来整个系统就卡死了AI没法在等待的同时做其他处理。改成asyncio之后才能实现多工具并行调用和流式处理。LLM解析层我用的是OpenAI的函数调用格式其实换成其他支持JSON输出的模型也行核心代码是跟provider解耦的只有一层封装负责“自然语言输入 → 结构化工具调用序列”的转换。3.2 工具配置与运行环境运行环境我建议直接上Linux或者macOSWindows上的差异处理起来太劝退。机器规格不用高我开发时用的是一台8GB内存的旧笔记本跑得很流畅。存储方面准备一个干净的workspace目录专门给各种测试命令霍霍不然执行引擎一旦出bug遭殃的是真实环境。依赖安装我用pipenv管理两条命令就能把环境拉起来。这里有个容易被忽略的坑有些CLI工具比如jq、ffmpeg需要提前装好并确认在PATH里。CLI-Anything只在启动时扫描一次PATH如果工具不在PATH里适配器加载时会给出明确警告而不是执行时才报错。这个设计在排障时帮了大忙。3.3 核心模块的代码骨架我直接贴一段执行引擎的核心代码这是整个项目的心脏读懂了它其他模块都好理解# core/executor.py import asyncio from dataclasses import dataclass from typing import Optional dataclass class CommandResult: stdout: str stderr: str exit_code: int duration_ms: int truncated: bool False async def execute(cmd: list[str], timeout: int 60) - CommandResult: 以结构化参数方式执行外部命令禁止shell拼接 proc await asyncio.create_subprocess_exec( *cmd, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, ) try: stdout, stderr await asyncio.wait_for( proc.communicate(), timeouttimeout ) return CommandResult( stdoutstdout.decode(errorsreplace), stderrstderr.decode(errorsreplace), exit_codeproc.returncode, duration_msint(proc.returncode or 0), ) except asyncio.TimeoutError: proc.kill() stdout, stderr await proc.communicate() return CommandResult( stdoutstdout.decode(errorsreplace), stderrstderr.decode(errorsreplace) \n[execution timeout], exit_code-1, duration_mstimeout * 1000, truncatedTrue, )你们注意看函数签名里的cmd是一个list不是字符串。这背后是一个安全原则所有参数都由LLM基于Schema生成是一个个独立的字符串元素绝不拼成一个shell命令交给subprocess去执行。这样即使用户说“删掉所有文件”模型产出的参数数组里也只会出现rm、-rf、/tmp/foo这类的独立参数但绝不会有拼接出来的恶意字符串被解释执行的路径。这其实也是Magenta出过重大安全事故的根本原因——他们让模型生成shell命令字符串结果如期翻车。3.4 适配器注册机制适配器用一个装饰器自动注册新工具接入非常方便# plugins/git_adapter.py from core import Adapter, CommandResult from core.executor import execute register class GitAdapter(Adapter): name git description 管理git仓库支持status/log/diff/branch/commit等常用子命令 # 参数schema会被LLM用来生成调用 def valid_params(self) - dict: return { subcommand: {type: string, enum: [status, log, diff, branch, show]}, path: {type: string, default: .}, max_count: {type: integer, default: 10}, } async def run(self, args: dict) - CommandResult: sub args.get(subcommand) if sub status: return await execute([git, -C, args.get(path, .), status]) if sub log: return await execute([git, -C, args.get(path, .), log, --oneline, -n, str(args.get(max_count, 10))]) if sub diff: return await execute([git, -C, args.get(path, .), diff]) ...写适配器的时候我特别在意“description”字段的措辞。大模型选择工具靠的就是这个字段如果写得模棱两可它就会把git操作发给docker适配器。一开始我吃过这个亏后来总结出一个经验描述里必须写清楚“它能干什么”和“它不能干什么”比如“git适配器只管仓库操作凡是涉及容器、镜像的一律不要用”。这种否定性约束模型反而记得更牢。3.5 LLM与工具循环的协作流程整个协作流程我把它做成一个循环用户输入一句话比如“帮我看下仓库最近改了什么文件”。LLM收到这句话同时收到系统中所有适配器的Schema输出一个JSON动作{tool: git, args: {subcommand: status}}。执行引擎校验这个动作合法则执行结果包装成CommandResult返回。把CommandResult再度喂给LLM让它做下一步决策或者整理成用户能直接看懂的回复。这里有个细节如果一次任务需要调用多个工具LLM的返回可能是一个数组而不是单个动作。比如“统计代码量并输出表格”需要先调用git适配器再把git的输出交给统计逻辑。我的做法是让LLM一次性产出多个动作按顺序交给执行引擎前一个的输出可能作为后一个的输入参数但我在工程上保守起见第一版只支持顺序执行避免复杂的并行状态处理。4. 实操过程与完整实现记录4.1 初始化项目与环境我先建了一个目录结构分得比较清楚cli-anything/ ├─ core/ │ ├─ executor.py # 执行引擎 │ ├─ adapter.py # 适配器基类与注册器 │ └─ router.py # LLM决策与工具路由 ├─ plugins/ │ ├─ git_adapter.py │ ├─ docker_adapter.py │ └─ system_adapter.py ├─ tools/ │ └─ main.py # CLI入口 ├─ store/ │ └─ audit.db # 审计日志库 └─ prompts/ └─ system.txt # 系统Prompt初始化用的是pipenv搭建时间不超过十分钟。这里提醒一句别图省事把所有插件都开起来一次只开两三个适配器做验证排查问题事半功倍。等链路通了再逐步加工具否则出了问题你都不知道是大模型选错了工具还是适配器写错了。4.2 实现统一命令执行器执行引擎我上面贴了核心代码这里补充几个工程化的处理。第一是“输出截断”很多CLI命令输出特别长比如git log --all动不动几百行直接拿来喂给LLM会让token炸掉。我的做法是指定最大输出长度默认8KB超出部分截断并用truncatedTrue标记然后丢给LLM一句“输出已被截断如果需要完整内容请指定更精确的参数重跑”。这样既保护上下文空间又给了模型纠错机会。第二是“退出码语义化”。很多新手容易忽略退出码但CLI工具非零退出码可能有很多种原因命令写错了126/127、被系统杀掉137、正常返回业务错误2。我在CommandResult里保留完整退出码适配器层会做一层翻译变成“执行失败原因找不到命令”这类人类能读懂的信息再喂给LLM回环处理。4.3 写一套工具适配器并验证我拿git适配器做第一个实操样例因为git命令简单、输出稳定、不会有太多权限风险。写完适配器后我手动测试调用它确认输出正确。这个测试环节不能省因为如果适配器本身输出的解析就有问题那么后面整个链路跑起来任何奇怪的行为都没法判断是大模型的问题还是适配器的问题。git适配器跑通后我又补了一个系统适配器只暴露三类操作查看磁盘占用df -h、查看端口占用lsof -i、查看进程列表ps aux。这里只开放只读操作就是为了避免在初始阶段引入不必要的风险。系统适配器的价值在于能够立刻演示“用自然语言查服务器状态”对用户来说感知非常直观。4.4 关键场景实测记录环境就绪后我跑了三个典型场景。第一个是“git仓库瘦身排查”我对系统说“帮我看下这个仓库里最大的5个文件是什么”。系统先调用git lfs ls-files输出的是标准文本但大模型需要的是文件名加体积适配器解析后转成表格再汇总给用户。整个流程30秒内完成全程没有人工干预。第二个是“容器环境检查”我跟系统说“列出所有运行中的容器并按内存占用从高到低排序”。这句话涉及两步操作先docker ps再docker stats。系统先调docker适配器拿到容器列表再按格式解析接着把统计结果排序输出。这个场景我第一次跑的时候失败了——docker stats输出的是实时动态刷新数据它会持续输出而不是一次性退出,导致超时。后来我在适配器里定了策略stats命令用--no-stream强制一次性输出问题立刻解决了。第三个场景是“日志分析”我丢了一句话“统计access.log里状态码是500的请求按IP聚合列出最多的3个”。系统调用cat取日志然后用awk加sort统计最后用jq格式化。这已经是一次典型的跨工具串联了用户完全不知道背后用了awk还是jq看到的结果就是一份整洁的TOP3列表。这个场景对适配器层最大的考验是“没有现成CLI”得现场组合——所以我默认支持了“工具链模板”机制允许在适配器里组合多个底层命令作为虚拟工具暴露给LLM。4.5 迭代中的性能调优系统跑通后我注意到两个性能瓶颈。一是每次LLM调用都要把所有适配器的Schema全部塞进上下文适配器一多token消耗直线上升。解决办法是加了一层“预筛选器”先用轻量级关键词匹配快速过滤掉明摆着无关的工具再只把候选工具的Schema交给LLM。token消耗直接降了40%左右。二是工具执行串行等待太慢。比如“查容器状态”和“查磁盘占用”两者互不依赖完全可以并行。我在执行引擎层加了asyncio.gather()把LLM返回的多个动作里无依赖的批量执行整个系统的响应时间从20秒降到了6秒体感提升非常明显。不过并行也有副作用如果其中某个命令输出异常排查难度会加大所以审计日志里我特意加了任务ID方便把并行任务串起来回溯。5. 常见问题与排查技巧实录5.1 LLM输出格式漂移第一个坑也是最大的坑明明我用JSON Schema约束了输出格式但LLM偶尔还是会不按规矩出牌。比如约定工具名是docker它非要给你返回docker_status或者container_list这类它自己臆想出来的名字。我一开始靠“解析失败就重试”硬扛但重试多了不仅慢还有概率连续出错。后来我在Prompt里加了“示例优先”的策略每个工具Schema后面附上一个完整的调用示例让模型照着抄。这个方法效果立竿见影格式漂移率直接下降了一个数量级。另外我在解析层加了“模糊匹配兜底”当工具名对不上时用两两相似度匹配最接近的注册工具实在找不到才报错。这两板斧下去系统才算真正稳定下来。5.2 超时和输出吞掉的疑难杂症第二个常见坑是“命令卡死”。特别是docker pull这类网络操作或者git fetch这种交互式命令一旦网络抖动整个任务被一条命令卡住几十秒。我设置了全局超时默认60秒可配置超时后杀掉子进程并返回timeout错误。但杀进程本身也有讲究python的proc.kill()只是干掉直接子进程如果这个命令自己又拉了子进程pipeline场景残留的孙子进程可能继续占资源。最后我用start_new_sessionTrue让每个执行进程独立进程组超时后os.killpg成组杀掉才算干净。至于“输出被吞”的问题多半是stderr和stdout混淆。有些CLI工具习惯把错误信息打向stdout比如ffmpeg就是典型。我在CommandResult里把stdout和stderr分开存储适配器层再统一合并或根据退出码判断该看哪边。这种设计虽然简单但在实际排障时非常救命。5.3 危险命令的确认机制做这类工具有两个绕不开的问题用户忘记设置白名单或者AI自作主张执行危险操作。最典型的危险情况是“大模型理解了用户想删缓存但参数里路径写错变成了删整个目录”。我第一版没有加确认机制的时候跑过一次“清理临时文件”的任务模型给出的路径从/tmp/多了一个斜杠变成/tmp//执行引擎倒是没出错但语义上差点把临时目录整个删了。吓出一身冷汗。现在的确认机制分两级第一级是适配器声明danger_level比如danger_level2表示这是删除类高危操作执行引擎一看level为2就自动暂停第二级是“数据量感知确认”如果命令输出显示影响条目超过阈值比如git branch -d一次删除超过3个分支也会触发确认。另外在交互端我实现了“确认超时自动取消”防止用户离开后危险命令被默认放行。5.4 已知问题速查表我把自己在这些天里踩过的坑整理成了一张表方便大家按图索骥排查现象可能原因解决办法模型总是调用错误的工具适配器description写得太宽泛缺少否定性约束在description里明确“能做什么不能做什么”执行引擎说找不到命令对应CLI工具没有安装或不在PATH里启动时用shutil.which做工具探测给出清晰报错git log输出太多导致上下文爆炸没有设置max_count或输出截断适配器里给log/diff这类命令默认加上-n参数Docker命令超时docker stats不带--no-stream持续刷新改成--no-stream或者直接解析/proc审计日志查不到某次调用并行任务日志缺少任务ID关联每次执行都生成唯一任务ID并在日志里带上模型输出的参数类型错误Schema里没有给出完整枚举/默认值尽量把参数定义为enum给出default降低自由度命令执行完但结果看起来不对忘了处理退出码语义把警告当做了错误退出码0时必须检查stderr是否有Warning级别输出5.5 独家的避坑心得最后分享几个压箱底的经验。第一个心得治模型的病优先改Prompt和Schema别改代码。我有一阵子为了让格式更稳疯狂在解析代码里打补丁结果代码越来越乱问题反而没解决。后来老老实实把工具描述重写了一遍几乎没改执行层代码稳定性就上来了。第二个心得第一批适配器宁少勿多。你理想中“接50个工具”很美好但实际调试时一个工具的输出格式差异就能耗掉你半天时间。我从三个适配器起家跑通了完整闭环之后才有胆子继续加。第三个心得一定要有审计日志哪怕只是本地SQLite。有一次我半夜跑了一个自动化任务第二天早上发现某个目录被清了一部分文件翻日志3分钟就定位到是某个适配器参数传错了。没有这个日志这种事故几乎是不可排查的。现在不管做多小的实验我都会把执行记录存下来。6. 关于架构取舍再聊几点实在话6.1 为什么不直接让Agent调用原生命令有人可能会想现在很多Agent框架不是已经支持工具调用了嘛为什么还要自己搭一套我的回答是通用框架面向的是API调用场景而CLI场景有明显的特殊性。一是CLI工具大多是本地进程有沙箱、超时、退出码、进程组这些额外状态要管二是CLI输出千奇百怪不做适配层Agent拿到的原始文本token损耗极高三是安全问题API调用只要管好TokenCLI调用等于把一个能执行任意系统命令的权限交给了模型这中间的审查和防线必须自己做。通用Agent框架给的“执行shell命令”工具本质上是不设防的。它把整个系统暴露给了模型没有白名单没有危险操作确认输出也不加工。CLI-Anything的价值不是说比它们能力强多少而是把CLI调用变成一种“受控的能力”——能力边界是代码定死的模型再怎么发挥都跑不出这个圈。6.2 关于token成本和性能优化的实际操作我这里有一个很土但很有效的优化思路不是所有工具都需要塞进每一次LLM调用里真正的调用频率呈二八分布。我统计过一周的使用日志git、系统状态、docker这三个适配器占到了所有调用的80%以上。所以我的“预筛选器”其实就是一个本地关键词表把高频工具的词根映射上去命中就直接进候选列表。其他冷门工具只有在用户明确提到时才会被加载进上下文。对于输出体量大的命令我还会用“分段Summarize”的方式先截取输出的前后各2000字节丢掉中段让模型先根据这部分做粗判。如果粗判觉得需要全量数据再主动要求二次提取。这个流程相当于给模型配了一个“先看摘要再决定要不要看完”的权限实际跑下来能省不少token。6.3 可能的扩展方向这个框架跑到现在我能看到几个明显的扩展空间。方向一是“定时任务化”让系统在无人值守的情况下按照cron的节奏自动执行运维巡检、日志聚合这类任务危险操作仍旧停在“必须人工确认”这一步。方向二是“多Agent协作”如果工具很多可以让不同Agent分管不同模块一个队管代码一个队管容器主Agent负责任务拆分和结果汇总。方向三是“可视化执行链路回放”每次任务自动生成执行过程的步骤图方便理解它到底干了什么。我还在调研的方向是“适配器市场”的思路把大家写的适配器集中到一个仓库里按工具名、类别、风险等级做索引用户一键安装不需要自己写代码。这个幻想如果落地那么“CLI-Anything”就不只是我个人电脑上的私有原型而是一套整个终端生态都能接入的公共设施。回到开头说的那些散落各处的CLI工具我现在终于看到一个可能的收敛方向工具还是那些工具命令还是那些命令但中间多了一层调度大脑以后它们就不再是孤岛了。我跑这套东西的实际感受是它没有让任何一个CLI工具变得更聪明而是让它们整体上有了协作的可能。说句心里话能把几十个用了多年的终端工具统一到一个入口下这种感觉比写任何一篇技术文章都来得爽。
网站建设高端定制企业官网