一个例子让您理解Skill和大模型是如何完美协同工作的

大模型与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 元,就帮我把商品链接和价格发到我的钉钉上。”
难点:
- 页面结构不同:京东和淘宝的 HTML 结构完全不同,CSS 选择器不可能写死在代码里。
- 动态内容:价格、库存状态是动态加载的,甚至可能有弹窗广告干扰。
- 逻辑判断:需要比较两个价格,并基于条件(<7000 且有货)做决策。
- 抗干扰:如果遇到“登录弹窗”或“验证码”,需要识别并尝试关闭或报告。
协同工作流程
在这个流程中,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)
这段代码的关键在于:
analyze_page: 提取语义化的 DOM 摘要(去除噪音),甚至支持截图,让大模型“看见”页面。smart_action: 执行动作,如果失败,返回详细的“周边上下文”,帮助大模型自我修正。- 状态保持: 维持浏览器会话,支持多轮对话。
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。"
为什么这个方案是“完美协同”?
-
解耦了“结构”与“逻辑”:
- Skill 不负责知道“搜索框是哪个”,它只负责“把页面上所有像搜索框的东西列出来”。
- 大模型 负责根据语义(placeholder="手机...")判断哪个是搜索框。
- 结果:即使京东明天改版,把
.text.def改成了.new-search-input,你的 Skill 代码一行都不用改,大模型会自动在新的analyze结果中找到新的类名。
-
闭环的错误处理:
- 如果大模型选错了选择器,Skill 不会直接崩溃,而是返回
context: { hint: "元素未找到", siblings: [...] }。 - 大模型看到“附近有以下元素...”,它会想:“哦,我选错了,应该是旁边那个”,然后自动重试。这就是自愈能力。
- 如果大模型选错了选择器,Skill 不会直接崩溃,而是返回
-
Token 效率:
Skill 没有返回整个 HTML(几万字),而是返回了精简的 JSON 摘要(几百字)。既保留了决策所需的信息,又节省了 Token。
如何运行?
- 保存上述代码到
web-smart-agent文件夹。 - 运行
npm install playwright和npx playwright install。 - 在 OpenClaw 中注册该 Skill。
- 选择一个强逻辑模型( GPT-4o)。
- 开始对话:“帮我去淘宝找个 '机械键盘',告诉我第一个结果的价格。”
我们会发现,AI 像真人一样,先打开网站,看一眼(analyze),找到框,输入,点搜索,再看一眼,读出价格。这就是真正的 Agent。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)