1. 项目概述当AI Agent学会“驾驭”浏览器最近在折腾一个挺有意思的项目核心就一句话让AI Agent能像真人一样在浏览器里“看”网页、“想”事情、“做”操作。这听起来有点像传统的网页自动化测试比如Selenium或者Playwright但内核完全不同。传统的自动化脚本是“死”的写好了点哪里、输什么它就按部就班执行。而基于浏览器的AI Agent我们称之为“Harness Engineering”的实践目标是打造一个“活”的智能体。它能够理解网页的语义结构根据自然语言指令或既定目标自主决策下一步该点击哪个按钮、在哪个输入框填写什么内容甚至处理验证码、弹窗等意外情况。这背后的驱动力是AI Agent技术的快速演进。过去我们谈论Agent可能更多是在聊天机器人或者代码生成的语境里。但现在Agent正在被赋予“手”和“眼睛”去操作真实世界的数字界面——浏览器无疑是其中最重要、最通用的一个。无论是自动化处理繁琐的日常办公流程比如每天登录十几个系统填报数据还是构建一个能够自主搜集、分析网络信息的智能助手甚至是开发一个能模拟用户行为进行复杂业务测试的AI测试员这个方向都充满了想象力和实用价值。简单来说这个项目就是为AI Agent打造一套“神经系统”和“运动系统”让它的“大脑”通常是大型语言模型发出的指令能够精准地控制浏览器这个“身体”完成从信息感知到动作执行的全链路闭环。接下来我会详细拆解我是如何设计并实现这套系统的包括核心架构、工具选型、实操步骤以及一路踩坑填坑的心得。2. 核心架构与工具选型解析要实现一个基于浏览器的AI Agent不能只靠一个模型或一个库。它需要一个分层、协作的架构。我的设计主要分为三层感知层、决策层和执行层。2.1 感知层让AI“看见”网页AI Agent要操作浏览器第一步是必须理解当前页面上有什么。这里不能简单截个图丢给视觉模型VLM虽然多模态模型如GPT-4V能力很强但成本高、速度慢且无法精确获取可操作元素如输入框的ID、按钮的XPath的元数据。我的方案是DOM树解析 关键信息抽取。核心工具Playwright/Puppeteer我最终选择了Playwright而不是更老牌的Selenium。原因有几个Playwright对现代Web框架React, Vue, Angular支持更好能自动等待元素加载内置了录制生成脚本的功能并且其API设计更现代、一致。它提供了强大的页面内容获取能力。信息抽取策略获取纯净DOM与计算样式通过Playwright获取页面的完整HTML。但原始DOM树非常冗杂包含大量脚本、样式标签和不可见元素。我编写了一个过滤函数只保留可见的、具有交互可能性的元素如input,button,a,select等并排除掉display: none或visibility: hidden的元素。构建结构化页面描述对于过滤后的每个元素提取其关键属性形成一个结构化的JSON对象。这个对象通常包括tagName: 标签名如 “button”, “input”。attributes: 关键属性如id,name,class,type,placeholder,aria-label。text: 元素内的可见文本修剪后。boundingBox: 元素在视口中的位置和大小用于辅助描述或未来可能的视觉校验。xpath/selector: 该元素的唯一CSS选择器或XPath这是后续执行操作的“坐标”。生成自然语言描述将上一步的结构化JSON转换或总结成一段自然语言描述提供给决策层的LLM。例如“页面顶部有一个导航栏包含‘首页’、‘产品’、‘关于我们’三个链接。页面中央是一个标题为‘用户登录’的卡片。卡片内有一个标签为‘用户名’的文本输入框ID: username一个标签为‘密码’的密码输入框ID: password以及一个文本为‘登录’的按钮。”注意这个描述生成环节至关重要。描述既要简洁又要包含所有关键操作元素和其语义。我尝试过让LLM直接解析简化后的DOM JSON但发现先将其转换为一段规整的文本描述能显著提高后续决策的准确性和降低提示词工程的复杂度。2.2 决策层AI的“大脑”与“规划器”这是整个系统的智能核心。它的任务是接收用户的自然语言指令如“帮我登录邮箱”和感知层提供的页面描述然后输出一个具体的、可执行的“动作序列”。核心工具大型语言模型LLM我主要使用GPT-4 Turbo和Claude 3系列模型进行实验。它们的推理和规划能力足以应对大多数网页操作任务。对于成本敏感的场景DeepSeek等开源模型经过精调后也是不错的选择。决策流程设计目标分解LLM首先将高层级指令如“查询北京明天天气”分解为原子步骤如1. 导航到天气网站2. 在搜索框输入“北京”3. 点击搜索或确认按钮4. 提取天气结果。上下文感知将当前页面描述、历史操作步骤记忆以及当前指令一起构成提示词Prompt输入给LLM。动作生成要求LLM输出严格格式化的动作指令。我定义了一个简单的JSON动作协议{ action: click | fill | scroll | wait | extract_text | ..., selector: CSS选择器或XPath, value: 仅fill动作需要表示要输入的文字, reasoning: 模型做出此决策的简要原因用于调试和迭代 }安全与验证在复杂操作中我会引入一个“验证步骤”。例如在执行“点击删除按钮”前让LLM再次确认当前页面上下文和元素文本或者设计一个二次确认的子流程防止危险操作。2.3 执行层精准的“手”决策层输出动作指令后需要可靠地执行。这就是Playwright再次登场的时候。动作映射将标准的动作类型click,fill,scroll等映射到Playwright具体的API调用如page.click(selector),page.fill(selector, value)。健壮性处理等待与重试在执行任何操作前确保目标元素已经处于可交互状态。Playwright内置的locator方法配合waitFor状态非常好用。我会为关键操作如点击登录按钮后跳转设置显式等待。异常捕获与恢复网络延迟、元素加载失败、弹窗干扰都是家常便饭。执行层需要捕获这些异常并将错误信息如“元素未找到”、“操作超时”连同当前页面快照和描述反馈给决策层。决策层LLM可以根据这些新信息重新规划或尝试替代方案例如“登录按钮没找到但发现一个‘Sign In’链接是否点击”。状态管理记录浏览器的状态如当前URL、cookies、localStorage等。这在需要保持会话的多步骤任务中非常重要。工具链总结浏览器自动化Playwright首选功能全面生态好AI决策引擎OpenAI GPT-4/3.5-Turbo Anthropic Claude 3或本地部署的DeepSeek、Qwen等。应用框架使用Python的asyncio进行异步编排因为Playwright和LLM API调用都是IO密集型操作。FastAPI可以用于构建一个提供服务的Web接口。辅助工具BeautifulSoup4或lxml用于辅助解析HTMLPydantic用于验证动作JSON的格式。3. 实操构建从零实现一个登录自动化Agent理论讲完了我们动手搭一个最简单的例子一个能自动登录某个论坛的AI Agent。假设我们不知道具体的登录页面结构。3.1 环境准备与初始化首先安装核心依赖pip install playwright openai playwright install chromium # 安装浏览器驱动初始化Playwright和浏览器上下文。我建议使用chromium.launch_persistent_context来保留用户数据目录这样可以保存cookies模拟真实用户会话。import asyncio from playwright.async_api import async_playwright import openai import json # 初始化OpenAI客户端示例请使用你的API Key openai.api_key your-api-key async def init_browser(): async with async_playwright() as p: # 启动一个带持久化上下文的浏览器方便管理cookies user_data_dir ./user_data browser await p.chromium.launch_persistent_context( user_data_dir, headlessFalse, # 调试时设为False可以看到浏览器操作 args[--disable-blink-featuresAutomationControlled] # 尝试绕过一些简单的反自动化检测 ) page await browser.new_page() return browser, page3.2 实现感知函数编写一个函数用于获取当前页面的结构化描述。async def get_page_description(page): # 执行JavaScript获取页面中所有潜在可交互元素的信息 elements_info await page.evaluate( () { const interactives [input, button, a, select, textarea, [role\button\], [role\link\], [contenteditable\true\]]; const items []; document.querySelectorAll(*).forEach(el { // 基础过滤可见且非禁用 const style window.getComputedStyle(el); const isVisible style.display ! none style.visibility ! hidden el.offsetWidth 0 el.offsetHeight 0; const isInteractive interactives.some(sel el.matches(sel)); if (isVisible isInteractive !el.disabled) { // 生成一个相对稳定的选择器这里简化处理生产环境需更健壮 let selector ; if (el.id) { selector #${el.id}; } else { // 尝试用tag和类名构造这不是最优方法仅作演示 selector el.tagName.toLowerCase(); if (el.className typeof el.className string) { selector . el.className.trim().replace(/\\s/g, .); } } items.push({ tag: el.tagName.toLowerCase(), text: (el.innerText || el.value || el.placeholder || ).slice(0, 100).trim(), selector: selector, type: el.type || , placeholder: el.placeholder || , name: el.name || , id: el.id || , }); } }); return items; } ) # 将元素信息组织成自然语言描述 description_parts [] for elem in elements_info[:20]: # 限制数量避免上下文过长 desc f一个{elem[tag]}元素 if elem[text]: desc f文本内容为“{elem[text]}” if elem[placeholder]: desc f占位符为“{elem[placeholder]}” if elem[type]: desc f类型是{elem[type]} if elem[selector]: desc f选择器参考{elem[selector]} description_parts.append(desc) full_description 当前页面主要包含以下可交互元素\\n \\n.join(description_parts) return full_description, elements_info # 返回描述和原始元素信息3.3 实现决策函数调用LLM构建提示词让LLM根据指令和页面描述决定下一步动作。async def decide_next_action(user_goal, page_description, action_history): prompt f 你是一个控制网页浏览器的AI助手。你的目标是{user_goal} 以下是当前页面的描述 {page_description} 你最近执行的操作历史用于参考 {json.dumps(action_history[-5:], indent2, ensure_asciiFalse)} 请根据目标和当前页面决定下一个最合理的原子操作。 你只能输出一个JSON对象格式必须严格如下 {{ action: click | fill | scroll_up | scroll_down | wait | navigate | finish, selector: 一个CSS选择器字符串用于定位元素。如果action是navigate或finish此项可为空字符串。, value: 仅当action为fill时需要表示要输入的文本。否则为空字符串。, reasoning: 用一句话解释为什么选择这个操作。 }} 动作说明 - click: 点击一个元素按钮、链接等。 - fill: 向输入框input, textarea填写文本。 - scroll_up/scroll_down: 向上/向下滚动页面。 - wait: 等待2秒用于页面加载。 - navigate: 导航到一个新URL需要与user_goal结合此处不常用。 - finish: 任务已完成或无法继续。 请确保selector尽可能精确且稳定。优先使用id选择器如#username其次是name、class等。 现在输出你的决策JSON response await openai.ChatCompletion.acreate( modelgpt-4-turbo-preview, # 或 gpt-3.5-turbo messages[{role: user, content: prompt}], temperature0.1, # 低温度使输出更确定 ) try: decision json.loads(response.choices[0].message.content) # 简单验证决策格式 assert decision[action] in [click, fill, scroll_up, scroll_down, wait, navigate, finish] return decision except (json.JSONDecodeError, AssertionError, KeyError) as e: print(fLLM返回了无效的决策: {response.choices[0].message.content}) print(f错误: {e}) # 返回一个安全的默认等待动作 return {action: wait, selector: , value: , reasoning: 解析LLM响应失败等待后重试。}3.4 实现执行函数与主循环将决策转化为真实的浏览器操作。async def execute_action(page, decision): action decision[action] selector decision[selector] value decision[value] print(f[执行] {action} on {selector} with value {value}。 原因{decision[reasoning]}) try: if action click: if selector: await page.click(selector) else: print(警告click动作缺少selector。) elif action fill: if selector and value is not None: await page.fill(selector, value) else: print(警告fill动作缺少selector或value。) elif action scroll_down: await page.mouse.wheel(0, 300) # 向下滚动 elif action scroll_up: await page.mouse.wheel(0, -300) # 向上滚动 elif action wait: await page.wait_for_timeout(2000) # 等待2秒 elif action navigate: if value: # 这里用value存储URL await page.goto(value) elif action finish: print(任务完成或终止。) return FINISH await page.wait_for_timeout(500) # 每个动作后稍作停顿 return SUCCESS except Exception as e: print(f执行动作时出错: {e}) return fERROR: {str(e)} async def main_agent_loop(user_goal, start_url): browser, page await init_browser() await page.goto(start_url) action_history [] max_steps 15 # 防止无限循环 step 0 while step max_steps: step 1 print(f\\n--- 步骤 {step} ---) # 1. 感知 page_desc, _ await get_page_description(page) print(f页面感知完成。) # 2. 决策 decision await decide_next_action(user_goal, page_desc, action_history) print(f决策: {decision[action]}) # 3. 执行 result await execute_action(page, decision) action_history.append({step: step, decision: decision, result: result}) # 检查是否完成或出错 if result FINISH: print(Agent判定任务完成。) break if result.startswith(ERROR): print(f执行出错尝试等待后继续...) await page.wait_for_timeout(3000) # 可以在这里加入更复杂的错误恢复逻辑 # 简单防呆如果连续三个动作都是wait或scroll可能陷入循环主动停止 recent_actions [h[decision][action] for h in action_history[-3:]] if len(recent_actions) 3 and all(a in [wait, scroll_up, scroll_down] for a in recent_actions): print(检测到可能的行为循环主动终止。) break await browser.close() print(\\n操作历史) for h in action_history: print(f 步骤{h[step]}: {h[decision][action]} - {h[result]}) # 运行Agent if __name__ __main__: asyncio.run(main_agent_loop( user_goal登录这个网站用户名是test_user密码是demo123, start_urlhttps://example.com/login # 替换成真实的登录页面URL ))这个简单的Agent已经具备了感知-决策-执行的基本循环。你可以看到它通过LLM理解页面并尝试找到登录框和按钮来完成操作。4. 高级技巧与工程化挑战实现基础循环只是第一步。要让这个AI Agent真正可靠、实用还需要解决一系列工程挑战。4.1 提升感知精度与效率更智能的元素选择器生成上面示例中的选择器生成非常简陋。在生产环境中需要使用更稳定的算法来生成唯一的CSS选择器或XPath。可以考虑使用playwright自身的locatorAPI或者开源库如selector-generator。视觉信息辅助对于纯CSS渲染的复杂组件如Canvas图表、验证码或者当DOM无法准确反映视觉布局时需要引入截图和视觉模型VLM。策略可以是先用DOM定位大致区域再对该区域截图用VLM识别具体内容或状态。但这会大幅增加成本和延迟需权衡使用。页面摘要与焦点识别不是所有元素都需要上报给LLM。可以先用一个轻量级模型或规则识别页面的主要功能区如导航栏、主内容区、侧边栏、弹窗并优先提供焦点区域内的元素描述减少LLM的上下文负担。4.2 优化决策提示词与规划能力分层任务规划对于复杂任务如“从A网站找到某产品价格然后去B网站比价”需要让LLM进行高层规划High-level Planning分解成多个子任务每个子任务再进入上述的感知-决策-执行循环。这需要设计更复杂的提示词或使用智能体框架如LangChain、AutoGen的任务规划能力。记忆与状态管理Agent需要记住它做过什么、看到了什么。简单的做法是把历史动作和关键的页面快照/描述作为上下文传递给LLM。更复杂的需要向量数据库来存储和检索长期记忆避免上下文窗口爆炸。工具使用Tool Use将浏览器操作click, fill、信息查询计算器、搜索、条件判断等都抽象成“工具”让LLM学会在合适的时候调用合适的工具。这符合当前AI Agent开发的主流范式如OpenAI的Function Calling。4.3 增强执行的健壮性反自动化对抗很多网站会检测Playwright/Selenium等自动化工具。对策包括使用p.chromium.launch_persistent_context并加载真实用户配置文件添加--disable-blink-featuresAutomationControlled等启动参数随机化操作间隔和鼠标移动轨迹必要时使用更底层的CDP协议或结合浏览器指纹混淆技术。异常处理与恢复策略建立一套分级恢复策略。元素找不到可以尝试等待更长时间、滚动页面、使用更宽松的选择器或者反馈给决策层“元素可能不存在请重新评估”。遇到验证码可以触发人工干预流程或者集成付费的验证码识别服务。并行与异步控制一个强大的Agent可能需要同时管理多个标签页或浏览器实例。需要妥善利用asyncio进行并发控制管理好各页面之间的状态隔离和数据同步。4.4 监控、评估与持续迭代可观测性记录每个循环的页面截图、DOM描述、LLM的决策包括其reasoning、执行结果。这些日志对于调试和后续分析至关重要。成功率评估定义清晰的任务成功标准如最终页面是否包含特定关键词、是否跳转到预期URL并自动化计算任务成功率。提示词迭代根据失败案例不断优化感知描述的方式和决策提示词。A/B测试不同的提示词模板对复杂任务成功率的影响。5. 常见问题与实战避坑指南在实际开发中我遇到了不少坑这里分享一些典型的解决方案。问题1LLM总是选错元素比如该点“登录”却点了“注册”。原因页面描述中“登录”和“注册”两个按钮的文本很接近LLM可能混淆。或者选择器不够唯一。解决丰富元素描述在生成描述时不仅提供文本还提供元素在页面上的相对位置如“位于表单中央的蓝色按钮”、邻近元素文本等上下文信息。强化提示词在决策提示词中强调“请仔细比较所有选项选择最符合目标‘登录’的那一个”。可以要求LLM在reasoning中详细写出排除其他选项的原因。后置校验在执行关键动作前增加一个“确认”步骤。例如让LLM输出它认为要点击的元素的完整文本和选择器然后用一个简单的规则或另一个轻量级模型调用进行校验。问题2页面动态加载元素还没出现Agent就尝试操作导致失败。原因感知函数执行时AJAX加载的内容可能尚未出现。解决显式等待在get_page_description函数中针对特定元素或页面状态增加等待逻辑。例如等待某个加载 spinner 消失或者等待关键区域出现。重试机制在执行层execute_action中用Playwright的locator.wait_for()代替简单的click()或fill()让Playwright自己处理等待逻辑。超时与重试策略为整个感知-决策-执行循环设置步骤级超时。如果某一步连续失败可以回退到wait动作然后重试感知。问题3任务有时会陷入无限循环例如不断在登录页面刷新。原因LLM的决策可能陷入局部最优或产生重复动作。解决引入循环检测像示例代码中那样检查最近N个动作是否都是wait/scroll等无效操作。丰富历史上下文将更长的动作历史包括成功和失败的提供给LLM帮助它意识到自己在重复。设置最大步数这是最后的安全网必须设置。定义终止状态在提示词中明确定义finish动作触发的条件如“当页面URL包含‘dashboard’时”、“当检测到‘登录成功’的文本时”引导LLM主动结束任务。问题4运行速度慢尤其是调用云端LLM API时。原因每个决策步骤都需要网络往返延迟很高。解决本地轻量模型对于简单的、模式固定的页面如各种网站的登录可以训练一个小的分类模型或使用规则引擎来决策完全绕过大模型。将大模型只用于处理复杂、未知的页面。批量预测如果任务步骤是可预测的线性序列可以尝试让LLM一次性规划出多个步骤然后依次执行。但这降低了应对动态变化的灵活性。缓存对相同的页面描述和用户目标缓存LLM的决策结果。问题5如何处理验证码、滑块等反机器人措施现实建议对于公开的、重要的服务绕过验证码可能违反服务条款。在合规的前提下人工介入点设计流程当检测到验证码时暂停Agent通知人工处理处理完毕后Agent再继续。第三方服务集成商业验证码识别API如2Captcha、DeathByCaptcha但这会产生额外成本且成功率并非100%。技术尝试仅供学习研究对于简单图形验证码可以使用OCR库如Tesseract尝试识别对于滑块可以分析缺口位置并用Playwright模拟拖动轨迹。但这是一场持续的“军备竞赛”且需谨慎评估法律风险。构建一个健壮的、基于浏览器的AI Agent是一个系统工程它融合了前端知识、自动化测试经验、提示词工程以及软件架构设计。从简单的脚本开始逐步迭代解决遇到的具体问题是通往成功最实际的路径。这个领域正在飞速发展新的框架和思路不断涌现但万变不离其宗让AI能可靠地感知、合理地思考、精准地执行。