我們如何運用 WebCMD 典範,將網頁導航延遲降低 95%,並減少 LLM token 消耗達 90%,同時建立一個確定性、經結構驗證的命令列介面。


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 個以上的 token)。
  • 脆弱的 UI 選擇器:網頁爬取依賴 CSS 選擇器或 XPath 查詢,當平台版面配置改變時就會失效,導致代理管線無聲失敗或產生幻覺。

為了解決這些效率問題,我們建立了 HN CLI (hn-cli),這是一個高性能的 Hacker News 命令列配接器,利用 WebCMD 註冊表。透過將網頁互動與瀏覽器渲染分離,我們為 AI 代理提供了一個結構化、快速且節省 token 的橋樑。


2. WebCMD 典範簡介

WebCMD 是一個允許開發者為網頁平台定義命令列配接器的框架。通常,WebCMD 會透過啟動無頭瀏覽器並在目標頁面執行腳本來執行命令。然而,WebCMD 支援一種稱為 Strategy.PUBLICbrowser: 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:核心輔助函式(網路重試、結構驗證、HTML 實體清理)。
  • tests/:用於執行離線測試的完整 Vitest 模擬套件。
  • scripts/:註冊配接器、驗證子程序執行以及程式碼檢查。

善用最佳 API

CLI 不爬取 news.ycombinator.com,而是直接與兩個官方端點通訊:

  1. Firebase Hacker News REST API:適合取得即時的熱門/最新/問答文章 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 結構驗證實現確定性

AI 代理期望特定結構的資料。如果 API 傳回缺少欄位,代理可能會遇到未定義的行為或產生幻覺。hn-cli 在將資料傳送到 stdout 之前,使用 validateSchema() 強制執行嚴格的結構檢查:

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

我們執行了效能基準測試,比較無頭 Chrome 爬蟲(Puppeteer)載入和爬取 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 Token 成本降低
原始 HTML 頁面 120 KB 約 30,000 個 token 0%
Markdown DOM 轉換 22 KB 約 5,500 個 token 81.6%
乾淨的 JSON 輸出 (hn-cli) 1.8 KB 約 450 個 token 98.5%

[!TIP]
透過消除 HTML 標籤、腳本、版面配置指令和 UI 元素,我們只提供 LLM 結構化鍵值,最大化情境視窗可用性並加速推理。


7. 擴充 CLI:新增自訂命令

hn-cli 的優勢之一是其簡單的擴充性。要新增一個取得 Hacker News「最佳」文章的命令(使用不同的端點):

  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 取得、強大的結構檢查和結構化 JSON 結果來建置介面,帶來了:

  • 極速執行(低於 200ms)
  • 可忽略的資源使用
  • LLM 大量節省 token
  • 確定性結果,永遠不會因視覺版面更新而中斷

AI 代理網頁互動的未來不在於模仿人類游標點擊,而在於標準化開發者友善的命令列配接器。


由 Antigravity Agent 開發。原始碼依 MIT 授權條款提供,可在以下儲存庫取得:https://github.com/scha54/hn-cli