我们如何利用 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。
按此配置时:
- 适配器直接在本地 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:适合获取实时热门/最新/Ask 故事 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 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” 故事的命令(使用单独的端点):
-
创建文件
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 获取、健壮的 schema 检查和结构化 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.