Skip to content

不给 AI 直接调 HTTP:把平台能力收敛成一个 CLI ​

让 AI 直接调平台 HTTP 接口,卡住的通常不是 AI,是接口面——它是给人(或给前端)写的,不是给"一个只会读文档、会犯错、需要重试的调用者"写的。 2026 年 8 月我写过一个接口自动化框架 apiflow,功能齐了却主动归档,原因是方向错:我在自造一个通用 CLI。 9 月到 10 月,同样形态的东西又做了一遍——这次是给 AI 用的,也已经用在实际项目的交付里。 这篇把两次的区别、CLI 与 skill 的分工、错误与幂等怎么设计、以及踩到的坑写清楚。

⚠️ 脱敏声明:文中命令名 platctl、接口路径、字段名、JSON 输出、对话内容全部是构造样例,不是任何真实平台的信息;客户名、内网地址、真实字段与凭据一律不出现。

0. 结论先行 ​

  1. 让 AI 直接调平台 HTTP 接口,卡住的地方通常不在 AI,而在接口面:认证、幂等、错误语义、编排,没有一样是为机器设计的。

  2. 把平台能力收敛成一个 CLI,粒度定在「AI 说得清的目标」上,再配一份 skill 当说明书,AI 才能稳定跑完整条业务流程。

  3. 我在这件事上先做错过一版:2026 年 8 月的 apiflow(37 个 Go 文件、约 5800 行、go test ./... 全绿),8 月 19 日自己把它归档了。9–10 月换成"给 AI 用的接口 CLI + skill",跑通并用在实际交付里。

  4. 两次的分界不是"CLI 好不好",而是判断标准变了:

    判据该不该自造命令面
    生态里已有标准入口(go test、pytest、框架自带 CLI)不要自造,接入标准入口
    命令的消费者是 AI,且生态里没有对应入口必须自己设计命令面

1. 起因:AI 直接调 HTTP 的四个坑 ​

最初的诉求很朴素:测试和排查时要反复调平台接口,让 AI 直接在对话里帮我调不就行了?

试下来发现不是"偶尔出错",而是每次都从零开始。举个当时的真实场景(构造样例):某个"提交申请单"的接口,长这样:

http
POST /open/v1/apply/create
X-Sign: <HMAC-SHA256(ts + nonce + body, secret)>
X-Ts: 1760000000
X-Env: test
Body: { "spec": "...", "applicant": "...", ... }

AI 要成功发出这个请求,得同时做对:算签名、带时间戳、选对环境、拼 spec 结构、处理业务码与 HTTP 码的双重语义。任何一步错,它都只能看到 401 或 500。

坑具体表现后果
认证 / 签名 / Token 刷新每次对话都要重新拼登录、拼 Header、拼签名AI 容易编错字段名,错一次就要来回好几轮
幂等与副作用AI 把超时当失败,然后重试重复下单、重复变更配置,人工去擦屁股
错误处理与重试HTTP 状态码和错误 JSON 对 AI 不可读AI 不知道下一步该重试、改参数,还是问人
编排(多接口串起来)每次自由发挥,步骤顺序和参数传递都不一样同一个业务流程,两次执行结果不一致

结论:不是模型不行,是"接口面"不适合 AI 直接摸。它缺的不是智能,是一个把平台复杂度挡在外面的中间层。

2. 反例在前:我 8 月造过一个"给人用的 CLI",然后归档了它 ​

先说错的那版,因为不说它,就说不清后面的设计为什么长这样。

2.1 当时做了什么(apiflow) ​

零第三方依赖的 Go 接口自动化框架,五层架构:

text
命令行层      run / api / scenario / dag / validate / list / report / gen / mock
组合业务 DAG  环检测、拓扑分层、并行调度、failFast
接口编排层    步骤顺序执行、变量传递、skipIf、loop
接口调用层    HTTP / 鉴权 / 重试退避 / JSONPath 提取 / 断言
接口录入层    YAML / JSON 加载、校验、注册表、AI 辅助生成

状态其实不差:go vet 零告警,go test ./... 全绿,端到端实测通过(mock 服务 + 场景 13 条断言 + DAG 23 条断言并行 + 重试 500→200 + JSON/HTML 报告),37 个 Go 文件约 5800 行。

然后我把它归档了。

2.2 为什么归档:方向错误,不是实现缺陷 ​

