把 HTTP/RPC 能力沉淀为可审计 CLI 契约层,让 Agent 接入更稳、更安全。
推荐阅读 :AI 小老六
AI 工程化最容易踩的坑,是把基础设施建在变化最快的东西上。模型会换,Agent 框架 会换,工具协议也会换;真正值得沉下去建设的,是那些能长期描述业务能力、权限边界和调用语义的契约。
这篇文章讨论一种更稳的做法:把业务接口整理成可生成、可校验、可审计的 CLI 契约层,再用 Skill 把多步命令收成面向任务的能力包。它不绑定某个具体 Agent,也不要求业务团队押注某个短期热门框架。上层怎么变,底层能力都还能被复用。
变化分层:Agent 基建应该压在哪一层
如果把 AI 应用的技术栈按变化速度拆开,会发现它不是一条直线,而是一组稳定性差异很大的层。
图:越往底层越适合承载长期业务契约。
模型和 Agent 框架适合快速试错,不适合作为业务基础设施的根。原因很简单:它们变化太快,团队越是把能力绑定在这些层上,后续迁移成本越高。
更稳的切入点是业务能力契约。它向下连接 HTTP / RPC,向上暴露给不同 Agent,自己负责把接口能力、权限边界、调用说明和返回结构固定下来。这样一来,业务系统不需要跟着每个 Agent 框架重做一遍接入。
CLI Contract Layer:把能力边界变成模型看得懂的命令
工具协议当然有价值,但它并不总是最低成本的入口。对于大量业务接口来说,CLI 有几个天然优势。
第一,命令行是模型熟悉的表达方式。预训练语料里有大量 shell、CI/CD、运维脚本和命令帮助信息,模型知道如何读 -h、如何看 Usage、如何组合子命令。第二,CLI 支持渐进式披露。模型不需要一开始拿到全部工具 schema,它可以先看一级命令,再看二级命令,最后只读取目标命令的参数。第三,命令可以被真实执行、干跑、记录和审计,工程闭环比较直接。
这并不意味着 CLI 要取代所有工具协议。更合理的定位是:把 CLI 作为业务能力的稳定契约层,让 MCP、Workflow、Agent Runtime 等上层系统按需消费它。
图:CLI 位于 Agent 编排与后端接口之间,承接稳定调用契约。
这里的关键不是"写一堆命令",而是把命令变成契约:它必须知道自己面向谁、能做什么、不能做什么、参数怎么裁剪、结果怎么返回、失败时如何解释。
Command Metadata:用配置把接口编译成命令
手写 CLI 命令很快会失控。业务接口多、字段多、变更频繁,如果每接一个接口都写一份命令代码,最后维护成本会压过收益。
更可维护的方案是让接口配置成为事实来源。研发只描述接口的路由、入参、出参、分页规则和说明文案,构建器根据配置生成命令定义,再由 CLI 框架动态注册命令树。
图:接口配置成为命令生成、注册、打包和调用的事实来源。
一个脱敏后的 API 层配置可以长这样:
method: "POST"
path: "/api/domain/order/list"
service: "order.service"
pagination:
enabled: true
page_param: "page.page_no"
size_param: "page.page_size"
default_page: 1
default_page_size: 20
max_page: 20
max_page_size: 50
merge_path: "data.items"
total_path: "data.total"
input:
mode: "include"
params:
- name: "resource-id"
request_name: "resource_id"
type: "string"
required: true
description: "资源 ID,只能传当前上下文允许访问的资源。"
example: "--params '{\"resource-id\":\"123\"}'"
output:
mode: "include"
params:
- "data.total"
- "data.items[*].id"
- "data.items[*].status"
- "data.items[*].updated_at"
docs:
summary: "查询资源列表"
description: "按资源 ID 查询当前身份可见的列表数据。"
notes:
- "只允许传入 resource-id,其他字段由服务端上下文注入。"
- "分页参数由网关统一处理,调用方不要手写下游分页结构。"
这类配置看起来普通,但它解决的是一个非常实际的问题:命令不再是散落在代码里的手工逻辑,而是一份可生成、可测试、可审计的能力说明。
Runtime Gateway:模型只表达意图,安全留在服务端
让 Agent 直接拼接口请求,是很危险的设计。模型可能传错身份、误填资源 ID、把不该暴露的字段带出去,也可能在上下文混乱时调用到错误环境。
更稳的做法是让 CLI 只负责表达意图,把真正的鉴权、权限判断、环境识别、字段裁剪和转发都放在服务端网关。
图:安全边界收敛在服务端网关,而不是交给模型自行判断。
这个设计的分工很清楚:Agent 负责理解用户要做什么,CLI 负责把意图变成标准命令,网关负责判断这件事能不能做。身份和权限不交给模型决定。
通用安全闸口通常包括这几类:
| 闸口 | 服务端要判断什么 | 为什么不能交给模型 |
|---|---|---|
| 执行环境识别 | 命令来自哪个宿主、是否允许调用 | 模型可能无法区分测试环境、生产环境和本地模拟环境 |
| 登录态校验 | 当前身份是否真实有效 | 登录态不能进入模型上下文,更不能由模型拼接 |
| 垂直权限校验 | 当前身份是否有该功能权限 | 模型不知道组织权限树,也不应猜测 |
| 水平权限校验 | 当前身份是否能访问目标资源 | 资源 ID 必须来自服务端上下文或可信入口 |
| 命令白名单 | 只允许已注册命令执行 | 防止模型构造未开放能力 |
| 参数裁剪 | 只接收配置允许的字段 | 防止模型把多余字段传给下游 |
| 输出裁剪 | 只返回 Agent 需要的字段 | 降低敏感字段暴露风险 |
有了这层网关,上层可以换 Agent,也可以换编排框架,但安全边界不跟着漂移。
API 与 Actions:铺量和意图不要混在一起
CLI 命令通常需要分两层做。
API 层追求覆盖速度。一个配置对应一个 HTTP / RPC 接口,适合把现有系统能力快速变成命令。它的命名可能比较贴近接口,优点是接入快、成本低。
Actions 层追求语义准确。一个命令对应一个业务动作,背后可以串多个接口、补参数、做判断、加确认点。它的命名应该更接近用户意图,也更适合被 Skill 编排。
| 维度 | API 层命令 | Actions 层命令 |
|---|---|---|
| 目标 | 快速覆盖已有接口 | 把多步能力封成业务动作 |
| 命令粒度 | 一个接口一个命令 | 一个动作一个命令 |
| 命名方式 | 接近 method / path / resource | 接近领域动作,如 case diagnose、resource submit |
| 编排能力 | 通常只做转发和字段裁剪 | 可串联 HTTP / RPC,可做参数加工和状态判断 |
| 适用阶段 | 初期铺量、能力盘点、内部调试 | 面向 Agent 和 Skill 的稳定交付 |
| 风险控制 | 依赖通用网关 | 叠加动作级权限、确认点和灰度开关 |
Actions 层配置可以更像"写给模型的操作说明":
enable: true
cmds: "case diagnose"
strategy: "case_diagnose"
permission:
path: "/domain/case/diagnose"
pagination:
default_page: 1
default_page_size: 10
max_page: 20
max_page_size: 50
params:
- name: "case-id"
type: "string"
required: true
description: "问题单 ID。只有用户明确给出问题单时才传。"
example: "--case-id=CASE123"
- name: "with-detail"
type: "bool"
required: false
description: "是否返回明细。用户要求解释原因或排查证据时才传 true。"
docs:
summary: "诊断问题单"
description: "查询问题单状态、关联资源和可解释的诊断结果。只用于诊断,不执行修改。"
notes:
- "没有 case-id 时不要猜测,应先向用户确认。"
- "本命令不会提交、撤销或修改任何资源。"
- "需要执行修改时,应切换到带确认流程的 action。"
这里最有价值的不是 YAML 本身,而是这些描述把模型的取舍写清楚了:什么时候该传参数,什么时候不能执行,什么时候要换命令。
Progressive Disclosure:让模型按需读取命令面
工具太多时,把所有 schema 一次性塞进上下文,会让模型注意力变差。CLI 的 list 和 -h 可以把这个问题拆开。
模型先看一级命令,确定领域;再看二级命令,确定动作;最后只读取目标命令的参数说明和示例。上下文更小,误选工具的概率也会下降。
# 查看可用领域
domain-cli list
# 查看某个领域下的动作
domain-cli case list
# 查看目标命令的参数、示例和边界
domain-cli case diagnose -h
# 先 dry-run,看即将发出的标准请求
domain-cli case diagnose --case-id=CASE123 --with-detail=true --dry-run
为了让模型真的用得稳,通用 flag 也要统一设计:
| Flag | 作用 | 设计要点 |
|---|---|---|
--params |
传查询参数 | 只接受 JSON object,避免任意字符串透传 |
--data |
传请求体 | 与 --params 分开,降低 GET / POST 混用 |
--page-all |
自动翻页 | 服务端控制最大页数 |
--page-size |
单页条数 | 有默认值和上限 |
--page-limit |
最大翻页数 | 防止模型无限拉取 |
--dry-run |
预览请求 | 修改类动作必须先经过确认流程 |
--env |
指定环境 | 默认生产环境时要更谨慎,测试包可注入默认隔离环境 |
-h |
||
/--help |
查看帮助 | 帮助文案要写清楚"什么时候不要用" |
好的帮助信息不是参数字典,而是决策指南。它要告诉模型:这个命令解决什么问题,不能解决什么问题,哪些参数只有在用户明确表达时才能传。
Skill Envelope:把命令链收成会办事的能力
CLI 解决"能力能不能被调用",Skill 解决"任务能不能被办完"。
真实用户很少会说"请调用某个接口查询某个字段"。他们会说"帮我排查为什么失败""看看这个资源能不能提交""把符合条件的项整理出来"。这些请求背后往往不是一条命令,而是一段流程。
Skill 的职责就是把命令链、判断逻辑、确认点和输出格式封起来:
图:Skill 负责把命令链、判断逻辑、确认点和输出格式收成任务能力。
一个成熟的 Skill 至少要写清楚这些内容:
| 模块 | 要写清楚什么 |
|---|---|
| 适用场景 | 用户用什么说法会触发这个能力 |
| 可用命令 | 允许调用哪些 CLI,禁止调用哪些 CLI |
| 参数取舍 | 哪些参数必须来自用户,哪些来自上下文,哪些不能由模型编 |
| 风险动作 | 哪些步骤需要二次确认,确认文案怎么写 |
| 输出格式 | 返回表格、诊断结论、下一步建议还是执行结果 |
| 失败处理 | 权限不足、资源不存在、下游失败时怎么解释 |
这样封装后,业务团队交付给 Agent 的就不是一堆接口,而是一组可复用、可治理、可升级的能力。
测试门禁:先验证会拦,再谈上线
很多团队测试 CLI 时只看"能不能调通"。这不够。对 Agent 可调用能力来说,更重要的是确认它会在该拦的地方拦住。
上线前至少要跑四类检查:
| 检查项 | 要验证什么 |
|---|---|
| 框架校验 | 命令注册、参数类型、帮助信息、输出裁剪是否符合配置 |
| 登录态失败 | 缺失或过期身份是否被拒绝 |
| 垂直越权 | 没有功能权限的身份是否无法调用 |
| 水平越权 | A 资源上下文是否无法访问 B 资源 |
| dry-run | 写操作是否能先生成可读的执行预览 |
| 审计日志 | 每次调用是否能追到命令、参数摘要、身份、环境和结果 |
本地模拟也有价值,但它只能解决"能否快速调试"的问题。真正的安全边界要在接近真实的环境里验证,因为身份、权限和资源隔离往往只有在完整链路里才会暴露问题。
构建和发布也建议拆出环境:
# 本地构建并生成命令树
make build TARGET=internal
# 打测试包,默认连到隔离环境
make package TARGET=internal CHANNEL=test DEFAULT_ENV=sandbox
# 只在测试验证完成后发布正式包
make publish TARGET=internal CHANNEL=release
版本管理要非常克制。只要命令行为、参数含义或输出结构发生变化,就应该有可追踪的版本记录;如果某个命令仍在灰度,最好显式关闭或只在测试包里暴露。
结语:别把 Agent 接口做成一次性脚手架
AI 应用会继续变化,短期内也很难出现一个所有团队都满意的统一工具形态。越是在这种阶段,越应该把基础设施建在更稳的层上。
把 HTTP / RPC 能力整理成 CLI 契约层,是一种偏工程的选择:它不炫,但能管住身份、权限、参数、输出和审计。再往上一层,用 Skill 把多条命令封装成可交付任务,业务能力就不会被某个 Agent 框架锁死。
真正值得建设的不是"今天最流行的 Agent 接入方式",而是一套长期能被不同 Agent 消费的能力契约。接口会变,模型会变,框架也会变;只要契约层稳住,业务能力就不用每次从头再接一遍。