大模型与Skill真正的完美协同发生在:大模型负责“动态决策”和“模糊理解”,而 Skill 负责“精准执行”和“环境感知”。在这个场景中,大模型不再是简单的“翻译官”,而是指挥官。它需要根据网页的实时反馈(Skill 返回的信息)来调整策略,甚至处理它从未见过的页面结构。Skill 与大模型的完美协同,本质上是:

  • Skill 负责“看见”(把复杂的网页变成结构化的数据/摘要)和“动手”(精准操作 DOM)。
  • 大模型 负责“理解”(看懂 Skill 返回的数据,识别异常)和“规划”(决定下一步点什么,怎么改策略)。

在这种模式下,你写的 Skill 代码量可能只有以前的 50%(因为不需要写死各种 if-else 来处理异常和不同网站),但系统的智能程度却提升了 10 倍。这就是 OpenClaw 架构的魅力所在。

角色 传统写法 (你之前的担忧) 完美协同写法 (OpenClaw + LLM)
CSS 选择器 写死在代码里。
const selector = '#jd-search-box'
(网站改版,代码就废了)
由大模型动态生成。
Skill 返回 DOM 摘要 -> 大模型识别出 #key 是搜索框 -> 下发指令。
(网站改版,大模型能自动适应新结构)
异常处理 硬编码规则。
if (popup.exists()) click('.close')
(遇到新类型的弹窗就挂了)
语义理解。
Skill 报告“发现促销弹窗” -> 大模型理解这是干扰项 -> 决定寻找“关闭”、“取消”或“X”按钮。
(能处理未见过的弹窗)
业务逻辑 写在 JS 里。
if (price < 7000) send()
在大模型脑子里。
大模型理解“低价”、“有货”、“比价”等自然语言概念,自主决定何时调用发送技能。
灵活性 低。只能做预设好的事。 高。用户可以随时改变需求(例如:“只要白色的”、“排除京东自营”),大模型能即时调整 Skill 的调用参数。

场景演示:跨电商平台的“智能比价与库存监控”

任务目标
用户说:“帮我看看京东和淘宝上‘iPhone 15 Pro 256G 黑色’现在的最低价是多少,如果有货且价格低于 7000 元,就帮我把商品链接和价格发到我的钉钉上。”

难点

  1. 页面结构不同:京东和淘宝的 HTML 结构完全不同,CSS 选择器不可能写死在代码里。
  2. 动态内容:价格、库存状态是动态加载的,甚至可能有弹窗广告干扰。
  3. 逻辑判断:需要比较两个价格,并基于条件(<7000 且有货)做决策。
  4. 抗干扰:如果遇到“登录弹窗”或“验证码”,需要识别并尝试关闭或报告。

协同工作流程

在这个流程中,Playwright Skill 变成了大模型的“眼睛”和“手”,而大模型是“大脑”。为了实现“大模型动态决策 + Skill 精准执行”的完美协同,我们需要把 Skill 从简单的“执行器”升级为“感知与执行一体”的智能代理。这个 Skill 的核心不再是一堆写死的 click('#id'),而是提供“观察(Analyze)”和“自适应执行(Smart Act)”的能力,把“找元素”和“做决策”的权力交给大模型。以下是完整的、可运行的 web-smart-agent Skill 实例。

项目结构

web-smart-agent/
├── package.json
├── index.js            # 核心逻辑:包含感知、执行、错误反馈
├── skill-definition.json # 技能定义:告诉大模型它能做什么
└── README.md

代码实现 (index.js)

这段代码的关键在于:

  1. analyze_page: 提取语义化的 DOM 摘要(去除噪音),甚至支持截图,让大模型“看见”页面。
  2. smart_action: 执行动作,如果失败,返回详细的“周边上下文”,帮助大模型自我修正。
  3. 状态保持: 维持浏览器会话,支持多轮对话。
const { chromium } = require('playwright');

// 单例模式:保持浏览器会话,支持多轮交互
let browser = null;
let page = null;
let context = null;

/**
 * 初始化或获取浏览器实例
 */
