基于LLM的网页AI Agent:从原理到实践,手把手搭建LaVague智能体
1. 项目概述:当LLM学会“看”和“点”
最近在AI圈里,一个叫LaVague的开源项目热度不低。它解决了一个非常具体且挠人的问题:如何让大语言模型(LLM)不仅能“说”,还能“做”——具体来说,就是能像人一样操作网页浏览器。想象一下,你告诉AI“帮我在购物网站上找一款500元以内的无线耳机,然后加入购物车”,它就能自己打开浏览器,搜索、筛选、点击、输入,一气呵成。这不再是科幻电影里的场景,LaVague正在把它变成现实。
本质上,LaVague是一个基于大语言模型的AI Agent框架,专门用于网页自动化。它不是一个简单的脚本录制工具,而是一个具备“感知-思考-行动”闭环的智能体。其核心价值在于,它将LLM强大的自然语言理解和推理能力,与浏览器自动化的执行能力结合了起来。这意味着,你不再需要为每一个网页操作编写繁琐的、脆弱的XPath或CSS选择器代码,而是可以用人类语言描述任务,由AI来理解并执行。
这个项目特别适合几类人:一是希望将重复性网页操作(如数据抓取、表单填写、监控、测试)自动化的开发者或业务人员;二是对AI Agent技术感兴趣,想亲手搭建一个能“动手”的智能体的技术爱好者;三是希望探索LLM在具身智能或RPA(机器人流程自动化)领域应用的研究者。对于新手来说,它提供了一个绝佳的、看得见摸得着的AI Agent入门实践;对于老手,其架构设计和对开源模型的整合思路也很有借鉴意义。
2. LaVague核心架构与工作原理拆解
要理解LaVague如何工作,我们需要把它拆解成几个核心组件。它的设计遵循了经典的AI Agent范式,但在网页这个特定领域做了精妙的适配。
2.1 核心组件:大脑、眼睛和手
LaVague的架构可以形象地理解为“大脑”、“眼睛”和“手”的协同。
-
大脑(World Model - 世界模型) :这是整个系统的决策中枢,由一个或多个大语言模型担任。它的核心职责是理解用户的自然语言指令,并将其分解、规划成一系列具体的、可执行的网页操作步骤。例如,用户说“查看我Github仓库的star数”,大脑需要推理出步骤:打开浏览器 -> 导航到github.com -> 登录 -> 找到我的仓库页面 -> 定位star计数器的元素。这个大脑不仅要做任务规划,还要根据“眼睛”看到的结果,动态调整计划。
-
眼睛(Action Engine - 行动引擎) :这是系统的感知模块。它的任务是将浏览器当前页面的状态(即DOM树、截图等)转换成一个LLM能够理解的、信息丰富的“描述”。LaVague没有直接给LLM扔过去一整棵庞大的DOM树,而是采用了一种更聪明的方式:它通常会提取页面的关键结构化信息,比如按钮的文本、输入框的标签、链接的URL,并结合可访问性属性,生成一个简洁的页面上下文。有些实现还会结合视觉模型(如GPT-4V)来分析屏幕截图,以理解那些纯DOM无法捕获的视觉布局信息。这样,LLM就能“看到”页面上有什么。
-
手(Web Driver) :这是系统的执行器。它接收来自“大脑”的具体操作指令,如
click(selector=“#login-button”)、type(text=“username”, selector=“input[name=‘user’]”)、navigate(url=“https://example.com”),并通过Selenium、Playwright或Puppeteer这类浏览器自动化工具来实际执行。它负责将高层的抽象指令转化为底层的浏览器API调用。
这三者形成一个闭环:用户给出指令 -> 大脑规划第一步行动 -> 眼睛观察当前页面状态并描述给大脑 -> 大脑根据描述决定具体操作(点击哪里、输入什么)-> 手执行操作 -> 页面状态改变 -> 眼睛再次观察... 如此循环,直到任务完成或无法继续。
2.2 关键技术:从指令到动作的翻译
这里最精妙的部分在于“大脑”如何将自然语言指令翻译成浏览器能执行的动作。这涉及到几个关键技术点:
- 指令分解与规划 :LLM需要具备强大的推理和规划能力。当接到一个复杂任务时,它不能直接生成最终动作,而必须将其分解为原子操作序列。这通常通过思维链(Chain-of-Thought)或更高级的任务分解提示工程来实现。LaVague的提示词模板会引导模型按照“目标 -> 子目标 -> 具体动作”的层次进行思考。
- 上下文管理 :网页操作是状态相关的。LLM在决定下一步动作时,必须清楚当前页面处于什么状态(例如,是在登录页还是主页,弹窗是否打开)。LaVague通过不断将“眼睛”捕获的页面描述作为上下文喂给LLM,来维持其对当前状态的理解。这个上下文窗口的管理至关重要,既要包含足够信息,又不能过长导致模型性能下降或成本激增。
- 动作空间定义 :LLM输出的必须是一套标准化、可解析的动作指令。LaVague会明确定义一个有限的“动作词汇表”,比如
click,type,scroll,wait,extract_text等,并为每个动作定义参数格式。然后通过提示词要求LLM严格按照这个格式输出,后续再通过解析器将其转化为真正的API调用。
注意 :这个“翻译”过程的可靠性直接决定了Agent的成败。一个常见的失败模式是LLM“幻觉”,即它可能生成一个在当前页面上根本不存在的元素选择器,或者误解了页面布局。因此,增强“眼睛”的感知能力(如结合视觉)和设计鲁棒的故障恢复机制(如重试、 fallback策略)是工程上的重点。
3. 从零搭建你的第一个网页AI Agent
理论讲得再多,不如亲手跑一遍。下面我将以LaVague开源项目(或其类似理念的实现)为例,带你一步步在本地搭建一个基础的网页AI Agent。我们会使用目前比较流行的技术栈:Ollama本地运行LLM + Playwright进行浏览器控制。
3.1 环境准备与工具选型
首先,我们需要一个“大脑”。为了完全本地化、可控且免费,我选择 Ollama 来在本地运行开源大模型。Ollama极大地简化了在本地运行LLM(如Llama 3、Mistral、Qwen等)的过程。对于网页自动化任务,模型需要较强的指令遵循和推理能力,我推荐从 Llama 3 8B Instruct 或 Mistral 7B Instruct 开始,它们对硬件要求相对友好(至少16GB RAM,有GPU更好)。
其次,我们需要“手”。 Playwright 是一个现代浏览器自动化库,比Selenium更快速、可靠,并且自带浏览器,无需单独安装驱动。它的API设计也很清晰。
最后,我们需要编写胶水代码,也就是LaVague的核心逻辑,将大脑和手连接起来,并实现“眼睛”的功能。我们将用Python来完成。
环境准备步骤:
-
安装Ollama :前往Ollama官网,根据你的操作系统(Windows/macOS/Linux)下载并安装。
# 安装后,在终端拉取并运行模型,例如Llama 3 8B ollama pull llama3:8b ollama run llama3:8b运行成功后,Ollama会在本地启动一个API服务(默认端口11434),我们的Python代码可以通过这个API与模型对话。
-
创建Python虚拟环境并安装依赖 :
python -m venv lavague-env # Windows lavague-env\Scripts\activate # macOS/Linux source lavague-env/bin/activate pip install playwright ollama beautifulsoup4 lxml # 安装Playwright的浏览器 playwright install chromiumbeautifulsoup4和lxml将用来解析HTML,充当“眼睛”的一部分。
3.2 构建简易的“眼睛”:页面状态提取器
“眼睛”的目标是把复杂的HTML页面变成一段简明的文字描述,供LLM分析。一个简单有效的策略是提取所有交互元素和关键文本。
import asyncio
from playwright.async_api import async_playwright
from bs4 import BeautifulSoup
class WebPageObserver:
def __init__(self, page):
self.page = page
async def get_page_description(self):
"""获取当前页面的结构化描述"""
# 获取页面HTML和截图(备用)
html = await self.page.content()
# 可以保存截图路径,如果需要结合视觉模型的话
# screenshot_path = “page.png”
# await self.page.screenshot(path=screenshot_path)
soup = BeautifulSoup(html, 'lxml')
description_parts = []
# 1. 提取页面标题和主要标题
title = soup.title.string if soup.title else “无标题”
description_parts.append(f”页面标题: {title}“)
h1_tags = soup.find_all('h1')
if h1_tags:
description_parts.append(f”主要标题: {‘; ‘.join([h.get_text(strip=True) for h in h1_tags])}“)
# 2. 提取所有链接(<a>标签)
links = []
for a in soup.find_all('a', href=True, string=True):
link_text = a.get_text(strip=True)
if link_text and len(link_text) < 50: # 避免过长文本
links.append(f”‘{link_text}’ -> {a[‘href’]}“)
if links:
description_parts.append(f”可用链接: {‘ | ‘.join(links[:10])}“) # 只取前10个避免过长
# 3. 提取所有按钮和输入框(关键交互元素)
interactive_elements = []
for btn in soup.find_all(['button', ‘input’]):
elem_type = btn.name
elem_id = btn.get(‘id’, ‘’)
elem_name = btn.get(‘name’, ‘’)
elem_value = btn.get(‘value’, ‘’)
placeholder = btn.get(‘placeholder’, ‘’)
text = btn.get_text(strip=True) if btn.name == ‘button’ else ‘’
label = ‘或’.join(filter(None, [elem_id, elem_name, placeholder, text, elem_value]))
if label:
# 生成一个简易的选择器提示
selector_hint = f”{elem_type}"
if elem_id:
selector_hint += f”# {elem_id}“
elif elem_name:
selector_hint += f”[name=‘{elem_name}’]“
interactive_elements.append(f”{selector_hint} (标签: ‘{label}’)“)
if interactive_elements:
description_parts.append(f”交互元素: {‘ | ‘.join(interactive_elements[:15])}“)
# 4. 提取主要段落文本(前几段)
main_texts = []
for p in soup.find_all('p'):
text = p.get_text(strip=True)
if 20 < len(text) < 200: # 取长度适中的段落
main_texts.append(text)
if main_texts:
description_parts.append(f”页面内容摘要: {‘... ‘.join(main_texts[:3])}“)
return “\n”.join(description_parts)
这个观察者类会生成一段类似这样的描述:
页面标题: GitHub - 登录
主要标题: 登录 GitHub
可用链接: ‘Sign in’ -> /login | ‘Forgot password?’ -> /password_reset | ‘Create an account’ -> /join
交互元素: input#login_field (标签: ‘login_field’) | input#password (标签: ‘password’) | input[name=‘commit’] (标签: ‘Sign in’)
页面内容摘要: 欢迎回来!请登录以访问您的仓库和项目。...
这段描述远比原始HTML简洁,但包含了LLM做决策所需的关键信息:有什么可点的,有什么可填的。
3.3 构建“大脑”:LLM驱动规划与决策
接下来,我们构建大脑。它会接收用户指令和页面描述,然后输出一个标准化的JSON动作指令。
import ollama
import json
class LLMBrain:
def __init__(self, model=“llama3:8b”):
self.model = model
# 定义我们支持的动作类型
self.action_schema = {
“type”: “object”,
“properties”: {
“action”: {“type”: “string”, “enum”: [“click”, “type”, “navigate”, “scroll”, “wait”, “extract”, “stop”]},
“selector”: {“type”: “string”, “description”: “CSS选择器或元素描述,用于click/type动作”},
“text”: {“type”: “string”, “description”: “需要输入的文字,用于type动作”},
“url”: {“type”: “string”, “description”: “需要导航到的网址,用于navigate动作”},
“reason”: {“type”: “string”, “description”: “选择此动作的原因”}
},
“required”: [“action”, “reason”]
}
def decide_next_action(self, user_goal, page_description, previous_actions=[]):
"""根据目标和页面状态,决定下一个动作"""
prompt = f”””
你是一个网页自动化助手。你的目标是:{user_goal}
当前页面状态描述:
{page_description}
历史操作(最近3步):
{‘; ‘.join(previous_actions[-3:]) if previous_actions else ‘无’}
请根据当前页面状态,决定下一步最合理的单个动作,以推进完成目标。
你只能从以下动作中选择:{list(self.action_schema[‘properties’][‘action’][‘enum’])}
输出必须是一个严格的JSON对象,符合以下格式:
{json.dumps(self.action_schema, indent=2)}
请仔细分析页面上的交互元素。选择器(selector)应尽可能精确,优先使用ID(如#login-button),其次使用name属性,再其次是文本内容。
如果页面上没有能推进目标的元素,或者目标已达成,则 action 设为 “stop”。
“””
response = ollama.chat(model=self.model, messages=[{‘role’: ‘user’, ‘content’: prompt}])
llm_output = response[‘message’][‘content’]
# 尝试从LLM输出中提取JSON
try:
# 处理可能包含在代码块或多余文本中的JSON
start = llm_output.find(‘{‘)
end = llm_output.rfind(‘}’) + 1
if start != -1 and end != 0:
json_str = llm_output[start:end]
action = json.loads(json_str)
# 简单验证
if action.get(‘action’) in self.action_schema[‘properties’][‘action’][‘enum’]:
return action
except json.JSONDecodeError as e:
print(f”LLM返回了非JSON内容: {llm_output}“)
# 如果解析失败,返回一个安全的等待动作
return {“action”: “wait”, “reason”: “无法解析LLM响应,暂停一下”}
这个大脑的核心是一个精心设计的提示词(Prompt)。它明确了任务(user_goal)、提供了上下文(page_description)、限定了动作范围,并要求输出结构化JSON。这大大降低了LLM“胡说八道”的概率。
3.4 整合与主循环:让Agent动起来
最后,我们将“眼睛”、“大脑”和“手”组装起来,形成主控制循环。
import asyncio
class WebAutomationAgent:
def __init__(self, model_name=“llama3:8b”):
self.brain = LLMBrain(model_name)
self.actions_history = []
async def run(self, user_goal, start_url=“about:blank”):
async with async_playwright() as p:
# 启动浏览器(“手”)
browser = await p.chromium.launch(headless=False) # headless=False方便观察
page = await browser.new_page()
await page.goto(start_url)
# 初始化“眼睛”
observer = WebPageObserver(page)
max_steps = 20 # 防止无限循环
for step in range(max_steps):
print(f”\n=== 步骤 {step + 1} ===")
# 1. 观察(眼睛)
page_desc = await observer.get_page_description()
print(f”当前页面状态:\n{page_desc[:500]}...“) # 打印前500字符
# 2. 思考(大脑)
print(f”思考下一步动作以达成目标: {user_goal}“)
next_action = self.brain.decide_next_action(user_goal, page_desc, self.actions_history)
print(f”决策结果: {next_action}“)
# 3. 执行(手)
action = next_action.get(‘action’)
if action == “stop”:
print(“大脑判断任务已完成或无法继续。”)
break
elif action == “navigate”:
url = next_action.get(‘url’)
if url:
await page.goto(url)
print(f”导航到: {url}“)
elif action == “click”:
selector = next_action.get(‘selector’)
if selector:
try:
await page.click(selector)
print(f”点击: {selector}“)
except Exception as e:
print(f”点击失败 {selector}: {e}“)
elif action == “type”:
selector = next_action.get(‘selector’)
text = next_action.get(‘text’)
if selector and text:
try:
await page.fill(selector, text)
print(f”在 {selector} 中输入: {text}“)
except Exception as e:
print(f”输入失败 {selector}: {e}“)
elif action == “wait”:
await page.wait_for_timeout(2000) # 等待2秒
print(“等待2秒...”)
elif action == “scroll”:
await page.mouse.wheel(0, 300) # 向下滚动
print(“向下滚动...”)
else:
print(f”未知动作: {action},转为等待“)
await page.wait_for_timeout(1000)
# 记录历史
self.actions_history.append(str(next_action))
await page.wait_for_timeout(1000) # 每次动作后稍作停顿,让页面加载
print(f”\n任务结束。共执行 {len(self.actions_history)} 步。”)
await browser.close()
# 运行Agent
async def main():
agent = WebAutomationAgent(“llama3:8b”) # 确保Ollama已运行并拉取了该模型
# 示例任务:去百度搜索
await agent.run(user_goal=“在百度搜索框中输入‘人工智能’,然后点击‘百度一下’按钮”, start_url=“https://www.baidu.com”)
if __name__ == “__main__”:
asyncio.run(main())
这个简单的Agent已经具备了基础能力。运行后,你会看到浏览器自动打开百度首页,LLM分析页面后,可能会先尝试点击输入框( selector=“#kw” ),然后输入“人工智能”,最后点击搜索按钮( selector=“#su” )。虽然它可能不会100%成功(取决于模型对页面描述的解读和选择器的生成),但整个“感知-思考-行动”的闭环已经完整跑通。
4. 工程化挑战与优化策略实录
自己动手搭一遍,你立刻就会遇到LaVague这类项目在实际工程化中必须面对的挑战。下面是我在实验和参考业界方案后总结的几个核心问题及应对策略。
4.1 可靠性问题:LLM的“幻觉”与错误恢复
这是最大的痛点。LLM可能会:
- 生成无效选择器 :比如页面上根本没有
#super-button,它却硬要点击。 - 误解页面状态 :登录成功了,它还试图去找登录框。
- 陷入循环 :在两个页面间来回跳转,或者重复同一无效操作。
应对策略:
-
强化感知(眼睛) :
- 多模态输入 :除了HTML/DOM,引入屏幕截图,并使用视觉语言模型(VLM)如GPT-4V、Qwen-VL或开源的LLaVA来分析。视觉信息能弥补DOM缺失的布局、样式和实际渲染效果,对于识别图标、验证码、复杂组件至关重要。可以将VLM的描述与DOM描述融合,给LLM一个更全面的“视图”。
- 更智能的元素描述 :不要简单罗列所有元素。可以按区域(Header, Sidebar, Main Content, Footer)分组描述,或者使用聚类算法突出可能相关的交互元素。给元素打上置信度标签(如“很可能是一个提交按钮”)。
-
动作验证与重试(手与大脑之间) :
- 在执行动作前,可以增加一个验证步骤。例如,决定点击某个选择器前,先用Playwright的
page.locator(selector).count()检查该元素是否存在、是否可见、是否可点击。 - 如果动作执行失败(如元素未找到),不要直接崩溃。应该将这个失败信息作为新的上下文反馈给LLM,让它重新规划。可以设计一个“错误处理子循环”。
max_retries = 3 for retry in range(max_retries): action = brain.decide(...) try: execute_action(action) break # 成功则跳出重试循环 except ElementNotFoundError as e: error_feedback = f”上一步尝试{action}失败,原因:{e}。请重新评估页面并选择其他动作。” # 将错误反馈加入上下文,让大脑重新决策 page_desc += f”\n[上次错误]: {error_feedback}” - 在执行动作前,可以增加一个验证步骤。例如,决定点击某个选择器前,先用Playwright的
-
设定明确的终止条件 :
- 除了LLM自己说
stop,系统层面必须设置硬性终止条件:最大步数(如50步)、超时时间、任务成功检测器(例如,检查页面URL或特定元素文本是否包含“登录成功”、“订单提交”等关键词)。
- 除了LLM自己说
4.2 性能与成本考量
如果使用云端API(如GPT-4),每次观察和决策都是一次API调用,成本高昂且速度慢。对于复杂任务,可能需要几十次调用,延迟不可接受。
优化策略:
- 本地模型优先 :这正是我们使用Ollama+Llama 3的原因。虽然小模型能力稍弱,但零成本、零延迟、隐私无忧,对于许多标准网页操作足够用。7B/8B参数模型在消费级GPU甚至纯CPU上都能运行。
- 上下文压缩 :页面描述不能无限增长。需要设计算法来压缩历史上下文,只保留最相关的信息。例如,只保留最近3次页面描述的关键差异部分。
- 分层规划 :不要让LLM规划每一个原子点击。可以引入一个“高层规划器”先将大任务分解成几个阶段(Phase),每个阶段用一个“低层执行器”来完成。高层规划调用一次LLM,低层执行可能使用更轻量的规则或小模型。
- 缓存与记忆 :对于重复访问的网站(如公司内部系统),可以缓存页面结构和成功操作路径。下次执行相同任务时,可以直接复用缓存的动作序列,或只需LLM做少量确认,大幅减少调用次数。
4.3 可扩展性与技能学习
一个只会按部就班操作的Agent是脆弱的。我们希望它能学习新技能,适应新网站。
实现思路:
- 技能库(Skill Library) :将成功的操作序列(如“GitHub登录”、“淘宝搜索商品”)封装成可复用的“技能”。每个技能包含:触发条件(描述什么情况下用)、参数(如用户名、密码、搜索关键词)、动作序列模板。当用户提出新任务时,Agent可以先在技能库中匹配,匹配成功则直接调用,否则再让LLM从头规划。
- 演示学习(Learning from Demonstration) :通过录制用户手动操作的过程(包括鼠标点击、键盘输入、页面变化),自动生成对应的动作序列和页面状态描述,从而形成新的技能存入库中。这比纯靠LLM推理要可靠得多。
- 提示词模板化与微调 :针对特定领域(如电商、SaaS后台),可以优化提示词模板,甚至用该领域的数据对本地小模型进行微调(LoRA),使其更擅长理解该领域的术语和页面布局。
5. 进阶探索:从自动化脚本到真正智能体
我们搭建的基础版本只是一个“自动脚本生成器”。要让其进化成更智能的Agent,还有很长的路要走。以下是几个值得深入的方向:
5.1 引入长期记忆与自我反思
当前的Agent是“无状态”的,每次决策只基于当前页面和最近几步历史。一个真正的智能体应该有记忆。
- 短期记忆 :记录本次会话中的所有操作、页面状态变化和LLM的推理过程。这有助于解决需要多步回溯的问题(比如填表填错了,需要返回上一步)。
- 长期记忆 :将成功和失败的经验存储到向量数据库中。当遇到类似页面或任务时,可以进行检索增强生成(RAG),让LLM参考过去的经验,避免重蹈覆辙。例如,过去在某个网站登录时发现
#username选择器不行,但input[name=‘user’]可以,这个经验就可以被记住和复用。
5.2 多工具协同与外部知识
网页操作不是孤立的。一个强大的Agent应该能调用其他工具。
- 计算器/单位转换 :用户说“找价格低于50美元的商品”,Agent需要知道当前汇率。
- 日历/邮件 :完成任务后,可以自动预约会议或发送总结邮件。
- 数据库查询 :将抓取的数据自动存入数据库。 这需要为Agent设计一个“工具调用”接口,让LLM能够根据需求决定何时调用何种外部API。这类似于OpenAI的Function Calling或LangChain的Tool概念。
5.3 评估与持续改进
如何衡量一个AI Agent的好坏?不能只看任务是否完成。
- 成功率 :在100个测试任务中,成功完成的比例。
- 平均步数 :完成相同任务,步数越少效率越高。
- 冗余操作率 :无意义的点击、重复输入的比例。
- 人工干预频率 :需要人出手帮忙的次数。 建立一套评估体系,并利用这些数据持续优化提示词、页面描述方法和动作选择策略,是工程化落地的关键。可以采用A/B测试,对比不同策略的效果。
5.4 安全与伦理边界
让AI自动操作网页蕴含着风险:
- 账户安全 :自动填写密码需极其谨慎,建议使用环境变量或密钥管理服务,绝不硬编码。
- 操作权限 :明确界定Agent可以访问的网站和数据范围,防止越权操作。
- 速率限制 :严格遵守网站的
robots.txt和服务条款,设置合理的操作间隔,避免对目标网站造成DDoS攻击。 - 结果验证 :对于关键操作(如支付、删除),应设计确认机制,或仅允许在沙盒环境中运行。
从我实际折腾的经验来看,LaVague所代表的网页AI Agent方向,其魅力在于它将前沿的LLM技术与一个非常实用的场景结合。虽然目前完全依赖LLM实现全自动、高可靠的复杂流程还有距离,但它已经能够极大地提升开发者在编写自动化脚本时的效率——从“手写每一行代码”变成了“设计提示词和调试逻辑”。对于简单的、结构化的网页任务,它已经可以胜任。随着模型能力的提升和工程框架的完善,未来让AI接管更多重复性的网页工作,将会是一个不可逆的趋势。
更多推荐



所有评论(0)