免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于LangGraph构建Web Agent:从网页自动化到智能操作

基于LangGraph构建Web Agent:从网页自动化到智能操作 1. 项目概述从“对话”到“操作”的质变如果你在过去一年里折腾过各种AI Agent项目大概率会和我有同样的感受很多所谓的“智能体”更像是一个高级的聊天机器人。你给它一个任务比如“帮我查一下明天的天气”它能调用一个预设的天气API返回结果你问它“某公司的股价”它也能通过函数调用拿到数据。但这背后有一个巨大的限制——所有这些能力都依赖于开发者预先为它“焊接”好的工具Tools。一旦遇到一个没有被预先定义和接入的网页操作比如“去XX电商网站找到销量最高的蓝牙耳机把它的价格、评分和三条最新评价摘要给我”传统的Agent就立刻“抓瞎”了。它知道指令但不知道如何在一个真实的、动态的、充满JS交互的现代网页中去执行点击、滚动、输入、读取等具体操作。这就是“Web Agent”要解决的核心问题让Agent不再仅仅是一个“API调用者”而成为一个能真正像人一样“浏览”网页的“操作者”。这不仅仅是给Agent加一个浏览器那么简单。它涉及到如何让大语言模型LLM理解复杂的网页视觉布局和DOM结构如何将自然语言指令分解成一系列精确的底层浏览器操作指令如click,type,scroll以及如何设计一套鲁棒的流程来处理操作失败、页面状态变化等异常情况。最近随着LangGraph这类框架的成熟以及像Hermes、Crawlee等专注于网页自动化项目的出现构建一个工程化、可落地的Web Agent从理论快速走向了实践。今天我就结合自己最近用LangGraph搭建一个基础Web Agent的实战经验来拆解其中的核心思路、技术选型、实现细节以及那些官方文档里不会写的“坑”。2. 核心思路与架构选型为什么是LangGraph在动手之前我们得先想清楚架构。Web Agent的本质是一个具有状态和循环的执行系统。它接收一个目标User Objective然后进入“观察-思考-行动”的循环观察当前网页状态思考下一步该做什么动作执行该动作观察结果再思考……直到任务完成或无法继续。这个模式天然契合基于状态图StateGraph的工作流引擎。2.1 主流框架对比与LangGraph的胜出早期做自动化我们可能会直接用Selenium或Playwright写死脚本。但那是“自动化”不是“智能体”。要让LLM来驱动我们需要一个框架来管理状态流转和工具调用。市面上有几个选择LangChain Custom AgentLangChain的AgentExecutor提供了基础框架但其执行流程相对线性对于需要复杂状态维护、错误重试、子任务分解的Web浏览场景控制粒度不够细调试起来也比较麻烦。AutoGPT / BabyAGI风格的自循环Agent这类项目概念很吸引人但往往过于重量级且稳定性欠佳容易陷入死循环或执行无关操作。LangGraph这正是我选择它的原因。LangGraph将整个Agent的工作流明确定义为一个有向状态图。每个节点是一个函数如analyze_page,perform_action边代表状态流转的条件。这种显式建模带来了巨大优势清晰的可见性整个Agent的决策流程一目了然不再是黑盒。精细的控制可以轻松地在特定节点后添加验证、过滤或重试逻辑。易于调试执行轨迹Trace可以完整地以图的形式回放精准定位问题出在哪个环节。支持复杂模式如并行执行、子图Subgraph嵌套这对于未来实现多标签页协同操作至关重要。简单来说LangGraph把Agent的“大脑”运行逻辑画了出来而不仅仅是一段顺序执行的代码。这对于构建需要稳健性的Web Agent来说是基础设施级别的提升。2.2 Web Agent的核心组件拆解一个完整的Web Agent系统通常由以下几部分组成LangGraph负责将它们串联成一个有机整体状态State管理这是LangGraph的核心。状态是一个字典贯穿整个工作流。对于Web Agent状态里必须包含objective: 用户原始目标贯穿始终。current_url: 当前浏览器所在的页面URL。page_content: 当前页面的文本/结构化摘要供LLM观察。action_history: 已执行的操作历史列表用于上下文记忆和避免重复。error: 上一步操作产生的错误信息如果有。is_complete: 任务是否完成的标志。网页感知Perception模块负责将浏览器中的真实网页转化为LLM能理解的“观察结果”。这里不能简单地把整个HTML丢给LLM成本高且噪音大。通常策略是DOM简化与摘要使用类似BeautifulSoup或lxml的库过滤掉脚本、样式等无关标签提取主要文本内容和关键元素属性如id, class, aria-label。视觉定位辅助对于极度依赖视觉布局的页面可以结合截图使用视觉模型如GPT-4V或专门的定位模型来辅助理解。但在初期精简的DOM文本通常足够。结构化输出将摘要后的页面信息以清晰的结构如分区块描述提供给LLM。决策LLM与规划Planning模块这是Agent的“大脑”。它接收“状态”包含目标和当前页面观察输出下一个要执行的“动作”。这里的关键是提示词Prompt工程。你需要设计一个Prompt让LLM学会理解网页的简化表示。根据历史动作和当前目标判断下一步最佳操作。将操作输出为严格的、可被解析的格式如JSON包含action_typeclick, type, scroll等和action_parametersselector, text等。行动Action执行模块负责将LLM输出的抽象指令转化为具体的浏览器操作。这里我们通常选用Playwright因为它比Selenium更现代、速度更快并且对动态网页单页应用SPA的支持更好。这个模块需要处理元素定位将LLM给出的CSS选择器或XPath应用到真实DOM。操作执行点击、输入、滚动、等待导航等。异常捕获元素不存在、操作超时、页面崩溃等。验证与循环控制执行动作后需要验证结果。例如点击后页面是否跳转输入后是否有错误提示这个验证结果会更新到state中决定下一步是回到“观察”节点还是进入“任务完成”或“错误处理”节点。3. 基于LangGraph的Web Agent实战搭建理论讲完了我们上干货。下面我将一步步展示如何用LangGraph搭建一个基础的Web Agent。假设我们的目标是让Agent去GitHub搜索LangGraph仓库并返回其Star数量。3.1 环境准备与依赖安装首先创建一个新的Python环境推荐3.9并安装核心依赖。# 创建并激活虚拟环境可选 python -m venv web_agent_env source web_agent_env/bin/activate # Linux/Mac # web_agent_env\Scripts\activate # Windows # 安装核心库 pip install langgraph langchain-openai playwright beautifulsoup4 lxml # 安装Playwright浏览器内核 playwright install chromium这里我们选择langchain-openai作为LLM的接入层你也可以换成Anthropic、Groq或其他兼容OpenAI API的模型服务。Playwright选择Chromium作为浏览器兼顾了兼容性和性能。3.2 定义核心状态State在LangGraph中我们首先需要定义一个状态类来描述在整个工作流中流转的数据。我们使用TypedDict来获得更好的类型提示。from typing import TypedDict, List, Optional, Annotated import operator class AgentState(TypedDict): Web Agent的核心状态定义 objective: str # 用户目标如“获取LangGraph仓库的star数” current_url: Optional[str] # 当前浏览器URL page_content: Optional[str] # 当前页面摘要内容 action_history: List[dict] # 执行历史每个动作是一个dict last_action: Optional[dict] # 上一个执行的动作 last_action_result: Optional[str] # 上一个动作的结果成功/失败信息 error: Optional[str] # 错误信息 is_complete: bool # 任务是否完成 final_answer: Optional[str] # 最终答案 # 为了支持LangGraph的并行更新我们使用add_messages操作符 # 但在这个简单示例中我们主要使用operator.setitem3.3 实现网页感知节点Perception Node这个节点的功能是给定一个URL或当前页面获取其内容并提炼出对LLM有用的摘要。from playwright.sync_api import sync_playwright from bs4 import BeautifulSoup import urllib.parse def perceive_page(state: AgentState) - AgentState: 观察当前页面提取关键内容 url state.get(current_url) if not url: # 如果没有当前URL可能是初始状态可以设置为一个起点如搜索引擎 state[current_url] https://www.github.com url state[current_url] # 这里我们简单返回实际中可能需要先导航到该URL state[page_content] 初始页面GitHub首页。 return state # 使用Playwright获取页面内容 with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 无头模式调试时可设为False page browser.new_page() try: page.goto(url, wait_untilnetworkidle) # 等待网络空闲 # 获取页面HTML html_content page.content() # 使用BeautifulSoup解析并简化 soup BeautifulSoup(html_content, lxml) # 移除脚本、样式等无关标签 for script in soup([script, style, meta, link]): script.decompose() # 提取文本并做适当精简例如只取前5000字符避免上下文过长 text soup.get_text(separator , stripTrue) # 简单的段落化处理按换行或长句分割 lines [line for line in text.splitlines() if line.strip()] meaningful_text .join(lines[:100])[:3000] # 限制长度 # 提取可能有用的链接和按钮简化版 links [a.get(href) for a in soup.find_all(a, hrefTrue)][:10] buttons [btn.get_text(stripTrue) for btn in soup.find_all([button, input[typesubmit]])][:10] # 构建给LLM的页面描述 page_summary f 当前页面URL: {url} 页面内容摘要: {meaningful_text} 页面中的部分关键链接前10个: {chr(10).join(links)} 页面中的部分按钮/可交互元素: {chr(10).join(buttons)} state[page_content] page_summary except Exception as e: state[error] f访问页面 {url} 失败: {str(e)} state[page_content] None finally: browser.close() return state注意在实际生产环境中perceive_page函数需要更健壮例如处理登录弹窗、Cookie同意框、无限滚动等。这里的简化版仅用于演示核心流程。另外对于复杂页面可能需要结合视觉信息或更高级的DOM解析库如readability来提取主体内容。3.4 实现决策节点LLM Planning Node这是Agent的“大脑”。我们使用LLM这里用GPT-4来分析状态并决定下一步做什么。from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import JsonOutputParser from pydantic import BaseModel, Field import json # 定义LLM输出动作的格式 class WebAction(BaseModel): LLM应输出的动作指令 reasoning: str Field(description解释为什么选择这个动作) action_type: str Field(description动作类型必须是以下之一navigate, click, type, scroll, extract, finish, pattern^(navigate|click|type|scroll|extract|finish)$) selector: Optional[str] Field(defaultNone, descriptionCSS选择器或XPath用于定位元素click/type动作需要) text: Optional[str] Field(defaultNone, description需要输入的文字type动作需要) url: Optional[str] Field(defaultNone, description要导航到的URLnavigate动作需要) # 初始化LLM和输出解析器 llm ChatOpenAI(modelgpt-4-turbo-preview, temperature0) # 使用低temperature保证稳定性 parser JsonOutputParser(pydantic_objectWebAction) # 构建决策Prompt DECISION_PROMPT_TEMPLATE 你是一个专业的网页浏览助手。你的目标是{objective} ## 当前状态 当前页面URL: {current_url} 已执行的操作历史: {action_history} 上一个动作结果: {last_action_result} ## 当前页面内容 {page_content} ## 任务要求 请基于以上信息决定下一步操作。你的输出必须是一个严格的JSON对象包含以下字段 - reasoning: 你的思考过程。 - action_type: 动作类型。必须是以下之一 * navigate: 导航到一个新URL。 * click: 点击一个元素。 * type: 在输入框中输入文字。 * scroll: 滚动页面。 * extract: 从当前页面提取所需信息当任务目标已达成时。 * finish: 任务完成或无法继续结束流程。 - selector: 仅当action_type为click或type时需要。提供一个精准的CSS选择器或XPath来定位目标元素。优先使用有意义的属性如id、name、aria-label或包含特定文本的标签。 - text: 仅当action_type为type时需要。输入的文字内容。 - url: 仅当action_type为navigate时需要。完整的URL地址。 ## 重要准则 1. 优先使用click和type与页面交互。 2. 如果当前页面没有目标信息考虑使用搜索框type动作或点击相关链接click。 3. 只有在必要时才使用navigate直接跳转。 4. 当你认为已经从页面中找到了问题的答案使用extract动作并在reasoning中说明你找到了什么。 5. 如果任务无法完成如找不到元素、陷入循环使用finish并说明原因。 现在请输出你的决策JSON prompt ChatPromptTemplate.from_template(DECISION_PROMPT_TEMPLATE) decision_chain prompt | llm | parser def decide_action(state: AgentState) - AgentState: LLM决策下一步动作 # 如果已有错误或任务完成直接跳过决策 if state.get(error) or state.get(is_complete): return state # 准备Prompt输入 history_str chr(10).join([f- {act} for act in state.get(action_history, [])[-5:]]) # 只保留最近5条历史 input_dict { objective: state[objective], current_url: state.get(current_url, N/A), action_history: history_str, last_action_result: state.get(last_action_result, N/A), page_content: state.get(page_content, 页面内容获取失败) } try: # 调用LLM获取决策 action: WebAction decision_chain.invoke(input_dict) # 将决策存入状态供下一个节点执行 state[last_action] action.dict() state[last_action_result] None # 清空上一次结果 except Exception as e: state[error] fLLM决策失败: {str(e)} state[last_action] {action_type: finish, reasoning: f决策过程出错: {e}} return state这个Prompt是Web Agent成功与否的关键。它需要清晰地定义动作空间并引导LLM进行符合网页交互逻辑的推理。一个常见的坑是LLM输出的选择器不准确。因此在Prompt中我们强调要使用id、name、aria-label等稳定属性并鼓励LLM描述它是如何从页面内容中推断出这个选择器的在reasoning字段中。3.5 实现行动执行节点Action Execution Node这个节点负责将LLM的决策转化为真实的浏览器操作。def execute_action(state: AgentState) - AgentState: 执行LLM决定的动作 action state.get(last_action) if not action or state.get(is_complete): return state action_type action.get(action_type) reasoning action.get(reasoning, ) # 记录到历史 state[action_history].append(f[{action_type}] {reasoning}) if action_type finish: state[is_complete] True if 答案 in reasoning or extract in str(state.get(action_history, [])): state[final_answer] reasoning else: state[final_answer] f任务终止。原因{reasoning} return state if action_type extract: # 提取信息动作这里我们让LLM基于当前页面内容直接生成答案 # 在实际应用中可能需要更复杂的提取逻辑 state[is_complete] True # 简单起见我们让LLM再根据目标总结一次答案实际可优化 state[final_answer] f根据页面内容提取信息{reasoning} return state # 需要浏览器交互的动作 current_url state.get(current_url, ) result_msg try: with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() # 可以在此处配置上下文如视口大小、User-Agent page context.new_page() if current_url: page.goto(current_url, wait_untilnetworkidle) if action_type navigate: url action.get(url) if url: if not url.startswith(http): # 处理相对URL base_url current_url if current_url else https://www.github.com url urllib.parse.urljoin(base_url, url) page.goto(url, wait_untilnetworkidle) state[current_url] url result_msg f成功导航到 {url} else: raise ValueError(导航动作缺少URL参数) elif action_type click: selector action.get(selector) if selector: # 等待元素出现并点击 page.wait_for_selector(selector, stateattached, timeout10000) page.click(selector) # 点击后等待一小段时间让页面反应如导航、弹窗、内容加载 page.wait_for_timeout(2000) # 检查是否有导航发生 new_url page.url if new_url ! current_url: state[current_url] new_url result_msg f点击 {selector} 成功页面跳转到 {new_url} else: result_msg f点击 {selector} 成功页面未跳转。 else: raise ValueError(点击动作缺少selector参数) elif action_type type: selector action.get(selector) text action.get(text) if selector and text is not None: page.wait_for_selector(selector, stateattached, timeout10000) # 先清空输入框再输入 page.fill(selector, ) page.type(selector, text) # 模拟回车键提交常见于搜索框 page.press(selector, Enter) page.wait_for_timeout(3000) # 等待搜索结果加载 state[current_url] page.url result_msg f在 {selector} 中输入 {text} 并提交成功。 else: raise ValueError(输入动作缺少selector或text参数) elif action_type scroll: # 滚动页面可以指定滚动像素这里简单向下滚动一屏 page.evaluate(window.scrollBy(0, window.innerHeight * 0.8)) page.wait_for_timeout(1000) # 等待可能的懒加载内容 result_msg 页面向下滚动了一屏。 else: result_msg f未知动作类型: {action_type} # 执行动作后更新页面内容为下一次观察做准备 # 这里可以调用perceive_page的逻辑但为了简化我们只更新URL # 实际应用中应在执行动作后立即重新感知页面 state[last_action_result] f动作{action_type}执行成功。{result_msg} browser.close() except Exception as e: error_detail str(e) state[error] f执行动作 {action_type} 时出错: {error_detail} state[last_action_result] f动作执行失败: {error_detail} # 对于某些错误如元素未找到可以尝试重试或选择其他策略这里简单记录错误 return state实操心得执行节点是故障高发区。元素选择器不准、页面加载时间不确定、动态内容等因素都会导致失败。关键技巧包括1) 在click和type前使用wait_for_selector并设置合理超时2) 在执行可能引发页面跳转的动作后检查URL是否变化3) 对于type动作特别是搜索框模拟按Enter键比单纯输入更可靠4) 考虑使用page.wait_for_load_state(networkidle)来确保页面稳定。3.6 组装LangGraph工作流现在我们将各个节点用LangGraph的图Graph连接起来。from langgraph.graph import StateGraph, END # 创建图 workflow StateGraph(AgentState) # 添加节点 workflow.add_node(perceive, perceive_page) # 观察页面 workflow.add_node(decide, decide_action) # LLM决策 workflow.add_node(act, execute_action) # 执行动作 # 设置边定义流程 workflow.set_entry_point(perceive) # 从观察开始 # 观察 - 决策 workflow.add_edge(perceive, decide) # 决策 - 执行 workflow.add_edge(decide, act) # 执行后的条件路由根据执行结果决定下一步 def route_after_action(state: AgentState): 根据动作执行后的状态决定下一步是继续观察还是结束 if state.get(is_complete): return END if state.get(error): # 如果有错误可以进入一个错误处理节点这里简单返回END print(f发生错误: {state[error]}) return END # 否则继续观察页面因为执行动作后页面状态可能已变 return perceive workflow.add_conditional_edges( act, route_after_action, { perceive: perceive, END: END } ) # 编译图 app workflow.compile()这个图定义了一个简单的循环Perceive - Decide - Act - (检查) - Perceive ...。当act节点执行后route_after_action函数会检查状态。如果任务完成或有错误则结束否则回到perceive节点重新观察变化后的页面开始新一轮循环。3.7 运行与测试现在让我们用这个Agent来执行我们的目标。# 初始化状态 initial_state: AgentState { objective: 去GitHub搜索LangGraph仓库并返回其Star数量。, current_url: https://www.github.com, page_content: None, action_history: [], last_action: None, last_action_result: None, error: None, is_complete: False, final_answer: None } # 运行Agent print(开始执行Web Agent任务...) try: # 设置最大步数防止无限循环 max_steps 15 current_state initial_state for step in range(max_steps): print(f\n--- 步骤 {step1} ---) # 使用LangGraph的流式接口一次执行一个节点便于调试 # 这里我们模拟图的一步执行实际可以使用app.invoke但分步更清晰 # 为了演示我们手动按节点调用实际生产应使用app的流式或分步执行 # 简化演示我们直接调用app.invoke并打印关键状态 result app.invoke(current_state, config{recursion_limit: max_steps}) current_state result print(f当前URL: {current_state.get(current_url)}) print(f上一步动作: {current_state.get(last_action)}) print(f动作结果: {current_state.get(last_action_result)}) if current_state.get(is_complete): print(f\n任务完成最终答案{current_state.get(final_answer)}) break if current_state.get(error): print(f\n任务出错{current_state.get(error)}) break else: print(f\n达到最大步数限制({max_steps})任务未完成。) except Exception as e: print(f运行过程中发生异常: {e})4. 工程化挑战与优化策略上面我们完成了一个基础的原型。但要让它真正可靠还需要解决一系列工程化挑战。4.1 元素定位的稳定性Web Agent的阿喀琉斯之踵LLM输出的选择器如#search-input经常失效原因有1) 元素是动态生成的2) 选择器过于复杂或脆弱3) 页面结构微调。解决方案是多管齐下选择器回退策略让LLM输出多个备选选择器如primary_selector,fallback_selectors。执行节点按顺序尝试直到一个成功。视觉辅助定位对于关键操作如“登录按钮”可以结合截图使用视觉模型描述元素位置如“页面右上角的蓝色按钮”再通过计算机视觉库或Playwright的locatorAPI配合文本描述来定位。这能极大提升对动态生成或选择器不明确元素的鲁棒性。使用更稳健的定位器鼓励LLM优先使用>问题现象可能原因排查与解决思路LLM一直输出finish或无关动作1. Prompt指令不清晰。2. 页面摘要质量太差LLM无法理解。3. 目标过于模糊。1. 检查并优化Prompt明确动作空间和规则。在reasoning字段要求LLM详细解释。2. 改进perceive_page函数提供更结构化、更相关的页面信息如突出按钮、输入框。3. 将用户目标拆解成更具体、可执行的步骤。元素选择器总是定位失败1. LLM生成的选择器不准。2. 页面是动态加载的元素尚未出现。3. 页面内有iframe或Shadow DOM。1. 实现选择器回退机制让LLM提供多个备选。2. 在执行动作前增加wait_for_selector并适当增加超时时间。3. 在perceive_page阶段尝试深入iframe或Shadow DOM提取内容或在执行时切换到对应的frame。Agent陷入无限循环如反复点击同一个按钮1. 状态没有正确更新LLM每次看到的都是相同的“观察”。2. 动作执行后页面没有发生LLM期望的变化。1. 确保execute_action后current_url和page_content被正确更新。2. 在状态中加强action_history的记录并在Prompt中明确要求LLM避免重复历史动作。3. 实现循环检测逻辑当连续几步状态高度相似时强制跳出或尝试替代动作。任务执行速度极慢1. LLM调用延迟高。2. 页面加载或元素等待超时设置过长。3. 每次循环都重新启动浏览器。1. 考虑使用更快的模型如GPT-3.5-Turbo进行常规决策只在复杂规划时用大模型。2. 优化超时参数平衡成功率和速度。3. 在整个会话期间复用浏览器实例和Page对象而不是每次动作都开闭。在特定网站如需要登录失败1. 网站有反爬机制。2. 需要处理登录态、Cookie等。1. 在Playwright上下文中配置更真实的User-Agent、视口大小并启用浏览器持久化上下文来保存Cookie。2. 对于登录可以设计一个专门的“登录子图”或预先通过手动登录后导出Cookie文件加载。调试技巧启用无头模式可视化开发时将playwright.launch(headlessFalse)亲眼看着Agent操作浏览器能最直观地发现问题。详细日志记录记录每个节点的输入/输出状态特别是LLM的完整响应和页面摘要。将这些日志与浏览器截图关联起来。使用LangGraph的追踪TracingLangGraph内置了强大的追踪功能可以将每次运行以图的形式可视化清晰看到状态在每个节点间的流转是调试复杂逻辑流的神器。单元测试关键节点对perceive_page、decide_action用固定输入测试输出、execute_action测试特定选择器分别进行单元测试确保基础功能稳固。Web Agent的开发是一个持续迭代和调优的过程。没有一劳永逸的解决方案核心在于构建一个可观察、可调试、可扩展的框架这正是LangGraph的优势然后针对具体的应用场景和网站不断优化你的感知、决策和执行模块。从简单的信息查询任务开始逐步增加其复杂性和鲁棒性你会发现自己正在塑造一个真正能理解并操作数字世界的智能体。
返回列表