ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

终端AI命令行工具实战:CLI-Anything如何统一多模型API并嵌入工作流

终端AI命令行工具实战:CLI-Anything如何统一多模型API并嵌入工作流 平时工作里我至少有90%的时间泡在终端里。写代码、查日志、改配置、跑脚本几乎都能在命令行里完成。直到AI大规模普及之后我的工作流出现了一个新的割裂写代码时我会在终端里用各种CLI工具调大模型做点文字处理或者翻译又得切到浏览器里开网页偶尔想对比一下不同模型的回答质量更是得在好几个聊天窗口之间来回横跳。这种体验非常割裂也很浪费精力。所以我花了一个周末折腾出了一个叫“CLI-Anything”的小项目。说白了它是一个跑在终端里的统一AI命令行入口把OpenAI、Claude、Qwen这些主流模型的API全部封装在同一个工具里。装上之后我只需要记住一个命令就能在终端里轮流调用各家模型还能自定义系统提示词、自动切换模型供应商、把AI输出直接以markdown格式写到本地文件里。这篇文章就是把我从零搭这个工具的全过程、踩过的坑、以及最终的配置方案完整分享一下希望对同样习惯在终端里工作的朋友有帮助。1. CLI-Anything是什么为什么值得折腾1.1 先说说我在终端里调用AI时遇到的几个痛点在动手写代码之前我其实已经试过好几个AI命令行工具。有的是官方推出的功能很全但只支持自家模型比如用Claude的官方CLI就调不了Qwen用OpenAI官方的CLI想换个国产模型又得折腾代理和base_url非常麻烦。还有一些第三方聚合工具虽然号称支持多模型但配置界面做得特别复杂动不动就要填一堆OAuth回调地址和权限范围看完文档我就直接放弃了。最让我难受的是这些工具之间的参数不统一。有的用--input传参有的用-p有的干脆靠交互式对话框完全没有办法写进shell脚本里做自动化。我当时的真实需求其实很简单能在一个终端命令里指定“用哪个模型、回答什么问题”最好还能顺便把输出重定向到文件里这样我就能把AI接进自己的脚本流水线了。CLI-Anything就是冲着这个需求去的。它的设计目标非常朴素一条命令一个统一的参数风格底层可以自由切换不同厂商的大模型API。不追求花哨的TUI界面也不搞复杂的插件体系就是把“在终端里稳定调用多个AI模型”这件事做到顺手。1.2 为什么选择自己搭而不是直接用现成的轮子我认真想过这个问题。现成的多模型CLI工具不是没有但它们普遍面临一个尴尬的局面要么更新太慢官方API一调整就失灵要么把太多功能杂糅在一起光配置文件就要写上百行。我需要的不是一个大而全的平台而是一个“够用、可控、能自己改”的小工具。自己搭的好处非常明显。第一配置文件完全由我掌控想加一个模型厂商只需要在配置文件里加几行base_url和model列表不用等上游项目更新。第二我可以把公司内部的一些自定义逻辑直接写进去比如自动把聊天记录存成特定格式的日志文件或者统一走内部网关。第三整个工具的代码量不大出了问题我能直接看源码排查而不是去GitHub上提issue等回复。当然自己搭也有成本。你得处理不同API的认证方式差异、错误码差异、返回格式差异。这些事听起来琐碎但做完之后价值很大相当于你手里有了一套完全属于自己的AI基础设施。1.3 项目整体架构和选型思路CLI-Anything的核心结构不复杂主要分成三层。最底层是Provider适配层。每个模型厂商的API风格都有差异OpenAI用的是/v1/chat/completionsClaude用的是/v1/messagesQwen的兼容模式几乎照搬OpenAI格式。适配层的作用就是把这种差异隔离掉对外暴露统一的chat()方法接受一致的参数返回一致的Python字典结构。中间是配置管理层。我会维护一个config.yaml文件里面存放所有模型供应商的API Key、base_url、默认模型、超时时间、最大重试次数等信息。同时支持通过init命令交互式生成这份配置也支持直接用--set-key这样的参数临时覆盖配置。最上层是命令行入口。推荐用Python的argparse标准库来写足够简单可靠不需要引入click或者typer。参数风格保持统一--provider用来指定供应商--model指定具体模型--prompt传问题--output指定输出文件再加一个--interactive进入多轮对话模式。整个项目单文件就能跑通核心代码量大约在600行左右。如果再算上配置模板和使用文档整体还不到1000行。这个体量非常容易维护也很容易二次开发。2. 动手前必须想清楚的核心设计决策2.1 统一API格式把各家API的差异全部挡在外面写这个工具的第一道坎就是统一各家API的返回格式。OpenAI的chat.completions返回结果长这样choices[0].message.contentClaude返回的是content[0].text而Qwen放在兼容模式的output.choices[0].message.content里。如果不做一层统一上层代码就会写出一堆if provider openai之类的脏逻辑后期维护想哭。我的做法是定义了一个标准的数据结构所有Provider都必须把自家返回结果转换成这个结构再交给上层使用。转换后的数据统一包含四项content最终回答文本纯字符串去掉了所有格式包装usagetoken用量字典包含input_tokens和output_tokensmodel实际使用的模型名raw原始响应的JSON方便排查问题时定位这个设计大大简化了后续的插值、流式输出和日志记录逻辑。以后就算再加一家新的模型厂商我也只需要写一个新的适配函数完全不影响主流程。2.2 流式输出不要让用户干等十几秒调用大模型的时候如果不开流式模式用户就得眼睁睁看着光标转圈过个十几秒才一次性收到结果。这种体验在终端里特别糟糕因为终端用户天生就习惯即时反馈。而且流式输出还有一个额外的好处首字延迟大幅降低。一般模型在流式模式下1-2秒就开始出字了用户会觉得你这个工具“很快”。实现流式输出的方案不复杂。OpenAI系列用streamTrue参数然后在返回的iterator里逐段取delta.content即可。Claude的流式服务端事件SSE格式略有不同需要解析content_block_delta事件里的text字段。只要把这两种模式都封装好上层就可以统一用for chunk in chat_stream():来消费输出。我在实现流式时还额外加了一件事在标准错误流stderr里实时显示token用量统计这样既能保持stdout的纯净又能让用户随时知道这次提问消耗了多少token。2.3 流式与重试的冲突问题及对策重试机制和流式输出存在一个容易被忽略的冲突问题。如果你开了流式读取但网络中途断了你拿到的只是一半的流此时重试整个请求会让用户觉得回答过了一半又从头开始体验反差很大。我的处理策略是流式模式下默认不自动重试只把错误信息打出来提示用户手动重新发起请求。只有在非流式模式下才启用自动重试最多重试两次中间加上指数退避延时。这虽然不是最完美的方案但实测下来最符合直觉用户不会因为自动重试导致的内容重复而感到困惑。3. 最关键的实操环节从零搭建并跑通CLI-Anything3.1 准备开发环境项目我推荐用Python 3.10以上版本在macOS和Linux上都能直接跑。依赖尽量少主力用requests库发HTTP请求再加一个pyyaml解析配置文件总共就这两个第三方库。装环境的时候记得用虚拟环境这边给出一段可以直接跑的初始化命令mkdir cli-anything cd cli-anything python3 -m venv .venv source .venv/bin/activate pip install requests pyyaml如果你平时用macOS又恰好同时装了系统自带Python和Homebrew的Python建议先which python3确认一下版本路径避免装错环境。我一开始没注意这个细节结果依赖装进了系统Python里后面启动时怎么都找不到requests白白折腾了十分钟。3.2 配置文件的完整设计配置文件我放在了~/.config/cli-anything/config.yaml全局统一。这样不管你在哪个目录下执行命令工具都能找到配置。一个典型的配置文件长这样providers: openai: api_key: sk-xxx base_url: https://api.openai.com/v1 default_model: gpt-4o-mini timeout: 60 claude: api_key: sk-ant-xxx base_url: https://api.anthropic.com/v1 default_model: claude-3-5-haiku-20241022 timeout: 60 qwen: api_key: sk-xxx base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 default_model: qwen-plus timeout: 60有一个极其重要的细节API Key不要硬编码在代码里而是全部通过配置文件或者在运行时通过环境变量注入。我自己就犯过这个错误早期图方便把key直接写在了源码里差点顺手把Git仓库推到公共平台上。后来的解决方案是在代码里默认从系统环境变量读取比如OPENAI_API_KEY、ANTHROPIC_API_KEY、DASHSCOPE_API_KEY如果找不到再读配置文件。这个逻辑优先级是环境变量高于配置文件。3.3 核心代码结构和关键函数实现我把代码拆成了三个文件main.py负责命令行解析、providers.py存放各种API适配、config.py负责配置读写。下面这段是providers.py里OpenAI适配函数的核心骨架import requests def chat_openai(config, prompt, modelNone, streamFalse): api_key config[api_key] base_url config[base_url] model model or config[default_model] headers { Authorization: fBearer {api_key}, Content-Type: application/json, } payload { model: model, messages: [{role: user, content: prompt}], stream: stream, } resp requests.post( f{base_url}/chat/completions, headersheaders, jsonpayload, timeoutconfig.get(timeout, 60), ) resp.raise_for_status() data resp.json() content data[choices][0][message][content] usage { input_tokens: data[usage][prompt_tokens], output_tokens: data[usage][completion_tokens], } return {content: content, usage: usage, model: model, raw: data}Claude的适配函数就要多一步把结构化请求转换成Anthropic要求的格式def chat_claude(config, prompt, modelNone, streamFalse): api_key config[api_key] base_url config[base_url] model model or config[default_model] headers { x-api-key: api_key, anthropic-version: 2023-06-01, Content-Type: application/json, } payload { model: model, max_tokens: 1024, messages: [{role: user, content: prompt}], stream: stream, } resp requests.post( f{base_url}/messages, headersheaders, jsonpayload, timeoutconfig.get(timeout, 60), ) resp.raise_for_status() data resp.json() if stream: # 这里通常由上层单独处理SSE事件流不会走到这个分支 pass else: content .join(block[text] for block in data[content] if block[type] text) ...这里有个很容易踩的坑Claude的模型列表里既有带日期后缀的版本号也有不带日期的别名。不同时段官方会调整建议在配置文件里写清楚具体的模型ID不要用那种笼统的别名避免上线后突然失效。3.4 完整的操作演示从提问到脚本化工具跑起来之后直接输入下面命令就能用cli-anything ask --provider openai 用Python写一个斐波那契数列生成器默认输出完直接打印到终端。如果要写入文件加一个--output参数cli-anything ask --provider qwen --output answer.md 简述Kubernetes的架构这条命令会直接把markdown格式的回答写到answer.md。注意这里有一个小细节如果回答本身包含了多个markdown代码块CLI-Anything会原封不动保留所有内容而不是只保留正文。我这么设计是有意为之因为很多时候我需要的就是完整代码拆开反而多此一举。交互式对话模式用-i参数cli-anything ask -i --provider claude进入交互模式后工具会维护一个上下文列表自动把前面的对话记录传给模型实现多轮会话能力。退出交互模式的快捷键是CtrlC或者输入exit即可。3.5 模型供应商切换与参数覆盖CLI-Anything最实用的地方在于可以在命令行直接覆盖配置文件里的参数而不需要每次都去改文件。例如cli-anything ask --provider qwen --model qwen-max 解释一下什么是CAP定理这条命令会临时用qwen-max替换配置文件里的默认qwen-plus。是不是非常方便我还加了一个--format json选项可以让工具把原始的JSON响应原样输出到stdout。这个选项对集成到脚本里特别有用比如我想用jq从结果里提取token用量就可以直接解析完全不会受文本格式干扰。4. 常见错误与排查经验从入门到放弃的边缘4.1 “unable to locate the codex cli binary”的来龙去脉这个话题最近讨论度很高。很多朋友在试图安装OpenAI的Codex CLI时遇到了unable to locate the codex cli binary or required runtime components之类的报错。我自己也在类似场景里遇到过问题。这个报错的核心原因通常不是代码问题而是二进制文件路径对不上。排查路径其实就三件事。先确认你的Codex CLI到底装在哪个目录which codex的输出是不是为空如果为空说明根本没进PATH需要把安装目录加到~/.zshrc或~/.bashrc里。再确认Node.js运行时版本是否满足要求太低会导致CLI跑不起来。最后确认安装过程中有没有权限问题比如npm install -g时用了系统目录导致写入失败。我自己实际踩过的一个坑是我同时有多个Node版本管理器比如nvm和fnm它们各自维护独立的全局包目录Codex CLI装在了旧版本Node的环境里而当前shell激活的是新版本Node结果当然找不到二进制。解决方式是在访问前通过nvm use切换Expected版本再重新安装一次全局包。4.2 Claude CLI配合Qwen Key时认证与兼容性的坑“在Mac上用Claude CLI配置Qwen的Key”听起来就是个很有画面感的操作。思路是对的很多第三方的Key和网关都对外兼容OpenAI格式所以理论上可以给Claude CLI换个base_url和Key让它走飞书或者阿里云的通道。但这里有个常见的坑Claude CLI在很多版本里会因为ANTHROPIC_BASE_URL没有设置而默认打到Anthropic官方地址结果你填了个Qwen的Key对方根本不认识立刻返回401。我的建议是如果你真想这么玩需要把环境变量做全套export ANTHROPIC_BASE_URLhttps://dashscope.aliyuncs.com/compatible-mode/v1 export ANTHROPIC_API_KEY你的DASHSCOPE_KEY export ANTHROPIC_MODELqwen-max但请注意这种兼容模式对Claude CLI来说不一定全程好用因为Claude CLI某些功能会用到特殊的tools协议而Qwen的兼容层并不保证100%覆盖这些协议。如果你只是在终端里做简单的问答这种方案是可行的但如果要用到复杂的coding agent能力我建议还是老老实实用官方模型。这一点也直接影响了我开发CLI-Anything时的设计与其依赖单一厂商的CLI做兼容适配不如自己写一个薄薄的适配层把每家API的句法差异全部挡在外面这样反而更省心。4.3 时延过高、超时和网络问题排查终端用户对时延极其敏感。如果你发现某个provider响应特别慢先别急着怀疑模型本身按照这个顺序去排查第一步测网络连通性直接curl -I base_url看握手时间。如果握手都特别慢可能是网关问题。第二步查看是否走的代理环境变量在macOS终端里如果你曾经设置过HTTPS_PROXY那么很多请求会强制走代理代理不稳定就会表现成“时快时慢”。第三步检查配置文件里的超时时间是不是太短尤其是走内部网关的时候如果没有调整超时时间默认60秒经常会不够用。我自己实践下来普通问答场景下把超时时间设置在60秒是合理的但如果你要用大模型批量处理长文本建议提升到120秒以上。CLI-Anything里也支持在配置文件中单独为每个provider设置timeout字段非常灵活。4.4 各种API返回码的含义速查我在开发过程中整理了一张常见的API状态码速查表遇到问题对着查就行非常实用状态码含义排查重点401认证失败API Key是否正确环境变量是否生效403权限不足账号是否开通对应模型服务404接口路径错误base_url是否写错是否多拼了路径429请求过频token消耗是否超了限额是否需要降速500服务端异常先等几秒重试不要急着改代码503服务暂不可用官方在维护或扩容建议切换备用模型529Anthropic负载高Claude专用错误通常delay后重新发起即可这张表建议存一份会省下很多跟客服瞎扯的时间。5. 一些深度优化让CLI真正融入工作流5.1 用shell别名把CLI-Anything变成日常习惯CLI工具最怕的是“装了不用”。为了让自己形成肌肉记忆我在shell配置里加了一组别名把命令长度缩短到极致alias aicli-anything ask alias aiqcli-anything ask --provider qwen alias aiocli-anything ask --provider openai alias aiccli-anything ask --provider claude alias aiwebcli-anything ask --output /tmp/_ai_answer.md这样一来我只需要输入ai 这句话帮我翻译成英文回车就直接出结果输入aiq 帮我把这段代码优化一下就能切到通义千问。使用了大概两周之后我已经完全依赖这组别名了打开浏览器聊AI的次数明显下降。5.2 把CLI输出接进脚本流水线CLI-Anything最好的地方在于它不会擅自往stdout里混入无关信息。所有日志类内容都print到stderr只有AI回答本身才打印到stdout这意味着你可以把它嵌入到任何脚本里。举个例子我想写一个脚本每天定时把一份英文技术新闻摘要翻译成中文并保存到本地文件#!/bin/bash TEXT$(cat /tmp/news_raw_en.txt) cli-anything ask --provider qwen --prompt 请把以下内容翻译成中文保留技术术语原样$TEXT --output /tmp/news_zh.md换成Python脚本也一样直接用subprocess调用CLI工具解析stdout做后续操作全程不用手动切网页。5.3 隐私与Key管理的几点实操心得最后说点跟安全和习惯相关的。在真实项目里API Key的管理必须严肃对待。我的建议是尽量用环境变量而不是把Key写在配置文件里提交版本库。如果你非要把配置文件放进Git仓库务必在.gitignore里忽略掉config.yaml本身只提交config.example.yaml作为模板。还有一个容易被忽略的问题如果你在共享电脑上使用CLI工具交互模式下的对话记录可能会留在本地历史文件里因为history文件默认不加密。介意的话可以定期清理或者把对话历史存储目录指向加密盘分区。安全这个事做到顺手别做到焦虑才是一个正常开发者的状态。6. 后续可以继续扩展的方向如果你跟我一样把CLI-Anything跑通了后续其实还有很多可以玩的地方。比如给工具加上多模态支持让模型读取本地图片并生成描述或者做一个简单的插件系统允许用户注册自己的prompt模板比如专门用于commit message生成、sql优化、正则表达式调试等场景再或者把各家模型封装成一个统一的终端UI用fzf做前缀模糊匹配切换provider。就我个人而言下一步准备给CLI-Anything加上“批量prompt模式”让它从一个文件里顺序读取多行问题再依次调用不同模型回答统一输出到多个文件。这对于需要横向测评模型效果的朋友来说会非常省事。这个项目的价值不在于代码写得有多精妙而在于它把“多个AI服务”抽象成了一个简单、直接、可控的终端命令。用过之后你会发现原来命令行和AI结合的体验可以这么顺手。
返回列表