从 github cli 学习如何设计 agent friendly cli

GitHub 的 gh 应该是 vibe coding 里非常高频的命令了,而且它 CLI 本身也设计得很 agent-friendly,很适合拿来学习研究。

我觉得 gh 最值得学的,不是某几个具体 command,而是它处理了两个很关键的问题:

  • 一个是 GitHub 能力面太大,CLI command 很容易爆炸
  • 另一个是 agent 的 context 很贵,命令输出不能把无关信息全塞进来

1. 问题一:Command 爆炸

GitHub 的能力面非常大。如果每个能力都做成一个 command,很快会变成:

gh issue list
gh issue create
gh issue comment delete
gh repo deploy-key add
gh project item archive
...

command 越来越多,整个 --help 也会变得难以维护。agent 也永远要猜:这个操作到底有没有对应的 command

1.1. Resource Interface 的解法

gh api 用很取巧的方式解决了这个问题(当然这也建立在 GitHub 本身非常完善的 RESTful API 基础上):

gh api repos/epiral/bb-viewer/issues/comments/4517246421 -X DELETE

它不是简单地"可以直接调 API",而是把 RESTful API 里的 resource model 自然迁移到了 CLI 里:路径定位资源,HTTP method 表达动作,认证和输出格式由 gh 统一处理。

所以文档里看到:

DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}

几乎不用翻译,就能写成:

gh api repos/epiral/bb-viewer/issues/comments/4517246421 -X DELETE

这对 agent 特别重要。API 文档本身就可以变成 CLI 使用说明。 agent 不需要学一套和 API 文档完全不同的 DSL,也不需要等 CLI 作者给每个长尾能力都包一层 command

比如用 REST 路径直接查一个 PR:

gh api repos/cli/cli/pulls/13492 --jq '{number: .number, title: .title, state: .state}'

输出:

{"number":13492,"state":"open","title":"Replace SITE_DEPLOY_PAT with gh-cli-site-deployer App"}

路径就是资源定位,--jq 做字段裁剪,整个过程不需要记任何专用 command

这背后其实是一层 resource interface。 resource 层解决的是覆盖面问题:能力很多,但语法可以统一。

1.2. Resource vs Command

但 resource 不是万能的。RESTful 一直以来的问题就是,有些用户意图很难自然 resource 化。

比如 login。它不是对某个资源做 CRUD。

比如 clone。它既涉及远程 repo,也涉及本地文件系统和 git 状态。

比如 checkout。它不是更新一个远程资源,而是在本地切换工作区状态。

比如 merge。它经常包含多个底层动作,但用户表达的不是"修改这个字段,再删除那个分支",而是"把这个改动合进去"。

所以 CLI 里还需要 command 层。command 不是"多步骤编排"的同义词。 多步骤编排只是 command 的常见来源之一。command 的本质是承接那些无法自然 resource 化的用户意图。

1.3. 用 / 前缀区分 Resource 与 Command

如果从 gh 再抽象一步,可以把 resource 和 command 在语法上显式分开。

一个可能的方案是用 / 前缀表示 resource:

# command,没有 / 前缀,表达动作
cli login
cli clone epiral/bb-viewer
cli checkout 353
cli merge 353 --squash
cli status

# resource,用 / 前缀,表达对象路径
cli /issues list
cli /issues/42 get
cli /issues/42 update state=closed
cli /issues/42 delete
cli /issues/42/comments create body="LGTM"
cli /projects/4/items list

/ 的好处是它不占用任何单词。

如果用 api,会让人以为这是传统 API wrapper;如果用 resource,太啰嗦;如果直接写 issues list,又会有歧义:issues 到底是 command 还是 resource?

/issues 就很清楚:这是一个资源路径。

resource 层的动词可以收敛到一个很小的集合:

list
get
create
update
delete

这样 agent 学会一个资源,就基本学会了所有资源。

cli /issues list state=open author=me
cli /issues create title="Fix login" body="..."
cli /issues/42 update state=closed
cli /issues/42/comments create body="LGTM"

