LangChain驱动Playwright:构建智能RPA Agent实现跨站自动化
2026/7/2 22:47:33 网站建设 项目流程

1. 项目概述:当RPA遇见Agent,一场自动化思维的升维

最近在跟几个做企业流程自动化的朋友聊天,大家普遍有个感觉:传统的RPA(机器人流程自动化)项目,做到后面越来越像“打地鼠”。流程是固定写死的,一旦目标网页改个按钮位置、加个验证码,或者业务逻辑稍微变一下,整个机器人就“趴窝”了,需要人工介入重新调试脚本。维护成本高,适应性差,成了很多RPA项目难以规模化推广的痛点。

与此同时,AI领域特别是大语言模型(LLM)的Agent(智能体)概念火得一塌糊涂。Agent能理解自然语言指令,具备一定的规划、推理和工具使用能力。我当时就在想,如果把RPA的“手”(执行具体网页操作的能力)和Agent的“脑”(理解与决策能力)结合起来,会碰撞出什么火花?这不就是给RPA装上一个能随时应对变化的“大脑”吗?

于是,我动手尝试了一个项目:利用LangChain框架来驱动Playwright,构建一个能理解复杂任务、自主规划步骤、并执行跨站操作的智能自动化Agent。简单说,就是不再需要为每一个细微的网页操作编写死板的脚本,而是用自然语言告诉Agent一个目标,比如“帮我对比A电商平台和B电商平台上某款手机的价格与评价”,剩下的规划(先打开A站搜索、再打开B站搜索、最后整理数据)和执行(点击、输入、滚动、截图)都由这个“RPA+Agent”的组合体来完成。

这不仅仅是技术的简单叠加,更是一种自动化范式的转变。传统RPA是“流程驱动”,而“RPA+Agent”是“目标驱动”。后者在面对复杂、多变、跨系统的任务时,理论上拥有更强的鲁棒性和灵活性。接下来,我就把自己从构思到实现的核心思路、踩过的坑以及一些实操心得,详细拆解一遍。

2. 核心架构设计:LangChain如何为Playwright注入“思考”能力

要实现“RPA+Agent”,核心在于设计一个合理的架构,让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使用工具最有效的模式之一。整个系统的运行流程可以概括为以下几个循环步骤:

  1. 任务解析与规划:用户输入一个自然语言任务,例如“去知乎和豆瓣分别搜索‘LangChain’,并各保存前3条结果的标题”。LangChain中的LLM(例如GPT-4或本地部署的Qwen)首先理解这个任务,并初步规划出需要执行的步骤序列。它可能会想:“这个任务需要跨两个网站。第一步,打开知乎并搜索;第二步,提取结果;第三步,打开豆瓣并搜索;第四步,提取结果;第五步,汇总信息。”

  2. 工具选择与调用:根据当前步骤,LLM从我们预先定义好的“工具包”中选择最合适的工具。例如,对于“打开知乎并搜索”,它应该选择一个叫navigate_to_url_and_search的工具。这个工具本质上是一个Python函数,内部封装了Playwright的page.goto()page.fill()page.click()等操作。LangChain负责将LLM的思考(如“我需要用导航搜索工具,参数是url=‘https://www.zhihu.com’, query=‘LangChain’”)转换成对这个Python函数的调用。

  3. 行动执行与环境反馈:Playwright执行被调用的工具函数,完成真实的浏览器操作。操作完成后,工具函数会返回一个结果字符串,比如“已成功在知乎搜索‘LangChain’,当前页面标题为‘LangChain 搜索结果 - 知乎’”。这个结果作为环境观察(Observation)反馈给LLM。

  4. 下一步决策: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(headless=False) # 调试时可设为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(path=filename) return f"截图已保存为 {filename}" # 将异步函数包装成LangChain Tool self.tools = [ Tool(name="Navigate", func=partial(self._run_async, navigate_tool), description="导航到指定的URL。输入应是一个完整的网址。"), Tool(name="Screenshot", func=partial(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(agent=agent, tools=self.tools, verbose=True, handle_parsing_errors=True) 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策略层面双管齐下。

  1. 工具内建等待:在封装工具时,针对可能引发页面状态变化的操作(如导航、点击提交按钮),在执行后主动加入等待。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},页面导航完成。”
  2. 让Agent学会“等待”:我们可以专门创建一个wait_for_secondswait_for_element的工具。当LLM观察到页面似乎还在加载(如上个工具返回“正在跳转...”),或者下一个操作失败时,它可能会自主调用等待工具。这需要我们在工具描述中教它这一点。

  3. 状态感知与验证:更高级的做法是,让工具返回更丰富的上下文信息,比如操作完成后页面的关键特征(如URL变化、特定标题出现)。LLM可以根据这些信息更准确地判断页面状态。例如,登录工具返回“登录成功,当前URL包含‘/dashboard’,页面标题为‘我的主页’”。这样Agent就知道下一个操作应该针对仪表盘页面了。

4. 复杂跨站任务实战:以比价为例

理论说再多,不如看一个实际例子。我们来实现一个经典的跨站任务:“比较京东和淘宝上iPhone 15的价格,列出前三个最便宜的商品链接和价格。”

这个任务看似简单,但对传统RPA脚本来说,需要为两个网站分别编写抓取逻辑,且一旦网站改版就要维护两套代码。而用我们的“RPA+Agent”方案,我们只需要提供基础工具,然后用自然语言下达指令。

4.1 任务分解与Agent推理过程模拟

