
1. 项目概述当采集Agent遇上可访问性树最近在折腾一个基于大语言模型的网页信息采集Agent核心任务就是让AI能“看懂”网页并从中精准提取出结构化的数据。一开始我理所当然地选择了最直观的方案用Playwright这样的浏览器自动化工具把整个页面的DOM文档对象模型结构也就是HTML源码一股脑儿地扔给大模型去分析。这方法简单粗暴初期效果也还行。但很快成本问题就浮出水面了。大模型是按“token”计费的一个token大约相当于0.75个英文单词或一个中文字符。一个稍微复杂点的现代网页其DOM树动辄几万甚至几十万个token。每次采集都发送如此庞大的上下文不仅响应慢费用更是蹭蹭往上涨。更头疼的是DOM里充斥着大量与核心内容无关的噪音导航栏、页脚、广告脚本、样式定义、隐藏的调试元素……这些对AI理解页面主旨和提取目标数据帮助甚微却白白消耗了大量宝贵的token预算。就在我为此发愁四处寻找优化方案时一个灵感闪现我们人类在浏览网页时视觉上会忽略那些装饰性元素直接聚焦于文本、链接、按钮等可交互、可阅读的内容。对于视障用户屏幕阅读器也是通过“可访问性树”Accessibility Tree 简称 a11y 树来理解页面结构的。这棵树是浏览器基于DOM专门为辅助技术生成的一个简化、语义化的视图。它过滤掉了纯样式的元素只保留具有实际语义和交互功能的节点如段落文本、标题、链接、按钮、表单等。那么为什么不直接用Playwright抓取这个a11y树代替臃肿的原始DOM作为采集Agent的“眼睛”呢这个想法让我兴奋不已。经过一番实践和调优我发现这条路子确实走通了。用a11y树作为输入通常能将需要发送给大模型的token数量减少60%到80%同时因为信息更聚焦AI的理解和提取准确率反而有所提升。这篇文章我就来详细拆解一下这个“Playwright MCP a11y树”的采集Agent方案从设计思路、核心实现到避坑指南希望能给同样在做智能采集的朋友们提供一个高性价比的新思路。2. 核心思路与方案选型为什么是Playwright和a11y树2.1 传统DOM采集的痛点与a11y树的优势在深入技术细节前我们得先搞清楚为什么传统的全量DOM采集方式在AI Agent场景下显得如此笨重。全量DOM的“信息过载”问题一个典型的现代网页DOM其结构是为了渲染和交互而设计的并非为了高效的信息传递。它包含渲染骨架大量的div、span等布局容器它们本身没有语义只负责定位和样式。样式与脚本内联或外链的CSS、JavaScript代码对于内容理解来说是噪音。重复与隐藏内容为了响应式设计或交互状态而存在的重复DOM节点、display: none隐藏的元素。无关功能模块固定的页头、页脚、侧边栏、广告位等。当我们将这样的DOM送给大模型时相当于让AI先做一遍“前端逆向工程”从一堆砖瓦木材中识别出哪部分是承重墙哪部分是家具。这个过程不仅低效而且容易受到无关信息的干扰。a11y树的“语义化精简”特性可访问性树是浏览器内部的一个抽象层。它的生成过程可以简单理解为浏览器解析DOM和CSS应用样式规则然后根据WAI-ARIA标准和其他启发式规则为每个具有语义或可交互性的DOM节点创建一个对应的“可访问性节点”。这个过程会自动过滤纯视觉的、无语义的容器如很多仅用于布局的div。完全隐藏的元素display: none,visibility: hidden且无其他可访问性覆盖。冗余的装饰性元素。最终生成的a11y树节点更少结构更清晰每个节点都带有明确的语义角色role如heading、link、button、text以及名称name、状态state、值value等属性。这正好契合了AI理解内容的需求我需要知道“这里有一段正文”、“那是一个提交按钮”、“这是一个商品价格”而不是“这里有一个class为price-tag的span元素”。2.2 技术栈选型Playwright MCP Server明确了用a11y树作为数据源后接下来要选择实现工具。为什么是Playwright在浏览器自动化领域Selenium、Puppeteer和Playwright是三大主流。我选择Playwright主要基于以下几点强大的a11y API原生支持Playwright提供了page.accessibility.snapshot()方法可以直接获取当前页面的完整可访问性树快照。这个API返回的就是一个结构化的JSON对象开箱即用无需自己从DOM去模拟生成a11y树省时省力。出色的现代化Web支持Playwright由微软开发对现代JavaScript框架React, Vue, Angular等渲染的单页应用SPA支持非常好能可靠地等待动态内容加载完成。多浏览器与无头模式支持Chromium、Firefox、WebKit并且无头Headless模式运行高效非常适合后台采集任务。丰富的上下文操作除了抓取a11y树我们可能还需要与页面交互如点击“加载更多”、登录Playwright的API设计非常直观强大。为什么引入MCPModel Context ProtocolMCP是新兴的、用于连接大模型与外部工具和数据的协议。它的核心思想是将能力“服务器化”。在我们的场景中构建一个“网页采集MCP Server”具有显著优势能力标准化与复用将Playwright的采集逻辑封装成标准的MCP工具如capture_page_a11y任何兼容MCP的AI客户端如Cursor、Claude Desktop、自定义Agent都可以通过统一的协议调用这个工具无需在每个Agent项目里重复编写Playwright代码。环境隔离与资源管理浏览器实例尤其是带图形界面的是资源消耗大户。MCP Server可以作为一个常驻服务管理浏览器池避免为每个请求频繁启动/关闭浏览器大幅提升效率。灵活部署MCP Server可以部署在独立的服务器上客户端通过网络调用。这样可以将资源密集型的浏览器操作与轻量级的AI推理逻辑分离架构更清晰。因此最终的方案架构确定为构建一个Playwright MCP Server它对外提供抓取页面a11y树的服务我们的采集Agent则作为MCP客户端通过调用该服务来获取精简后的页面信息再交由大模型处理。3. 核心实现构建Playwright A11y采集MCP Server3.1 环境准备与项目初始化首先我们需要搭建MCP Server的开发环境。这里以Node.js环境为例。# 初始化项目 mkdir playwright-a11y-mcp-server cd playwright-a11y-mcp-server npm init -y # 安装核心依赖 npm install modelcontextprotocol/sdk playwright # Playwright需要安装浏览器内核推荐使用官方方式安装Chromium npx playwright install chromiummodelcontextprotocol/sdk是官方提供的用于快速构建MCP Server的SDK。playwright则是我们的核心浏览器自动化库。3.2 MCP Server基础框架搭建MCP Server的核心是声明它提供的“工具”Tools和“资源”Resources。我们的第一个工具就是抓取a11y树。创建一个名为server.js的文件const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { playwright } require(playwright); // 初始化MCP Server const server new Server( { name: playwright-a11y-capture, version: 0.1.0, }, { capabilities: { tools: {}, // 我们将在这里声明工具 }, } ); // 声明一个工具capture_a11y_tree server.setRequestHandler(tools/call, async (request) { if (request.params.name capture_a11y_tree) { const { url, waitForSelector, timeout } request.params.arguments || {}; if (!url) { throw new Error(URL参数是必需的); } // 启动Playwright浏览器 const browser await playwright.chromium.launch({ headless: true, // 无头模式适合服务器环境 }); const context await browser.newContext(); const page await context.newPage(); try { // 导航到目标URL await page.goto(url, { waitUntil: networkidle, timeout: timeout || 30000 }); // 可选等待某个特定元素出现确保主要内容已加载 if (waitForSelector) { await page.waitForSelector(waitForSelector, { timeout: timeout || 30000 }); } // 捕获完整的可访问性树快照 const a11ySnapshot await page.accessibility.snapshot(); // 关闭浏览器释放资源 await browser.close(); return { content: [ { type: text, text: JSON.stringify(a11ySnapshot, null, 2), // 格式化输出便于阅读 }, ], }; } catch (error) { // 确保发生错误时也关闭浏览器 await browser.close(); throw new Error(抓取页面失败: ${error.message}); } } // 可以在这里添加其他工具的处理逻辑 }); // 启动服务器使用stdio传输这是MCP客户端常见的连接方式 const transport new StdioServerTransport(); server.connect(transport).catch((error) { console.error(Server连接失败:, error); process.exit(1); });这个基础的Server已经可以工作了。它监听来自MCP客户端的请求当调用capture_a11y_tree工具时会启动一个无头Chromium浏览器打开指定URL等待页面加载完成然后调用page.accessibility.snapshot()获取a11y树最后以JSON格式返回。3.3 优化a11y树结构过滤与增强原始的accessibility.snapshot()返回的数据虽然已经比DOM精简但为了进一步节省token并提升AI处理效率我们还需要进行后处理。1. 过滤无用节点观察原始a11y树你会发现仍然存在一些对内容理解帮助不大的节点比如role为generic且没有有意义name的节点或者某些特定的布局容器。// 在捕获a11ySnapshot后添加过滤函数 function filterA11yTree(node) { // 如果节点被标记为隐藏直接过滤掉 if (node.ignored true) { return null; } // 过滤掉某些特定角色或无内容的通用容器 const uninformativeRoles [presentation, none, generic]; if (uninformativeRoles.includes(node.role) (!node.name || node.name.trim() )) { // 但需要递归处理其子节点因为内容可能在子节点里 if (node.children) { const filteredChildren node.children.map(filterA11yTree).filter(n n ! null); return filteredChildren.length 0 ? { ...node, children: filteredChildren } : null; } return null; } // 递归处理子节点 if (node.children) { node.children node.children.map(filterA11yTree).filter(n n ! null); } return node; } const filteredA11yTree filterA11yTree(a11ySnapshot);2. 增强语义信息我们可以根据节点的role和上下文为其添加更易理解的标签或类型帮助AI更好地分类。function enhanceA11yTree(node) { const roleLabelMap { heading: 标题, link: 链接, button: 按钮, textbox: 输入框, img: 图片, article: 文章区域, main: 主要内容区, // ... 可以扩展更多映射 }; // 添加一个自定义的semanticLabel字段 if (roleLabelMap[node.role]) { node.semanticLabel roleLabelMap[node.role]; } else if (node.role heading node.level) { node.semanticLabel ${node.level}级标题; } else if (node.role link node.name) { // 对于链接可以尝试判断是内部链接还是外部链接简单示例 node.linkType node.name.includes(http) ? 可能为外部链接 : 可能为内部导航; } // 对于文本节点如果内容过长可以添加一个摘要字段用于预览非完整替换 if (node.role text node.name node.name.length 150) { node.preview node.name.substring(0, 150) ...; } if (node.children) { node.children.forEach(enhanceA11yTree); } return node; } const enhancedAndFilteredTree enhanceA11yTree(filteredA11yTree);3. 扁平化与关键信息提取可选策略对于某些极度追求最小token用量的场景我们甚至可以不走树形结构而是将a11y树扁平化提取出所有带有文本name的节点并按视觉流或语义块重新组织成段落列表。function flattenToContentBlocks(node, blocks []) { // 定义哪些角色的节点可能是一个内容块的开始 const blockRoles [heading, article, main, section, list]; // 定义哪些角色的节点是内容载体 const contentRoles [text, link, button, img]; let currentBlock { type: paragraph, content: [] }; // 这是一个简化的深度优先遍历收集逻辑 // 实际实现会更复杂需要考虑节点层级和视觉顺序 if (contentRoles.includes(node.role) node.name) { currentBlock.content.push([${node.role}] ${node.name}); } if (node.children) { node.children.forEach(child flattenToContentBlocks(child, blocks)); } if (currentBlock.content.length 0) { blocks.push(currentBlock); } return blocks; } // 这种方案损失了结构信息但token数最少适合纯文本内容提取。在MCP Server中我们可以通过工具参数让客户端选择返回原始树、过滤增强树还是扁平化内容。3.4 添加高级功能与参数一个健壮的采集服务还需要考虑更多实际场景。1. 页面交互支持有些内容需要滚动、点击按钮如“加载更多”或填写表单后才能出现。我们可以在工具中增加交互步骤参数。// 在工具调用参数中增加 actions 数组 // actions: [{ type: scroll, selector: .load-more }, { type: click, selector: #submit-btn }] async function performActions(page, actions) { for (const action of actions) { switch (action.type) { case scroll: await page.evaluate((selector) { document.querySelector(selector)?.scrollIntoView(); }, action.selector); await page.waitForTimeout(1000); // 等待滚动后内容加载 break; case click: await page.click(action.selector); await page.waitForTimeout(1500); // 等待点击后页面变化 break; case fill: await page.fill(action.selector, action.value); break; // ... 其他操作类型 } } } // 在页面goto和waitForSelector之后调用 performActions(page, actions)2. 智能等待与超时使用waitUntil: networkidle是个好开始但对于高度动态的SPA可能不够。可以结合waitForSelector、waitForFunction来确保目标内容区域已渲染。3. 错误处理与重试网络不稳定、页面加载失败是常态。需要在工具逻辑中加入重试机制和更细致的错误分类将浏览器启动失败、导航超时、选择器未找到等错误清晰地返回给客户端。4. 资源管理浏览器池对于高频调用的Server为每个请求启动/关闭浏览器是不可接受的。需要实现一个简单的浏览器实例池复用浏览器和上下文但要注意隔离如使用不同的Context或Page。// 简化的浏览器池概念 class BrowserPool { constructor(maxBrowsers 5) { this.maxBrowsers maxBrowsers; this.browsers []; this.idleBrowsers []; } async acquire() { if (this.idleBrowsers.length 0) { return this.idleBrowsers.pop(); } if (this.browsers.length this.maxBrowsers) { const browser await playwright.chromium.launch({ headless: true }); this.browsers.push(browser); return browser; } // 等待有浏览器释放实际需要更复杂的队列机制 // ... } release(browser) { this.idleBrowsers.push(browser); } }将这些优化点整合进我们的MCP Server它就能成为一个功能相对完备、稳定高效的网页a11y信息采集服务。4. 采集Agent端集成与调用MCP服务有了MCP Server我们的采集Agent假设是一个基于LLM的自动化脚本或应用就可以通过标准的MCP协议来调用它了。4.1 客户端连接与工具调用以Node.js环境的Agent为例你需要使用MCP客户端SDK。这里展示核心调用逻辑// agent.js const { Client } require(modelcontextprotocol/sdk/client/index.js); const { StdioClientTransport } require(modelcontextprotocol/sdk/client/stdio.js); const { exec } require(child_process); const { promisify } require(util); const execAsync promisify(exec); async function capturePageWithA11y(url) { // 启动MCP Server进程假设server.js已编译或可直接用node运行 const serverProcess execAsync(node /path/to/your/server.js, { stdio: [pipe, pipe, pipe] }); // 创建MCP客户端并连接 const transport new StdioClientTransport({ command: node, args: [/path/to/your/server.js], }); const client new Client( { name: web-capture-agent, version: 1.0.0, }, { capabilities: {}, } ); await client.connect(transport); // 列出Server提供的工具可选用于发现 const toolsList await client.listTools(); console.log(可用工具:, toolsList); // 调用 capture_a11y_tree 工具 const result await client.callTool({ name: capture_a11y_tree, arguments: { url: url, waitForSelector: .main-content, // 等待主要内容区域加载 timeout: 45000, // actions: [{ type: scroll, selector: footer }] // 如果需要交互 }, }); await client.close(); // 注意实际应用中需要更优雅地管理Server进程生命周期 if (result.content result.content[0].type text) { const a11yTreeJson JSON.parse(result.content[0].text); // 现在你得到了过滤和增强后的a11y树JSON return a11yTreeJson; } else { throw new Error(从MCP Server获取数据失败); } } // 使用示例 (async () { try { const tree await capturePageWithA11y(https://example.com/article); console.log(捕获的节点数:, JSON.stringify(tree).length); // 可以粗略估算token // 接下来你可以将 tree 或从中提取的文本发送给LLM进行处理... } catch (error) { console.error(采集失败:, error); } })();4.2 设计Agent的提示词Prompt获取到a11y树后如何设计给大模型的提示词至关重要。我们的目标是将结构化的a11y信息转换成自然语言指令让LLM完成信息提取。基础提示词结构你是一个专业的网页信息提取助手。我将给你一个网页的可访问性树a11y tree的JSON表示它已经过滤了无关元素保留了核心的语义化内容。 请根据以下要求从提供的a11y树数据中提取信息 ## 目标数据字段 - 文章标题 (article_title) - 作者 (author) - 发布日期 (publish_date) - 文章正文主要内容 (main_content) - 文中所有图片的替代文本如果有 (image_alts) - 文中所有重要链接的文本和URL (important_links) ## a11y树数据 {这里是经过压缩和格式化的a11yTreeJson字符串} ## 提取规则 1. 文章标题通常是一个role为heading且level为1的节点或者具有特定语义的节点。 2. 作者和发布日期可能出现在role为text的节点中寻找包含“作者”、“发布于”、“Date”等关键词的文本。 3. 文章正文通常是role为article、main或一系列text节点的集合。请将连续的、有意义的文本段落合并。 4. 只提取role为img且name字段不为空的节点作为图片替代文本。 5. 提取role为link的节点但优先提取正文区域内的链接忽略导航栏和页脚的重复链接。 请以JSON格式输出提取结果确保字段准确。如果某个字段未找到其值设为null。提示词优化技巧结构化输入在提示词中明确说明a11y树的结构特点如role,name,children帮助LLM理解数据格式。举例说明如果提取规则复杂可以在提示词中给出一个简化的a11y树片段和对应的期望输出示例。指令清晰明确指定输出格式如JSON并定义好每个字段的含义。处理不确定性指示LLM在无法确定时做出合理推断或标记为null避免胡编乱造。4.3 后处理与数据验证从LLM拿到提取的JSON后还需要进行后处理格式验证检查JSON是否有效必填字段是否存在。内容清洗去除提取文本中可能残留的空白符、无关字符。逻辑校验例如发布日期格式是否合理正文长度是否过短可能提取失败失败重试如果提取结果质量很差如大部分字段为null可以考虑用不同的提示词或聚焦页面不同区域通过MCP工具的waitForSelector或actions参数重新采集。5. 性能对比、常见问题与优化策略5.1 Token节省效果实测为了量化收益我选取了三个不同类型的网站进行对比测试新闻文章页内容型电商商品详情页混合型有大量UI组件单页应用仪表盘高度动态复杂交互测试方法分别用Playwright获取完整page.content()即DOM和优化后的accessibility.snapshot()将结果字符串化后用近似LLM分词器的方法估算token数这里使用简单规则英文字词数 中文字数。页面类型全量DOM Token估算A11y树 Token估算节省比例提取准确度变化新闻文章页~12,500~3,200~74%基本持平正文提取更干净电商详情页~45,000~8,100~82%商品标题、价格等关键信息提取准确率提升干扰信息减少SPA仪表盘~38,000~6,500~83%对于主要数据面板的识别更准但部分复杂图表内容可能丢失结论a11y树方案在token节省上效果极其显著普遍能达到70%以上的缩减。对于内容密集、结构相对简单的页面如文章准确度不受影响甚至更好。对于复杂UI页面由于a11y树过滤掉了大量装饰性布局元素反而让AI更专注于有语义的内容提取关键信息的准确率有所提升。唯一的潜在损失是那些完全依赖视觉布局且未提供适当可访问性属性的内容如某些自定义图表但这部分信息本来从DOM中提取也很有挑战性。5.2 常见问题与解决方案1. a11y树为空或内容不全原因页面可能完全由Canvas、WebGL渲染或大量使用aria-hiddentrue、rolepresentation导致可访问性树信息缺失。解决方案降级策略在MCP Server中实现fallback逻辑。如果accessibility.snapshot()返回的树过小或关键区域缺失自动切换为使用Playwright提取特定CSS选择器区域的文本内容page.textContent(selector)虽然结构化信息少了但能保底获取文字。混合模式针对关键区域同时获取a11y树和该区域的DOM片段互补使用。2. 动态加载内容抓取不到原因页面内容通过JavaScript异步加载初始a11y树不包含这些内容。解决方案参数化等待如前所述在MCP工具中提供waitForSelector、waitForTimeout或自定义actions如滚动、点击参数让客户端指定如何触发内容加载。智能滚动在Server端实现自动滚动到底部的逻辑确保所有懒加载内容都被触发。3. 反爬虫机制干扰原因网站可能检测Playwright的无头浏览器特征。解决方案伪装UA和Viewport在创建Browser Context时设置常见的用户代理和窗口大小。启用浏览器上下文使用browser.newContext()而非直接browser.newPage()可以更好地隔离和模拟真实环境。谨慎使用Stealth插件社区有一些Playwright的stealth插件可以尝试但需注意维护性和稳定性。4. MCP Server性能瓶颈原因浏览器实例创建销毁开销大或单个页面加载耗时过长阻塞其他请求。解决方案实现连接池如前所述管理一个可复用的浏览器实例池。异步与超时确保Server的每个工具处理都是异步的并设置合理的全局和操作级超时防止单个请求拖死整个服务。健康检查与重启定期检查浏览器实例的健康状态对异常实例进行销毁和重建。5.3 高级优化策略1. 按需采集与局部树有时我们只需要页面的某个特定区域如评论区、价格框。可以扩展MCP工具支持传入CSS选择器然后利用Playwright的page.locator(selector).first()定位到该元素再调用elementHandle.accessibility.snapshot()仅获取该局部区域的可访问性树进一步减少数据量。2. a11y树的差异更新对于监控类任务需要定期抓取同一页面看是否有更新。可以计算两次a11y树的哈希值如对关键文本内容求MD5如果未变化则无需调用LLM处理节省大量成本。3. 与视觉模型结合对于a11y树无法覆盖的纯视觉信息如图片内容、复杂排版可以探索在MCP Server中集成轻量级视觉模型或OCR对页面截图进行分析将结果作为补充信息与a11y树一并返回给Agent。这构成了一个多模态的采集方案。4. 缓存层在MCP Server前增加一层缓存如Redis对相同的URL和参数组合的请求在一定时间内返回缓存的结果极大降低对目标网站的压力和自身资源消耗。构建一个基于Playwright和a11y树的MCP采集Agent核心价值在于找到了一个成本与效果的最佳平衡点。它用标准化的服务MCP封装了复杂的浏览器自动化逻辑又通过可访问性树这个“语义透镜”极大地净化了输入给大模型的信息源。这套方案不仅适用于通用的网页信息提取稍加定制也能用于自动化测试中的可访问性检查、竞品数据监控、知识库构建等多个场景。在实际部署中持续监控token消耗、提取准确率和系统性能并根据具体网站的特点微调a11y树的过滤和增强策略是保证项目成功的关键。