async function ensureBrowser() {
    if (!browser) {
        browser = await chromium.launch({ 
            headless: true, // 生产环境用 true,调试时可改为 false 看界面
            args: ['--no-sandbox', '--disable-setuid-sandbox']
        });
        context = await browser.newContext({
            viewport: { width: 1280, height: 720 },
            userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        });
        page = await context.newPage();
        
        // 拦截部分资源加速加载
        await page.route('**/*.{png,jpg,jpeg,gif,webp,css}', route => route.abort()); 
    }
    return page;
}

/**
 * 核心功能:分析页面结构 (给大模型的眼睛)
 * 返回简化的、语义化的 DOM 树,去除广告和无关脚本
 */
async function analyzePage() {
    const currentPage = await ensureBrowser();
    
    // 注入脚本提取语义化信息
    const domSummary = await page.evaluate(() => {
        const interactiveSelectors = ['a', 'button', 'input', 'select', 'textarea', '[role="button"]', '[onclick]'];
        const elements = document.querySelectorAll(interactiveSelectors.join(', '));
        
        const summary = Array.from(elements).map((el, index) => {
            // 过滤掉隐藏元素
            const style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || el.offsetWidth === 0) return null;

            return {
                id: el.id || null,
                class: el.className ? el.className.split(' ').filter(c => c.length > 0).slice(0, 3).join('.') : null,
                tag: el.tagName.toLowerCase(),
                text: el.innerText ? el.innerText.trim().substring(0, 50) : null, // 截取前50字
                placeholder: el.placeholder || null,
                ariaLabel: el.getAttribute('aria-label') || el.getAttribute('title') || null,
                type: el.type || null,
                // 生成一个临时的索引选择器,供大模型引用
                tempIndex: index 
            };
        }).filter(item => item !== null);

        return {
            url: window.location.href,
            title: document.title,
            interactiveElements: summary.slice(0, 50) // 只返回前50个关键元素,防止 Token 溢出
        };
    });

    // 可选:如果需要多模态模型,可以这里生成截图 base64
    // const screenshot = await currentPage.screenshot({ encoding: 'base64', type: 'jpeg' });

    return {
        status: 'success',
        data: domSummary,
        message: "页面分析完成。请根据 'interactiveElements' 中的 text, ariaLabel 或 tempIndex 来决定下一步操作。"
    };
}

/**
 * 核心功能:智能执行动作 (给大模型的手)
 * 支持大模型通过文本描述或临时索引来定位元素
 */
async function smartAction(actionType, params) {
    const currentPage = await ensureBrowser();
    const { target, value, description } = params; 
    // target 可以是 CSS 选择器,也可以是 analyzePage 返回的 tempIndex
    
    try {
        let locator;

        // 策略 A: 如果 target 是数字 (tempIndex),重新查询该元素
        if (typeof target === 'number') {
            // 重新运行 evaluate 获取该索引对应的真实选择器 (简化版:直接通过索引查找)
            // 在生产环境中,最好让大模型根据 analyze 结果自己构造 CSS,或者这里实现更复杂的映射
            // 这里为了演示,假设大模型会直接使用 CSS 选择器,或者我们提供一个基于文本的模糊查找
            throw new Error("建议大模型直接使用 CSS 选择器,或先调用 analyzePage 后构造选择器。本示例主要演示 CSS 选择器执行。");
        } 
        
        // 策略 B: 使用 CSS 选择器 (大模型根据 analyzePage 的结果构造)
        locator = currentPage.locator(target);

        // 执行动作
        switch (actionType) {
            case 'click':
                await locator.click({ timeout: 5000 });
                break;
            case 'fill':
                await locator.fill(value, { timeout: 5000 });
                break;
            case 'press': // 模拟回车
                await locator.press(value, { timeout: 5000 }); // value 这里是键名,如 'Enter'
                break;
            case 'hover':
                await locator.hover({ timeout: 5000 });
                break;
            case 'extract_text':
                const text = await locator.textContent();
                return { status: 'success', data: { text: text?.trim() } };
            default:
                throw new Error(`未知动作: ${actionType}`);
        }

        // 等待网络空闲或短暂延迟,确保页面稳定
        await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {}); 

        return {
            status: 'success',
            message: `成功执行 ${actionType} 于 ${target}`,
            currentUrl: page.url()
        };

    } catch (error) {
        // 【关键】错误增强:不仅返回错误,还返回“周围有什么”,帮大模型纠错
        const errorContext = await page.evaluate((sel) => {
            const el = document.querySelector(sel);
            if (!el) return { hint: "元素未找到。附近存在的类似元素有:" };
            
            // 查找父节点下的其他兄弟元素
            const parent = el.parentElement;
            const siblings = parent ? Array.from(parent.children).map(c => ({
                tag: c.tagName,
                text: c.innerText?.substring(0, 30),
                class: c.className
            })) : [];
            
            return { hint: "元素存在但操作失败", siblings: siblings.slice(0, 5) };
        }, target).catch(() => ({ hint: "无法获取上下文,可能是选择器语法错误" }));

        return {
            status: 'failed',
            error: error.message,
            context: errorContext,
            suggestion: "请检查选择器是否正确,或先调用 analyze_page 重新获取页面结构。"
        };
    }
}