当时的做法本来的标准答案代价
自造 9 个子命令 + 自研 flag 解析库 + go test 原生入口用户要学一套新命令;丢掉 go test -run 过滤、IDE 点击运行、-race、覆盖率、CI 零适配
自定义 JSON / HTML 报告格式JUnit XML / go test -jsonJenkins / GitLab / GitHub Actions / Allure 本来就能直接吃 JUnit,自定义格式等于自建适配层
YAML 定义 + 自研 runner 接管一切定义作数据、用例即 Go 代码失败定位、断言语义、子测试层级全部自己背

正确的形态应该是 apiflow.RunScenario(t, "scenarios/checkout.yaml")——场景/DAG 落到 t.Run 子测试,入口就是 go test ./...。

教训一:生态里已有标准入口时,自造 CLI 是在重造流量入口。

2.3 两个值得记住的技术坑 ​

  1. DAG 并行分支的变量竞态:嵌套场景节点与主链并行时会互相覆盖同名变量(比如 orderId),下游断言拿到的是别的分支的数据。解法是嵌套场景在隔离的变量空间里执行(快照入参、不外泄),而 API 节点保持共享上下文以支持跨节点传值。
  2. 零依赖的代价:当时本机网络访问不了 Go module proxy(goproxy 一律 302 回 HTTPS),为了不被依赖卡住,我把 YAML 解析器和 flag 解析都自己写了。这个"被迫"本身就是错的——依赖不可得时先解决网络,而不是重写生态。

第 2 条和第 2.2 节是同一个错误的两副面孔:用"我能写"替代"该不该我写"。

2.4 留下的资产(这才是这次能快速起步的原因) ​

资产价值
零依赖 YAML 子集解析器缩进嵌套 + 流式 []/{} + 块标量 + 注释,带单测
DAG 调度引擎Kahn 环检测、拓扑分层、goroutine 并行、failFast / continueOnError
电商域 mock 后端登录 / 用户 / 商品 / 订单,含 flaky(前 2 次 500)与 slow(800ms)端点
断言与 JSONPath 轻量实现data.items[0].id、data.items[*].id、headers.X、
AI 生成提示词模板api / scenario / dag 三类定义的提示词 + JSON 抽取容错

教训二:归档不等于白做。但"资产可复用"和"方向是对的"是两件事,要分开记。

3. 那么这次为什么做 CLI 是对的 ​

形态看着一样,差别只有一个,但足够决定性:服务对象不同。

apiflow(已归档)这次的接口 CLI
使用者人(测试同学)AI(对话式驱动)
生态里有没有现成入口有,go test没有,AI 需要一个"可调用的能力面"
自造 CLI 是否重复造轮子是,造了个更差的 go test不是,这是 AI 与平台之间的新接口层
输出格式自定义 JSON/HTML 报告(错)统一 JSON 给 AI 解析(对,但只在对话场景成立)
交付物一个框架能力面 + 说明书 + AI 运行环境

一句话:给 AI 造的能力面,和给人的命令行工具,不是同一个东西。

前者要求"命令粒度 = 业务动作、输出结构化、失败可自愈、权限即能力";后者要求"融进生态、别人不用学新东西"。apiflow 用后者的标准,做成了前者想做的事,所以两头不讨好。

4. 重新设计:把命令粒度定在"AI 说得清的目标"上 ​

9 月重新开始时,第一件事不是写代码,是先定"命令长什么样"。

4.1 不按端点设计,按业务动作设计 ​

端点式(错)目标式(对)
platctl request --method POST --path /open/v1/apply/create --body '{...}'platctl apply create --spec order.yaml
AI 需要知道路径、方法、body 结构、签名AI 只需要说清"我要建一条申请单,规格在这个文件里"
接口一改,命令语义不变但行为变命令即契约,版本号即接口契约版本

判断标准很简单:这个命令名,能不能被一句自然语言目标直接映射过来。如果需要三步推理才能选对命令,命令粒度就错了。

4.2 统一 JSON 输出,字段稳定 ​

所有命令都支持 --json,输出结构固定(构造样例):

json
{
  "ok": true,
  "action": "apply.submit",
  "target": { "env": "test", "id": 1024 },
  "changed": true,
  "before": { "status": "draft" },
  "after": { "status": "approving" },
  "warnings": [],
  "next": ["platctl apply status --id 1024"]
}

关键不是"输出 JSON",而是:字段名不随命令变(ok / action / changed / before / after / next 恒在)。失败时也是同一套结构:

json
{
  "ok": false,
  "action": "apply.submit",
  "error": {
    "code": "PRECHECK_FAILED",
    "message": "目标环境正在变更中",
    "retryable": true,
    "hint": "等待 2 分钟后重试,或加 --force 跳过预检(不建议)"
  }
}

