代理在使用传统CLI时表现更佳 (12分钟阅读)
摘要
微软的开发倡导者测试了代理在使用JSON负载还是传统CLI参数时表现更佳,发现扁平化的CLI参数在成功率、token效率以及延迟方面实际上表现更好。
微软测试了当命令行参数被单个JSON负载取代时,代理是否表现更好。
查看缓存全文
缓存时间: 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 是微软的首席开发者倡导者,专注于人工智能编码代理。他研究人工智能编码代理,并评估和改进微软产品和服务中的代理体验。
相似文章
面向智能原生(Agent-Native)CLIs 的设计原则
本文总结了 10 条设计智能原生命令行界面(CLI)的原则,这些原则汲取了在 Cloudflare 和 HeyGen 的实践经验,旨在提升 AI 智能体的可靠性。
@akshay_pachaar: https://x.com/akshay_pachaar/status/2053166970166772052
The article discusses a shift in AI agent tool usage from the 'MCP vs CLI' debate to 'Code Mode,' where agents write code to dynamically import tools, significantly reducing context window usage. It highlights Anthropic's approach and Cloudflare's implementation, demonstrating a 98.7% reduction in token consumption for specific tasks.
@LangChain: 在构建代理时应该使用MCP还是CLI?@BraceSproul + @jakebroekhuizen 爆料。
LangChain分享了Brace Sproul和Jake Broekhuizen之间关于在构建代理时比较使用MCP和CLI的讨论。
一位开发者分享关于如何最大化AI代理能力的见解,认为更简单的设置和理解核心原则比复杂的工具和库更有效。
一位开发者分享关于如何最大化AI代理能力的见解,认为更简单的设置和理解核心原则比复杂的工具和库更有效。
构建面向AI代理的原生CLI技能
一份关于专门为AI代理设计CLI的指南,强调可预测性、可脚本化和安全默认值。