/**
 * 导航功能
 */
async function navigate(url) {
    const currentPage = await ensureBrowser();
    try {
        await currentPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
        return { status: 'success', message: `已导航至 ${url}`, url: currentPage.url() };
    } catch (e) {
        return { status: 'failed', error: e.message };
    }
}

/**
 * 主入口函数
 */
async function executeSkill(params) {
    const { action, payload } = params;
    // action: 'navigate', 'analyze', 'act'
    
    switch (action) {
        case 'navigate':
            return await navigate(payload.url);
        case 'analyze':
            return await analyzePage();
        case 'act':
            return await smartAction(payload.type, payload);
        case 'close':
            if (browser) await browser.close();
            browser = null;
            return { status: 'success', message: "浏览器已关闭" };
        default:
            return { status: 'failed', error: `未知动作类型: ${action}` };
    }
}

module.exports = {
    name: 'web-smart-agent',
    description: '一个具备视觉感知能力的 Web 自动化技能。它能分析页面结构供 AI 决策,并能执行点击、输入等操作。支持错误自愈反馈。',
    execute: executeSkill,
    cleanup: async () => { if (browser) await browser.close(); }
};

技能定义 (skill-definition.json)

这是告诉大模型如何思考的关键。注意 description 和参数设计,引导它先“看”再“动”。

{
  "name": "web_smart_agent",
  "description": "用于执行复杂的 Web 自动化任务。此技能具有'视觉'能力。\n\n【重要工作流】:\n1. 必须先调用 action='analyze' 获取页面当前的交互元素列表 (DOM 摘要)。\n2. 根据分析结果,由你 (AI) 决定需要点击哪个元素或输入什么内容,并构造准确的 CSS 选择器。\n3. 调用 action='act' 执行具体操作。\n4. 如果操作失败,技能会返回错误上下文,请利用这些信息修正选择器并重试。\n\n不要硬编码选择器,因为网页结构是动态的。",
  "parameters": {
    "type": "object",
    "properties": {
      "action": {
        "type": "string",
        "enum": ["navigate", "analyze", "act", "close"],
        "description": "要执行的操作类型。"
      },
      "payload": {
        "type": "object",
        "description": "操作的具体参数,取决于 action 类型。",
        "properties": {
          "url": { "type": "string", "description": "目标网址 (仅当 action=navigate 时必需)。" },
          "type": { "type": "string", "enum": ["click", "fill", "press", "hover", "extract_text"], "description": "动作类型 (仅当 action=act 时必需)。" },
          "target": { "type": "string", "description": "CSS 选择器 (例如 '#login-btn', '.search-input')。必须基于 analyze 的结果推断。" },
          "value": { "type": "string", "description": "输入的文本或按键名称 (例如 'Hello', 'Enter')。" },
          "description": { "type": "string", "description": "对该操作的人类语言描述,用于日志记录。" }
        }
      }
    },
    "required": ["action"]
  }
}

完美协同的实战演示 (Prompt & Flow)

现在,看看大模型如何使用这个 Skill 完成之前提到的“京东比价”任务。

用户输入:

“去京东搜一下 'iPhone 15',看看第一个商品多少钱。”

大模型的内部思考与执行流:

Step 1: 导航

  • Thought: 用户要去京东。我需要先打开网页。
  • Tool Call:
    { "action": "navigate", "payload": { "url": "https://www.jd.com" } }
  • Skill Return: { "status": "success", "message": "已导航至 https://www.jd.com" }

