缓存时间:
2026/07/09 07:39
# 不要为了代理重写你的 CLI - 微软开发者
来源:https://developer.microsoft.com/blog/dont-rewrite-your-cli-for-agents
2026年7月7日
0 条评论
首席开发者倡导者
有一种建议正在流传:将你的 CLI 参数替换为一个单一的 `--json` 载荷,这样代理就能更有效地使用你的工具。理由是,代理已经以结构化格式思考,嵌套的数据可以自然地映射到 JSON。而扁平参数则迫使使用一些笨拙的约定,例如重复 `--service-name` 来分隔多值组,这本身就存在歧义。更不用说,代理还需要正确理解所有值的类型。
这是一个合理的假设,我们想知道它是否经得起实际测量的检验。我们收集的数据显示了一些有趣的现象。
## 我们测试了什么
我们构建了一个名为 `podctl` 的合成 CLI,用于创建多服务部署。场景特意设计得很复杂:两个服务具有独立的配置,三层嵌套(`services[].resources.cpu.request`),数组,混合类型,以及服务之间的交叉引用。整个部署规范中有超过 30 个不同的值。下面是参数调用的样子:
```
podctl create \
--name "api-gateway-prod" \
--env production \
--region us-east-1 \
--replicas 3 \
--service-name auth \
--service-image ghcr.io/acme/auth:v2.4.1 \
--service-port 8080 \
--service-protocol grpc \
--service-cpu-request 250m \
--service-cpu-limit 500m \
--service-mem-request 128Mi \
--service-mem-limit 256Mi \
--service-health-path /healthz \
--service-health-interval 10s \
--service-health-timeout 3s \
--service-health-retries 3 \
--service-env DB_HOST=db.internal \
--service-env DB_PORT=5432 \
--service-name gateway \
--service-image ghcr.io/acme/gateway:v1.8.0 \
--service-port 443 \
--service-protocol https \
--service-cpu-request 500m \
--service-cpu-limit 1000m \
--service-mem-request 256Mi \
--service-mem-limit 512Mi \
--service-health-path /ready \
--service-health-interval 15s \
--service-health-timeout 5s \
--service-health-retries 5 \
--service-env UPSTREAM=http://auth:8080 \
--service-depends-on auth \
--scaling-min 2 \
--scaling-max 10 \
--scaling-metric cpu \
--scaling-target 70 \
--rollout-strategy canary \
--rollout-canary-percent 10 \
--rollout-canary-pause 300
```
注意 `--service-name` 出现了两次,以开始一个新的服务块。代理必须弄清楚第二个 `--service-name` 之后的标志属于 gateway,而不是 auth。下面是同一个部署的 JSON 载荷:
```
podctl create --json '{
"name": "api-gateway-prod",
"environment": "production",
"region": "us-east-1",
"replicas": 3,
"services": [
{
"name": "auth",
"image": "ghcr.io/acme/auth:v2.4.1",
"port": 8080,
"protocol": "grpc",
"resources": {
"cpu": {"request": "250m", "limit": "500m"},
"memory": {"request": "128Mi", "limit": "256Mi"}
},
"healthCheck": {
"path": "/healthz",
"interval": "10s",
"timeout": "3s",
"retries": 3
},
"env": {"DB_HOST": "db.internal", "DB_PORT": "5432"}
},
{
"name": "gateway",
"image": "ghcr.io/acme/gateway:v1.8.0",
"port": 443,
"protocol": "https",
"resources": {
"cpu": {"request": "500m", "limit": "1000m"},
"memory": {"request": "256Mi", "limit": "512Mi"}
},
"healthCheck": {
"path": "/ready",
"interval": "15s",
"timeout": "5s",
"retries": 5
},
"env": {"UPSTREAM": "http://auth:8080"},
"dependsOn": ["auth"]
}
],
"scaling": {
"min": 2,
"max": 10,
"metric": "cpu",
"targetPercent": 70
},
"rollout": {
"strategy": "canary",
"canaryPercent": 10,
"canaryPauseSec": 300
}
}'
```
你可以看到它的吸引力。JSON 版本使层次结构明确。每个服务都是一个独立的对象,资源自然嵌套,并且对于哪个字段属于哪里没有任何歧义。理论上,这应该让代理更容易正确完成。
我们使用了两个独立的 CLI 作为测试对象。一个只接受单个参数,另一个只接受 `--json` 载荷。两者共享相同的验证后端,并标准化为相同的规范结构。在测试时,当前激活的变体被重命名为 `podctl`,以便代理看到一致的二进制名称。由于 CLI 对代理来说是未知的,它必须从头开始发现输入模型。对于参数,这意味着调用 CLI 的帮助。对于 JSON,这也意味着调用帮助,但还需要调用一个额外的命令来返回 JSON 模式。
我们使用两个独立的二进制文件,是因为如果单个 CLI 同时支持两种模式,代理可以选择其偏好的方法,这可能会使结果产生偏差。
我们使用 GitHub Copilot Chat 作为测试工具,对每个模型在每个输入模式下运行五次,使用相同的提示和评估标准。我们选择的模型是 Claude Haiku 4.5、Claude Sonnet 4.6、Claude Sonnet 5、GPT-5.3-Codex 和 MAI-Code-1-Flash,这些都是常用于编码的模型。
## 正确性:参数完胜
无论使用哪种模型,每个参数的配置在所有五次运行中都实现了完美的正确性。测试中最小的模型 Haiku 4.5 生成正确部署的可靠性也与 Sonnet 5 一样。
而对于 JSON,情况则不同。三个最强的模型在参数和 JSON 上都达到了 5/5 的正确率。两个较小的模型则没有。Haiku 4.5 在 JSON 模式下只完成了 5 次中的 2 次正确部署。MAI-Code-1-Flash 完成了 5 次中的 3 次。
| 模型 | 参数 | JSON |
| :--- | :--- | :--- |
| Claude Sonnet 5 | 5/5 | 5/5 |
| Claude Sonnet 4.6 | 5/5 | 5/5 |
| GPT-5.3-Codex | 5/5 | 5/5 |
| MAI-Code-1-Flash | 5/5 | 3/5 |
| Claude Haiku 4.5 | 5/5 | 2/5 |
参数将输入空间约束得足够小,以至于即使是较小的模型也能持续产生正确的输出。JSON 则要求模型管理自己的结构,这是一种并非所有模型都能承担的能力成本。
## 成本
开发者按 token 付费,而不是按尝试次数付费。因此,我们将 token 消耗细分为输入、缓存和输出 token,并使用 GitHub Models 定价计算每个任务的成本。
| 模型 | 参数 | JSON | 比率 |
| :--- | :--- | :--- | :--- |
| GPT-5.3-Codex | $0.05 | $0.54 | 11x |
| MAI-Code-1-Flash | $0.01 | $0.08 | 10x |
| Claude Sonnet 4.6 | $0.05 | $0.47 | 9x |
| Claude Haiku 4.5 | $0.03 | $0.23 | 8x |
| Claude Sonnet 5 | $0.08 | $0.32 | 4x |
分组柱状图显示了每个模型每个任务参数与 JSON 的成本对比。参数柱非常细小,JSON 柱高耸于其上,高度是前者的 4 到 11 倍。(https://devblogs.microsoft.com/wp-content/uploads/2026/07/dont-rewrite-your-cli-for-agents-cost.webp)
对于每个模型,JSON 模式下的每个任务成本是参数模式的 4 到 11 倍。这些是单次调用的数字,对于一个团队来说,如果多次运行这样的任务,成本会迅速累积。
原始 token 差距甚至更大(4 到 14 倍),但缓存缓解了影响。在 JSON 模式下,代理会多次重试,每次重试的输入大部分以较低的速率被缓存。然而,缓存无法抵消的是输出 token。每次重试都意味着代理会推理错误并产生新的解决方案,这些解决方案会变得很有创意:通过变量传输 JSON、写入临时文件、尝试不同的转义策略。所有这些都属于输出,而输出 token 是最昂贵的类别。JSON 重试产生的输出 token 是参数路径的 7 到 14 倍。这就是成本的来源。
这些额外的 token 都用在哪里了?重试。当模型构建一个 JSON 载荷而 CLI 拒绝它时,代理会读取错误,尝试修复,并再次尝试。JSON 有更多失败的方式(语法错误和错误的嵌套是明显的),而且正如我们将在下一节中看到的,shell 转义又增加了另一层失败。更多的失败模式意味着更多的重试,更多的重试意味着更多的花费。
## Shell 转义的成本
然而,重试并不是全部。还有一点是“为 JSON 重写”的建议没有考虑到的:shell 转义。
当代理在命令行上传递 JSON 载荷时,它必须为 shell 转义引号。不同的 shell 对嵌套引号字符串有不同的转义规则,而且没有一个简单的。模型经常产生有效的 JSON,但在 shell 转义上出错:在单引号块内的双引号字符串,或者 shell 以不同方式解释的嵌套转义。
主要实验在 Windows 上进行,代理默认使用 PowerShell。Haiku 4.5 的 JSON 失败清楚地说明了转义问题。五次运行中有三次从未达到成功调用。代理产生了正确的 JSON 内容,但无法通过 shell 传递。它尝试了 base64 编码、文件管道输入、here-string 和 `cmd.exe` 透传,每次尝试都消耗了大量 token,但最终还是失败了。
我们使用 Sonnet 4.6 在 macOS 上进行了同样的实验,代理默认使用 Bash,以查看 shell 选择是否会影响结果。在 PowerShell 上,参数和 JSON 之间的成本差距是 9 倍。在 Bash 上,差距缩小到了 1.5 倍。相同的模型,相同的载荷,相同的正确性。唯一的区别在于每个 shell 如何处理带引号的 JSON 字符串。另一方面,参数在两个 shell 上都是稳定的:PowerShell 上 $0.05,Bash 上 $0.07。这就是关键:JSON 引入了一种对 shell 引号行为的依赖,这种行为在不同环境中差异很大。参数则不会。
分组柱状图比较了 Sonnet 4.6 在 PowerShell 和 Bash 上的 token 消耗。PowerShell JSON 柱高高耸立于其他三个之上,其他三个的高度大致相似。(https://devblogs.microsoft.com/wp-content/uploads/2026/07/dont-rewrite-your-cli-for-agents-shells.webp)
## 为什么约束有助于代理
更深入的发现是关于约束对模型的作用。参数以 JSON 所没有的方式约束了代理的输出。
使用参数时,`--help` 文本定义了确切的有效输入集。每个选项都有一个名称和类型,有时还有一个有效值的枚举。模型从任务描述映射到特定的选项名称和值。
使用 `--json` 时,模型获得一个模式(或从 `--help` 推断出一个),并构造一个必须满足该模式的自由形式对象。语法必须有效,嵌套必须正确。字段名称和类型必须匹配。而且整个东西必须能在 shell 转义中幸存。每一个都是 JSON 的设计所消除的失败模式,而参数则消除了它们。
流程图比较了参数路径(4 步,全部为中性)和 JSON 路径(8 步,最后四步被标记为故障点:验证语法、验证嵌套、验证字段名称和类型、为 shell 转义)。(https://devblogs.microsoft.com/wp-content/uploads/2026/07/dont-rewrite-your-cli-for-agents-failure-modes.webp)
强大的模型最终能正确使用 JSON,但“最终”这个词包含了很多工作。他们仍然会遇到 shell 转义失败,只是能够从中恢复。即使正确性相同,这种恢复的成本也比参数路径高出 4 到 11 倍。较小的模型会遇到相同的转义失败,但无法恢复,这就是正确性下降的原因。对于较小的模型,参数提供的约束是持续成功和频繁失败之间的区别。事实证明,缩小有效输入空间可以弥补模型能力上的差距。模型不需要正确处理 JSON 嵌套,因为根本就没有 JSON 嵌套需要处理。
## 这对你的 CLI 意味着什么
如果你正在构建一个代理会使用的 CLI,数据指向的方向与“用 `--json` 重写它”不同。保留你的参数。它们在整个模型能力范围内都有效,并且没有依赖环境的故障模式。它们还消耗更少的 token 和金钱。
但是,如果你的输入数据确实很复杂呢?我们的测试场景有超过 30 个值,分布在嵌套服务中,而参数在所有模型上都完美地处理了它。如果你想提供一个 `--json` 选项用于程序化使用或批处理操作,那没问题。但是,不要为了 JSON 而移除参数,也不要期望 JSON 能改善代理的结果。在这个实验中,JSON 从未提高正确性,而且总是增加成本。
JSON *应该* 工作得更好的直觉在纸面上是有道理的。但是“应该”在接触到实际操作系统上实际 shell 中运行的实际模型时,就无法存活了。请注意,参数是受约束的,并且跨 shell 可预测。JSON 富有表现力但脆弱。对于代理驱动的 CLI 使用,有约束的获胜。
## 在重写之前先测量
这是贯穿整个 AX 系列(https://developer.microsoft.com/blog/the-ax-stack-whats-fixed-where-you-can-win)的同一个教训:看似合理的建议和实际测量的结果是不同的东西,当它们不一致时,测量结果获胜。如果有人建议重构你的 CLI 的输入模型,请先进行实验。选择一个场景,定义评估标准,为你的受众选择合适的工具、模型和操作系统,然后对两种方法进行几次运行。你将在一天内得到答案,而无需重写任何你不需要重写的东西。
### 分类
### 主题
## 作者
Waldek Mastykarz
首席开发者倡导者
Waldek 是微软的首席开发者倡导者,专注于人工智能编码代理。他研究人工智能编码代理,并评估和改进微软产品和服务中的代理体验。