用Skill封装浏览器动作,优化Codex网页自动化的Token消耗与速度 直接说结论Codex 操作浏览器变慢、Token 消耗偏高很多时候不是模型能力问题而是让模型反复用自然语言推理去完成“打开网页、点按钮、填表单、读结果”这一整条链路每个中间步骤都在消耗 Token。更稳的做法是写一个 Skill把浏览器操作封装成一组可复用动作让 Codex 只负责定方向和看结果不负责每一步的细节推理。这个思路我在本地任务里验证过单轮 Token 能省下来不少整体执行速度也明显更稳。这篇文章适合两类人一是刚开始用 Codex 做网页自动化但发现它经常在页面交互上绕圈子的二是已经写了几个 Claude Code 或 Codex 的 Skill想搞清楚浏览器类型 Skill 到底该怎么设计结构和参数的。看完之后你会知道一个最小可用的浏览器 Skill 长什么样怎么接进 Codex 的单任务和批量任务以及哪些报错其实不是 Codex 的问题而是 Skill 本身的边界没理清。1. 先搞清楚 Codex 操作浏览器慢和费 Token 的根源1.1 Codex 默认方式每一步都要大模型“想”出来如果直接让 Codex 操作浏览器它通常会先拆任务再写代码再执行然后读取页面反馈接着判断下一步。听起来没问题但实际跑起来会发现一个特点模型在每一步都会收到页面状态、HTML 片段、控制台输出、代码报错等一系列信息尤其是遇到需要等待页面加载、需要识别按钮状态、需要处理弹窗的场景模型会反复生成新的操作代码。这里的关键问题是Codex 不是人它不会“看到”页面它只能通过接收到的文本信息来推测页面长什么样。页面越复杂DOM 里无用的节点越多模型需要处理的上下文就越长Token 消耗自然上去了。速度变慢也顺着这个逻辑来一次操作如果从发指令到拿到结果需要两轮或三轮模型推理等于每一步都在排队等模型输出。一句话总结默认玩法下浏览器操作被 Codex 当作“开放式推理任务”来处理而不是“确定的工具调用任务”。这也是它又慢又费 Token 的第一层原因。1.2 Skill 的根本变化把“思考过程”变成“固定参数”Skill 的做法完全不同。它把常见的浏览器动作拆成一批已经写好的函数每个函数有明确的输入、输出和失败回调。Codex 拿到任务后不需要每次重新发明按钮点击逻辑也不需要反复读大量 DOM 结构来猜下一步它只需要从 Skill 里挑出合适的动作填入参数然后等结果。举个例子让 Codex“打开 example.com 并截图保存”默认方式可能是先尝试写启动浏览器的代码再写打开网页的代码再写等待页面加载的代码最后写截图的代码。遇到失败还要重新生成。而用 Skill 之后Codex 只需要拿到一个参数列表比如{ action: goto, url: https://example.com, wait_seconds: 3, then: screenshot, output_path: screenshots/example.png }整个任务的复杂度从“生成一段交互式脚本”变成“匹配一个已封装动作”。模型要处理的 Token 量大幅下降执行速度也会更快。再往深层说Skill 的本质是减少大模型在低层级操作上的随机性和重复推理。我观察到很多人对 Skill 有一个误解以为它是给 Codex 写提示词让模型“按提示词操作”。实际上 Skill 更像是提供一组标准化接口Codex 是调用方不是实现方。提示词那种方式还得让模型自己理解上下文Skill 则尽量把上下文压缩成少量参数。2. 浏览器 Skill 的核心设计动作库、参数和错误回调2.1 不要把 Skill 做成一个大而全的“浏览器操作百科”很多第一次写浏览器 Skill 的人会犯同一个错误想把所有浏览器功能一次性封装进去。打开页面、点击、输入、上传、下载、截图、切换标签、处理弹窗、读取 Cookie、模拟键盘……全写在一个文件里。结果就是配置特别长Codex 在匹配动作时也会犹豫Token 消耗反而上升。我更建议从三类基础能力开始页面导航打开 URL、刷新、后退、前进。页面提取获取标题、获取可见文本、截图、读取指定元素的属性。页面交互点击按钮、输入文字、选择下拉框、提交表单、等待元素出现。这三个类别已经能覆盖八成以上的网页自动化场景。需要更复杂的动作再按需追加不要一开始就铺开。Skill 的目录里应该有独立的能力描述文件让 Codex 知道什么时候该用哪个动作。描述可以用简短中文写清楚用途、输入参数、返回值、失败情况下会怎么做。这里描述越精确Codex 选错动作的概率越低。2.2 每个动作都必须有明确的输入输出约束浏览器自动化的动作函数不能像普通脚本那样“跑通就行”因为调用方是 Codex必须让函数结果可被解析、可被判断。以 Selenium 或 Playwright 封装的函数为例我会要求每个函数输入参数全部有默认值必要参数必须校验。返回值统一成 JSON 结构包含成功标记、消息、耗时和数据。失败时返回结构化错误不要只抛一个 Exception 让 Codex 猜。一个最小动作实现大概是这个结构def goto(driver, url: str, wait_seconds: int 3): try: driver.get(url) time.sleep(wait_seconds) return {success: True, title: driver.title, url: driver.current_url} except Exception as e: return {success: False, error: str(e)}Codex 拿到返回值后只要看success字段就能决定下一步。如果返回的是纯文本或裸异常Codex 又要做一层解析Token 又浪费了。2.3 页面等待参数是省 Token 的另一把钥匙浏览器自动化最常见的隐性浪费是隐式等待过长。很多人习惯time.sleep(5)一写到底问题在于每次等待都会拖慢任务Codex 拿到反馈的时间变长如果等待时间超过它的预期模型可能还会再次生成动作Token 消耗跟着翻倍。我一般不用固定等待而是优先用显式等待比如等待某个元素出现超时再报错。这样页面正常时任务走得快页面异常时能快速失败。显式等待写成参数形式{ action: click, selector: #submit-button, wait_for: #result-panel, timeout_ms: 10000 }Codex 看到wait_for就知道这个动作的完成标志是#result-panel出现而不是盲等。这里其实是 Skill 设计最容易被低估的部分页面等待写得好Token 消耗和任务速度都会有明显变化。3. 自己动手写一个最小可用的浏览器 Skill3.1 先设计目录和动作清单我建议目录结构保持简单browser-skill/ ├── skill.json ├── actions.py ├── requirements.txt └── README.mdskill.json是给 Codex 看的动作描述文件actions.py是实际执行的 Python 函数。不要在一个文件里塞太多东西否则多了之后排错很难受。skill.json可以这样写{ name: browser-actions, description: 用于浏览器自动化的基础动作包含页面导航、页面提取、页面交互三类能力。, actions: [ { name: goto, description: 打开指定网页并返回页面标题和当前 URL。, parameters: { url: {type: string, required: true}, wait_seconds: {type: integer, default: 3} } }, { name: extract_text, description: 获取页面可见文本用于内容提取和结果验证。, parameters: { selector: {type: string, default: body} } }, { name: click, description: 点击匹配选择器的页面元素可等待目标出现后再返回。, parameters: { selector: {type: string, required: true}, wait_for: {type: string, default: }, timeout_ms: {type: integer, default: 10000} } }, { name: fill_and_submit, description: 在指定输入框填入文本并提交表单。, parameters: { selector: {type: string, required: true}, text: {type: string, required: true}, submit_selector: {type: string, default: } } } ] }这里有个细节动作描述不要写“点击红色按钮”这种口语化描述而要写“点击匹配选择器的页面元素”。Codex 需要的是明确的参数规则不是视觉描述。3.2 落一个能跑的actions.py示例不重复造轮子的话可以直接用 Selenium 做底层。先装依赖pip install selenium webdriver-manager然后写一个最小实现import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def create_driver(): options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) driver webdriver.Chrome(optionsoptions) return driver def goto(driver, url, wait_seconds3): driver.get(url) time.sleep(wait_seconds) return {success: True, title: driver.title, url: driver.current_url} def extract_text(driver, selectorbody): element driver.find_element(By.CSS_SELECTOR, selector) return {success: True, text: element.text} def click(driver, selector, wait_for, timeout_ms10000): element WebDriverWait(driver, timeout_ms / 1000).until( EC.element_to_be_clickable((By.CSS_SELECTOR, selector)) ) element.click() if wait_for: WebDriverWait(driver, timeout_ms / 1000).until( EC.presence_of_element_located((By.CSS_SELECTOR, wait_for)) ) return {success: True, clicked: selector} def fill_and_submit(driver, selector, text, submit_selector): input_element driver.find_element(By.CSS_SELECTOR, selector) input_element.clear() input_element.send_keys(text) target submit_selector or selector driver.find_element(By.CSS_SELECTOR, target).submit() return {success: True}这套代码背后的思路是先把最容易出错的等待逻辑收拢到函数内部Codex 调用时不需要关心 Selenium 的显式等待怎么写只要给参数。3.3 Codex 调用 Skill 的最小示例Skill 文件放好后在 Codex 任务里可以直接描述目标让它从browser-actions中调用动作。一个典型任务是用 browser-skill 打开 https://example.com获取页面文本并截图保存到 output/example.png。Codex 会根据skill.json的描述匹配goto和extract_text甚至会和 actions.py 里预置的其他方法组合。但有一点要注意Codex 的版本不同Skill 的加载方式也有差异。我这里给出的结构是我本地验证过的通用组织方式如果你的 Codex 版本对 Skill 有专门的目录约定优先按它自带的模板来调整。如果你只是手写一个提示词把skill.json的内容直接贴进 Codex 的任务描述里也能达到一部分效果。但这种做法的可维护性很差动作一多提示词会变得很长反而比直接用 Skill 更费 Token。能装成正式 Skill 结构就不要图省事塞提示词。4. 接入实际工作流从单任务到批量任务4.1 单任务验证是第一步不要直接上批量Skill 写完后第一件事不是让它一次处理几十个 URL而是先跑通单条任务。我用一个本地测试页面做验证时会按这个顺序看打开页面是否成功。页面标题和 URL 是否正确返回。需要点击的元素是否有明确的唯一选择器。表单提交后页面是否出现预期结果。截图输出路径是否存在文件是否生成。任何一步失败都要先回到 Skill 本身排查而不是继续堆任务。很多时候问题出在页面选择器不唯一、页面结构变化、未登录状态导致跳转这些都不是 Codex 的问题。单任务跑通后再写批量逻辑。批量任务的核心不是并发数而是输入列表、输出命名和失败重试。默认情况下我建议先用 for 循环顺序处理不要一上来就开多线程或异步并发。浏览器自动化任务和普通 API 请求不一样每个任务都需要独立浏览器上下文并发开太大机器内存、CPU、浏览器实例都会扛不住。4.2 批量任务里的 Token 策略批量处理时Token 消耗的主要来源不再是单个动作的解析而是 Codex 每轮都要读取任务结果、判断下一步。如果要处理 20 个网址最省 Token 的做法是让 Codex 只做一次任务规划然后用脚本循环执行而不是让 Codex 对每个网址都重新推理一遍。一个可行的分工是Codex 负责生成 URL 列表和输出目录。Python 脚本循环调用 Skill 里的动作。每处理一个 URL记录成功或失败结果写入日志。Codex 只在最后读取日志汇总失败项。这样 Codex 参与推理的次数会大幅减少。如果每个 URL 都要 Codex 亲自操作Token 消耗会随着任务数量线性增长完全失去了 Skill 的意义。4.3 输出命名和失败重试处理批量任务时最容易翻车的不是网页打不开而是输出文件名冲突。建议输出文件名里带上任务序号或 URL 的哈希值避免覆盖output_name f{index}_{hashlib.md5(url.encode()).hexdigest()[:8]}.png失败重试不要无限重试控制在 2 到 3 次。每次重试前加一个短延时同时记录失败原因。重试仍然失败的把 URL 和错误信息写进一个failed.txt最后统一给 Codex 看。这样才能判断是网站本身不可达还是 Skill 里的选择器需要更新。5. 性能判断Token 省了多少、速度提升了多少不能靠感觉5.1 Token 消耗怎么测不看 Token 用量就评估 Skill 的效果很容易被“感觉快了”带偏。最简单的方法是分别跑两个任务一个用默认方式让 Codex 直接操作浏览器另一个用 Skill。输入同样一批网址输出同一套结果然后对比 Token 用量。在我本地测试中同样处理 5 个网页并截图默认方式的 Codex 会频繁读取页面文本和 DOM 信息Token 消耗明显偏高。使用 Skill 之后模型主要处理的是动作参数和返回结果Token 主要集中在任务描述和结果日志上。不同任务之间差异会很大但方向是一致的把动作封装得越厚模型需要处理的中间上下文就越少。5.2 速度提升怎么判断速度不能只看总耗时还要看模型推理轮数。Codex 操作浏览器时如果总耗时 3 分钟其中模型生成代码和解析页面可能占了 2 分半浏览器本身只跑了 30 秒那优化的重点应该在推理轮数和上下文长度。Skill 的价值就是把这里的推理轮数压下来。你可以打开 Codex 的日志观察一次任务里模型输出了多少轮。轮数越多Token 消耗越高执行时间也越长。Skill 用得好任务轮数会明显减少尤其是在重复动作比较多的场景。5.3 稳定性比速度更重要浏览器自动化里稳定性通常比速度更有价值。一次跑通 20 个页面比一次跑通 5 个但波动很大更有实际意义。稳定性可以从三个维度看成功率成功任务数除以总任务数。失败原因分布是超时、选择器找不到、还是登录失效。可重复性同一批任务间隔一段时间后再跑结果是否一致。如果成功率低于九成先不要优化 Token先把失败原因找出来。很多时候是 Skill 里页面等待不够或者是页面结构偶发变化。6. 常见报错、坑点和排查顺序6.1 Codex 找不到 Skill 或动作描述失效这个问题常见于第一次安装完 CodexSkill 目录没有放对位置。排查顺序是确认 Skill 目录路径是否符合 Codex 当前版本的要求。确认skill.json里的name字段是否清晰。用最简单的动作让 Codex 调用一次比如goto看是否能正确匹配。如果仍然匹配不到把skill.json的内容精简后放进 Codex 的附加上下文里先验证动作本身能跑通再回头检查目录结构。我遇到过很多次“动作没问题但 Codex 不调用”的情况最后发现是描述里没有写清楚使用场景。描述写得太抽象Codex 不知道什么时候该用。6.2 浏览器驱动报错和页面元素找不到这类问题要按环境、页面、等待三个层次排查环境层确认浏览器版本和 driver 版本是否匹配。用webdriver-manager能省很多事但第一次运行时要能正常下载驱动这里对网络环境有要求。页面层打开页面后先用extract_text获取页面文本确认页面实际加载内容。很多时候是页面跳到了登录页、验证页或空白页。等待层检查是等待超时还是元素根本不在 DOM 里。如果元素是动态加载出来的CSS 选择器参数要改成等待目标元素出现后再点击。还有一个容易踩的坑选择器不过唯一。页面上可能有多个按钮的 class 相同Codex 传给 Skill 的选择器命中多个元素Selenium 默认点击第一个结果可能点错。能加id就用id没有id就要用更精确的 XPath。6.3 登录态、Token 状态相关报错这类报错和 Skill 本身无关常见于访问需要登录的页面时登录信息没有持久化或者访问目标提示 token 过期、地域访问限制。遇到这种情况不要在 Skill 里硬写“模拟登录绕过”之类的逻辑合规的做法是先确认目标页面在当前网络和账号条件下是否能正常访问。登录类任务建议使用已保存的浏览器 Session或使用官方提供的自动化测试账号。如果页面返回 token 相关错误优先检查账号状态、Cookie 是否过期、请求是否带上必要的会话信息。如果目标站点明确限制某些地区的访问应该尊重站点的访问策略而不是写脚本绕过。这里最要避免的是把 Skill 写成一个“专门用来规避限制”的工具这种方向不论从合规角度还是从稳定性角度都不建议做。6.4 并发开太高导致资源被占满批量跑浏览器自动化时进程卡住、页面打不开、截图全黑很多时候不是代码逻辑问题而是浏览器实例开太多内存和 CPU 被耗尽。判断方法很简单看任务期间的 CPU 占用和内存占用。如果内存长期在 80% 以上就该降低并发数量。一个通用建议是顺序执行的稳定性优于并发执行。必须并发时把并发数控制在 2 到 3 个浏览器实例以内并且给每个实例独立配置 temp 目录和 user-data-dir避免浏览器锁冲突。7. 更进一步Skill 的复用和扩展方向7.1 从一个网站扩展到一类网站很多人的浏览器自动化需求不是只跑一个站点而是跑一类站点。比如下载页面截图、抓取页面正文、批量查询数据。这类需求可以把动作再抽象一层从“网页操作”升级成“站点任务”。例如做批量查询时可以写一个search_and_extract动作参数只有 URL 模式、关键词和结果选择器。Codex 只需要调用一个动作就可以完成搜索、等待、提取三个步骤。这样做的好处是 Codex 的 Token 消耗进一步降低代码也更稳定缺点是需要针对具体站点的页面结构做适配。7.2 和其他工具配合使用Codex 不是唯一能用 Skill 思路的智能体Claude Code 也有类似机制。如果你之前给 Claude Code 写过 Skill可以借鉴同样的动作封装思路把浏览器操作部分抽成独立的 Python 模块让不同的智能体共用同一个函数库。这样维护成本更低。还可以把浏览器 Skill 和外部数据源配合。比如从文件里读取 URL 列表处理完成后把结果写回 CSV 或数据库。Codex 只负责组织流程真正跑重复动作的是封装好的脚本。7.3 什么情况下不需要写 Skill不是所有浏览器任务都值得写 Skill。如果你只需要手动跑一次自动化脚本写完即弃那直接让 Codex 生成普通脚本就可以。只有当任务会反复执行、动作组合相对固定、Token 成本开始成为顾虑时才值得投入时间做 Skill。我个人的判断标准是同一个任务跑超过三次并且每次执行步骤几乎一样就值得把它 Skill 化。如果每次都面对完全不同的页面结构Skill 反而会成为负担因为你要不断调整选择器和等待逻辑。最后说一个不容易察觉但很关键的点Skill 的维护不是“写完就不管”。网页结构会变选择器会失效等待时间需要调整。真正长期使用的 Skill应该在某次任务失败后主动去改动作参数而不是反复让 Codex 重试同一个动作。把这个习惯建立起来Codex 操作浏览器的场景才会越来越省心。