LangChain驱动Playwright:构建智能RPA Agent实现跨站自动化
1. 项目概述当RPA遇见Agent一场自动化思维的升维最近在跟几个做企业流程自动化的朋友聊天大家普遍有个感觉传统的RPA机器人流程自动化项目做到后面越来越像“打地鼠”。流程是固定写死的一旦目标网页改个按钮位置、加个验证码或者业务逻辑稍微变一下整个机器人就“趴窝”了需要人工介入重新调试脚本。维护成本高适应性差成了很多RPA项目难以规模化推广的痛点。与此同时AI领域特别是大语言模型LLM的Agent智能体概念火得一塌糊涂。Agent能理解自然语言指令具备一定的规划、推理和工具使用能力。我当时就在想如果把RPA的“手”执行具体网页操作的能力和Agent的“脑”理解与决策能力结合起来会碰撞出什么火花这不就是给RPA装上一个能随时应对变化的“大脑”吗于是我动手尝试了一个项目利用LangChain框架来驱动Playwright构建一个能理解复杂任务、自主规划步骤、并执行跨站操作的智能自动化Agent。简单说就是不再需要为每一个细微的网页操作编写死板的脚本而是用自然语言告诉Agent一个目标比如“帮我对比A电商平台和B电商平台上某款手机的价格与评价”剩下的规划先打开A站搜索、再打开B站搜索、最后整理数据和执行点击、输入、滚动、截图都由这个“RPAAgent”的组合体来完成。这不仅仅是技术的简单叠加更是一种自动化范式的转变。传统RPA是“流程驱动”而“RPAAgent”是“目标驱动”。后者在面对复杂、多变、跨系统的任务时理论上拥有更强的鲁棒性和灵活性。接下来我就把自己从构思到实现的核心思路、踩过的坑以及一些实操心得详细拆解一遍。2. 核心架构设计LangChain如何为Playwright注入“思考”能力要实现“RPAAgent”核心在于设计一个合理的架构让LangChain作为大脑能够有效地指挥Playwright作为手脚。这里的关键是理解两者如何协同工作。2.1 为什么是LangChain Playwright首先看工具选型。Playwright作为新一代的浏览器自动化工具其优势在于跨浏览器Chromium, Firefox, WebKit支持、自动等待、强大的选择器以及可靠的录制功能。对于RPA场景它的稳定性和丰富的API如网络拦截、文件下载、移动端模拟比Selenium更友好。而LangChain是一个用于构建LLM应用的框架它提供了链Chains、代理Agents、工具Tools等高级抽象能极大地简化我们让LLM使用外部工具在这里就是Playwright的过程。这个组合的巧妙之处在于LangChain负责将模糊的自然语言任务分解成明确的、可执行的步骤序列规划并将每个步骤映射到对应的Playwright操作工具调用Playwright则忠实地执行这些原子操作并将结果如页面文本、截图、状态返回给LangChain进行下一步决策观察。这就形成了一个“感知-思考-行动”的闭环。2.2 智能体Agent的工作流设计我设计的核心工作流基于LangChain的“ReAct”模式Reasoning Acting这是目前让Agent使用工具最有效的模式之一。整个系统的运行流程可以概括为以下几个循环步骤任务解析与规划用户输入一个自然语言任务例如“去知乎和豆瓣分别搜索‘LangChain’并各保存前3条结果的标题”。LangChain中的LLM例如GPT-4或本地部署的Qwen首先理解这个任务并初步规划出需要执行的步骤序列。它可能会想“这个任务需要跨两个网站。第一步打开知乎并搜索第二步提取结果第三步打开豆瓣并搜索第四步提取结果第五步汇总信息。”工具选择与调用根据当前步骤LLM从我们预先定义好的“工具包”中选择最合适的工具。例如对于“打开知乎并搜索”它应该选择一个叫navigate_to_url_and_search的工具。这个工具本质上是一个Python函数内部封装了Playwright的page.goto()和page.fill()、page.click()等操作。LangChain负责将LLM的思考如“我需要用导航搜索工具参数是url‘https://www.zhihu.com’ query‘LangChain’”转换成对这个Python函数的调用。行动执行与环境反馈Playwright执行被调用的工具函数完成真实的浏览器操作。操作完成后工具函数会返回一个结果字符串比如“已成功在知乎搜索‘LangChain’当前页面标题为‘LangChain 搜索结果 - 知乎’”。这个结果作为环境观察Observation反馈给LLM。下一步决策LLM接收到上一步的行动结果后进行新一轮的推理Reasoning。它会判断当前子任务是否完成以及接下来该做什么。比如收到知乎搜索成功的反馈后它可能推理出“搜索已完成下一步是提取前3条结果的标题。我需要使用‘提取列表内容’工具。” 然后循环回到步骤2。这个过程会一直持续直到LLM认为原始用户任务已经完成并输出最终答案。注意这个循环完全由LLM驱动这意味着我们不需要预先编写“打开A站-抓取数据-打开B站-抓取数据-生成报告”的固定流程。我们只需要提供一套完备的、原子化的网页操作工具如导航、点击、输入、读取文本、截图等并清晰地用文档描述它们的功能LLM就能自己组合这些工具来完成复杂任务。这是与传统RPA最根本的区别。2.3 工具Tools的抽象与封装工具是连接LangChain思维和Playwright行动的桥梁。设计得好不好直接决定了Agent的效率和能力上限。我的经验是工具要设计得“原子化”且“功能清晰”。原子化每个工具只做一件小事。比如不要做一个“登录并搜索”的复合工具而是拆分成navigate_to_urlfill_inputclick_buttonget_element_text等多个小工具。这样LLM组合起来更灵活也更容易理解和调用。功能清晰每个工具的函数名和描述description必须用自然语言清晰说明其作用和参数。因为LLM就是靠这些描述来学习如何使用工具的。例如from langchain.tools import tool from playwright.sync_api import sync_playwright tool def extract_element_text(selector: str) - str: 提取当前页面中符合CSS选择器的第一个元素的文本内容。 Args: selector: 目标的CSS选择器例如 h1.title 或 #result-list li:first-child。 Returns: 提取到的文本内容。如果未找到元素返回‘元素未找到’。 # 假设 page 是当前Playwright页面对象的全局或上下文引用 global page element page.query_selector(selector) if element: return element.inner_text() else: return 错误未找到选择器对应的元素。这里的函数描述文档字符串至关重要LLM会仔细阅读它来理解何时以及如何使用这个工具。在项目中我通常会封装十几到二十个这样的基础工具构成一个网页自动化工具包。然后将这个工具包提供给LangChain的AgentExecutor。3. 关键技术实现细节与避坑指南有了架构设计接下来就是具体的代码实现。这里有几个关键的技术细节和容易踩坑的地方。3.1 Playwright上下文与LangChain工具的集成最大的一个挑战是如何在LangChain的工具函数内部访问到Playwright的浏览器上下文BrowserContext和页面Page对象。因为工具函数是被LangChain的Agent调用的它本身可能运行在一个与主程序不同的线程或环境中。解决方案使用上下文管理器或全局状态谨慎使用。我采用的是一种基于上下文变量的方法利用LangChain的RunnableConfig或自定义上下文来传递Playwright对象。一种相对清晰的模式是创建一个PlaywrightSession类来管理浏览器生命周期并将page对象通过回调或绑定机制注入到每个工具中。简化代码如下import asyncio from langchain.agents import AgentExecutor, create_react_agent from langchain.tools import Tool from playwright.async_api import async_playwright class PlaywrightAutomationAgent: def __init__(self, llm): self.llm llm self.browser None self.page None self.tools [] async def start_browser(self): 启动Playwright浏览器和页面 playwright await async_playwright().start() self.browser await playwright.chromium.launch(headlessFalse) # 调试时可设为False self.page await self.browser.new_page() await self.page.set_viewport_size({width: 1920, height: 1080}) def _build_tools(self): 构建工具集并将page对象绑定到工具函数上 # 注意这里需要一种方式让工具函数能访问到self.page # 方法一使用闭包或functools.partial from functools import partial async def navigate_tool(url: str): await self.page.goto(url) return f已导航至 {url}当前标题{await self.page.title()} async def screenshot_tool(filename: str screenshot.png): await self.page.screenshot(pathfilename) return f截图已保存为 {filename} # 将异步函数包装成LangChain Tool self.tools [ Tool(nameNavigate, funcpartial(self._run_async, navigate_tool), description导航到指定的URL。输入应是一个完整的网址。), Tool(nameScreenshot, funcpartial(self._run_async, screenshot_tool), description对当前页面进行截图。输入可以是文件名默认为screenshot.png。), # ... 更多工具 ] async def _run_async(self, async_func, *args, **kwargs): 一个帮助函数用于在同步的Tool.func中运行异步函数 return await async_func(*args, **kwargs) async def run_agent(self, task: str): 运行Agent执行任务 await self.start_browser() self._build_tools() # 创建ReAct Agent agent create_react_agent(self.llm, self.tools) agent_executor AgentExecutor(agentagent, toolsself.tools, verboseTrue, handle_parsing_errorsTrue) try: result await agent_executor.ainvoke({input: task}) print(任务结果, result[output]) finally: await self.browser.close()实操心得在实际开发中更推荐使用LangChain较新版本中对异步的原生支持如tool装饰器可以直接装饰异步函数或者使用asyncio.run在同步上下文中小心地管理异步调用。上述代码是一个原理性示例重点在于展示如何将Playwright的页面对象self.page与工具函数关联起来。确保浏览器页面Page的生命周期覆盖整个Agent执行过程是关键否则会出现“页面已关闭”的错误。3.2 提示词Prompt工程教会Agent使用工具即使有了工具如果提示词没写好LLM也可能不会用或者瞎用。LangChain的Agent自带了不错的默认提示词但对于网页自动化这种特定领域进行定制化优化效果会好很多。核心优化点在于“系统指令System Message”和“工具描述Tool Description”。系统指令需要明确告诉Agent它的角色、能力和约束。例如 “你是一个专业的网页自动化助手。你可以通过我提供的工具与浏览器进行交互。你的目标是逐步完成用户的任务。在行动前请先思考当前步骤的目标和最适合的工具。一次只使用一个工具并等待工具返回结果后再做下一步判断。如果操作失败如元素未找到请分析错误信息并尝试替代方案如使用不同的选择器。你操作的是一个真实的浏览器请谨慎操作避免无限循环。”工具描述如前所述必须清晰、无歧义。除了功能最好加入使用示例和常见错误处理。例如对于点击工具可以描述“click_element点击页面上符合CSS选择器的第一个元素。输入一个有效的CSS选择器字符串。注意确保元素在点击前是可见和可交互的。如果点击后页面跳转或大量加载请等待加载完成再进行下一步。”我通常会创建一个专用的提示词模板将系统指令、工具列表及其描述、以及对话历史都整合进去然后喂给create_react_agent或其他Agent创建函数。3.3 处理动态内容与等待策略网页是动态的这是网页自动化永恒的挑战。Playwright本身提供了强大的自动等待机制如page.click()会等待元素可点击但在Agent自主决策的场景下我们还需要考虑更复杂的情况。问题Agent命令“登录”工具执行了输入用户名密码和点击登录按钮。但登录后页面有一个重定向或者需要几秒加载用户仪表盘。如果下一个工具如“提取欢迎信息”立即执行可能会因为页面未加载完而失败。解决方案在工具层面和Agent策略层面双管齐下。工具内建等待在封装工具时针对可能引发页面状态变化的操作如导航、点击提交按钮在执行后主动加入等待。Playwright提供了多种等待方式async def click_and_wait_for_navigation(selector: str): # 方式1等待导航如果点击会触发新页面加载 async with self.page.expect_navigation(): await self.page.click(selector) # 导航完成后可以再加一个网络空闲等待 await self.page.wait_for_load_state(networkidle) return f“已点击 {selector}页面导航完成。”让Agent学会“等待”我们可以专门创建一个wait_for_seconds或wait_for_element的工具。当LLM观察到页面似乎还在加载如上个工具返回“正在跳转...”或者下一个操作失败时它可能会自主调用等待工具。这需要我们在工具描述中教它这一点。状态感知与验证更高级的做法是让工具返回更丰富的上下文信息比如操作完成后页面的关键特征如URL变化、特定标题出现。LLM可以根据这些信息更准确地判断页面状态。例如登录工具返回“登录成功当前URL包含‘/dashboard’页面标题为‘我的主页’”。这样Agent就知道下一个操作应该针对仪表盘页面了。4. 复杂跨站任务实战以比价为例理论说再多不如看一个实际例子。我们来实现一个经典的跨站任务“比较京东和淘宝上iPhone 15的价格列出前三个最便宜的商品链接和价格。”这个任务看似简单但对传统RPA脚本来说需要为两个网站分别编写抓取逻辑且一旦网站改版就要维护两套代码。而用我们的“RPAAgent”方案我们只需要提供基础工具然后用自然语言下达指令。4.1 任务分解与Agent推理过程模拟当我们把任务丢给Agent后结合verbose日志可以观察到它大致的思考过程以下为模拟思考用户需要比较两个网站上iPhone 15的价格。我需要先访问京东搜索商品提取价格信息然后访问淘宝重复此过程最后汇总。第一步我应该导航到京东首页。行动调用Navigate工具输入https://www.jd.com。观察已导航至京东当前标题“京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物”思考我已经在京东首页。接下来需要在搜索框输入“iPhone 15”并进行搜索。我需要找到搜索框并输入。行动调用FillInput工具输入选择器#key和值iPhone 15。这里假设Agent通过工具描述知道京东搜索框的ID是#key或者通过前期“探索”工具学习过。观察已在输入框#key中输入“iPhone 15”。思考输入已完成现在需要点击搜索按钮。行动调用ClickElement工具输入选择器.button或input[typesubmit]。实际中可能需要更精确的选择器。观察已点击搜索按钮页面跳转到搜索结果页标题“iPhone 15 - 商品搜索 - 京东”。思考现在在搜索结果页。我需要提取商品列表中的价格和链接。页面可能有多个商品项。我应该先定位商品列表的容器。行动调用ExtractList工具输入一个能匹配多个商品项的选择器如.gl-item并指定需要提取每个项内的价格元素如.p-price和链接元素.p-name a。观察提取到10个商品项。示例数据[{“price”: “5999”, “link”: “//item.jd.com/100123456.html”}, ...]。思考京东的数据已获取。现在需要同样的流程处理淘宝。我需要导航到淘宝。行动调用Navigate工具输入https://www.taobao.com。观察已导航至淘宝...思考后续步骤类似Agent会尝试在淘宝首页找到搜索框输入点击然后提取数据。这里可能遇到网站结构不同带来的挑战比如淘宝的搜索框选择器可能是#q商品列表的CSS类名也不同。这正是考验工具抽象和Agent适应能力的时候。最终思考两个网站的数据都已获取。现在需要整理将两个列表合并按价格排序取前三名并格式化输出。行动调用一个我们预先写好的DataProcess工具或者LLM直接进行文本推理和格式化输入之前提取的原始数据。观察处理完成。最终输出“根据搜索价格最低的三个iPhone 15商品如下1. [京东] XXX商品价格5999元链接...2. [淘宝] YYY商品价格5899元链接...3. [京东] ZZZ商品价格6099元链接...”4.2 实现中的挑战与应对在这个例子中你会遇到几个典型问题网站反爬京东、淘宝都有反爬机制。单纯用Playwright可能很快被识别。解决方案是在Playwright上下文中使用更真实的浏览器指纹如context browser.new_context(viewport..., user_agent...)并合理设置操作间隔page.wait_for_timeout避免行为像机器人。对于更复杂的验证码目前纯Agent方案处理起来还比较困难可能需要引入专门的OCR或打码工具并将其也封装成Agent可调用的工具。页面结构差异两个网站的HTML结构天差地别。这就要求我们的“元素提取”类工具足够鲁棒或者我们需要为不同网站准备不同的“适配层”。一个更智能的思路是教会Agent使用更通用的定位方法。除了CSS选择器我们可以提供XPath工具甚至提供“通过文本内容查找元素”的工具。LLM在遇到“找不到元素”的错误时可能会尝试换用其他定位方式。数据清洗与格式化从网页抓取的数据往往是杂乱无章的包含多余空格、货币符号等。我们可以选择在工具层做初步清洗如float(price_text.strip(‘¥’))也可以将原始文本丢给LLM让它利用强大的文本理解能力来提取结构化信息。后者更灵活但消耗更多Token。5. 性能优化、错误处理与边界情况一个能用的原型和一个健壮的系统之间隔着大量的细节处理。5.1 性能优化减少Token消耗与加速执行限制Agent的“脑补”范围通过提示词明确约束例如“只使用我提供的工具不要想象或创建不存在的工具”、“对于数据提取任务优先使用ExtractStructuredData工具而不是用自然语言描述整个页面”。工具结果的摘要Playwright工具返回的可能是大段的HTML或文本。直接把这些塞给LLM会爆Token且干扰判断。我们应该在工具层做摘要。例如ExtractList工具返回的不是原始HTML而是一个简明的JSON列表只包含关键字段价格、标题、链接。会话记忆Memory管理复杂的多步任务会产生很长的对话历史。使用LangChain的ConversationSummaryBufferMemory或ConversationTokenBufferMemory来保持关键记忆的同时控制长度。对于超长任务可以考虑阶段性让Agent输出检查点Checkpoint摘要。并行执行可能性对于任务中独立的子任务如同时抓取京东和淘宝理论上可以启动多个Playwright页面甚至浏览器实例并行执行。但这需要更复杂的Agent协调机制如LangGraph可以用于编排有向无环图目前简单的ReAct Agent是顺序执行的。5.2 错误处理与鲁棒性提升Agent在未知环境中探索出错是常态。必须建立完善的错误处理机制。工具层的错误捕获每个工具函数内部都应该有完善的try-catch并返回结构化的错误信息而不是抛出异常导致Agent崩溃。async def click_element_tool(selector: str): try: await self.page.wait_for_selector(selector, state“visible”, timeout5000) # 增加等待 await self.page.click(selector) return f“成功点击元素{selector}。” except TimeoutError: return f“错误在5秒内未找到可见元素 ‘{selector}’。请检查选择器是否正确或页面是否已加载。” except Exception as e: return f“点击时发生未知错误{str(e)}。”返回的错误信息要足够友好能引导LLM进行下一步决策如重试、换选择器、报告失败。Agent层面的重试与回退策略在创建AgentExecutor时可以设置max_iterations最大迭代次数和early_stopping_method来防止无限循环。更高级的可以自定义一个“错误处理工具”当主Agent多次失败后调用这个工具来尝试恢复如刷新页面、回到首页等。超时控制给整个Agent任务设置一个总超时。Playwright操作也可以设置单独的超时。防止因网络卡顿或Agent“陷入沉思”导致任务永久挂起。5.3 常见边界情况与应对策略弹窗与通知突然弹出的登录框、Cookie同意框会阻断自动化流程。可以在启动浏览器上下文时预先设置或者编写一个“处理常见弹窗”的工具让Agent在遇到阻塞时调用。无限滚动页面对于需要加载更多内容的页面如社交媒体流。可以提供scroll_page工具并描述“此工具将页面向下滚动一定像素或直到底部用于加载更多内容”。LLM在发现需要的信息不在首屏时可能会主动调用它。条件分支用户任务可能有条件逻辑如“如果A网站价格高于5000则去B网站查看”。ReAct Agent具备基础推理能力可以处理简单的“如果-那么”逻辑。我们需要在提示词中鼓励它进行这种比较和判断。结果验证任务完成后让Agent做一个简单的自我验证。例如在比价任务最后可以提示它“请检查最终输出的列表是否包含了来自两个平台的数据并且价格是否已排序。”这可以通过在最终步骤让LLM自我审查来实现。构建一个稳定的“RPAAgent”系统是一个持续迭代的过程。你需要不断用各种边缘案例去测试它观察Agent的失败模式然后有针对性地优化工具、提示词或流程。这个过程本身就像在训练一个数字员工看着它从笨手笨脚到逐渐熟练其中乐趣与挑战并存。