面向智能原生(Agent-Native)CLIs 的设计原则
摘要
本文总结了 10 条设计智能原生命令行界面(CLI)的原则,这些原则汲取了在 Cloudflare 和 HeyGen 的实践经验,旨在提升 AI 智能体的可靠性。
查看缓存全文
缓存时间: 2026/05/08 08:39
10 项面向 Agent 原生 CLI 的设计原则
上个月我写了《7 项面向 Agent 友好型 CLI 的设计原则》。从那以后,我深入参与了 CLI 的开发工作,观察 Agent 如何使用它们,并目睹了它们以各种有趣的方式“崩溃”。4 月中旬,@Cloudflare 发布了《Cloudflare 的统一 CLI》,描述了他们如何围绕一个 TypeScript 模式(Schema)重建 Wrangler,该模式从单一源生成 CLI、SDK、Terraform 提供程序以及 MCP 服务器。他们的 Code Mode MCP 在不到 1,000 个 Token 的开销下服务于整个约 3,000 个操作的 API。他们添加了 /cdn-cgi/explorer/api,这是一个为 Agent 设计的、形状类似于 OpenAPI 的运行时端点。此外,他们在整个 CLI 界面中强制推行命名规范:始终使用 get,绝不用 info;始终使用 --force,绝不用 --skip-confirmations;始终使用 --json。他们对此的解释是:“通过人工评审来强制执行一致性就像瑞士奶酪一样(漏洞百出)。”
不久之后,@HeyGen 推出了他们的 CLI,自那时起我便重度使用它。通过 Agent 生成视频、轮询任务、将产物路由到 Webhook。正是这种实践经验让它跻身于此。许多公司都发布了 CLI,但这是我所用过的最对 Agent 友好的一个。
我最初写的 7 项原则是防御性的:CLI 必须做对的事情,否则 Agent 每次调用都要为此付出代价。不要在 TTY 检查上挂起,返回 JSON,让错误具有可操作性,限制输出边界。这一层仍然必要,但已不足够。下一层关乎的是“复利效应”而非仅仅“不崩溃”。CLI 随着 Agent 使用次数的增加而变得更有用,因为 Agent 具备持久化身份、异步工作流、需要落地某处的输出,以及维护者应当听到的摩擦点。
下面的 10 项原则源于我个人的 CLI 工作(新项目即将推出!)以及 Cloudflare 和 HeyGen 发布的内容。分为两个层级。前五项浓缩了原来的七项,后五项则是新增的。
第一层:基本门槛(Table Stakes)
不要搞垮 Agent。 Agent 很擅长自行解决问题,但如果未满足以下条件,局面将对它们不利。每一个漏洞都会导致更多的 Token 消耗、更多的重试,以及更多直到生产环境才暴露的故障模式。
1. 默认非交互式
当 Agent 调用命令时,命令必须能够运行而无需交互式提示。当子代理启动后台进程时,没有任何东西会回答提示。命令会挂起。
# 永远挂起,等待永远不会到来的确认
$ mycli post delete post_8f2a < /dev/null
Are you sure you want to delete post_8f2a? [y/N]: ^C
# 使用 --force:绕过提示,Agent 顺利执行
$ mycli post delete post_8f2a --force
{"deleted":"post_8f2a"}
理想状态: 在每个可能提示用户的命令上提供 --no-input 或等效选项;诚实的 TTY 检测,将非 TTY 环境视为无头模式;使用 --yes 绕过确认;对于以前通过交互菜单收集的任何内容,通过标志或文件提供结构化输入。Cloudflare 标准化使用 --force 作为破坏性操作的绕过选项,并明确禁止 --skip-confirmations。选定惯例,然后强制推行。在提示上静默挂起是阻断性问题;子命令间提示绕过行为不一致是摩擦点;Agent 可以依赖的、无需逐命令查找的全面非交互模式是优化目标。
2. 结构化、可解析的输出
带有 ANSI 颜色的漂亮对齐表格是给人看的。Agent 提取帖子 ID 需要 JSON。
# 数据在 stdout 上,可以直接用 jq 解析
$ mycli post list --json | jq '.posts[0].id'
"post_8f2a"
# 错误去 stderr,退出码指示故障类型
$ mycli post get post_does_not_exist --json
$ echo $?
4 # stderr → "error: post not found: post_does_not_exist"
理想状态: 在每个返回数据的命令上使用 --json;成功时退出码为 0,失败时为非零值(如果可能的话,提供稳定的分类法);结果输出到 stdout,诊断信息输出到 stderr;当输出不是终端时抑制 ANSI。来自 Cloudflare 的新动态:选定一个标志。始终使用 --json,而不是有些命令用 --format=json,有些用 --output json。这一层的不一致性本身就是一类故障。完全没有结构化输出是阻断性问题;某些命令支持 JSON 而其他不支持的覆盖缺口是摩擦点;在整个 CLI 中统一使用 --json,并配合清晰的 stdout/stderr 分离以及文档化的退出码分类法是优化目标。
3. 具有教导性且枚举详细的错误
最初的原则是“快速失败并提供可操作的错误”。这一点仍然成立,但我第一次遗漏了一个改进点。当失败原因是“你为 X 传递了无效值”时,错误信息应包含有效的值集合。
# 无用:Agent 必须阅读 --help,解析、猜测、重试
$ mycli post create --json --visibility=secret --content="hi"
error: invalid visibility
# 更好:错误命名了有效集合,Agent 在一次重试中自我纠正
$ mycli post create --json --visibility=secret --content="hi"
error: --visibility must be one of: public, private, unlisted (got: "secret")
HeyGen 的 CLI 始终如一地应用这一点:传递未知的交付方案,你会得到一个结构化拒绝,并列出支持的内容。这种模式是通用的。任何时候你的 CLI 根据枚举、枚举状资源列表或模式拒绝用户输入,都应在错误中展示该枚举。错误是 Agent 获得的高信号上下文,因为它们正是在 Agent 不知道下一步该做什么时触发的。
理想状态: 在产生副作用之前尽早验证错误;错误文本中包含正确的调用语法;当原因是枚举时,列出有效值;提供具体示例而非堆栈跟踪。静默或模糊的失败是阻断性问题;指出了问题但未指出解决方案的错误是摩擦点;包含有效值集合和工作示例的错误是优化目标。
4. 安全重试和显式的突变边界
Agent 会重试。人类瞥见重复行时会注意到;Agent 不会。
# 幂等创建——第二次调用返回现有资源,而非重复项
$ mycli post create --json --content="hello world"
{"id":"post_8f2a","existing":false}
$ mycli post create --json --content="hello world"
{"id":"post_8f2a","existing":true}
# 破坏性操作需要显式标志;--dry-run 显示将会发生什么
$ mycli post delete post_8f2a --dry-run
{"would_delete":"post_8f2a","status":"dry_run"}
理想状态: 创建操作使用幂等性令牌或自然键,以便重试创建时返回现有资源而非重复项;对任何重大操作使用 --dry-run;破坏性操作使用显式的、非默认的标志;每个突变响应中都返回标识符,以便 Agent 在下一次调用时有参考依据。新的变化涉及异步,我将在原则 8 中回头讨论。对长时间运行操作的重试不仅关乎提交时的幂等性,还关乎整个提交-轮询-收集弧度的幂等性。如果 Agent 的第一次调用提交了一个任务,然后在轮询中途失去连接,第二次调用需要找到进行中的任务,而不是启动一个新任务。持久化的任务账本可以解决此问题。重试时的静默复制或状态损坏是阻断性问题;无需预览即可脚本化的破坏性命令是摩擦点;幂等突变、持久的任务状态和显式的破坏性标志是优化目标。
5. 每一层都有边界限制
Token 意味着金钱和上下文。大输出有时是合理的,但默认值应当是狭窄的。
# 默认页面大小有限制;截断告诉 Agent 如何缩小范围
$ mycli post list --json
{"posts":[...20 items...],"truncated":true,"hint":"add --limit=N or --filter=author:..."}
# 用于显式继续的光标
$ mycli post list --json --cursor=abc123
{"posts":[...],"next":null}
原始原则涵盖了运行时输出:列表返回一万行,日志无限倾倒。Cloudflare 添加了原始原则遗漏的一层:工具描述表面本身也消耗 Token。他们的 Code Mode MCP 在不到 1,000 个 Token 下服务于 3,000 多个操作。我所见过的大多数 MCP 服务器仅在一个工具的描述上就消耗 1,000 个 Token。这两层都很重要。臃肿的 MCP 描述永远不会被人类阅读,但每一个加载它的 Agent 在每次调用时都要支付代价。
理想状态: 对所有列表类命令进行过滤、分页和限制;简洁模式与详细模式;教 Agent 如何缩小下一次查询范围的截断消息;先摘要后详情的响应。对于 MCP 包装器:每个工具描述设定一个预算,在构建时审计,而不是“觉得自然解释多少就多少”。常规命令倾倒无界输出是阻断性问题;宽泛的默认值但可用狭窄化选项是摩擦点;引导更好查询的有限默认值,加上每个工具描述适合发推特的 MCP 表面,是优化目标。
第二层:复利效应(Compounding)
赋能 Agent。 第一层让你留在比赛中。第二层让 CLI 随着使用次数的增加变得更好。这些是我在写原始版本时没有看到、但现在觉得显而易见的原则。
6. 跨 CLI 的词汇一致性
这是我最确信的原则,也是原始版本中陈述最少的原则。Agent 不是逐个记忆 CLI 的。它们根据所见过的所有 CLI 构建关于 CLI 做什么的通用模型。当你的工具使用 info 而每个其他工具都称之为 get 时,Agent 不会失败;它会缓慢成功,在消耗 Token 阅读 --help 并多次重试之后。将其乘以每周数千次 Agent 调用,成本是真实的。
# 符合惯例——Agent 立即识别这些
$ wrangler kv namespace list --json
$ heygen videos list --json
$ mycli posts list --json
# Agent 必须为每个工具重新学习的偏离惯例版本
$ mycli posts ls # 使用 list,而非 ls
$ mycli posts info abc # 使用 get,而非 info
$ mycli post delete abc \
--skip-confirmations # 使用 --force,而非 --skip-*
$ mycli post list \
--format=json # 使用 --json,而非 --format=json
Cloudflare 明确化了这一点。他们的模式层规则:
- 始终使用
get,绝不用info - 始终使用
list,绝不用ls - 始终使用
--force,绝不用--skip-confirmations - 始终使用
--json,绝不用--format=json
他们使用的框架是正确的:“通过人工评审来强制执行一致性就像瑞士奶酪一样(漏洞百出)。”词汇一致性必须在代码生成或模式层通过机械方式强制执行,因为人工评审总会让边缘情况溜走。该原则超越了 Cloudflare 的具体列表。选择更广泛社区已经使用的惯例(Unix 的 --yes 用于跳过提示,--limit 用于分页,get/list/create/update/delete 动词集),除非有强有力的理由,否则不要偏离。当你确实因为概念真正新颖而不得不发明词汇时,在自己的命令中保持一致地命名,并突出、一次性地记录它。
理想状态: 文档化的命名策略;CI 中的静态检查,在遇到禁止的动词和标志别名时失败;与语言社区中主导惯例相匹配的正统名称。违背普遍惯例的动词和标志(用 info 代替 get,用 --skip-confirmations 代替 --force)是阻断性问题;子命令间的内部不一致性是摩擦点;在初次遇到时就能被基于邻近 CLI 训练的 Agent 识别的模式强制词汇是优化目标。
7. 三层内省
这里的原则原本是“渐进式帮助发现”:顶级 --help 列出命令,子命令 --help 显示用法。这仍然正确,但它现在是三层堆栈的最底层。每一层回答不同的问题。
# 第 1 层——这个命令做什么?(面向人类的文本)
$ mycli --help
mycli Manage posts and accounts.
USAGE:
mycli [flags]
COMMANDS:
post Manage posts
account Manage accounts
jobs Inspect async jobs
profile Manage saved configurations
feedback Send feedback upstream
# 第 2 层——一切的结构是什么?(结构化、版本化)
$ mycli agent-context | jq '.schema_version, (.commands | keys)'
"1"
["account","feedback","jobs","post","profile"]
$ mycli agent-context | jq '.commands.post.subcommands.create.flags'
{
"--content": {"type":"string","required":true},
"--visibility": {"type":"enum","values":["public","private","unlisted"]},
"--json": {"type":"bool","default":false},
"--dry-run": {"type":"bool","default":false}
}
# 第 3 层——我何时使用这个?(长格式技能清单)
$ cat $(mycli skill-path)/SKILL.md
# Publishing a post end-to-end
1. Save a profile for your default audience.
2. Create the post with --wait so the artifact returns synchronously.
3. Use --deliver=webhook:... to ship it downstream.
--help 是必要的,因为有些 Agent 在接触其他内容之前会先碰到它,也因为人类需要它才能进入终端。agent-context 是内省 Agent 实际应该消耗的内容:版本化的、机器可读的 JSON,描述完整的结构。Cloudflare 的 /cdn-cgi/explorer/api 是这个想法的运行时版本;对于 CLI 而言,等效物是一个顶级子命令。我一直生成的 CLI 都附带带有 schema_version 字段的 agent-context,正是为了让消费 Agent 能够检测到破坏性的结构变更。技能清单是第三层:长篇散文,教导 Agent 如何将操作组合成有用的工作流。HeyGen 在其 CLI 旁边附带了一个包含 SKILL.md 文件的技能仓库,Cloudflare 的 MCP 服务器也是等效物:从 Agent 可能使用它的任务角度描述 CLI,而不是它暴露的命令角度。
理想状态: 存在所有三层,每一层都有版本,每一层都由相同的生成步骤与实现保持同步。只有 --help 而没有结构化内容的 CLI 是阻断性问题;存在但未版本化的 agent-context,或与实际命令表面脱节的技能清单,是摩擦点;三层、模式版本化、针对实际实现进行机器验证,是优化目标。
8. 感知异步的执行
大多数 CLI 处理异步 API 的方式与底层 HTTP 端点相同:提交返回任务 ID,轮询返回状态,这是 Agent 的问题。随之而来的是两种故障模式。要么 Agent 编写自己的轮询循环(浪费 Token 且微妙地出错),要么不编写,工作流失败,因为在下一步运行时结果尚未准备好。解决方法是 --wait。
# 没有 --wait:Agent 必须编写自己的轮询循环
$ mycli video render --script=story.txt
{"job_id":"job_8f2a","status":"queued"}
$ mycli video status job_8f2a
{"job_id":"job_8f2a","status":"running","progress":0.34}
$ mycli video status job_8f2a
{"job_id":"job_8f2a","status":"running","progress":0.71}
$ mycli video status job_8f2a
{"job_id":"job_8f2a","status":"complete","url":"https://.../out.mp4"}
# 使用 --wait:相同工作流,一个命令,无轮询逻辑
$ mycli video render --script=story.txt --wait
{"job_id":"job_8f2a","status":"complete","url":"https://.../out.mp4"}
# 任务账本在调用间存活
$ mycli jobs list
JOB_ID STATUS KIND STARTED DURATION
job_8f2a complete video.render 2026-04-30T18:22:11 37s
job_7c14 running video.render 2026-04-30T18:24:02 12s
--wait 阻塞直到完成。在其背后,CLI 运行带有退避的轮询循环,并将任务状态写入本地账本。jobs 命令暴露账本:jobs list 显示进行中和最近的任务,jobs get 检索状态,jobs prune 清除旧条目。这将多个 Agent 回合折叠为一个。相同的工作流,更少的 Token,无需 Agent 正确处理的轮询逻辑。任务账本对重试至关重要(见原则 4):如果 Agent 的 --wait 调用在轮询中途被杀死,下一次调用将找到现有的任务,而不是提交新任务。
理想状态: 每个包装异步 API 的提交命令上使用 --wait;带有指数退避和抖动的轮询实现;持久的任
相似文章
构建面向AI代理的原生CLI技能
一份关于专门为AI代理设计CLI的指南,强调可预测性、可脚本化和安全默认值。
为AI代理打造一个开源CLI编排层是否有意义?
本文探讨了为AI代理编排CLI使用而构建一个开源层的想法,解决了代理在与多个CLI交互时面临的权限、沙箱和审计追踪等挑战。
HKUDS/CLI-Anything
CLI-Anything 是一个开源框架,能够自动为任何软件生成命令行界面,使其对 AI 智能体可访问。它包含一个社区构建的 CLI 中心,并支持多种 AI 智能体平台。
@jakevin7: AI 自己总结出来了 Agent-native 这个词告诉我 我还是有点吃惊的。 project_opencli_design_principle.md,核心三条: - OpenCLI 第一用户是 AI agent,不是人类开发者。所有能…
OpenCLI 项目提出 Agent-native 设计理念,将 AI agent 作为 CLI 的第一用户,所有能力设计以提升 agent 成功率为衡量标准。
给初涉生产环境 AI Agent 开发的 10 条忠告
一位从业者分享了在生产环境部署 AI Agent 时的十条关键经验,强调应通过代码约束、上下文管理和安全机制来保障系统,而非单纯依赖提示词。