从 CLI-Anything 到 CLI-Any-Webapi —— 我是如何为 AI Agent 打造 API 调用利器的

0 阅读7分钟

说明:本文是笔者在实践 HKUDS 推出的 CLI-Anything 项目后,独立开发 CLI-Any-Webapi 的完整心路历程。

背景

CLI-Anything 是香港大学数据科学实验室(HKUDS)推出的一个 Claude Code 插件,已经开源。其核心理念非常有意思:CLI 是 AI Agent 的"原语"

什么意思呢?传统的自动化方案往往是让 AI 模拟人类操作浏览器——点击按钮、填写表单、等待加载……这种方式不仅效率低下,还极其脆弱。而 CLI 不一样,它天然就是程序化调用的最佳载体:

  • 输入输出结构化(JSON)
  • 无需维护 UI 状态
  • 可组合、可管道
  • 错误处理清晰(exit code)

CLI-Anything 正是基于这个理念,提供了一套 CLI Harness 方法论,指导 Agent 如何将任意系统转换为 CLI 工具。这让我眼前一亮——如果能把公司内部的 Web API 全部转成 CLI,那 AI Agent 岂不是可以直接调用后端接口,完成各种自动化任务?

实践遇坑

说干就干。笔者按照 CLI-Anything 的指引,尝试让 Claude 为一个 NestJS 后端项目生成 CLI。

结果嘛……怎么说呢,能跑,但不好用

具体问题如下:

问题表现
类型推断粗糙大部分参数是 any,失去了 TypeScript 的意义
命令结构混乱没有统一的三级命令规范,每次生成风格都不一样
认证方式错误简单使用 Cookie 作为认证机制,没有考虑到 Token 认证
依赖过重动不动就引入 axios、got 等库,包体积膨胀
API缺失部分API没有识别出来

最无法接受的是最后一点。缺少 API 那还玩个毛线啊!!!

于是,一个想法在脑海中萌生:能不能基于 CLI-Anything 的方法论,开发一个专门针对 Web API 的生成器?

剖析 CLI-Anything,搭建核心框架

在动手之前,我先仔细研究了 CLI-Anything 的设计思路。核心在于它的 Harness 方法论——一套标准化的生成流程:

   启动
    │
    ▼
1.  🔍 分析 - 扫描源码,映射 API
2.  📐 设计 - 规划命令结构
3.  🔨 实现 - 构建 CLI
4.  📋 规划测试
5.  🧪 编写测试
6.  📝 文档生成
    │
    ▼
可用的 CLI 工具,可发布安装

这套方法论的精髓在于:Agent 不是漫无目的地生成代码,而是按照严格定义的阶段逐步推进。每个阶段有明确的输入、输出和验证标准。

但 CLI-Anything 是个通用框架,它要处理各种类型的系统——文件系统、数据库、消息队列……这种通用性反而成了它的软肋:针对 Web API 这个垂直场景,它没有做足够的特化。

基于此,我确定了 CLI-Any-Webapi 的设计方向:

1. 聚焦 Web API 场景

不追求通用,只做一件事:把 HTTP REST API 转换为类型安全的 TypeScript CLI

2. 强化类型推断

CLI-Anything 的类型推断依赖 Agent 的"临场发挥",质量参差不齐。考虑到web api 部署的复杂性(这一点无法在源码中得到体现)。我的补偿方案是引入 API 请求日志(HAR、CSV、JSONL)作为类型推断的数据源。真实的请求响应数据,比任何猜测都准确。

3. 统一命令结构

定义死三级命令格式,不给 Agent 发挥空间:

<project>-api-cli <module> <method> <path> [options]

示例:
my-app-api user get /api/v1/user --params='{"page":1}'
my-app-api order post /api/v1/order --params='{"productId":1}'

4. 配置驱动认证

支持多种认证方式,认证信息(Token、Cookie)全部放到项目本地的 config.json,代码里只做注入,不做存储:

{
  "baseUrl": "http://localhost:3000",
  "headers": {
    "Cookie": "name=xxx;tts=xxx"
    "Authorization": "Bearer <your-token>"
  },
  "timeout": 30000
}

切换测试环境?改配置文件就行,CLI 代码一行不动。

5. 详尽的 AI 文档

生成的 CLI 必须附带 SKILL.md,且 每个端点都要有独立的调用示例。这份文档就是 AI Agent 的"说明书"——没有它,Agent 就是睁眼瞎。

实施方案:拥抱原生 API

设计确定后,进入实施阶段。这里重点说一个关键决策:HTTP 客户端选型

告别 axios,拥抱原生 fetch

CLI-Anything 生成的代码通常会引入 axios 作为 HTTP 客户端。这很合理——axios 的 API 设计确实优雅,拦截器、超时、错误处理都很方便。

但在我的场景里,axios 是个"累赘":

特性axios原生 fetch (Node >= 18)
外部依赖1 (+ 传递依赖)0
包体积~40KB (gzip)0
超时控制timeout 选项AbortController
类型安全AxiosRequestConfig自定义 ClientRequestOptions

Node.js 18+ 已经内置了全局 fetch API,功能完全够用。既然如此,何必多此一举?

于是我在 HARNESS.md 里写死了一条硬性约束:

严禁引入任何第三方 HTTP 库(axios、got、node-fetch、undici、ky、superagent 等),违反此规则视为生成失败。

这不是偏执,而是"约束即自由"——把选择权从 Agent 手里拿走,反而能保证生成结果的一致性。

HTTP 客户端模板实现

来看看最终的 http-client.ts.template

import { loadConfig } from './config.js';

const cfg = loadConfig();