retryable 和 hint 是给 AI 的,不是给人的——它决定 AI 下一步是重试、改参数,还是停下来问人。这一条解决的是"AI 死循环重试"和"AI 一问三不知"两个极端。

4.3 写操作分级 + 二次确认 ​

级别行为例子
只读直接放行list / get / status / diff
写操作默认只做 --dry-run,必须 --confirm 才真执行create / submit / scale
危险操作除 --confirm 外还需显式 --i-know 与操作留痕delete / rollback

--dry-run 的输出必须包含"会发生什么变化",AI 才能把变化翻译成人话念给人听:

bash
$ platctl apply submit --id 1024 --dry-run --json
{
  "ok": true,
  "action": "apply.submit",
  "changed": false,
  "plan": [
    "申请单 #1024 状态 draft → approving",
    "触发目标环境 test 的重启(预计中断 40s)",
    "通知审批人 2 名"
  ]
}

4.4 其余四条硬约束 ​

  1. 幂等键:每个写命令带 --idempotency-key(不给时由 CLI 按"动作 + 目标 + 参数哈希"生成),重试不会产生第二条申请单。这条不能靠 AI 自觉,得是 CLI 的默认行为。
  2. 凭据不进上下文:Token 从环境变量或凭据文件读,绝不作为参数出现在命令行里,更不进入对话。AI 看不到密钥,也就泄漏不了。
  3. 可发现性同源:--help 和给 AI 的 skill 文档由同一份命令定义生成,防止两边漂移。
  4. 能力白名单:CLI 只暴露"允许 AI 做的事",平台接口里那些不该被自动化的(删除账号、改权限)根本不提供子命令。

第 4 条是整个方案里我最满意的一点:权限控制落在命令上,而不是落在提示词上。不给某个命令,AI 就做不到那件事;这比在 prompt 里叮嘱"不要做某事"可靠得多。

4.5 整体架构 ​

text
平台 API(前后端代码 / 接口定义)
        │  同步接口信息
        ▼
  接口 CLI 工具(platctl)
  ├─ 认证:登录 / 取 token / 刷新
  ├─ 单接口调用(统一 JSON 输出)
  ├─ 编排:把多接口串成一个业务动作
  └─ 写操作分级(只读默认放行,写操作需确认)
        │
        ▼
  skill + 说明文档(渐进式披露:先做什么、命令怎么用、失败怎么办)
        │
        ▼
  harness / DSH 内的 AI(说目标 → 选命令 → 看结果 → 下一步)
        │
        ▼
  用 DSH 打包 ──► 交付

分工:CLI 是手(能做什么),skill 是说明书(怎么用),AI 是操作者(做什么)。AI 的犯错空间被限制在"选命令、填参数"这一层,而不是"重建整个协议"那一层。

5. 让 AI 会用:skill 是说明书,不是文档堆 ​

CLI 写好了,AI 不一定用对。这一步的经验是:别把接口文档丢给它,要给"决策路径"。

5.1 渐进式披露的三段结构 ​

  1. 入口段:一句话说清这个 CLI 能干什么、什么时候用、什么时候别用(比如"需要人工审批的动作不要自动化")。控制在十几行。
  2. 常用命令段:按业务目标组织("发布一个配置"、"查审批状态"),每条给一条可直接复制的命令 + 一个真实输出样例。
  3. 排错段:把 AI 实际踩过的坑写进去——鉴权过期怎么处理、预检失败该等还是该改、哪些错误重试无意义。

第 3 段投入产出比最高:它不是文档,是"你上次错在哪"的沉淀。

写第 3 段时有个反直觉的经验:要明确写"什么情况下停下来问人"。只写"怎么重试"的文档,会让 AI 在权限不足、环境不对这类问题上无限重试。写上"遇到鉴权失败不要重试,先确认环境指向哪个环境",行为立刻稳定。

另外,文档要吸收 AI 的错法:每次它用错,就把那条错法写进排错段,而不是去改模型的提示词。

5.2 跑起来:harness 里的对话式编排 ​

实际使用形态就是四步循环:说目标 → 选命令 → 看结果 → 决定下一步。下面是脱敏后的完整流程(尖括号内全是占位符):

text
人:dev 环境的 <service> 现在是什么状态?

AI:api status --service <service> --output json

    {"service":"<service>","replicas":{"desired":3,"ready":2},
     "conditions":[{"type":"Available","status":"False"}]}

