我們如何運用 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.PUBLIC 且 browser: false 的次要執行策略。
當以此方式設定時:
- 配接器會直接在本地 Node.js 環境中執行。
- WebCMD 完全跳過啟動 Chromium,消除了渲染與啟動開銷。
- 配接器透過公開的 REST 端點與目標網站互動,產生 低於 200ms 的延遲。
- 輸出預設格式化為結構化 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 命令定義、核心公用程式邏輯、測試和組態腳本:
善用最佳 API
CLI 不爬取 news.ycombinator.com,而是直接與兩個官方端點通訊:
- Firebase Hacker News REST API:適合取得即時的熱門/最新/問答文章 ID 以及文章詳細資料。
- Algolia Hacker News Search API:適合依文字相關性或日期查詢文章。
4. hn-cli 中的關鍵工程模式
為確保 CLI 適合自主代理執行,程式碼庫實作了三個重要模式:並行性、韌性以及確定性。
A. 使用 Promise.all 實現並行性
Hacker News Firebase API 被建構為一組獨立的文章端點。要取得前 30 篇熱門文章,我們必須:
- 取得前 500 篇熱門文章 ID 的清單(一次 HTTP 呼叫)。
- 取得前 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「最佳」文章的命令(使用不同的端點):
-
建立檔案
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
-
在
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
- 部署更新:
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
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.