export interface ClientRequestOptions {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  path: string;
  params?: Record<string, unknown>;
  body?: Record<string, unknown>;
  headers?: Record<string, string>;
  baseUrl?: string;
}

export const client = {
  async request<T = unknown>(options: ClientRequestOptions): Promise<T> {
    const baseUrl = (options.baseUrl ?? cfg.baseUrl).replace(/\/+$/, '');
    let url = `${baseUrl}${options.path}`;

    // Query 参数拼接
    if (options.params && Object.keys(options.params).length > 0) {
      const qs = new URLSearchParams();
      for (const [k, v] of Object.entries(options.params)) {
        if (v !== undefined && v !== null) qs.append(k, String(v));
      }
      url += `?${qs.toString()}`;
    }

    // 超时控制:AbortController
    const controller = new AbortController();
    const timeout = cfg.timeout ?? 30000;
    const timer = setTimeout(() => controller.abort(), timeout);

    try {
      const res = await fetch(url, {
        method: options.method,
        headers: {
          'Content-Type': 'application/json',
          ...cfg.headers,  // 从配置注入认证头
          ...options.headers,
        },
        body: options.body ? JSON.stringify(options.body) : undefined,
        signal: controller.signal,
      });

      clearTimeout(timer);

      const text = await res.text();
      let data: unknown;
      try {
        data = JSON.parse(text);
      } catch {
        data = text;
      }

      if (!res.ok) {
        console.error(JSON.stringify({ error: true, status: res.status, data }, null, 2));
        process.exit(1);
      }

      return data as T;
    } catch (err: unknown) {
      clearTimeout(timer);
      if (err instanceof DOMException && err.name === 'AbortError') {
        console.error(JSON.stringify({ 
          error: true, 
          status: 0, 
          data: `Request timeout after ${timeout}ms` 
        }, null, 2));
      } else {
        console.error(JSON.stringify({ error: true, status: 0, data: String(err) }, null, 2));
      }
      process.exit(1);
    }
  },
};

代码不复杂,但每一行都有讲究:

  1. 超时用 AbortController:这是原生 fetch 处理超时的标准方式,比 Promise.race 更优雅
  2. Headers 三层合并:Content-Type → config.headers → options.headers,优先级递增
  3. 错误输出到 stderr:方便 Agent 通过 exit code 判断执行结果
  4. JSON 解析兜底:响应不是 JSON?没关系,返回原始文本

配置文件设计

另一个关键点是配置文件的位置。最初我想放到 ~/.cli-any-webapi/config.json——全局统一管理,多方便!

但转念一想,这会出问题:用户可能同时生成多个 CLI 项目(连接不同后端),公共配置会互相覆盖。

最终决定:每个 CLI 项目独享自己的配置文件,放在项目根目录下:

my-backend-api-cli/
├── config.json        ← 项目本地配置
├── package.json
├── src/
│   └── config.ts      ← 配置读取逻辑
└── ...

config.ts 的定位逻辑:

function getConfigPath(): string {
  // 从编译产物位置(dist/)向上一级即为项目根目录
  const __dirname = dirname(fileURLToPath(import.meta.url));
  const projectRoot = resolve(__dirname, '..');
  return resolve(projectRoot, 'config.json');
}

这样一来,npm link 后全局调用也能正确找到配置文件。

7 阶段生成流程

最后,整理出完整的生成流程(详见 HARNESS.md):

Phase任务输出
0输入验证环境检查通过
1路由发现端点映射表 (TEST.md Part 1)
2类型推断TypeScript 类型定义
3模块分组module → endpoints 映射
4类型生成src/types/<module>.ts
5命令生成src/commands/<module>.ts
6配置生成package.json, tsconfig.json, SKILL.md
7验证npm build + --help 测试

每个阶段都有明确的校验标准,Agent 必须逐一完成才能进入下一阶段。这种"流水线"式的约束,是保证生成质量的关键。

效果展示

开发完成后,来看看实际效果。以一个 NestJS 后端为例:

# 生成 CLI(源码 + HAR 日志)
/cli-any-webapi generate ./my-backend --logs ./api-logs.har

# 构建安装
cd my-backend-api-cli
npm install && npm run build && npm link

# 配置认证
cat > config.json << 'EOF'
{
  "baseUrl": "http://localhost:3000",
  "headers": { "Authorization": "Bearer eyJ..." }
}
EOF

# 开始使用!
my-backend-api user get /api/v1/user --params='{"page":1,"pageSize":20}'
my-backend-api user post /api/v1/user --params='{"username":"alice","email":"alice@x.com"}'
my-backend-api user delete /api/v1/user/:id --path-params='{"id":"42"}'

生成的 SKILL.md 包含每个端点的详细调用示例:

GET /api/v1/user — 获取用户列表

my-backend-api user get /api/v1/user --params='{"page":1,"pageSize":20}'

请求参数 (query):

参数类型必填说明
pagenumber页码,默认 1
pageSizenumber每页条数

响应示例:

{ "total": 100, "list": [{ "id": 1, "username": "alice" }] }

AI Agent 拿到这份文档,就能准确调用任意 API——这才是"AI 友好"的真正含义。

总结

  1. CLI-Anything 提供了优秀的 Harness 方法论,但在 Web API 场景下还不够特化;
  2. CLI-Any-Webapi 聚焦 HTTP REST API,通过引入请求日志实现精准类型推断;
  3. 拥抱原生 API(fetch、AbortController、URLSearchParams),减少外部依赖;
  4. 配置驱动认证 + 项目本地配置,解决多环境切换问题;
  5. 强制生成 SKILL.md,让 AI Agent 能够"自学"如何调用 CLI。

项目已开源:CLI-Any-Webapi