网站交给 Coding Agent 之前先想想 Token 浪费在哪最近我一直在折腾 Coding Agent 相关的自动化工作流把网页抓取、表单填写、登录态验证这类活儿交给智能体去干结果账单一出来直接愣了一下一次平平无奇的页面交互吃掉 3 万到 5 万 token 是常事稍微复杂点的流程能冲到 10 万以上。问题不在模型能力而是大部分人把 Coding Agent 用成了戴着眼罩逛超市——它明明只需要一瓶酱油却得把整排货架的照片全部带回给大模型识别。后来我换了个思路把 Playwright CLI 从浏览器自动化测试工具这个老定位里拽出来让它跑在 Coding Agent 和网页之间当一层信息预处理层。实测下来同一个任务 token 消耗直接从 4 万出头降到 8 千左右节省超过 75%而且稳定性比让 agent 直接操作浏览器好得多因为每一轮交互都是确定性的、可断言的不需要模型去猜页面元素的位置。这篇文章把我沉淀下来的方法、代码和踩坑记录拆开讲对人机协作式的网页自动化感兴趣的朋友应该能找到不少能直接抄作业的东西。这套打法的核心逻辑一句话讲清楚别让 Coding Agent 替你看网页让 Playwright CLI 替你消化网页。Coding Agent 擅长的是推理、规划、写代码不是像素级定位一个按钮的坐标。而 Playwright CLI 天生就是干浏览器自动化这行的它可以用最少的输出把网页变成 agent 最容易消费的结构化数据。下面按设计思路、实操落地、成本对比和故障排查四个维度展开。1. 先搞清楚 Token 到底烧在哪Coding Agent 操作网页的低效本质1.1 为什么 Coding Agent 自己开浏览器会这么烧 Token你要是用过 Coding Agent 直接驱动浏览器肯定见过这种画面agent 收到一个打开某页面把商品列表抓下来的指令于是它调用浏览器工具打开页面之后为了知道页面上有什么它得把整页的 HTML 或截图发给大模型。一个普通的企业官网首页DOM 节点动辄几千个转换成 token 轻松突破 2 万如果再算上样式类名、内联脚本、隐藏元素4 到 5 万也就是正常水平。关键是这个过程要重复好几次。Agent 不是看一眼就完事它看完第一屏要滚动第二屏又要拿一屏新的 HTML滚动后判断有没有加载更多按钮又要截图确认点击之后页面异步更新又要重新提取内容。每一步都是一次大规模上下文传递token 就像流水一样哗哗走。我做过一次统计让 agent 自动完成搜索关键词、进入详情页、提取标题和价格这个三步骤任务总 token 消耗在 4 万左右其中真正用于思考怎么处理数据的 token 可能不到 1 万剩下的全是页面内容搬运费。1.2 上下文污染的隐性成本比 token 数量更麻烦的是上下文污染。页面 HTML 里除了有用信息还有导航菜单、页脚、广告、埋点脚本、各类无关属性。这些噪音全部灌进上下文之后模型要做两件事一是浪费 token 存储这些垃圾二是要在垃圾里面找有用的内容这可能引入幻觉——尤其页面里出现特价免费已售罄这类词时模型容易把营销文案当成真实数据提取出来。一次提取错误就会触发后续的纠错、重试而每次重试又意味着新一轮页面读取成本呈螺旋式上升。1.3 把浏览器交互拆出去一个朴素的省 token 思路搞清楚痛点之后省 token 的思路就非常清晰了不要让大模型承担看页面的职责让工具承担。像 Playwright CLI 这种工具它本身不消耗 token它可以精确地打开网页、等待元素加载、点击按钮、提取指定区域的文本和属性然后只把干净的结果交给 Coding Agent。Agent 拿到的数据从整个 DOM变成一小段 JSON 或几行文本上下文立刻缩小一个数量级。有人可能会说那我直接用 Python Requests 爬网页不就得了为什么非要 Playwright因为 Playwright 能处理 JavaScript 渲染的页面能执行点击、滚动、翻页这类真实交互还能维持登录会话。现代网页大量依赖前端渲染Requests 拿到的只是空壳 HTML而 Playwright 拿到的是完整渲染后的结果——它比 Requests 强又比让 agent 看截图省 token正好卡在一个甜点上。2. Playwright CLI 的定位它是浏览器前端不是测试工具2.1 我们说的 Playwright CLI 到底指什么Playwright 官方提供了一套命令行工具通常叫playwright命令安装playwright/test或playwright包之后就能用。大多数人知道它是因为playwright install装浏览器、playwright test跑测试、playwright codegen录制脚本。但很多人没意识到这套 CLI 还可以直接执行自定义脚本通过node script.js或者python script.py的方式来驱动浏览器——它本质上是 Playwright API 的命令行入口。在实际工作中我更常用的方式是写一个 Node.js 脚本然后用命令行传参数控制行为。比如node fetch-page.js --url https://example.com/products --selector .product-item --output json脚本内部启动一个 Chromium 实例打开 URL等待选择器出现提取数据打印 JSON 后退出。整个过程完全不经过大模型不消耗任何 tokenCoding Agent 只需要调用这个 CLI读 stdout就把任务完成了。这就是整套省 token 方案的核心——把浏览器操作变成调用一个函数。2.2 为什么 CLI 比交互式浏览器工具更省钱、更可控让 Coding Agent 自己操作浏览器每次都得把截图或 HTML 塞进上下文用 CLI则是在进程外完成所有浏览器动作只回传一个简洁的结果。这意味着三件大事第一上下文长度被硬性压缩。Agent 每次只需要拿到几百个 token 的结果而不是几万个 token 的页面源码。第二行为是确定性的。CLI 脚本对页面等待什么、点击什么、提取什么都是写死的逻辑不存在模型看错了点歪了的问题即使页面结构变化导致脚本失败错误信息也是明确的selector 超时而不是模型一本正经地编造一个不存在的数据。第三成本是可预测的。用 CLI 时每个操作的成本基本固定你可以像算函数调用费一样估算整体开销不用担心模型突然决定我再多看一眼页脚导致费用失控。2.3 什么场景适合用 Playwright CLI 给 Coding Agent 打前站不是所有网页自动化的活都适合套这个模式但以下场景很典型需要登录态的页面数据采集、交互式分页列表抓取、表单自动填写后验证结果、前端渲染导致的动态内容提取、需要跨多个页面保持会话的流程。这些场景共同的特点是操作路径相对固定、数据提取逻辑明确、但页面本身复杂且需要真实浏览器能力。反过来如果任务本身就是探索性的——比如随便看看这个网站有什么值得关注的那让 Agent 直接操作可能更有价值因为你也不知道要提取什么CLI 反而成了束缚。3. 实操落地从零搭好 Playwright CLI Coding Agent 管线3.1 环境准备安装和目录结构设计我建议把 Playwright CLI 脚本独立成一个项目目录不要和 Coding Agent 的主项目混在一起。原因有两个一是依赖隔离Playwright 的浏览器二进制文件体积不小单独放方便用 CI 或 Docker 时做缓存二是权限控制Coding Agent 只需要被授予执行这个 CLI 脚本的权限不用暴露整个 Node 环境的写权限。mkdir playwright-tools cd playwright-tools npm init -y npm install playwright npx playwright install chromium要注意npx playwright install chromium这一步在中国大陆网络环境下可能需要配镜像具体是PLAYWRIGHT_DOWNLOAD_HOST环境变量指向镜像地址我后面会单独讲。装完之后写一个最简单的测试脚本能打开 example.com 并打印标题确认环境能跑通再继续往下走。3.2 核心实现一个通用的页面内容提取 CLI我写了一个比较通用的提取脚本核心逻辑是接收 URL 和一个 CSS 选择器可选地接收等待时间、登录态的 cookie 文件路径然后用 Playwright 打开页面等待选择器提取匹配元素列表里的文本和链接输出为 JSON。这里给出简化版本// fetch-page.js const { chromium } require(playwright); const fs require(fs); (async () { const args process.argv.slice(2); const url args[args.indexOf(--url) 1] || ; const selector args[args.indexOf(--selector) 1] || body; const waitFor parseInt(args[args.indexOf(--wait) 1] || 3000, 10); const cookieFile args[args.indexOf(--cookies) 1] || ; const outputFile args[args.indexOf(--output) 1] || ; const browser await chromium.launch(); const context await browser.newContext(); if (cookieFile fs.existsSync(cookieFile)) { const cookies JSON.parse(fs.readFileSync(cookieFile, utf8)); await context.addCookies(cookies); } const page await context.newPage(); await page.goto(url, { waitUntil: domcontentloaded, timeout: 30000 }); await page.waitForSelector(selector, { timeout: 10000 }).catch(() { console.error([WARN] selector ${selector} not found); }); await page.waitForTimeout(waitFor); const items await page.$$eval(selector, els els.map(el ({ text: el.innerText ? el.innerText.trim().slice(0, 500) : , href: el.href ? el.href : , })) ); const result JSON.stringify({ url, count: items.length, items }, null, 2); if (outputFile) { fs.writeFileSync(outputFile, result, utf8); } else { console.log(result); } await browser.close(); })();这段脚本看起来简单但已经能覆盖大部分抓取场景了。注意几个细节waitUntil: domcontentloaded比load更快因为很多页面的外部资源加载很慢我们只关心 DOM 结构waitForSelector用.catch()包住避免页面结构变化时整个脚本崩溃每个元素的文本做了 500 字符截断防止某个奇怪的节点把输出撑爆。实际使用中--selector参数是最关键的它决定了提取的粒度——比如提取整个商品卡片区域还是只提取价格标签。3.3 打通 Coding Agent两个接入方向CLI 脚本写好后接入 Coding Agent 有两种主流方式。第一种是agent 通过 shell 工具调用 CLI。大部分 Coding Agent包括 Codex CLI、开源的 Agent 框架、甚至自研的 agent都允许模型在执行任务时调用命令行工具。你把fetch-page.js配成一个工具描述写成输入 URL 和选择器返回页面提取结果返回费用极低agent 就会优先调用它而不是自己去开浏览器。从我的实测看只要工具描述里写清楚比直接浏览器操作便宜 75%模型的工具选择倾向会非常明显。第二种是通过 MCP 协议包装。如果你用的是支持 MCP 的客户端可以写一个十几行的 MCP server把fetch-page.js包装成一个叫fetch_page的工具注册时带好参数 schema。这种方式的好处是参数校验和错误信息更规范缺点是多了一层复杂度不太适合快速原型验证。我个人的建议是先走 shell 工具的方式跑通流程确认收益明显之后再考虑要不要 MCP 化。3.4 一个完整任务示例抓取列表后让 Agent 做分类假设业务需求是打开某招聘网站的技术岗位列表把所有 Java 岗位的标题和公司名提取出来按薪资排序。传统方案下agent 自己开浏览器拿到整版 HTML自己解析、筛选中含Java的职位上下文轻松突破 6 万 token。用 Playwright CLI 方案可以写一个针对性更强的脚本fetch-jobs.js因为网站结构固定可以直接在脚本里完成筛选和排序node fetch-jobs.js --url https://example-jobs.com/tech --selector .job-card --keyword Java --sortBy salary脚本返回的 JSON 只有 20 个匹配的职位每个职位 30 个 token总共 600 token。Agent 拿到的就是一个干净的数组它可以专注于判断这 20 个岗位里哪些值得投、怎么写申请邮件——这才是它擅长的推理工作。这一步的 token 差距非常悬殊整页 HTML 5 万 vs 提取结果 0.6 千省下来的比率远超 75%。4. 成本对比实测同样任务两种跑法的差距有多大4.1 Token 消耗拆解表我自己做了一个标准测试任务访问一个典型的企业官网列表页提取第一页的 20 条商品记录包括名称和价格。任务不算复杂但页面有动态渲染必须要真实浏览器。我用两种方式各跑了 5 次取平均值结果如下环节Agent 直接操作浏览器Playwright CLI 预处理页面初始加载含 HTML 快照18,200 token0 token滚动/等待加载后的增量读取9,400 token0 token模型解析与决策7,800 token2,400 token结果格式化2,100 token800 token错误重试平均 0.8 次5,300 token400 token总计42,800 token3,600 token看了这个表你就明白为什么说能省 75%以上——实际上测试里省了超过 90%。因为 CLI 方案里页面加载和增量读取的成本全部归零了模型只需要处理一个 3.6 千 token 的 JSON。当然这里要说明我的测试脚本针对性比较强--selector --keyword直接在脚本里把活干完了如果你只做一个通用提取脚本把筛选判断留给 agent那么最终 token 可能在 1 万到 1.5 万左右仍然能省 60% 到 75%。这取决于你用多大的力度把页面逻辑下沉到 CLI 里。4.2 省钱之外的另一笔账时间和稳定性Token 成本只是最直观的收益还有一笔容易被忽略的账是时间和稳定性。Agent 直接操作浏览器时每读一次页面都要等大模型生成响应这个过程通常要 3 到 10 秒一个 5 步的交互流程光看页面就花了一分钟。而 Playwright CLI 脚本的执行时间主要就是页面加载时间一般在 1 到 3 秒内完成模型只需要在最后生成一次响应。我实测同一个任务端到端耗时从 4 分 20 秒降到了 1 分 10 秒耗时减少约 73%。稳定性方面的差异更大。Agent 直接操作时可能因为截图分辨率、页面滚轮位置、弹出框遮挡等因素判断失误而 CLI 脚本里的waitForSelector和waitForTimeout是确定的只有找到了和超时两种结果。超时了就明确报错不会出现模型一本正经地胡说八道。所以在生产环境里我强烈建议把高频、重复的网页操作都固化成 CLI 脚本让 Agent 只做低频的决策类工作。4.3 什么情况下省不了钱也要说清楚这套方案不是银弹。如果你处理的页面每次结构都完全不同而且提取逻辑没法写成固定脚本那 CLI 的优势就发挥不出来。另外如果你的任务本身只需要一个 URL 就能拿到数据比如调用公开 API那连 Playwright 都不需要直接用 HTTP 请求更省。Playwright CLI 的适用边界是需要真实浏览器但交互路径相对可预测的场景。在这个边界内它是 token 效率的最优解之一。5. 从零排雷Playwright CLI 实战中的高频问题与处理方案5.1 安装和启动阶段的坑npx playwright install chromium在国内经常卡在下载二进制文件的环节这应该是最先遇到的坑。解决方式是设置镜像环境变量export PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright/ npx playwright install chromiumnpm 包本身也建议用镜像源否则安装playwright包那 30 多 MB 也可能很慢。另外如果服务器环境没有系统级依赖比如 CentOS 缺少 libgobject启动浏览器时会报一堆.so文件缺失这时候跑一下npx playwright install-deps chromium会自动装系统依赖。在 Docker 环境里建议直接用官方mcr.microsoft.com/playwright镜像省去手动装依赖的麻烦。5.2 登录态处理别让 Agent 每次重新登录很多网页自动化任务需要登录如果每次跑 CLI 都走一遍登录流程又慢又费事还可能触发验证码。我的做法是把浏览器 cookie 序列化到本地文件里CLI 启动时加载进去。第一次手动登录然后从 DevTools 里导出 cookie 保存为 JSON或者用 Playwright 的context.cookies()在脚本里自己导一次const cookies await context.cookies(); fs.writeFileSync(cookies.json, JSON.stringify(cookies), utf8);之后每次跑 CLI用context.addCookies()恢复会话。要注意 cookie 有有效期一般几天到几周不等需要再登录的时候重导一次。为了减小被风控盯上的概率建议同一个会话的请求间隔控制在 2 到 5 秒CLI 脚本里可以用page.waitForTimeout()控制节奏也可以用一个简单的随机延时函数。5.3 动态页面等待多久才合适很多人的 CLI 脚本会栽在waitForTimeout上——设短了元素没加载出来设长了整个任务拖得又慢又费资源。更专业的方式是等待条件而不是等待时间。Playwright 的waitForSelector是首选还可以用waitForFunction等待某个 JS 表达式返回真值await page.waitForFunction(() { const el document.querySelector(.product-item); return el el.innerText.includes(价格); }, { timeout: 15000 });这样脚本只在条件满足时继续既不会提前抓空也不用固定等最坏情况。如果页面是滚动加载的可以用page.mouse.wheel(0, 3000)模拟滚动再加一个waitForFunction检测列表数量是否增加。5.4 Cookie 失效和 Token 续期问题这是另一个和 Token 强相关的坑虽然不是大模型的 token而是网站登录态的 token。我用 CLI 跑定时采集任务时经常遇到Session 过期导致采集结果全是登录跳转页。排查方法是给 CLI 加一个结果 sanity check——比如提取结果里如果出现登录sign in等关键词或者 URL 跳转到了/login就立即退出并返回明确错误码。Agent 拿到这个错误码就知道登录失效了它会去触发重新登录流程而不是傻乎乎地把登录页数据当成正常结果塞进业务逻辑里。5.5 反爬虫与风控应对的边界Playwright CLI 启动的浏览器是有头或无头 Chromium比普通 HTTP 请求的指纹能力强很多但依然可能遇到反爬策略。最常见的应对是设置用户代理、视口尺寸以及使用chromium.launch({ headless: false })在有显示器的情况下跑但服务器没有图形环境就只能靠xvfb-run这类工具辅助。更重要的原则是控制频率、随机化行为、不要攻击性采集。我只会用这套方案做合规的数据采集和业务自动化不会碰需要绕过强风控的高价值目标那不是一个良性的技术使用方式。5.6 CLI 脚本如何让 Coding Agent 更好地理解输出给 Agent 用的 CLI 脚本输出格式非常重要。我发现三种格式最实用纯 JSON结构化数据、单行文本简短摘要、和明确的退出码 stderr 错误消息。一个容易忽略的细节是CLI 的 stderr 不要输出大段堆栈——Agent 看到大段报错反而容易慌。我在脚本里统一用console.error(ERROR: reason)格式输出一行可读错误退出码用 1这样 Agent 能快速识别失败原因。也可以直接定义一套错误码0 成功、2 选择器超时、3 登录失效、4 网络异常。6. 扩展思路这套打法还能怎么玩6.1 让 Playwright CLI 自己长出需要的新能力很多人担心脚本一旦写死页面改版就得改脚本是不是维护成本太高。我的经验是把 CLI 工具当作积木来拼。写一个基础库提供 open、click、extract、scroll 这样的小函数然后在业务层组合它们。页面改版通常只影响某个业务层的选择器改一行就行。更进一步可以让 Coding Agent 在页面改版后自动修复脚本做法是CLI 报错后把错误信息抛给 AgentAgent 对比新旧页面结构生成新的选择器自己改脚本再跑一遍。这会形成一个人机协同的闭环很好的弥补了脚本固定和网页动态变化之间的缝隙。6.2 批量任务里的并发控制单个 CLI 脚本跑一次只处理一个 URL如果有几千个 URL性能是不够的。更好的做法是脚本支持传入一个 URL 列表内部用 Promise 控制并发数。经验值是并发数设为 3 到 5太多容易触发反爬也容易把本地机器的 CPU 打满。每次跑完一批把成功和失败的 URL 分别写进两个文件失败的下次单独重试。Coding Agent 只需要发起一次调用等所有结果回来后再统一分析。这个模式在处理整个分类下的所有商品页这类任务时很管用。6.3 和 MCP、Agent 框架的更深层整合如果你的 Agent 框架支持 MCP不用只是简单加一个工具链接。可以考虑用 MCP resource 的方式暴露页面结构元数据Agent 在做计划时可以查询页面的可用操作点然后精准地调用对应 CLI 脚本而不是盲目试错。这相当于让 Agent 多了一双望远镜——它先看地图页面结构元数据再决定派哪支队伍CLI 脚本去打效率和成功率都会提升。最后一点实战心得从第一次发现结算账单漂移到搭完这套管线我的核心体会是省 token 不是省模型的钱而是省模型的注意力。你喂给 Coding Agent 的上下文越干净它做决策的质量就越高。Playwright CLI 在这套体系里是一个朴实但关键的角色它把浏览器操作从模型的负担变成了工具的责任让两者各司其职。这个方向应该还有很大的挖掘空间尤其是当 Coding Agent 的能力越来越强、网页越来越复杂的时候预处理层的价值只会越来越大。如果你也在调试类似的工作流建议先挑一个高频重复的任务做试验田把 CLI 脚本跑稳再看 agent 的账单和任务成功率会看到立竿见影的变化。