Appearance
不给 AI 直接调 HTTP:把平台能力收敛成一个 CLI
让 AI 直接调平台 HTTP 接口,卡住的通常不是 AI,是接口面——它是给人(或给前端)写的,不是给"一个只会读文档、会犯错、需要重试的调用者"写的。 2026 年 8 月我写过一个接口自动化框架 apiflow,功能齐了却主动归档,原因是方向错:我在自造一个通用 CLI。 9 月到 10 月,同样形态的东西又做了一遍——这次是给 AI 用的,也已经用在实际项目的交付里。 这篇把两次的区别、CLI 与 skill 的分工、错误与幂等怎么设计、以及踩到的坑写清楚。
⚠️ 脱敏声明:文中命令名
platctl、接口路径、字段名、JSON 输出、对话内容全部是构造样例,不是任何真实平台的信息;客户名、内网地址、真实字段与凭据一律不出现。
0. 结论先行
让 AI 直接调平台 HTTP 接口,卡住的地方通常不在 AI,而在接口面:认证、幂等、错误语义、编排,没有一样是为机器设计的。
把平台能力收敛成一个 CLI,粒度定在「AI 说得清的目标」上,再配一份 skill 当说明书,AI 才能稳定跑完整条业务流程。
我在这件事上先做错过一版:2026 年 8 月的 apiflow(37 个 Go 文件、约 5800 行、
go test ./...全绿),8 月 19 日自己把它归档了。9–10 月换成"给 AI 用的接口 CLI + skill",跑通并用在实际交付里。两次的分界不是"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 -json | Jenkins / GitLab / GitHub Actions / Allure 本来就能直接吃 JUnit,自定义格式等于自建适配层 |
| YAML 定义 + 自研 runner 接管一切 | 定义作数据、用例即 Go 代码 | 失败定位、断言语义、子测试层级全部自己背 |
正确的形态应该是 apiflow.RunScenario(t, "scenarios/checkout.yaml")——场景/DAG 落到 t.Run 子测试,入口就是 go test ./...。
教训一:生态里已有标准入口时,自造 CLI 是在重造流量入口。
2.3 两个值得记住的技术坑
- DAG 并行分支的变量竞态:嵌套场景节点与主链并行时会互相覆盖同名变量(比如
orderId),下游断言拿到的是别的分支的数据。解法是嵌套场景在隔离的变量空间里执行(快照入参、不外泄),而 API 节点保持共享上下文以支持跨节点传值。 - 零依赖的代价:当时本机网络访问不了 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 其余四条硬约束
- 幂等键:每个写命令带
--idempotency-key(不给时由 CLI 按"动作 + 目标 + 参数哈希"生成),重试不会产生第二条申请单。这条不能靠 AI 自觉,得是 CLI 的默认行为。 - 凭据不进上下文:Token 从环境变量或凭据文件读,绝不作为参数出现在命令行里,更不进入对话。AI 看不到密钥,也就泄漏不了。
- 可发现性同源:
--help和给 AI 的 skill 文档由同一份命令定义生成,防止两边漂移。 - 能力白名单: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 渐进式披露的三段结构
- 入口段:一句话说清这个 CLI 能干什么、什么时候用、什么时候别用(比如"需要人工审批的动作不要自动化")。控制在十几行。
- 常用命令段:按业务目标组织("发布一个配置"、"查审批状态"),每条给一条可直接复制的命令 + 一个真实输出样例。
- 排错段:把 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 执行,
会带幂等键,重复执行不会重复改。这段流程里有四个细节值得单独说:
- 每一步都有真实的原始输出。AI 的判断建立在 JSON 字段上(
"ready":2可以被断言),不是建立在它对平台"应该是什么样"的记忆上。一句"部分实例未就绪"是没法当证据的。 - 危险动作先 dry-run。人只需要审 diff,不需要审命令拼写。
- AI 主动给下一步命令。是 skill 第 3 段让它能提出
--only-not-ready,而不是泛泛地说"需要进一步排查"。 - 幂等键在人类没提的时候也必须带。这是 CLI 的默认行为,不是 AI 的自觉。
6. 交付:把 AI 运行环境一起打包
最终交付的形态不是一个 exe,是"一套能被对话驱动的能力":
| 交付物 | 内容 |
|---|---|
| CLI 本体 | 单个二进制/脚本,平台侧只提供接口,不在客户环境装依赖 |
| skill 文档 | 上面的三段结构,随 DSH 加载,AI 装了就能用 |
| 使用说明 | 给"人"的:装在哪、怎么登录、常见问题 |
| 边界说明 | 明确哪些动作不开放给 AI 自动执行 |
客户拿到的使用方式就是一句话:
text
把 <业务对象> 在 <环境> 上按 <规格文件> 发出去,先给我看会变什么,我确认后再执行。安全阀(这几条是上线前提,不是可选项)
- 权限:CLI 用自己的最小权限凭据,不复用客户的超级权限。
- 留痕:每个写操作落一条审计日志(谁、什么时候、对什么、before/after)。
- 危险操作拦截:即使 AI 要求,
delete/rollback这类也必须人显式确认。 - 能力白名单:不提供就是最硬的限制。
7. 结论与踩坑清单
值得复用的模式一句话:CLI 是手,skill 是说明书,AI 是操作者。三者分开之后,任何一方升级都不会把另外两方搞崩。
如果只记一条判据:别自造生态已有的入口;但消费者是 AI 时,命令面必须自己设计。
踩坑清单(按代价排序):
- 先做错了一版"给人的通用 CLI":5800 行、测试全绿,仍然要归档。方向错误时,完成度越高越浪费。
- 命令粒度按端点设计:AI 要拼路径、方法、签名、body 才能发一个请求,等于把接口面的复杂度原样转给了它。
- 输出字段不稳定:字段名随命令变化,AI 的解析逻辑就得跟着变,最终变成"每次都可能失败"。
- 错误信息对 AI 不可读:没有
retryable/hint,AI 只能靠猜,猜错的代价是重复的副作用。 - 写操作缺幂等:AI 把超时当失败并重试,产生重复单据——这是唯一可能造成真实损失的一条。
- 凭据进了对话上下文:一次截图、一段日志就泄露;凭据必须留在环境/凭据文件里。
- 依赖不可得时重写生态(8 月那次):先解决网络,再考虑动手。
--help与 skill 文档不同源:AI 不会主动怀疑过期的文档,只会拿着旧命令名一直撞墙。
8. 相关
- 文中反例 apiflow 的完整归档记录(放弃原因 / 可复用资产 / 技术坑):见本仓库
archive/2026-08-19-apiflow/ARCHIVED.md - 另一条同源实践(把浏览器能力给 AI 用):
ai-ui-testing,已发布 PyPI 0.2.2