给大模型装上双手:基于CDP的浏览器Agent设计与实现
给大模型装一双手一个能自己开 Chrome 把票买完的 Agent是怎么设计出来的不卖关子先说结论我最近做了一个挺有意思的小项目核心目标就一句话——让大模型不仅能聊天还能自己动手操作 Chrome 浏览器把一件完整的活儿干完。最直观的演示场景是抢票告诉它明天下午去北京的高铁票帮我买一张它能自己打开浏览器、进到 12306、搜索车次、选座、填乘车人、提交订单一路走到支付确认页。整个过程不需要人碰鼠标键盘Agent 自己根据页面变化决定下一步干什么。说白了这是我从大模型聊天机器人跨向大模型数字员工的一次实打实的尝试。市面上大家聊 Agent 已经聊得很多了但大多数停留在用 API 调工具的层面真正能让模型操纵键盘鼠标、操作浏览器里每一个按钮和输入框的其实不多。我也踩了不少坑绕了不少弯好在这一版终于能跑了。这篇把设计与实现过程完整拆出来包括方案选型、架构设计、核心代码逻辑、常见翻车案例希望能给正在搞 Agent 开发的朋友一点参考。1. 项目从哪来为什么大模型必须得有一双手1.1 场景痛点从聊得好到办得成还差一个浏览器我先说个很直接的感受纯聊天式的 AI 助手无论底层模型多强都只能停留在建议层面。你说帮我买票它给你列出操作步骤、告诉你哪个 App 便宜然后呢你还是得自己打开 App自己一个个点。这个最后一公里极其磨人也恰恰是 Agent 最大的机会所在。操作 Chrome 完成真实任务和调用 API 处理数据有一个本质区别API 是确定性的、结构化的输入输出是死的而浏览器里的网页是动态的、不确定的同一个按钮可能在不同时间有不同状态登录弹窗、验证码、加载动画随时会冒出来。要让大模型具备处理这种不确定性的能力就必须给它装上眼睛和手——眼睛负责观察页面发生了什么手负责执行点、输入、滚动、跳转这些原子操作。1.2 我对会买票的 Agent的边界定义项目开始前我给自己设定了一个清晰的目标范围不然这种项目特别容易失控。我要求这个 Agent 至少能做到以下这些能从一句自然语言任务出发自己拆解成可以按顺序执行的子任务列表。能启动或者连接一个真实的 Chrome 实例像人一样在一个普通浏览器里完成操作。能实时感知页面状态当前地址是什么、页面上有哪些可点击按钮、表单要填什么内容。每个操作之间能自己总结经验、修正错误而不是一条道走到黑。在购票这种真实场景里能一次跑通搜索-选择-填写-提交-确认的完整链路。我刻意没有把自动完成支付当成项目的终点因为支付环节涉及太多安全和合规问题而且各家支付的验证逻辑五花八门硬要给 Agent 加支付能力既不稳妥也没有必要。设计上明确到确认订单页为止既展示了技术闭环也守住了边界。1.3 和市面上现成方案的对比为什么非要自己造开始之前我并不是没有调研过现成的开源项目。当时市面上确实有一些偏向通用助手的框架但试用过后我有几个痛点一是太重引入了很多我用不上的模块二是太聪明它更擅长跟你对话而不是精准操作某几个按钮三是最关键的那些框架对浏览器的控制往往封装得特别厚一旦页面有自定义组件或复杂交互底层细节被挡住排查问题非常痛苦。所以这个项目我从一开始就决定走轻量但本源的技术路线自己掌控浏览器控制协议、自己设计大模型和浏览器之间的交互逻辑。这样的好处非常明显出了问题我能直接看到最底层的信息迭代起来速度极快。2. 架构设计思路怎么给大模型装上眼睛和手2.1 整体架构一句话讲清系统怎么转起来把这个项目的架构用大白话讲就是三条线大模型是大脑Chrome 是身体连接大脑和身体的是一套神经协议——Chrome DevTools Protocol简称 CDP。每次任务启动后系统进入一个循环大模型看当前页面的状态产出下一步该做什么的决策然后调用浏览器控制模块去执行具体的操作操作完之后再截一张图或者拉一份最新的页面状态反馈给大模型让它判断这一步成功没有、下一步干什么。这个循环会一直持续到任务完成或者出现无法修复的错误为止。用买票的场景来说模型先看 12306 首页决定输入出发地、目的地和日期然后点击查询等页面加载出车次列表再分析列表选中合适的车次以此类推。2.2 方案选型CDP 到底强在哪儿如果只是想做浏览器自动化市面上常见的方案还有 Selenium 和 Playwright。这三者之间我做了非常认真的对比最终选择直接基于 CDP 自己做开发原因有三个。第一CDP 是 Chrome 亲儿子能拿到最底层的页面信息。Selenium 做的是WebDriver 协议层面的封装很多东西被框架吃了遇到疑难杂症很难定位。CDP 直接暴露 DOM 节点、网络请求、JS 执行结果、页面渲染状态等连性能分析、网络拦截这种操作都能做。第二CDP 天然支持实时双向通信。它走的是 WebSocket意味着浏览器里发生的任何事件都可以推给我的控制脚本比如页面加载、DOM 变化、弹窗出现、网络请求完成这比轮询脚本去检查状态实时性高得多也稳得多。第三CDP 可以在不占用屏幕的情况下运行。Selenium 那套一般要在当前桌面开一个看得见的浏览器进程而 CDP 协议允许你在后台驱动 Chrome非常符合服务端部署的诉求。当然直接基于 CDP 开发也意味着自己要处理更多底层细节比如建立 WebSocket 连接、管理消息 ID、处理目标靶页面切换等。但以我的经验这些底层细节恰恰是项目做得稳不稳的分水岭。现在网上也有像 Puppeteer 这种把 CDP 封装得很成熟的库如果你不想踩裸 CDP 的坑用它也是很理智的选择。我这个项目为了绝对可控用的是 Python 直接操作 WebSocket后面会说代码。2.3 核心循环感知、规划、执行、验证四步走这套系统的灵魂是一个四步循环我是参考了 ReActReasoning Acting模式来设计的。第一步是感知。系统从 Chrome 拉取当前页面信息。这里有两条路线一条是拉 DOM 快照把 HTML 文本整理成精简版另一条是截图让多模态大模型直接看。两条线我都有试后面详细讲。这一步的产出是一个当前页面状态交给大模型。第二步是规划。大模型拿到页面状态和用户最初的任务目标用 prompt 引导它基于这一步的信息给出下一步动作。在早期版本里我让模型输出先用英语解释一下当前页面情况再给出要执行的动作这样做的好处是模型的推理过程被显式地写出来出 Bug 的时候我能看到它的思考链路是不是正常。第三步是执行。根据规划的结论调用具体浏览器控制函数比如 click_button(index)、fill_input(text, index)、scroll_page(direction) 这种原语级别的操作。每一步只做一件事不做复合操作。第四步是验证。操作完之后不能急着进入下一步要重新拉取页面状态让大模型判断刚才的步骤是不是真的执行成功了。比如点击了查询按钮之后如果页面还在加载就要等一等再检查如果有弹窗出现就得优先处理弹窗。这一环在最开始设计时容易被忽略但实际跑下来发现它是稳定性最关键的环节。2.4 操作粒度为什么每次只做一件小事在早期设计时我想着既然大模型能力那么强让它一次干完三步应该没问题吧比如搜索车票然后选择第一个车次我给模型一个高自由度让它自己连续操作多个步骤。结果测试时翻车翻得很惨——模型容易在下一次操作时忘了上一步的结果或者因为页面加载速度和预期不一致导致操作落空。后来我把操作粒度缩小到最小级别一次只让模型做一个 click 或者一次 fill。粒度越小每一步的验证成本就越低模型的决策压力也越小。虽然循环次数变多了但整个系统的可预测性明显提升。这就好比你让一个实习生独立负责整个活动流程他大概率手忙脚乱但你让他一步步来、每做完一步报告一次成功率一定更高。2.5 为什么不做纯视觉控制而选择混合感知这里要重点讲讲我试过的两条感知路线。第一条是纯视觉方案用 CDP 截全屏截图把截图丢给多模态大模型比如 GPT-4o 这类让模型看图来理解页面上的元素位置然后输出要点击的坐标。听起来很酷像人一样看屏幕。但实际用下来稳定性堪忧因为网页上的元素位置会随屏幕尺寸、分辨率、缩放比例而变化同一个按钮在不同环境下坐标完全不一样。而且截图是平面化的信息被遮挡的、需要 hover 才出现的元素根本看不到。第二条是纯 DOM 方案直接把当前页面的 HTML 拉下来转成文本摘要然后用文本方式让模型理解。这种方案的好处是拿到的是结构化的语义信息元素 ID、类名、标签类型都在坏处是网页上的内容往往非常多直接把整页 HTML 塞给大模型既浪费 token 又容易让模型抓不住重点。最终我采用的是混合方案先用 DOM 提取出当前页面上可交互元素列表按钮、链接、输入框、下拉框、选项卡这些加上简单的文字描述和序号然后把这份精简的交互清单配合完整的页面摘要一起交给模型。模型做决策时只要能回答选哪个序号、做什么操作就行。截图只作为辅助信息在模型不确定或者页面状态异常时才启用。这是我在反复测试中摸索出来的稳定性最高的组合。3. 核心模块拆解与关键技术实现3.1 浏览器连接控制模块Chrome 是被遥控的这个模块是整个系统的地基也是最底层的一层。我在项目里封装了一个 ChromeSession 类核心功能有三个启动 Chrome、获得 CDP 连接、发送命令和接收事件。先说启动 Chrome。为了方便调试和部署我用的命令是这样chrome --remote-debugging-port9222 --user-data-dir/tmp/chrome-profile --disable-blink-featuresAutomationControlled这里有两个关键点。--remote-debugging-port9222是让 Chrome 开放调试端口这样外部脚本就能通过 CDP 协议控制它--disable-blink-featuresAutomationControlled是去掉浏览器里自动化控制的标记降低被网站检测出来的概率——当然这个只解决技术层面的检测问题并不是鼓励拿来做任何违规操作。--user-data-dir也很重要如果不指定Chrome 每次启动都是全新 profile登录状态完全保持不了。启动完之后系统通过 HTTP 请求http://127.0.0.1:9222/json获取当前所有打开的标签页列表找到我们要控制的那个 tab 的 WebSocket 地址然后用 Python 的 websocket-client 库连上去。import json import requests import websocket # 获取标签页列表 tabs requests.get(http://127.0.0.1:9222/json).json() # 选择第一个页面类型的标签 tab next(t for t in tabs if t[type] page) ws_url tab[webSocketDebuggerUrl] # 建立WebSocket连接 ws websocket.create_connection(ws_url, timeout10)连接上之后我要给 WebSocket 维护一个自增的消息 ID每条命令发出去之后保存 ID 和回调函数的对应关系当收到响应时通过 ID 找回调函数。CDP 的协议是基于 JSON 的一次典型的命令长这样def navigate(url): cmd { id: next_id(), method: Page.navigate, params: {url: url} } ws.send(json.dumps(cmd))浏览器收到命令后执行并返回结果。同时CDP 还会主动推送各种事件比如Page.loadEventFired、Runtime.consoleAPICalled、Page.javascriptDialogOpening等。我专门写了一个事件监听线程把事件收集到一个队列里供上层决策使用。3.2 页面状态感知模块怎么让大模型看懂浏览器刚才说了系统需要把页面状态转成模型能理解的格式。这一步我花的时间最多因为处理质量直接决定了整个 Agent 的智商高低。我一开始的做法是直接把document.body.innerText拿过来结果发现页面里的文本噪声太大到处都是广告、推荐位、页脚之类的东西模型经常被干扰。后来我改成精细化提取用 JavaScript 在页面里遍历所有可交互元素提取它们的标签名、可见文本、属性信息以及它们之间的层级关系。核心代码大致是这样function getInteractiveElements() { const elements document.querySelectorAll(button, a, input, select, textarea, [rolebutton], [rolelink]); const list []; let index 0; elements.forEach((el, i) { const rect el.getBoundingClientRect(); // 过滤掉不可见元素 if (rect.width 0 rect.height 0) return; if (getComputedStyle(el).visibility hidden) return; if (getComputedStyle(el).display none) return; const label el.innerText?.trim()?.substring(0, 50) || el.value || el.getAttribute(aria-label) || el.name || el.id || ; list.push({ index: index, tag: el.tagName.toLowerCase(), label: label, placeholder: el.getAttribute(placeholder) || , href: el.getAttribute(href) || , id: el.id || }); }); return list; }然后把这份 JSON 数组转成文本格式发布给大模型每一行代表一个可操作元素前面带一个序号。为了让模型更准确地决策在 prompt 里我会清晰地告诉它你要使用的操作列表中每个元素前面带序号点击时使用 click(序号)输入文本时使用 fill(序号, 内容)如果有多个输入框按从上到下从外到内的顺序排列。值得单独说的一点是一定要把视觉信息也保留下来。虽然有 DOM 就够了但有些场景只有 DOM 信息不足比如页面弹出一个视觉上的验证码、图片上的文字、地图上的某个位置。我的策略是默认优先用 DOM 文本如果模型判断需要观察视觉信息比如页面是图表或验证码才主动调一次截图接口把 base64 图片传过去。3.3 任务规划与决策模块大模型的正确用法有了页面状态感知接下来就是核心的大脑环节。这个模块没有特别复杂的框架核心就是把大模型当成一个 step-by-step 的决策器。系统每次请求 LLM 的 prompt 结构是稳定的包含以下几个区块系统人设区告诉模型你是一个浏览器操作助手你的任务是通过操作页面元素来完成任务目标你只能输出合法的动作指令。任务目标区把用户最初的自然语言目标放进去。页面状态区把上面提取到的交互元素列表放进去同时附上当前 URL、页面标题、最近一次操作的结果。历史操作区只保留最近几轮的操作摘要防止上下文过长。输出格式区强制要求模型按固定 JSON 格式输出例如{thought: 用户想看车次列表当前页面已经搜索完成接下来选择第一个车次, action: click, parameter: {index: 1}}。我特别强调这一步里的输出格式强制约束。大模型有时候会啰嗦或者输出和操作指令混在一起的解释所以我用 JSON schema 来约束它。如果解析失败就重试重试两次还失败的话就放弃本次决策并上报错误。这里要给一个很重要的建议不要在 prompt 里给模型太大的自由。最开始的版本我告诉模型你可以用任何方式完成任务结果它有时候给我输出一段 JavaScript 代码让我执行有时候跳转到一个完全无关的页面完全不按套路出牌。后来我把动作集固定成几个原子操作类型——navigate跳转、click点击、fill填表、select下拉选择、scroll滚动、screenshot截屏、submit提交表单、wait等待——让模型只能在动作集里选。有了约束模型的表现稳定了很多。另外上下文管理也是大坑。购票这类长任务整个流程可能要跑几十个步骤每一步的页面信息都很大很容易就把 token 窗口撑爆。我的策略是每一个循环完成后把页面内容从 prompt 里丢掉只保留操作历史摘要。这个摘要也是让大模型生成的类似于已输入出发地北京目的地上海已点击查询按钮当前等待页面加载结果。这样即使任务很长输入模型的 token 数量也基本保持恒定。3.4 验证与容错模块Agent 不翻车的最后防线验证模块是我在做了无数遍测试之后才逐渐完善的也是这套系统从能跑到稳了的关键。每执行完一个动作系统不会立刻进入下一步而是先做一次效果确认。效果确认包含三个层次。第一层是执行器确认比如点击操作浏览器有没有找到这个元素并触发 click 事件执行器返回的响应是不是正常的。第二层是状态变化确认操作后重新拉取页面状态比对前后差异如果点了查询按钮但页面 URL 和内容都没变那很可能是个异常情况。第三层是大模型确认把操作前后的状态浓缩后发给大模型让它判断本轮操作是否达到了预期目的。如果三层中任何一层判定失败系统会进入自动纠错流程。纠错逻辑我设计成多级策略。第一级是重试清除页面网络延迟的影响后再执行一次同样的操作第二级是刷新页面有些异常刷新就解决了第三级是换条件比如点不到下一步按钮就检查是不是弹窗挡住了先点掉弹窗再操作最后一层是换方案如果前三层都失败就让大模型重新分析整个页面的状态给出一个完全不同的执行策略。这个验证模块给我省了无数精力没有它Agent 经常会在第 5 步的时候因为第 3 步的一个微小偏差全盘崩溃。有了它即便出现局部失败也能在最接近失败点的地方恢复而不是从头再来。4. 实操实录从零写一个能买票的 Agent4.1 环境准备与项目结构说实话这个项目对硬件和软件的要求都不太挑剔。我自己用的是一台普通的 MacBookPython 版本是 3.10Chrome 是稳定版。依赖库非常少核心就是requests、websocket-client、openai三个外加一个处理图片的Pillow偶尔用来压缩截图。项目结构我按模块拆得很清爽agent-browser/ ├── main.py # 主入口负责整体调度循环 ├── chrome_session.py # Chrome 启动、CDP 连接、命令封装 ├── page_parser.py # 页面状态提取把 DOM 转成模型可读格式 ├── agent_core.py # 大模型决策逻辑prompt 组装和响应解析 ├── actions.py # 原子操作执行器click/fill/scroll 等 └── config.py # 配置文件模型名、超时时间、启动参数主入口代码的核心逻辑并不复杂就是一个循环不断调用各个模块def main(): session ChromeSession() session.connect() page PageParser(session) agent AgentCore(modelgpt-4o) task 帮我买明天从北京到上海的高铁票出发时间下午2点以后 page.navigate(https://www.12306.cn) for step in range(50): state page.get_state() action agent.decide(tasktask, statestate, historyhistory) result execute_action(session, action) confirmed agent.confirm(action, state, page.get_state_after()) if not confirmed: result handle_failure(action, state, page) history.append(condense(result)) if agent.is_task_done(state): break4.2 控制 Chrome 的第一步代码从环境准备到实现控制关键的第一步是让脚本和 Chrome 之间建立 CDP 连接。我先把启动参数的封装说明写一下。class ChromeSession: def __init__(self, port9222, user_data_dirNone): self.port port self.user_data_dir user_data_dir or f/tmp/chrome-agent-{os.getpid()} self.ws None self.msg_id 0 self.pending {} def start(self): cmd [ google-chrome, f--remote-debugging-port{self.port}, f--user-data-dir{self.user_data_dir}, --disable-blink-featuresAutomationControlled, --no-first-run, ] subprocess.Popen(cmd, stdoutsubprocess.DEVNULL, stderrsubprocess.DEVNULL) time.sleep(2) self.connect()这里有一个经验值得分享指定独立的user-data-dir能让你拥有一个完全独立的 Chrome 环境。不用系统默认 profile之后无论是登录态、插件、缓存都不会影响你自己的浏览器。配合定时任务甚至可以同时启动多个隔离的 Chrome 实例互相之间零干扰。4.3 页面提取和 LLM 决策的完整配合再往下是核心的决策链路。我把 page_parser 提取到的交互元素传给 agent_core然后让模型输出一个结构化的动作。为了防止模型给出的参数格式不对我加了一段严格解析def decide(self, task, state): prompt self.build_prompt(task, state) response self.client.chat.completions.create( modelself.model, messages[{role: user, content: prompt}], response_format{type: json_object} ) result json.loads(response.choices[0].message.content) action result.get(action) params result.get(parameter, {}) return action, params为了让解析更可靠我还会加一层逻辑校验。比如 action 是click而params里没有index字段就直接判为非法输出要求模型重新回答。如果连续三次非法我就降低模型输出难度——采用 step-by-step 引导先让模型只回答页面上有哪些关键内容再让它把下一步操作单独拿出来写。4.4 让 Agent记住前面的操作不迷失方向长任务最怕的就是做到后面忘了前面。我参考 ReAct 模式的思路做了一个操作摘要记忆环。每完成一步操作后系统让模型总结一句话类似用户要买票我已经搜索了北京到上海的车次当前有15个结果下一步根据条件筛选车次。这个摘要会作为下一轮的上下文输入。这样有几个好处模型只需要关心当前页面上看到什么 刚才做了什么摘要 用户目标不需要把所有历史页面数据都塞进去。token 省了模型的注意力也能更集中。4.5 调通全流程从搜索车次到确认订单的全链路演示我拿 12306 购票做个完整的环节演示这里只是技术演示不涉及任何真实违规操作。任务输入进去之后Agent 的执行链路大概是这样的首先Agent 通过 Page.navigate 进入 12306 首页。因为登录页经常有滑块验证我先做了一个合理的设计第一次使用时自动打开一个浏览器窗口并停留在登录页面让用户手动完成登录和验证码登录态通过持久化 profile 保持在本地。后续 Agent 每次启动加载同样的 profile就已经是登录状态了不需要重复登录。这个设计非常关键它把人机协作放在最初的登录环节之后把整个流程全部交给 Agent 自主执行。进入首页后模型通过页面状态感知模块识别出出发地输入框、目的地输入框、出发日期输入框的位置。依次执行 fill(出发地, 北京)、fill(目的地, 上海)、fill(日期, 明天)。然后是点击查询按钮。查询后页面跳转到车次列表。模型需要在这个列表里理解哪些是车次号、哪些是出发时间、哪些是余票信息。我这里没有选用大模型直接读原始 HTML 的做法而是把每一行车次提取成结构化的 JSON比如车次号、出发时间、到达时间、历时、座位等级及余票数然后让模型按用户要求做筛选。筛选完成后点预定按钮之后进入乘客信息填写页面。如果乘车人在常用联系人里模型可以直接选择而不是手动输入。然后是确认订单页面模型点击提交订单按钮弹出确认框后再点确认。到这里整个流程就走完了。最后我设置了一个is_task_done判断让模型看到订单已提交或请完成支付的页面后明确返回一个完成信号而不是继续执行操作。4.6 日志和可视化调试 Agent 的救命稻草Agent 开发过程中调试是一件特别痛苦的事。系统在循环模型在决策每一步都可能出错而且错误原因可能来自 DOM 解析、prompt 设计、网络状态、模型随机性等多个层面。为了能快速定位问题我专门加了一套步骤日志系统。每执行一步操作我会记录当前任务的步数、模型的思考内容、最终执行的动作和参数、页面的关键状态、操作执行的结果。这些日志统一打到一个 JSON Lines 文件里既方便回放也方便做数据分析。遇到精度问题的时候我会把这些 JSON 日志加载成表格在本地查看效率高很多。这个日志系统后来成了我的时间机器。每次跑完一个失败案例我不需要重新复现直接看日志就能知道模型在第几步走了弯路是因为页面信息不全还是因为页面加载太慢或者是因为提示词里的某个关键词起了误导作用。5. 常见问题与排查技巧实录5.1 元素定位不到一劳永逸的思路这是做浏览器 Agent 遇到的最频繁的问题。原因有很多元素在 iframe 里、元素是懒加载的还没渲染出来、元素用的是 div 模拟而不是原生 button、页面开了 shadow DOM 等。我自己做一个简单的优先排查表原因判断方法解决方案页面没加载完成元素列表为空或明显不完整加长等待时间或监听 loadEventFired 事件元素在 iframe 里document.querySelector 查不到先切换 focus 到对应 frame 再获取元素懒加载滚动后元素才出现先执行滚动操作再重新拉取状态自定义组件遮挡元素存在但不可点击提取上层可视元素先关闭弹窗或隐藏遮挡层shadow DOM常规 selector 找不到使用递归查询 shadowRoot 链路我的建议是把页面状态感知做成一个独立的、频繁调用的接口每一次环境变化后都重新拉取一次不要缓存任何 DOM 信息。网页是动态的永远以最新状态为准。5.2 iframe 内部元素怎么处理12306 的登录页就是一个典型的 iframe 场景还有不少第三方登录框都是嵌套 iframe 的。直接用document.querySelector是拿不到 iframe 内部内容的于是我把页面解析函数做成递归式先遍历页面的主 document然后查找所有 iframe 节点再切换到每个 iframe 的 document 里递归遍历交互元素。在返回结果的时候我会给每个元素额外标记一个frameId或者framePath字段执行操作时先切换到对应的 frame 再执行。这个功能看着小但少了它很多真实网站根本走不通。5.3 登录态怎么保持在早期版本每次启动 Agent 都是一个全新的、未登录的浏览器每次都在登录环节卡住非常浪费时间和 token。后来我改用上面说到的独立user-data-dir方案效果立竿见影。登录一次所有的 cookie、localStorage 都持久化到了这个目录里下次启动直接带着登录状态。我还额外做了一层兜底每次任务启动前访问目标网站验证当前是否处于登录状态如果发现跳转到了登录页就提示用户手动介入一次登录登录完成后自动继续。5.4 模型输出非法 JSON 或乱发散这也是一个高频问题。我在模型调用端做了两件很重要的事第一启用response_format强制 JSON 输出实测下来几乎不会出现非 JSON 响应第二用 Pydantic 校验输出的字段合法性和类型。如果校验失败我会把错误信息附加到 prompt 里继续让模型修正。这个方法对大多数错误都有效。5.5 页面加载慢怎么办页面加载慢是浏览器 Agent 最大的隐患因为 Agent 的操作节奏由页面加载速度决定。等太短元素没加载完等太长白白浪费时间。我的做法是用 CDP 的事件监听Page.loadEventFired和Network.loadingFinished来判断关键资源是否加载完成等不到事件就做一个兜底超时比如 10 秒。另外在操作后的验证阶段我会加一个页面变化检测对比操作前后页面内容的哈希值如果内容还在变化说明页面仍在加载继续等待。5.6 任务的意外中断和恢复有一次跑了一个超长任务跑到第 20 多步网络闪断导致 WebSocket 连接断开整个进程直接崩溃了。后来我做了两件事一是给 WebSocket 加了自动重连机制二是给整个任务加了断点续跑。所谓断点续跑就是定时把当前的操作历史摘要和任务状态存成快照文件进程重启后可以加载快照、继续执行。这个功能对长任务尤其重要不然每次中断都要从头开始效率和稳定性都跟不上。5.7 安度验证码这道坎的正确姿势说句实话验证码是浏览器 Agent 目前最难完全绕过的坎。弹窗式、滑块式、点选式都有各种方式背后还有复杂的行为分析。我的策略是分场景处理文本类验证码可以通过 OCR 模型识别滑块类验证码可以模拟人的拖动轨迹点选类验证码则比较难我会设计成保留人工介入接口让用户在紧急情况下接管一次。这里我特别想说一下开发这类工具一定要守住底线目标是解决正常使用过程中的体验问题不是为了突破网站的安全机制。所以在设计上我给人工接管留了通道绝不诱导模型去对抗安全验证设施。6. 稳定性提升的七个细节有了上面的基础逻辑项目跑通已经没问题了。但如果想让 Agent 真正能长时间、稳定地工作下面这些细节一个都不能少。第一动作之间的间隔必须随机化。固定间隔不仅容易被网站检测为机器人行为而且容易引发资源竞争。我给每次操作加了一个 300 到 800 毫秒的随机延迟。第二尽量模拟人类的操作习惯。比如在页面滚动时不使用window.scrollTo(0, document.body.scrollHeight)这种瞬移而是分段滚动每次滚一屏中间加短暂的停顿点击按钮前可以小幅、随机地移动鼠标坐标。第三不要忽略页面焦点问题。在点击某些元素之前必须先保证页面处于正确的窗口和标签页。如果页面有多个标签页Agent 频繁切换导致上下文错乱我干脆在任务执行期间禁止用户手动操作浏览器避免干扰。第四超时策略永远要有。不管是页面加载超时、模型响应超时还是 WebSocket 命令超时都要有明确的超时时间和失败处理。否则任何一个环节卡住整个 Agent 就死在那里了。第五LLM 响应过于缓慢的时候要用降级方案。比如模型调用连续失败 3 次系统自动切换到一个轻量模型或者内置规则引擎即使智能程度降低也至少保证任务不断流。第六给模型设置护栏prompt。比如你只能操作页面元素列表中存在的元素不得访问当前任务无关的网址如果连续失败 5 次停止并请求人工帮助。这些护栏能最大程度避免模型失控。第七记录每个任务的指标。我做了每次操作的成功率、平均耗时、模型调用次数、token 消耗等指标的统计。有指标之后优化才有方向。我曾经通过统计发现某个页面的元素提取耗时占整个请求的 60%随后优化了提取逻辑整体效率直接翻倍。7. 最后分享几点我的实操感受项目做到现在最深的感受是给大模型装一双手这句话听起来简单实际做起来难点根本不在大模型调用上而在于怎么把浏览器这个环境彻底变成模型可以理解、可以操作、可以验证的数字空间。大模型本身的能力已经足够强强到你只需要给它准确的状态信息、给它清晰的决策约束、给它可靠的执行工具它就能完成看起来相当复杂的任务。反过来如果页面状态信息是脏的、乱的、不完整的再强的模型也会给出荒唐的决策。第二个感受是Agent 的稳定性是一个系统工程不是某个模块能做到的事。感知、决策、执行、验证每个环节都有各自的问题只有当它们形成完整闭环、相互校验的时候整个系统才真正有了靠谱的感觉。每修一个 Bug系统就硬一点。这个过程很慢但很有成就感。最后给正在做类似项目的朋友一个建议从一个极小的、真实的场景切入。不要一开始就想着做一个万能浏览器助手先选一个完整的、有明确终点的任务比如查天气、比价、查快递一步步把稳定闭环跑通再慢慢扩大动作集和任务范围。等我把买票这类多步骤任务稳定跑通之后再回头看之前那些看起来简单的小任务发现根本就是降维打击。如果你也在搞 Agent 开发或者正在纠结怎么让大模型真正动手干活希望这篇文章能给你一个明确的技术路线和一套可以直接借鉴的设计思路。后面我还在规划加入更多网站的适配方案和更复杂的任务编排能力有新的进展我会再写一篇跟大家分享。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →