Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力

Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力

变化分层: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 diagnoseresource 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