我们如何利用 WebCMD 范式将网页导航延迟降低 95%,减少 LLM token 消耗 90%,并建立一个确定性的、schema 校验的命令行接口。


1. AI 代理的网页导航困境

在当前 AI 驱动的自动化环境中,软件代理经常需要浏览网页平台以收集情报、监控讨论或与系统交互。传统上,这通过无头浏览器(如 Puppeteer、Playwright 或 Selenium)实现。

虽然无头浏览器具有高度灵活性,但也伴随着巨大的隐藏成本:

  • 高启动和渲染延迟:初始化 Chromium 实例、进行 DNS 解析、加载 CSS/JS 资源以及渲染 DOM,每个请求耗时 3 到 5 秒
  • 巨大的资源占用:并行运行多个无头 Chrome 进程会激增 CPU 和内存使用,造成可扩展性瓶颈。
  • Token 膨胀:将原始 HTML 或 markdown 转换后的 DOM 发送给大型语言模型(LLM)会消耗数千 token。一个简单的故事列表网页表示可能轻松达到 20KB–50KB(相当于 5,000+ tokens)。
  • 脆弱的 UI 选择器:网页抓取依赖于 CSS 选择器或 XPath 查询,一旦平台布局发生变化就会失效,导致代理流水线静默失败或产生幻觉。

为了解决这些低效问题,我们构建了 HN CLI (hn-cli),一个利用 WebCMD 注册表的高性能 Hacker News 命令行适配器。通过将网页交互与浏览器渲染解耦,我们为 AI 代理提供了一个结构化、快速且 token 高效的桥梁。


2. 介绍 WebCMD 范式

WebCMD 是一个允许开发者为网页平台定义命令行适配器的框架。通常,WebCMD 通过启动无头浏览器并在目标页面上执行脚本来运行命令。然而,WebCMD 支持一种称为 Strategy.PUBLIC 的辅助执行策略,并设置 browser: false

按此配置时:

  1. 适配器直接在本地 Node.js 环境中执行。
  2. WebCMD 完全跳过启动 Chromium,消除渲染和启动开销。
  3. 适配器通过公共 REST 端点与目标站点交互,实现 亚 200ms 延迟
  4. 输出默认格式化为结构化 JSON,可立即被 LLM 代理读取。
graph TD
    subgraph Execution flow with WebCMD
        Agent[AI Agent or Developer] -->|webcmd hn top| Registry[WebCMD Registry]
        Registry -->|Load Adapter| Adapter[HN Adapter]
        Adapter -->|Import helpers| Utils[adapters/utils.js]
        Utils -->|REST HTTP Get| API[Firebase HN API / Algolia API]
        API -->|JSON Response| Utils
        Utils -->|Validate Types| Schema[Schema Validator]
        Schema -->|Clean Output| Print[Stdout JSON / WebCMD Table]
    end

Enter fullscreen mode Exit fullscreen mode


3. Hacker News 适配器架构

hn-cli 仓库采用清晰的模块化结构,分离了 CLI 命令定义、核心工具逻辑、测试和配置脚本:

  • adapters/:包含命令定义。每个文件注册一个子命令。
    • top.js:获取热门故事。
    • newest.js:获取新提交的故事。
    • ask.js:检索“Ask HN”帖子。
    • jobs.js:获取职位发布详情。
    • item.js:解析单个帖子的详细信息(包括自述文本)。
    • search.js:对接 Algolia 搜索 API。
    • utils.js:核心辅助函数(网络重试、schema 校验、HTML 实体清理)。
  • tests/:完全模拟的 Vitest 测试套件,用于离线测试。
  • scripts/:注册适配器、验证子进程运行和代码检查。

利用最佳 API

CLI 没有抓取 news.ycombinator.com,而是直接与两个官方端点通信:

  1. Firebase Hacker News REST API:适合获取实时热门/最新/Ask 故事 ID 以及项目详情。
  2. Algolia Hacker News Search API:适合按文本相关性或日期查询文章。

4. hn-cli 中的关键工程模式

为了确保 CLI 适合自主代理执行,代码库实现了三种重要模式:并发性弹性确定性

A. 使用 Promise.all 实现并发

Hacker News Firebase API 由一系列单独的项目端点组成。要获取前 30 个故事,我们需要:

  1. 获取前 500 个故事 ID 列表(一次 HTTP 调用)。
  2. 获取前 30 个 ID 的元数据(30 次单独的 HTTP 调用)。

顺序执行将导致极差的延迟(30 × 100ms = 3 秒)。相反,hn-cli 使用 Promise.all 并发获取:

const storyIds = await fetchJson('https://hacker-news.firebaseio.com/v0/topstories.json');
const idsToFetch = storyIds.slice(0, limit);
const fetchedItems = await Promise.all(
  idsToFetch.map(id => fetchJson(`https://hacker-news.firebaseio.com/v0/item/${id}.json`).catch(() => null))
);

Enter fullscreen mode Exit fullscreen mode

B. 通过指数退避实现弹性

AI 代理运行成本高昂。如果单个网络数据包丢失或触发 API 限制,代理不应失败。我们在 fetchWithRetry 中构建了带指数退避的健壮重试循环:

export async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1000) {
  const timeoutMs = options.timeout || 10000;
  for (let i = 0; i < retries; i++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const res = await fetch(url, { ...options, signal: controller.signal });
      clearTimeout(timeoutId);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      clearTimeout(timeoutId);
      if (i === retries - 1) {
        throw new Error(`Failed to fetch ${url} after ${retries} attempts: ${err.message}`);
      }
      // Wait: 1s, 2s, 4s...
      await new Promise((resolve) => setTimeout(resolve, backoff * Math.pow(2, i)));
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

C. 通过 JSON Schema 校验实现确定性

AI 代理期望特定结构的数据。如果 API 返回缺失字段,代理可能会遇到未定义行为或产生幻觉。hn-cli 在将数据发送到 stdout 前,使用 validateSchema() 执行严格的 schema 检查:

export function validateSchema(data, schema) {
  const items = Array.isArray(data) ? data : [data];
  for (const item of items) {
    for (const [key, expectedType] of Object.entries(schema)) {
      const value = item[key];
      if (value === undefined || value === null) {
        throw new CommandExecutionError(`Validation Error: Missing required field "${key}" in output schema.`);
      }
      const actualType = typeof value;
      if (!expectedType.split('|').includes(actualType)) {
        throw new CommandExecutionError(`Validation Error: Field "${key}" is "${actualType}" but expected "${expectedType}".`);
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode


5. 实现代码演练

让我们看看如何注册一个 WebCMD 适配器。下面是 top.js 的简化实现,演示如何定义元数据和执行例程。

定义命令

import { cli, Strategy } from '@agentrhq/webcmd/registry';
import { fetchJson, getRelativeTime, handleOutput, validateSchema, validatePositiveInt } from './utils.js';

const STORY_SCHEMA = {
  rank: 'number',
  title: 'string',
  url: 'string',
  hn_url: 'string',
  points: 'number',
  author: 'string',
  comments: 'number',
  age: 'string'
};

cli({
  site: 'hn',
  name: 'top',
  access: 'read',
  description: 'Return the top stories from Hacker News',
  domain: 'news.ycombinator.com',
  strategy: Strategy.PUBLIC,
  browser: false,
  defaultFormat: 'json',
  args: [
    { name: 'limit', type: 'int', default: 30, help: 'Number of stories to return' }
  ],
  columns: ['rank', 'title', 'url', 'hn_url', 'points', 'author', 'comments', 'age'],
  func: async (args) => {
    const limit = validatePositiveInt(args.limit ?? 30, 'limit');

    // Fetch story IDs
    const storyIds = await fetchJson('https://hacker-news.firebaseio.com/v0/topstories.json');

    // Slice and fetch details in parallel
    const idsToFetch = storyIds.slice(0, limit + 5); 
    const fetchedItems = await Promise.all(
      idsToFetch.map(id => fetchJson(`https://hacker-news.firebaseio.com/v0/item/${id}.json`).catch(() => null))
    );

    const results = [];
    let rank = 1;
    for (const item of fetchedItems) {
      if (results.length >= limit) break;
      if (!item || item.deleted || item.dead || !item.title) continue;

      const hnUrl = `https://news.ycombinator.com/item?id=${item.id}`;
      results.push({
        rank: rank++,
        title: item.title,
        url: item.url || hnUrl,
        hn_url: hnUrl,
        points: item.score ?? 0,
        author: item.by || '[deleted]',
        comments: item.descendants ?? 0,
        age: getRelativeTime(item.time)
      });
    }

    validateSchema(results, STORY_SCHEMA);
    return handleOutput(results, args);
  }
});

Enter fullscreen mode Exit fullscreen mode


6. 基准测试:无头浏览器 vs WebCMD

我们进行了性能基准测试,比较使用 Puppeteer 无头 Chrome 抓取器加载并抓取 news.ycombinator.com 与 WebCMD CLI (hn-cli) 的表现。

延迟和执行速度

方法 平均延迟(ms) 加速比 开销详情
Puppeteer (Chromium) 3,850ms 1.0x 浏览器二进制启动、DNS + TCP、布局/绘制、页面脚本执行。
WebCMD CLI (hn-cli) 180ms - 320ms 约 15x - 20x 原生 Node 子进程、优化的并行 HTTP 获取、直接网络流。

Token 消耗(上下文效率)

当代理读取网页内容时,每个字符都转化为 LLM API 成本。

传递给代理的格式 载荷大小 估算 LLM Tokens 成本降低
原始 HTML 页面 120 KB 约 30,000 tokens 0%
Markdown DOM 转换 22 KB 约 5,500 tokens 81.6%
干净的 JSON 输出 (hn-cli) 1.8 KB 约 450 tokens 98.5%

[!TIP]
通过消除 HTML 标签、脚本、布局指令和 UI 装饰,我们只向 LLM 提供结构化键,最大化上下文窗口利用率并加速推理。


7. 扩展 CLI:添加自定义命令

hn-cli 的优势之一是其简单的可扩展性。要添加一个获取 Hacker News “best” 故事的命令(使用单独的端点):

  1. 创建文件 adapters/best.js
   import { cli, Strategy } from '@agentrhq/webcmd/registry';
   import { fetchJson, getRelativeTime, handleOutput, validateSchema, validatePositiveInt } from './utils.js';

   cli({
     site: 'hn',
     name: 'best',
     access: 'read',
     description: 'Hacker News best stories',
     strategy: Strategy.PUBLIC,
     browser: false,
     args: [
       { name: 'limit', type: 'int', default: 30 }
     ],
     columns: ['rank', 'title', 'url', 'hn_url', 'points', 'author', 'comments', 'age'],
     func: async (args) => {
       const limit = validatePositiveInt(args.limit ?? 30, 'limit');
       const ids = await fetchJson('https://hacker-news.firebaseio.com/v0/beststories.json');
       const fetched = await Promise.all(
         ids.slice(0, limit).map(id => fetchJson(`https://hacker-news.firebaseio.com/v0/item/${id}.json`).catch(() => null))
       );
       // Map to schema, validate, output...
     }
   });

Enter fullscreen mode Exit fullscreen mode

  1. scripts/install-adapters.js更新注册,确保新文件被复制到 WebCMD 的活动适配器目录:
   const FILES_TO_COPY = ['top.js', 'newest.js', 'ask.js', 'jobs.js', 'item.js', 'search.js', 'best.js', 'utils.js'];

Enter fullscreen mode Exit fullscreen mode

  1. 部署更新
   npm run register

Enter fullscreen mode Exit fullscreen mode

该命令现在可通过 webcmd hn best --limit 10 全局使用。


8. 结论

随着 AI 代理成为软件工程和自动化的核心部分,我们必须从构建专为人类视觉设计的网站,转向暴露代理友好的 API 接口

通过使用 WebCMD 范式构建 hn-cli,我们展示了完全绕过无头浏览器的性能和资源税是可能的。使用原生 node 适配器、并发 API 获取、健壮的 schema 检查和结构化 JSON 结果来构建接口,可带来:

  • 极快的运行速度(亚 200ms)
  • 可忽略的资源使用
  • 为 LLM 节省大量 token
  • 确定性结果,永远不会因视觉布局更新而失效

AI 代理网页交互的未来不在于模仿人类光标点击,而在于标准化开发者友好的命令行适配器。


由 Antigravity Agent 开发。源代码以 MIT 许可证在以下仓库提供:https://github.com/scha54/hn-cli