前阵子做了一个小项目,名字叫 Agent-Reach。起因其实特别朴素:市面上大多数跑得通的AI应用,基本都停留在"回答"这一层,你问它一个问题,它给你一段漂亮的回复,但真要让它去操作浏览器、填表单、点按钮、抓页面数据、做出有依据的判断,能做到的就少很多了。
我当时手里有一堆基于 Playwright 写的采集脚本,每换一个站点、每遇到一次页面改版,都得手动改选择器、调等待逻辑,维护成本越来越高。后来想明白一件事:与其写死每一段流程,不如把"决定下一步做什么"这件事交给大模型,让它自己根据当前页面状态、任务目标去动态组合操作。Agent-Reach 就是这个思路的落地产物——给大模型接上一套"触达层",让它能真正伸手够到外部系统,而不是仅仅输出 JSON。
这篇文章会把项目的来龙去脉、架构设计、核心代码、踩坑记录一次性讲清楚。不绕弯子,适合正在做 Agent 工程化、AI 自动化测试、或者被"模型只会聊天、不会干活"这个问题卡住的同学参考。
1. Agent-Reach 要解决的三个核心痛点
1.1 大模型只能"想"不能"做"
不管是 GPT、Claude 还是国产开源模型,它们的强项都是文本生成和推理,但模型本身运行在云服务器里,没有手臂也没有眼睛,它不可能自己去打开一个网页、移动鼠标、点击按钮。所谓的"Agent 能力",本质上靠的是外围工程去补足这些物理动作。
大多数团队做 AI 应用时,都默认模型只需要返回文本答案就满足了。比如你做一个客服机器人,用户问"这个商品什么时候下架",机器人要么查一下知识库给个标准回答,要么直接说"我无法确定"。但真实业务里,用户需要的往往是一个可执行的结果:去商品页看一眼库存状态、去后台查一下物流轨迹、去后台管理系统里提交一个变更工单。
这些需求有一个共同特点:数据不在知识库里,而在某个真实系统的界面上。没有触达层,模型再聪明也只能干瞪眼。Agent-Reach 就是把这层触达能力做成了一套可复用的基础设施,让模型不仅能"说出答案",还能"把事办了"。
1.2 浏览器自动化与 LLM 之间缺一个"翻译层"
传统的浏览器自动化,比如 Selenium、Playwright、Puppeteer,本质上是人类用编程语言一条一条告诉计算机"做什么"。这个过程非常精确,但也很脆弱:流程一旦定死,页面结构稍有变化脚本就失效。
LLM 不一样,它理解的是自然语言和抽象意图,它知道"用户想搜索某个关键词"这件事,但不知道具体应该调用fill()还是type(),更不知道需要等待几秒、元素什么时候会出现。也就是说,模型和自动化框架之间,存在一个天然的语义鸿沟。
Agent-Reach 的核心设计,就是把"意图"翻译成"操作原语"。模型执行函数调用(function calling),返回的不是一串自由文本,而是一个结构化的工具调用请求,比如调一个叫fill的函数,参数是选择器和文本。这套协议既保留了模型的灵活性,又保证了自动化执行的确定性。简单说,模型负责出脑子,框架负责出手脚。
1.3 稳定性不是可选项,是命根子
只要跑过真实业务脚本的人都知道,浏览器自动化最麻烦的不是功能实现,而是稳定性。页面加载慢几秒、弹窗突然多一个、按钮被遮挡住、网络请求超时、登录状态过期……任何一个小问题都能让流程中断。
LLM 的加入会让局面更复杂,因为模型选择的动作本身就有不确定性。同样一个问题,今天跑可能 5 步完成,明天模型更新了,可能它决定先截个图看看页面再动手,就变成 8 步。
所以 Agent-Reach 在设计初就把稳定性放在和功能同等重要的位置。每一类操作都有超时限制,每一步之后都有状态反馈,执行失败会有重试和降级策略,甚至模型自己犯错时,系统也会把错误包装成页面反馈喂回给模型,让它自我修正。这不只是一个 demo 级别的玩具框架,而是奔着"能连续跑上一周不崩"的标准去做的。
2. 整体架构与模块拆解
2.1 四层链路:规划、执行、反馈、记忆
Agent-Reach 的运行逻辑可以拆成四个层,每一层解决一个独立问题。
规划层(Planner)负责理解任务目标、分析当前页面、决定下一步动作。这一层的实现是 LLM 本身,它接收系统提示词、任务描述、以及最近几步的页面观察结果,输出结构化的函数调用。
执行层(Executor)负责把模型给出的动作翻译成真实的浏览器操作。这里我选的是 Playwright,后面会细说原因。执行层接收函数名和参数,调用封装好的BrowserAgent方法,比如点击、输入、跳转、提取文本。
反馈层(Observer)负责把操作后的页面状态收集起来,转成模型能理解的信息格式。包括当前 URL、页面标题、可见文本片段、截图等。这一层非常关键,模型能不能做出正确决策,很大程度上取决于反馈信息够不够清楚。
记忆层(Memory)负责保存任务进度和历史动作。实现上不复杂,就是把多轮消息追加到对话上下文中,但需要控制消息数量,避免 token 爆炸。这四层合在一起,就构成了一个完整的"感知-思考-行动"循环。
2.2 为什么选 Playwright 而不是 Selenium 或 Puppeteer
我最早用的是 Selenium,后来在几个项目里切到了 Playwright,感受差异非常明显。这里不空谈,直接上对比。
| 维度 | Playwright | Selenium | Puppeteer |
|---|---|---|---|
| 浏览器支持 | Chromium、Firefox、WebKit | 主流通用 | 仅 Chromium |
| 自动等待机制 | 内置auto-wait,元素出现后才能操作 | 需要显式WebDriverWait | 部分需要手写等待 |
| 选择器能力 | CSS、XPath、文本、角色、表单标签等 | 偏 CSS 和 XPath | 基本都是 CSS |
| 调试工具 | Trace Viewer、录制脚本、UI 模式 | 依赖第三方插件 | 依赖 Chrome DevTools |
| 事件处理 | 原生支持page.on系列事件 | 支持但写法繁琐 | 支持良好 |
| 内置存储状态 | storage_state直接保存登录态 | 需要自己手动管理 Cookie | 需要手动处理 |
单从"哪个更好"来比没意义,关键是 Playwright 的定位更贴近 LLM 操作场景。它的 locator 机制非常关键:模型不需要给出绝对精确到像素的定位方式,可以用get_by_role、get_by_text这种语义化方式去描述元素,页面发生变化时容错率高很多。另外 Playwright 原生支持 Firefox 和 WebKit,当 Chromium 环境下被对方站点识别异常时,可以快速切换到其他内核,这在真实业务里非常实用。
还有一个更实际的原因:Playwright 的 Trace Viewer 能看到每一步操作的完整细节,包括网络请求、DOM 快照、截图、控制台日志。调试大模型驱动的自动化流程时,这个功能几乎等于救命稻草,后面调试章节会细讲。
2.3 把工具注册成模型能看懂的样子
让模型调用工具,并不是在系统提示词里写一句"你要用浏览器"就完事。模型需要知道有哪些工具、每个工具是干什么的、参数怎么传。这套东西在 OpenAI 的 API 里叫 function calling,在 Anthropic 里叫工具调用,在国产模型里各有叫法,但核心思路是一样的:向模型声明一组函数,每个函数附带 JSON Schema 描述。
Agent-Reach 里我维护了一个工具注册表,每加入一个新操作就加一条记录。比如navigate函数描述大致长这样:
{ "type": "function", "function": { "name": "navigate", "description": "跳转到指定 URL。当任务需要访问新页面时调用。", "parameters": { "type": "object", "properties": { "url": { "type": "string", "description": "完整目标地址,必须包含协议头,例如 https://example.com" } }, "required": ["url"] } } }这里有两个细节值得注意。
第一个是 description 里一定要写清约束条件。比如 URL 必须带协议头,这个看起来像废话,但模型真的会把example.com直接传进来,结果浏览器报错。描述里如果明确写了"必须包含协议头",出错的概率会明显降低。
第二个是参数约束要尽量具体。比如click函数的选择器参数,我要求它必须是 CSS 选择器格式,而不是"搜索框"这种自然语言描述。模型如果传了"搜索框"三个字进来,执行层根本没法用。写清楚约束,模型就会在生成参数时主动往合规格式上靠。
2.4 会话隔离与浏览器实例管理
一个任务对应一个浏览器上下文,这是 Agent-Reach 从一开始就定下的规则。Playwright 里有 browser、context、page 三个层级,context 相当于一个独立的浏览器会话,cookie、localStorage、缓存都是隔离的。两个任务如果共用 context,很容易出现登录状态串号、缓存互相干扰的问题。
另外我做了登录态持久化。第一次跑任务时如果用户手动登录过,就把 context 的状态存成state.json,下次启动时直接加载,省去重复登录的麻烦。实现方式很简单:
# 保存登录态 await context.storage_state(path="state.json") # 恢复登录态 context = await browser.new_context(storage_state="state.json")这个操作看起来很常规,但它对 Agent 的稳定性提升是决定性的。很多自动化任务都是长周期任务,如果每次重启都让模型去解决登录问题,光这一步就能消耗掉大量步骤和 token。预先处理好登录态,等于把最容易出错的环节提前解决。
由于浏览器进程相对重,Agent-Reach 还做了超时自动回收机制。任务超过设定时间或者异常退出,强制关闭进程,避免服务器上堆满僵尸浏览器。
3. 核心链路的高效实现
3.1 环境准备与依赖安装
Agent-Reach 的后端实现我用了 Python,主要原因是 LLM 生态的 SDK 在 Python 里最成熟,调试也方便。浏览器操作依赖 Playwright,安装分两步:
pip install playwright playwright install chromium第二步是下载浏览器内核文件。需要注意playwright install chromium会同时安装必要的依赖和字体库,在 Linux 服务器上如果缺系统库,还需要跑一下playwright install-deps。这一步容易漏,建议在部署文档里直接写上。
安装完成后,先写一个最小的浏览器启动脚本验证环境,不要急着堆代码:
import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as pw: browser = await pw.chromium.launch(headless=True) page = await browser.new_page() await page.goto("https://example.com") print(await page.title()) await browser.close() asyncio.run(main())能正确输出Example Domain,说明环境没问题。之后的开发都在这套基础上进行。
3.2 浏览器操作层封装
Agent-Reach 把 Playwright 的操作封装在BrowserAgent类里。封装的目标是让上层代码不需要关心 Playwright 的异步细节,只跟简单的函数打交道。
from playwright.async_api import async_playwright class BrowserAgent: def __init__(self): self.pw = None self.browser = None self.context = None self.page = None async def start(self, headless=True, storage_state_file=None): self.pw = await async_playwright().start() self.browser = await self.pw.chromium.launch(headless=headless) self.context = await self.browser.new_context( storage_state=storage_state_file, viewport={"width": 1280, "height": 720} ) self.page = await self.context.new_page() async def stop(self): if self.browser: await self.browser.close() if self.pw: await self.pw.stop() async def navigate(self, url: str): await self.page.goto(url, wait_until="load", timeout=30000) await self.page.wait_for_load_state("networkidle") async def click(self, selector: str): locator = self.page.locator(selector).first await locator.scroll_into_view_if_needed() await locator.click(timeout=10000) async def fill(self, selector: str, text: str): locator = self.page.locator(selector).first await locator.fill(text, timeout=10000) async def press_key(self, key: str): await self.page.keyboard.press(key) async def extract(self, selector: str) -> str: locator = self.page.locator(selector).first return await locator.inner_text(timeout=10000)封装过程中有几个细节值得说。
navigate里的wait_until="load"和wait_for_load_state("networkidle")不是随便加的。页面跳转后如果立即执行下一步操作,很可能元素还没渲染出来,模型就会误判页面结构。先等load事件,再等networkidle,能在大多数场景下保证资源加载完成。
click里的scroll_into_view_if_needed也很关键。模型经常想要点击一个页面底部的按钮,但此时按钮还不在可视区域,Playwright 会报元素不可见。先滚动到可视区域,能避免这类问题。
这里有个经验之谈:如果触发的是异步请求,点击之后不要立刻进入下一轮模型推理,先给 1~2 秒的过渡时间,或者直接等待某个网络事件完成。不然模型看到的是旧页面的快照,前后的状态对不上,推理逻辑就会错乱。
3.3 观察结果收集与反馈构造
模型每一轮决策都需要参考最新的页面状态。Agent-Reach 用observe方法收集三类信息:URL 和标题、页面可见文本片段、截图。
import base64 async def observe(self) -> dict: url = self.page.url title = await self.page.title() body_text = await self.page.inner_text("body") snippet = body_text[:2000] + ("..." if len(body_text) > 2000 else "") screenshot = await self.page.screenshot(type="jpeg", quality=50) screenshot_b64 = base64.b64encode(screenshot).decode() return { "url": url, "title": title, "text": snippet, "screenshot_b64": screenshot_b64 }文本片段必须截断,这是我在实测里踩过坑才改的。很多网页正文有几十万字符,全量塞进上下文,几个来回 token 就爆了。截断到 2000 字符左右,既能反映主要内容,又不会让上下文窗口压力过大。如果页面信息确实很多,模型可以通过滚动或点击的方式继续查看。
截图是为了弥补文本信息的盲区。有些页面内容是图片、图表、Canvas 绘制的,模型透过文本看不到任何东西。加上截图,模型就能"看到"页面实际样子。现代多模态模型对截图的解析能力很强,我实测下来,一张 1280 宽度的页面截图,模型能准确识别出布局和按钮位置。
但同时,不要每一轮都传截图。截图是 base64 编码,一张图可能上万个 token,带几张图之后整个请求会非常慢。我的策略是:默认传文本,模型主动请求截图时才传截图,或者每 5 步强制传一次,平衡感知和成本。
3.4 主调度循环:让模型一步步干活
核心调度逻辑是 Agent-Reach 最重要的部分,它负责维护多轮对话、解析模型的工具调用、执行操作、把结果回传。这里给出一个精简版实现:
import json from openai import AsyncOpenAI client = AsyncOpenAI(api_key="YOUR_API_KEY") SYSTEM_PROMPT = """你是一个能操作浏览器的自动化助手。 请你根据当前任务和页面状态,选择最合适的工具完成一步操作。 每一步只能调用一个工具,除非非常必要,否则不要连续调用多个工具。 执行完操作后,观察页面反馈,再决定下一步。 当你认为任务已完成,直接输出最终结果,不要调用工具。""" async def run_agent(task: str, agent: BrowserAgent, max_steps: int = 20): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task} ] for step in range(max_steps): observation = await agent.observe() messages.append({ "role": "user", "content": f"当前页面状态:\nURL: {observation['url']}\n标题: {observation['title']}\n可见文本: {observation['text']}" }) response = await client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools_list, tool_choice="auto" ) msg = response.choices[0].message if not msg.tool_calls: return msg.content messages.append({ "role": "assistant", "content": msg.content or "", "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments } } for tc in msg.tool_calls ] }) for tc in msg.tool_calls: result = await execute_tool(agent, tc.function.name, tc.function.arguments) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False) }) # 控制上下文窗口,只保留最近 10 轮消息 if len(messages) > 50: messages = messages[:4] + messages[-46:] return "任务在最大步数内未完成"这个循环是 Agent-Reach 的心脏。我不保留完整多轮历史,而是砍掉中间消息,只保留最新的一批。理由是模型决策主要依赖最近几步的上下文,太早的历史记录不但没有帮助,反而会干扰对当前状态的判断。
execute_tool负责分发执行:
async def execute_tool(agent: BrowserAgent, name: str, arguments: str): params = json.loads(arguments) try: if name == "navigate": await agent.navigate(params["url"]) return {"status": "ok", "result": "页面跳转成功"} elif name == "click": await agent.click(params["selector"]) return {"status": "ok", "result": "点击完成"} elif name == "fill": await agent.fill(params["selector"], params["text"]) return {"status": "ok", "result": "输入完成"} elif name == "extract": text = await agent.extract(params["selector"]) return {"status": "ok", "result": text[:500]} else: return {"status": "error", "result": f"未知工具: {name}"} except Exception as e: return {"status": "error", "result": str(e)}异常处理有个重点:工具执行失败的信息不能直接丢回去,要包装成结构化错误返回给模型。这样一来,模型能"看到"报错原因,自己调整策略,而不是整个流程直接崩溃。这也是 Agent-Reach 比传统 RPA 脚本耐用的根本原因——它有自我修正的闭环。
4. 实操记录:让 Agent 自己完成一次网络调研
4.1 任务定义与提示词设计
写一个真实的任务来验证整套流程。我设计了一个网络调研场景:让 Agent 打开搜索引擎,搜索一个关键词,点进第一条自然搜索结果,然后总结页面核心观点。
任务描述文本:
请帮我完成以下调研任务: 1. 打开搜索引擎 https://www.bing.com 2. 搜索关键词"Playwright 2025 更新" 3. 点击第一条自然搜索结果 4. 阅读页面内容,总结出三条核心观点这个任务看起来简单,但涉及跳转、搜索、点击、内容提取、总结,包含了 Agent 需要的基本能力。同时我刻意用 Bing 而不是其他搜索,因为 Bing 页面结构相对稳定,适合验证。
4.2 第一次运行暴露的问题
第一次跑,问题很快就来了。模型在点击搜索框时,传了一个在我看来极其抽象的参数:
{"selector": "搜索框"}执行层收到后直接报错:element not found: 搜索框。这里有一个非常典型的 Agent 问题:模型根据页面文本"看到了"搜索框,但在生成选择器时,把自然语言描述直接当成了 CSS 选择器传回来。模型不理解程序执行层对选择器的语义要求。
这个问题的根源在于工具描述里没有讲清楚:selecter 参数要求的是 CSS 选择器语法,不能是自然语言名称。只靠描述文字约束还不够,模型该犯还是犯。
解决办法是加一条限制性描述,同时在click工具执行时增加选择器格式校验:
def validate_selector(selector: str): # 简单判断是否为合法 CSS 选择器 if " " in selector and not selector.startswith((".", "#", "[")): raise ValueError("参数必须是 CSS 选择器,不能是自然语言描述")加上这层校验后,模型生成参数时会明显更注意格式。实测下来,这类"传了自然语言当参数"的问题减少了七成以上。
4.3 增加"行为推理"后的运行效果
另一个明显改进行为,是在这轮里给模型增加了一个推理步骤。我告诉模型:每次调用工具之前,先输出一行reasoning,说明你为什么做这个决定。这样有两个好处:一是提高决策可解释性,二是倒逼模型去真正思考页面状态和任务进度的关系。
在代码层面,我在消息中要求模型先输出推理,再调用工具:
user_message = f""" 请基于当前页面状态,决定下一步操作。 在调用工具之前,先用一行文字说明你的推理过程。 推理说明不会被执行,不用过长。 当前任务:... 当前页面状态:... """实测下来,带推理后模型的行为模式发生了有趣的变化。它不再机械地点击每个可见元素,而是会主动跳过与本任务无关的内容,有时候甚至会先滚动页面查看底部信息,再决定是否需要点击。步骤次数明显下降,第一次跑用了 12 步,加入推理约束后稳定在 7~8 步左右。
值得注意的是,推理文本也会占用 token。不要让它变成一大段长篇分析,所以我在提示词里限定了"不用过长"。太长反而挤占了后续有效信息的位置,得不偿失。
4.4 加入截止条件后的完整链路
任务顺利跑通后,我加入了一个关键配置:最大步数限制。这听起来基础,但它决定了这个系统是"可控的工具"还是"脱缰的野马"。模型偶尔会在一件事上死磕,比如反复点击同一个元素,或者反复调用搜索函数。如果不加以限制,单次任务可能跑出上百步,既浪费时间也烧 token。
我在调度器里加入了类似的逻辑:
if step >= max_steps: return "已达到最大步数,请基于已获取的信息输出当前结论"这个"温柔截止"的方式比直接强行中断要好。当达到最大步数时,不是抛异常终止,而是通知模型:"时间到了,基于现有信息尽快收尾"。模型会理解这种指令,基于已采集的数据给出合理的摘要,而不是慌乱报错。
经过调整后,这个调研任务可以稳定地一气呵成。搜索、点击、读取、总结、回答,整条链路基本不再需要人工介入。这个结果验证了 Agent-Reach 的设计思路:LLM 负责规划和理解,Playwright 负责执行和感知,两者配合能有效替代原先大量手写的采集脚本。
5. 高频问题与排查技巧实录
5.1 问题速查表
汇总一下 Agent-Reach 开发和测试中遇到的高频问题,供遇到类似情况的人快速对照。
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| JSON 解析报错,工具参数残缺 | 模型生成的参数格式不规范 | 加强 description 约束;增加 schema 校验;对参数做容错解析 |
| 元素定位不到,点击无效 | 页面动态渲染慢,或元素在 iframe 内 | 加等待条件;检查 iframe;启用滚动到可见区域 |
| 页面状态和模型认知脱节 | 模型读到的快照滞后 | 在工具执行后插入短等待;用 networkidle 确保加载完成 |
| 每次任务 Token 消耗过高 | 全文文本和截图塞入过多 | 截断文本到 2000 字符;截图降采样并控制频率 |
| 模型反复执行同一个动作 | 缺少状态记忆,模型不知道"已经做过" | 在观察信息里增加"最近执行动作"摘要 |
| 任务一直收不了尾 | 模型在无限探索页面 | 设置最大步数,到点强制输出结论 |
| 登录态丢失 | 浏览器上下文被重建 | 使用 storage_state 持久化登录状态 |
5.2 定位问题的三个调试利器
AI Agent 项目最头疼的就是"模型为什么这么干"。它不像普通代码,崩溃了可以看堆栈。模型想一出是一出,出错时很难事后追溯。我总结了三个调试方法,组合使用效果非常明显。
第一个是 Playwright 的 Trace Viewer。在浏览器执行启动时开 trace:
await context.trace.start(screenshots=True, snapshots=True)任务运行结束后,Trace Viewer 会展示每一步的页面截图、DOM 快照和网络请求信息,配合模型在每步的推理文本一起看,基本能定位到是模型决策问题还是执行层问题。
第二个是消息日志转储。我的做法是每轮调度后把 messages 数组 dump 成 JSON 文件,保存到本地。出问题时直接翻 JSON,看模型在看到哪个页面状态之后做出了什么样的判断,这条路比 Debugger 还直观。
第三个是最笨但最有效的方式:打开 headless=False 模式,把浏览器窗口显示出来,自己看模型操作。模型点击哪里、输入什么、页面怎么反应,一目了然。调试阶段别省这点资源,视觉反馈带来的信息量远大于任何日志。
5.3 token 成本的三个控制策略
Agent-Reach 跑起来之后,最大的开销往往不是服务器,而是大模型接口费用。每一个步骤都要调用一次模型 API,一次任务十几步,乘以页面文本和历史的 token,费用累积非常快。我实验下来,有三个控制策略性价比最高。
第一,控制页面文本长度。可以通过 DOM 结构过滤,只取main区域或特定区间的文本,减少无关信息进入上下文。这个方法比简单截断效果更好。
第二,截图分离。默认情况下不传截图,只有模型显式要求时传。在工具列表中增加一个see_page_screenshot工具,模型需要从视觉上确认布局时才会调用,这样能显著降低频繁传图的 token 开销。
第三,限定上下文窗口。只保留最近 10~15 轮消息,超出后删除前缀。实测下,模型在没有早期历史信息时,只要能看到当前页面状态和最近几步动作,决策质量并不会有明显下降,但费用能省下 30% 以上。
5.4 安全与副作用控制
让大模型操作浏览器,一定会涉及到一个问题:我凭什么信任它的操作。Agent-Reach 上线前,安全控制是必须设计的。
目前我加了三条防线。第一是操作白名单,只有注册过的函数才能被调用,未注册的操作一律拒绝。第二是 URL 白名单,navigate只能访问配置过的域名集合,防止模型被诱导跳转到不可控的外部地址。第三是危险操作确认,比如涉及提交表单、删除数据、支付、下载附件等操作,执行前强制人工审核。
这三道防线看起来简单,但它们保证了 Agent-Reach 能在真实环境里落地。任务跑完以后,系统还会输出一份完整操作日志,包含所有调用过的工具、参数、结果、耗时,需要时可以直接作为审计依据。
5.5 给模型一个"反悔机制"
最后说一个很多人不会注意,但实用性非常高的设计:给模型一个取消操作的入口。
Agent-Reach 的工具列表中,有一个工具叫report_uncertainty,模型调用它时,表明当前无法完成任务,或者遇到了它不确定是否应该继续执行的情况。调用这个接口后,系统会打印出警告日志,同时把任务状态标为"需人工介入"。
我是在一次失控操作之后意识到需要它的。那次模型读到页面上的一个弹窗,为了关闭它,反复调用了几次点击都不成功,最后差点触发了一个危险按钮。从那以后,我把"承认无法处理"作为一个合法的操作路径提供给模型。让系统在不确定时停下,比让它硬着头皮继续,要安全得多。
6. 还可以往哪个方向扩展
6.1 多 Agent 协作与任务编排
当前 Agent-Reach 是单个 Agent 在驱动整个流程,遇到特别复杂的任务时,模型需要同时兼顾规划、观察、总结,压力比较大。
更稳健的扩展方向是拆分角色:一个 Agent 专管任务规划,负责拆解目标、分派动作;一个 Agent 专管 DOM 分析和信息提取,负责把页面结构解释给规划 Agent 听;再有一个 Agent 专管结果校验,负责判断任务是否真正完成。多个 Agent 之间通过共享上下文协作,避免单一模型的 token 窗口成为瓶颈。我的实际测试是,这种拆分方式在长任务场景下成功率提升非常明显,只是工程复杂度相对更高。
6.2 视觉驱动的操作回退机制
当 CSS 选择器和文本定位全部失效时,Playwright 这条路基本就走不通了。这时候如果项目里接了视觉模型,可以做一个回退机制:把当前页面截图发给视觉模型,让它判断目标元素在页面中的坐标位置,再通过page.mouse.click(x, y)执行点击。
这种思路在处理 Canvas、SVG、复杂图表的时候尤其有用。也可以用这种方式处理那些"看起来像按钮但实际上是个 div 通过 JS 绑定事件"的元素,传统 locator 方式拿它没办法,但视觉模型可以轻松定位。
6.3 让工具协议甩掉浏览器
Agent-Reach 的通用的工具协议,天然适合扩展到浏览器以外的领域。这套"LLM 决策 + 手工实现分发"的骨架,同样可以用来调用 REST API、执行 SQL 查询、操作命令行、发送邮件、读写文件。
也就是说,Agent-Reach 的本质并不是一个浏览器自动化库,而是一个通用的 Agent 工具执行框架。浏览器只是当前最复杂的落点之一,未来可以把所有的系统接口,无论 GUI 还是 API,统一放进同一个工具注册表,让模型在一个框架内调度所有能力。
我在实际使用中最深的一个感受是:这类项目最怕一上来就追求"全自动"。理想很丰满,现实很骨感,第一周跑通率可能连 50% 都不到。但只要你把错误分层处理了、把上下文控制住了、把安全边界划清楚了,成功率会一天一天往上走。Agent-Reach 目前仍然在迭代中,下一步准备把多 Agent 协作调稳,再把工具注册方式改成配置驱动,后续有机会再单独写一篇拆解。