当我们把任务丢给Agent后,结合verbose日志,可以观察到它大致的思考过程(以下为模拟):

  1. 思考:用户需要比较两个网站上iPhone 15的价格。我需要先访问京东,搜索商品,提取价格信息;然后访问淘宝,重复此过程;最后汇总。第一步,我应该导航到京东首页。行动:调用Navigate工具,输入https://www.jd.com观察:已导航至京东,当前标题:“京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物!”

  2. 思考:我已经在京东首页。接下来需要在搜索框输入“iPhone 15”并进行搜索。我需要找到搜索框并输入。行动:调用FillInput工具,输入选择器#key和值iPhone 15。(这里假设Agent通过工具描述知道京东搜索框的ID是#key,或者通过前期“探索”工具学习过)。观察:已在输入框#key中输入“iPhone 15”。

  3. 思考:输入已完成,现在需要点击搜索按钮。行动:调用ClickElement工具,输入选择器.buttoninput[type=submit]。(实际中可能需要更精确的选择器)。观察:已点击搜索按钮,页面跳转到搜索结果页,标题:“iPhone 15 - 商品搜索 - 京东”。

  4. 思考:现在在搜索结果页。我需要提取商品列表中的价格和链接。页面可能有多个商品项。我应该先定位商品列表的容器。行动:调用ExtractList工具,输入一个能匹配多个商品项的选择器,如.gl-item,并指定需要提取每个项内的价格元素(如.p-price)和链接元素(.p-name a)。观察:提取到10个商品项。示例数据:[{“price”: “5999”, “link”: “//item.jd.com/100123456.html”}, ...]。

  5. 思考:京东的数据已获取。现在需要同样的流程处理淘宝。我需要导航到淘宝。行动:调用Navigate工具,输入https://www.taobao.com观察:已导航至淘宝...

  6. 思考:(后续步骤类似,Agent会尝试在淘宝首页找到搜索框,输入,点击,然后提取数据。这里可能遇到网站结构不同带来的挑战,比如淘宝的搜索框选择器可能是#q,商品列表的CSS类名也不同。这正是考验工具抽象和Agent适应能力的时候。)

  7. 最终思考:两个网站的数据都已获取。现在需要整理:将两个列表合并,按价格排序,取前三名,并格式化输出。行动:调用一个我们预先写好的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的ConversationSummaryBufferMemoryConversationTokenBufferMemory来保持关键记忆的同时控制长度。对于超长任务,可以考虑阶段性让Agent输出检查点(Checkpoint)摘要。
  • 并行执行可能性:对于任务中独立的子任务(如同时抓取京东和淘宝),理论上可以启动多个Playwright页面甚至浏览器实例并行执行。但这需要更复杂的Agent协调机制(如LangGraph可以用于编排有向无环图),目前简单的ReAct Agent是顺序执行的。

5.2 错误处理与鲁棒性提升

Agent在未知环境中探索,出错是常态。必须建立完善的错误处理机制。

  1. 工具层的错误捕获:每个工具函数内部都应该有完善的try-catch,并返回结构化的错误信息,而不是抛出异常导致Agent崩溃。

    async def click_element_tool(selector: str): try: await self.page.wait_for_selector(selector, state=“visible”, timeout=5000) # 增加等待 await self.page.click(selector) return f“成功点击元素:{selector}。” except TimeoutError: return f“错误:在5秒内未找到可见元素 ‘{selector}’。请检查选择器是否正确,或页面是否已加载。” except Exception as e: return f“点击时发生未知错误:{str(e)}。”

    返回的错误信息要足够友好,能引导LLM进行下一步决策(如重试、换选择器、报告失败)。

  2. Agent层面的重试与回退策略:在创建AgentExecutor时,可以设置max_iterations(最大迭代次数)和early_stopping_method来防止无限循环。更高级的,可以自定义一个“错误处理工具”,当主Agent多次失败后,调用这个工具来尝试恢复(如刷新页面、回到首页等)。

  3. 超时控制:给整个Agent任务设置一个总超时。Playwright操作也可以设置单独的超时。防止因网络卡顿或Agent“陷入沉思”导致任务永久挂起。

5.3 常见边界情况与应对策略

  • 弹窗与通知:突然弹出的登录框、Cookie同意框会阻断自动化流程。可以在启动浏览器上下文时预先设置,或者编写一个“处理常见弹窗”的工具,让Agent在遇到阻塞时调用。
  • 无限滚动页面:对于需要加载更多内容的页面(如社交媒体流)。可以提供scroll_page工具,并描述“此工具将页面向下滚动一定像素或直到底部,用于加载更多内容”。LLM在发现需要的信息不在首屏时,可能会主动调用它。
  • 条件分支:用户任务可能有条件逻辑,如“如果A网站价格高于5000,则去B网站查看”。ReAct Agent具备基础推理能力,可以处理简单的“如果-那么”逻辑。我们需要在提示词中鼓励它进行这种比较和判断。
  • 结果验证:任务完成后,让Agent做一个简单的自我验证。例如,在比价任务最后,可以提示它:“请检查最终输出的列表是否包含了来自两个平台的数据,并且价格是否已排序。”这可以通过在最终步骤让LLM自我审查来实现。

构建一个稳定的“RPA+Agent”系统,是一个持续迭代的过程。你需要不断用各种边缘案例去测试它,观察Agent的失败模式,然后有针对性地优化工具、提示词或流程。这个过程本身,就像在训练一个数字员工,看着它从笨手笨脚到逐渐熟练,其中乐趣与挑战并存。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询