AI:期望 3 副本、就绪 2,Available=False。要拉一下异常实例的日志吗?
    对应命令是 api logs --service <service> --only-not-ready

人:拉。

AI:api logs --service <service> --only-not-ready --tail 200

    {"instances":[{"name":"<service>-7d9f-2xk4","state":"CrashLoopBackOff",
      "lastExit":{"code":137,"reason":"OOMKilled"}}]}

AI:实例 <service>-7d9f-2xk4 是 OOMKilled(退出码 137),内存上限被打满,
    不是配置错误。要不要看一下当前内存 limit 与实际用量的对比?

人:要,然后顺便把 limit 调到 2Gi,先 dry-run。

AI:api query --resource <service> --field resources --output json
    api flow run <business-action> --param service=<service> \
        --param memoryLimit=2Gi --dry-run

    {"dryRun":true,"steps":[
      {"action":"update-resource","target":"<service>","diff":
        {"resources.limits.memory":{"from":"1Gi","to":"2Gi"}}}]}

AI:dry-run 结果如上,只动了一个字段。确认的话我加 --apply 执行,
    会带幂等键,重复执行不会重复改。

这段流程里有四个细节值得单独说:

  1. 每一步都有真实的原始输出。AI 的判断建立在 JSON 字段上("ready":2 可以被断言),不是建立在它对平台"应该是什么样"的记忆上。一句"部分实例未就绪"是没法当证据的。
  2. 危险动作先 dry-run。人只需要审 diff,不需要审命令拼写。
  3. AI 主动给下一步命令。是 skill 第 3 段让它能提出 --only-not-ready,而不是泛泛地说"需要进一步排查"。
  4. 幂等键在人类没提的时候也必须带。这是 CLI 的默认行为,不是 AI 的自觉。

6. 交付:把 AI 运行环境一起打包 ​

最终交付的形态不是一个 exe,是"一套能被对话驱动的能力":

交付物内容
CLI 本体单个二进制/脚本,平台侧只提供接口,不在客户环境装依赖
skill 文档上面的三段结构,随 DSH 加载,AI 装了就能用
使用说明给"人"的:装在哪、怎么登录、常见问题
边界说明明确哪些动作不开放给 AI 自动执行

客户拿到的使用方式就是一句话:

text
把 <业务对象> 在 <环境> 上按 <规格文件> 发出去,先给我看会变什么,我确认后再执行。

安全阀(这几条是上线前提,不是可选项) ​

  1. 权限:CLI 用自己的最小权限凭据,不复用客户的超级权限。
  2. 留痕:每个写操作落一条审计日志(谁、什么时候、对什么、before/after)。
  3. 危险操作拦截:即使 AI 要求,delete / rollback 这类也必须人显式确认。
  4. 能力白名单:不提供就是最硬的限制。

7. 结论与踩坑清单 ​

值得复用的模式一句话:CLI 是手,skill 是说明书,AI 是操作者。三者分开之后,任何一方升级都不会把另外两方搞崩。

如果只记一条判据:别自造生态已有的入口;但消费者是 AI 时,命令面必须自己设计。

踩坑清单(按代价排序):

  1. 先做错了一版"给人的通用 CLI":5800 行、测试全绿,仍然要归档。方向错误时,完成度越高越浪费。
  2. 命令粒度按端点设计:AI 要拼路径、方法、签名、body 才能发一个请求,等于把接口面的复杂度原样转给了它。
  3. 输出字段不稳定:字段名随命令变化,AI 的解析逻辑就得跟着变,最终变成"每次都可能失败"。
  4. 错误信息对 AI 不可读:没有 retryable / hint,AI 只能靠猜,猜错的代价是重复的副作用。
  5. 写操作缺幂等:AI 把超时当失败并重试,产生重复单据——这是唯一可能造成真实损失的一条。
  6. 凭据进了对话上下文:一次截图、一段日志就泄露;凭据必须留在环境/凭据文件里。
  7. 依赖不可得时重写生态(8 月那次):先解决网络,再考虑动手。
  8. --help 与 skill 文档不同源:AI 不会主动怀疑过期的文档,只会拿着旧命令名一直撞墙。

8. 相关 ​

  • 文中反例 apiflow 的完整归档记录(放弃原因 / 可复用资产 / 技术坑):见本仓库 archive/2026-08-19-apiflow/ARCHIVED.md
  • 另一条同源实践(把浏览器能力给 AI 用):ai-ui-testing,已发布 PyPI 0.2.2

用 VitePress + Meoo CLI 发布

Meoo By Meoo 秒悟