参数也可以分清楚:key=value 是资源参数,--flag 是 CLI 行为控制。

cli /issues list state=open --json --jq '.[].title'

resource 层负责覆盖长尾能力,command 层负责表达高层意图。 两者不是替代关系,而是互补关系。

2. 问题二:输出污染

在 agent workflow 里,命令输出不是越多越好。无关字段进入 context,不仅浪费 token,还会污染语义空间,干扰后续推理。

gh 的 --json / --jq 很值得学。

比如不做裁剪,agent 拿到的可能是这种输出:

gh pr list -R cli/cli --limit 1 --json number,title,author,labels,state,reviewDecision,updatedAt
[
  {
    "author": {
      "id": "MDQ6VXNlcjE2MTE1MTA=",
      "is_bot": false,
      "login": "williammartin",
      "name": "William Martin"
    },
    "labels": [],
    "number": 13492,
    "reviewDecision": "REVIEW_REQUIRED",
    "state": "OPEN",
    "title": "Replace SITE_DEPLOY_PAT with gh-cli-site-deployer App",
    "updatedAt": "2026-05-22T16:54:37Z"
  }
]

但如果下一步只是要知道 PR 标题,真正需要进入 context 的只有一行:

gh pr list -R cli/cli --limit 1 --json title --jq '.[0].title'
Replace SITE_DEPLOY_PAT with gh-cli-site-deployer App

--jq 的价值不是"省一个管道",而是把信息裁剪发生在进入 LLM context 之前。 先减少 token 浪费,再减少无关字段对后续推理的干扰。

2.1. 默认输出语义化

还有一种情况:默认输出本身就是语义化的。

gh pr view 13492 -R cli/cli
title:    Replace SITE_DEPLOY_PAT with gh-cli-site-deployer App
state:    OPEN
author:   williammartin
reviewers: copilot-pull-request-reviewer (Commented), BagToad (Requested)
number:   13492
url:      https://github.com/cli/cli/pull/13492
additions: 26
deletions: 2
--
## Summary

Replaces the personally-held SITE_DEPLOY_PAT used by the release workflow...

这类输出比一整坨 JSON 更适合 LLM 直接理解。

所以 JSON 的定位应该是串联和精确抽取,不是默认认知界面。默认输出应该尽量语义化。 这个点前一篇展开过,这里只 callback 一下:自然语言是模型更擅长的表征。

如果设计自己的 resource 风格 CLI,也可以沿用这个思路:

cli /issues/42 get

默认输出:

Issue #42: Fix login bug
State:   open
Author:  epiral
Labels:  bug, auth
Updated: 2h ago

Login fails when session expires.

需要串联时:

cli /issues/42 get --json --jq '.title'
Fix login bug

默认语义化,需要时结构化。

3. 执行层面的设计

最后还有一些执行层面的设计也值得学。

gh 的 flags 很一致:--repo--assignee--label--json--jq--web 在不同 command 里复用。对人是降低学习成本,对 agent 是提高泛化能力。

--web 是一个自然的 fallback:

gh pr view --web

CLI 不需要假装覆盖所有交互。 有些事情就是 Web 更合适。

还有非交互模式:

GH_PROMPT_DISABLED=1 gh pr create --title "fix bug" --body "..."

--yes 跳确认,--dry-run 做预览,token 走环境变量。这些都是 agent 能稳定使用 CLI 的基础设施。

4. 总结

所以 agent-friendly CLI 不只是 machine-readable CLI。

machine-readable 解决的是程序串联;agent-friendly 还要解决语义理解。

结构上要稳定可组合:resource path、统一动词、--json--jq

语义上要适合 LLM:默认自然语言输出、清晰错误信息、少 token 噪音。

从 gh 里可以学到的核心就是这个方向:用 resource 层避免 command 爆炸,用 command 层承接无法 resource 化的动作,再用输出裁剪避免污染 agent 的 context。