Step 2: 感知 (关键步骤)

  • Thought: 页面打开了,但我不知道搜索框的选择器是什么。我不能瞎猜。我要调用 analyze 看看页面上有什么。
  • Tool Call:
    { "action": "analyze", "payload": {} }
  • Skill Return (模拟数据):json
    {
      "status": "success",
      "data": {
        "url": "https://www.jd.com",
        "interactiveElements": [
          { "tag": "input", "text": "", "placeholder": "手机、数码、京东电器...", "tempIndex": 0, "class": "text.def" },
          { "tag": "button", "text": "搜索", "tempIndex": 1, "class": "btn-search" },
          { "tag": "a", "text": "我的订单", "tempIndex": 2 }
          // ... 其他元素
        ]
      }
    }

Step 3: 决策与构造

  • Thought:

    • 我看到了一个 input 标签,placeholder 是 "手机、数码...",这肯定是搜索框。虽然它没有 ID,但类名是 .text.def
    • 还有一个 button 文本是 "搜索",类名 .btn-search
    • 策略:先 fill 输入框,再 click 搜索按钮。
  • Tool Call 1 (输入):

    { 
      "action": "act", 
      "payload": { "type": "fill", "target": "input.text.def", "value": "iPhone 15", "description": "在搜索框输入 iPhone 15" } 
    }
  • Skill Return{ "status": "success", ... }

  • Tool Call 2 (点击):

    { 
      "action": "act", 
      "payload": { "type": "click", "target": "button.btn-search", "value": "", "description": "点击搜索按钮" } 
    }
  • Skill Return{ "status": "success", ... } (此时页面跳转到搜索结果页)

Step 4: 再次感知与提取

  • Thought: 搜索完成了,现在需要看第一个商品的价格。页面变了,我需要再次 analyze 或者直接尝试提取。为了稳妥,我先 analyze 一下搜索结果页的结构。
  • Tool Call{ "action": "analyze", "payload": {} }
  • Skill Return:
    {
      "data": {
        "interactiveElements": [
          { "tag": "div", "text": "Apple iPhone 15 (A3092) 128GB 黑色...", "class": "p-name", "tempIndex": 5 },
          { "tag": "i", "text": "¥5999.00", "class": "p-price", "tempIndex": 6 },
          // ...
        ]
      }
    }
  • Thought: 找到了!.p-price 类的元素文本是 "¥5999.00"。
  • Tool Call:
    { 
      "action": "act", 
      "payload": { "type": "extract_text", "target": ".p-price", "description": "提取第一个商品的价格" } 
    }
  • Skill Return{ "status": "success", "data": { "text": "¥5999.00" } }

Step 5: 最终回答

  • Response to User: "我在京东搜索了 'iPhone 15',第一个商品的价格是 ¥5999.00。"

为什么这个方案是“完美协同”?

  1. 解耦了“结构”与“逻辑”

    • Skill 不负责知道“搜索框是哪个”,它只负责“把页面上所有像搜索框的东西列出来”。
    • 大模型 负责根据语义(placeholder="手机...")判断哪个是搜索框。
    • 结果:即使京东明天改版,把 .text.def 改成了 .new-search-input,你的 Skill 代码一行都不用改,大模型会自动在新的 analyze 结果中找到新的类名。
  2. 闭环的错误处理

    • 如果大模型选错了选择器,Skill 不会直接崩溃,而是返回 context: { hint: "元素未找到", siblings: [...] }
    • 大模型看到“附近有以下元素...”,它会想:“哦,我选错了,应该是旁边那个”,然后自动重试。这就是自愈能力。
  3. Token 效率

    Skill 没有返回整个 HTML(几万字),而是返回了精简的 JSON 摘要(几百字)。既保留了决策所需的信息,又节省了 Token。

如何运行?

  1. 保存上述代码到 web-smart-agent 文件夹。
  2. 运行 npm install playwright 和 npx playwright install
  3. 在 OpenClaw 中注册该 Skill。
  4. 选择一个强逻辑模型( GPT-4o)。
  5. 开始对话:“帮我去淘宝找个 '机械键盘',告诉我第一个结果的价格。”

我们会发现,AI 像真人一样,先打开网站,看一眼(analyze),找到框,输入,点搜索,再看一眼,读出价格。这就是真正的 Agent。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