在接触过不少业务系统、写过不少自动化脚本之后,我越来越确定一件事:真正耐用的Web Agent,核心不在模型选得多强,而在外围的架构怎么搭。模型迭代太快了,今天用的方案,三个月后可能就被新出的模型碾压。但如果架构设计得足够“外挂”、足够“万金油”,那模型换血只是换一个模块的事,业务逻辑、工具调用、上下文管理这些骨架可以原封不动地复用。
这篇笔记想分享的,就是我目前在自用的一套Web Agent架构。它不是某个框架的二次封装,而是我从零开始搭的一套“外挂式”方案。所谓“外挂式”,指的是这套Agent并不需要深度侵入你的业务系统,也不要求在目标网站里埋任何SDK,它就是一个独立运行的旁路系统:通过可观测性去“看”,通过协议去“操作”,通过记忆去“沉淀”。整套东西的核心目标就三个字:不绑定。不绑定具体模型、不绑定具体业务场景、不绑定具体网页结构。
如果你正在做网页自动化、智能客服、RPA替代方案,或者单纯想给自己的业务系统加一个“能自己干活”的智能层,这篇文章应该能给你一些启发。我会把架构设计的思路、核心模块的实现细节、以及我在实际使用中踩过的坑,尽量完整地记录在这里。
1. 内容整体设计与思路拆解
1.1 为什么是“外挂式”,而不是嵌入式
一开始我尝试过嵌入式方案——直接在业务系统里加Agent逻辑,让Agent能直接调用内部函数。但很快发现了几个难以回避的问题:
第一,侵入性太强。业务系统的代码里要揉进Agent调度逻辑、上下文管理、工具注册,代码耦合度直线上升。今天想让Agent多一个能力,就得改业务代码;明天想换一个Agent框架,几乎等于重构。
第二,维护成本太高。业务系统一升级,Agent的逻辑可能就挂了。因为你跟的是业务系统的内部实现,而不是对外暴露的稳定接口。业务代码一改内部函数,Agent那边的调用关系全断。
第三,多系统复用时特别难受。如果你手上有三套系统——一套是内部管理系统、一套是客户门户、一套是数据分析后台——嵌入式方案意味着要在三个系统里分别埋Agent逻辑,同样的代码写三遍,改起来也要改三遍。
所以后来我换了思路:Agent完全独立运行,通过“外挂”的方式与目标网站交互。它不关心目标网站的代码是怎么写的,只关心两件事——能观察到什么(可观测性)、能操作什么(可操作性)。这就好比你在教一个人使用一台仪器,你不需要把这个人装进仪器里,你只需要让他能看到仪表盘、能按到按钮、能拿到读数。这个思路的本质,是把Agent的能力跟业务系统的实现解耦。
1.2 “万金油”式的通用性从哪里来
“万金油”意味着这套架构不能只解决一个特定网站的问题,而是要能快速适配不同类型的Web应用:传统多页应用、现代SPA(单页应用)、复杂表单流程、多标签页协作、需要登录鉴权的后台系统,最好都能覆盖。
要让架构具备这种通用性,核心在于抽象层设计。我在实践里总结出一个三层抽象模型:
- 资源抽象:把网页中的按钮、输入框、下拉框、弹窗、表格统一抽象为“可操作资源”,通过语义化ID和只读DOM结构暴露给Agent。
- 操作抽象:把点击、输入、选择、滚动、等待统一抽象为“原子动作”,配套一个协议层,把Agent的语言指令翻译成真实可执行的浏览器操作。
- 对话抽象:把一次任务拆解为多轮人机协作过程,每轮包含感知(观察当前状态)、决策(确定下一步动作)、执行(调用工具操作页面)、记忆(记录有用信息)四个环节。
这三层抽象一旦建立好,换什么模型、跑什么业务都只是配置问题,而不是架构问题。我在实际使用中也验证了这点:同一套架构,接GPT-4o可以跑,接Claude可以跑,接开源的Qwen-VL也能跑,业务部分完全不用动。
1.3 这套架构解决的三个核心矛盾
第一是“Agent能力”与“系统安全”的矛盾。外挂式设计可以做到完全只读操作生成、写操作需授权,在系统层面加一道安全闸门,而不是让Agent直接在业务系统里裸奔。
第二是“任务复杂”与“过程可控”的矛盾。Agent跑着跑着跑偏了怎么办?外挂式架构允许你在每一步动作前插入人工确认环节,也可以随时打断Agent的决策循环,让系统从“自动驾驶”切回“手动驾驶”。
第三是“一次性任务”与“持续优化”的矛盾。Agent执行完之后产生的轨迹日志、页面状态、决策序列都是可以被重新回放和分析的。做一次任务,留下一次可复用的经验;做十次任务,几乎就沉淀出了一个针对该系统的操作知识库。
2. 核心细节解析与实操要点
2.1 可观测层:如何让Agent“看懂”网页
可观测层是整套架构的地基。Agent再聪明,如果它拿到的页面信息是残缺的、失真的、过时的,那决策就不可能对。这块我的设计思路是:不是直接把整页HTML丢给模型,而是做一次信息蒸馏,提取出一个精简的语义化页面快照。
首先是可见区域的智能识别。我开发了一套轻量级的重点元素提取器,会遍历DOM树,过滤掉脚本标签、样式标签、隐藏元素(display:none 或 visibility:hidden),只保留可见的、可交互的元素。然后给每个元素打上语义化ID,比如btn_submit_order、input_username、link_help_center,这样Agent在决策时不需要面对一长串DOM路径,只需要引用语义化ID。
其次是状态标注。提取器会给元素追加“可交互状态”的描述,例如按钮是可用状态还是禁用状态(disabled属性判断)、输入框是否已预填内容、下拉菜单当前选中了哪一项、弹窗是否在视口内(用getBoundingClientRect()判断)。这样Agent就能感知到“现在页面处于什么状态”,而不是像一个盲人一样在迷雾里瞎猜。
再就是动态区域的增量更新。SPA页面的DOM变化特别频繁,如果每次都给Agent发完整快照,不仅token消耗大,而且信息噪音会稀释关键信息。我用MutationObserver监听DOM变化,只对变化区域生成增量补丁,只有在任务关键节点(比如提交表单、页面跳转、弹窗出现)才发送完整快照。这套策略实测下来,单次决策的token消耗能下降60%以上,而且决策准确率反而因为噪音变少而提升了。
2.2 操作层:如何让Agent“操控”网页
光能看懂还不够,Agent必须能把决策变成真实动作。操作层我用的方案是:通过CDP(Chrome DevTools Protocol)与无头浏览器通信,由Agent生成结构化的操作指令,再通过指令执行器解析并驱动浏览器执行。
操作指令我定义为JSON格式,每个命令包含三个核心字段:action(动作类型)、target(目标元素的语义ID)、params(动作参数)。比如:
{ "action": "click", "target": "btn_submit_order", "params": {} }{ "action": "input", "target": "input_username", "params": {"text": "test_user", "clear_first": true} }{ "action": "select", "target": "ddl_city", "params": {"option_label": "上海", "option_value": "shanghai"} }这里有一个关键的细节:Agent不直接接触DOM元素,而是通过语义ID间接操作。语义ID是操作层的“中间语言”,它解耦了Agent与页面结构的依赖——页面的CSS类名变了、DOM层级变了,只要语义ID没有变化,Agent的操作逻辑就完全不用调整。
在执行策略上,我遵循“先验证后执行”的原则。指令执行器在真实操作之前,会先校验目标元素是否存在、是否可见、是否可交互,校验不通过就返回错误码,让Agent重新决策,而不是盲目执行导致操作落空。
2.3 调度层:多Agent协作怎么组织
单个Agent处理简单的线性任务很顺畅,但遇到复杂任务时,我的经验是拆成多个Agent协作更合理。调度层的设计借鉴了经典的“主从模式”:一个Planner Agent负责全局拆解,多个Worker Agent负责具体执行。
Planner Agent的任务分解逻辑我调了很多轮才稳定。最早是让Planner直接输出步骤列表,但效果不理想,因为任务边界不清晰,子任务之间往往有隐含的依赖关系。后来我改成让Planner先输出一个任务依赖图——每个节点是一个子任务,每条边是依赖关系,然后再根据依赖图生成执行队列。这个改动让复杂任务的完成率提升了一截,因为Agent能知道“哪些步骤可以在填完表单之后并行推进”这件事了。
Worker Agent之间通过共享黑板模式通信。所谓黑板,就是一个共享的信息存储区,每个Worker可以把中间结果、关键信息写到黑板上,后续的Worker从黑板上读取前序结果继续操作。这个模式的好处是解耦:Worker之间不需要直接通信,各自只需要面向黑板工作,新增一个Worker只需要约定好黑板上的读写格式,非常利于横向扩展。
调度器本身还内置了失败重试和降级策略:某个Worker如果连续失败三次,调度器会根据失败原因判断是换一种策略重试,还是直接将子任务回退给Planner重新规划。
3. 实操过程与核心环节实现
3.1 整体代码架构怎么组织
我习惯把整套系统的代码分成几个清晰独立的模块,工程结构大概是这样的:
web-agent/ ├── agent_core/ # Agent核心逻辑(感知、决策、执行、记忆循环) ├── tools/ # 工具集(浏览器操作、页面解析、数据提取) ├── memory/ # 记忆系统(短期上下文、长期向量记忆) ├── scheduler/ # 任务调度器(任务分解、worker协作、重试降级) ├── protocol/ # 指令协议层(JSON指令定义、校验器) ├── adapters/ # 模型适配层(兼容不同LLM接口) ├── browser/ # 浏览器控制层(CDP封装、无头浏览器管理) └── dashboard/ # 可视化控制台(任务监控、人工干预入口)模块边界我用了一个“强制依赖方向”的规则:tools → protocol → agent_core → scheduler。也就是说tools只依赖protocol定义的数据结构,不依赖agent_core;agent_core只调用tools的接口,但不了解tools内部的具体实现。这样的依赖方向保证了一个关键属性——替换任何一个底层模块都不会影响上层模块。
3.2 Agent Runtime:感知-决策-执行的核心循环
Agent Runtime是整个系统的引擎室,核心是一个循环,这个循环每一轮都要回答一个问题:当前状态下,下一步做什么。
循环的第一步是“感知”。感知模块从可观测层拿到当前页面的状态快照,并结合记忆模块提供的上下文信息(比如这个任务已经进行到哪一步了、之前在某个页面遇到过什么异常),组装成一个结构化的状态描述。
第二步是“决策”。决策模块把状态描述、任务目标、可选工具列表一起塞给LLM,让模型输出一个结构化指令。这里我把决策拆成了两个子阶段:先用一个轻量级模型做意图分类(判断当前需要哪种能力:填表?点按钮?查数据?还是需要外部API?),再把完整状态交给重量级模型做细粒度决策。这个两阶段设计在成本控制上效果很明显,大约有三成左右的决策轮次只需要走轻量级模型就够了。
第三步是“执行”。执行模块拿到LLM生成的指令,先做格式校验和参数合法性检查(防止模型生成幻觉参数),检查通过后交给底层浏览器控制层执行。执行完会把结果反馈给感知模块——页面变了,开始下一轮循环。
第四步是“记忆”。每一轮循环结束时,系统会把这一轮的关键信息(页面状态摘要、决策指令、执行结果)压缩成一条结构化记忆写入短期上下文;当检测到任务里程碑时(比如成功提交了一条订单),还会把这条经验总结后写入长期记忆。
这个循环的执行节奏我调成了一秒级间隔:每轮循环结束后系统会根据当前任务的进度评估——任务是否接近完成、是否需要继续下一步、还是需要停下来等待人工确认。节奏太快容易产生无效轮次,浪费token;节奏太慢又会拉长整体完成时间。1秒的间隔在绝大多数网页场景下已经足够从容。
3.3 关键实测:同一套架构适配三个不同类型的系统
为了验证“万金油”这个说法不是自嗨,我拿这套架构实际跑了三个完全不同的系统。
第一个是传统的后台管理系统,页面是服务端渲染的多页应用,靠表单提交完成数据流转。这类系统DOM结构稳定,但对操作时序要求严格——表单没加载完就提交是典型的失败场景。这套架构接入使用的方案是“预检指令”:每次表单操作前,先发一个“waitFor”指令,等待关键表单控件全部处于可交互状态,再继续后续操作。实测下来,原本人工操作需要5分钟的表单录入流程,Agent跑完约1分20秒,且连续跑20次没有出现一次时序竞态错误。
第二个是现代的SPA单页应用,用的是Vue框架,页面大量依赖动态渲染。第一次接入时,Agent总是点击不到动态加载的按钮,因为按钮在DOM里还没挂载完,语义ID提取器根本找不到目标。后来我在可观测层加了“元素等待补偿逻辑”:当提取器发现某个预期的语义ID缺失时,会先轮询等待2秒,并重新提取一次快照;如果二次提取还是找不到,才把“元素缺失”作为状态上报给决策层。这个补偿逻辑基本解决了SPA的动态加载问题。
第三个是一个第三方SaaS系统,登录需要短信验证码。这里我没让Agent直接处理验证码,而是设计了一个“人工介入点”:Agent检测到验证码输入框出现时,会暂停任务流转,通过控制台推送通知给人工,人工输入验证码后按“继续”键,Agent自动从暂停点接着往下执行。这个功能看起来简单,但在实际部署中极其重要——因为你永远不知道验证码会以什么形式出现,短信、邮件、扫码、滑块,与其训练Agent适配每一种,不如设计好暂停与恢复机制,让人在必要的节点做一次低成本干预。
三次测试下来,我比较满意的不是Agent“什么都能干”,而是接入成本确实降到了一个合理的水平——三个系统从零到跑通首条任务,平均耗时都在一天以内。这个成绩的关键,正是“外挂式”架构带来的:不需要改目标系统任何一行代码,只需要约定语义ID的提取规则和操作协议即可。
4. 常见问题与排查技巧实录
这套架构从初版跑通到现在,踩过的坑不少。我挑几个典型的问题,连同排查思路一起记录下来,希望能帮你少走弯路。
4.1 Agent“看错”了页面,操作落空
最早遇到的频率最高的问题,就是Agent明明决策了一个操作,但执行层反馈“目标元素不存在”。排查之后发现,绝大多数情况不是模型的问题,而是可观测层给模型的信息是过期的——Agent决策所依赖的页面快照,跟它决策之后实际执行时的真实DOM状态已经不一致了。尤其是网络慢的页面,一个简单的按钮点击可能触发了路由跳转,等到下一个指令执行时页面已经换了。
这个问题的根治办法是“指令-快照绑定”:每次执行指令前,执行层都会重新检查目标元素是否存在,如果发现元素状态与Agent决策时看到的快照不一致,就把最新的状态反馈给Agent,强制Agent基于最新状态重新决策,而不是盲目执行一个基于旧状态生成的指令。
4.2 模型开始“幻觉”,生成不存在的操作指令
LLM在长任务中容易产生幻觉,尤其是上下文被压缩过之后,模型可能会“记错”之前的操作状态,从而生成一个跟当前状态完全不匹配的操作指令。比如表单已经提交成功了,模型还在尝试输入表单内容。
处理这个问题,我建议在决策环节对模型的输出做约束:首先,决策模块不直接输出操作指令,而是先输出一个“状态确认字段”,让模型先描述它认为的当前页面状态,再生成下一步指令;其次,操作指令生成后必须经过“前置校验器”,校验器会检查指令中的目标元素是否在最新的语义索引中存在、操作参数是否符合元素类型。这两道关卡叠加下来,幻觉指令的触发率能降低到之前的十分之一以下。
4.3 长任务跑着跑着,上下文爆炸或者遗忘
Agent跑三四十分钟的长任务时,上下文管理是最大的挑战。如果每一轮循环都把完整页面快照塞进上下文,很快上下文就满了;但如果不塞,Agent又会遗忘早期的关键信息(比如任务开始时用户输入的约束条件)。
我的实践经验是用三级记忆架构:第一级是“工作记忆”,只保留最近五轮的完整操作记录,保证连贯性;第二级是“里程碑记忆”,当检测到任务有重要进展时(成功新增了一条数据、成功通过了一项校验),把这段经验提炼成摘要写入长期记忆;第三级是“核心约束记忆”,在任务开始时就把用户的硬性要求(比如“不要删除任何数据”“只能用管理员账户操作”)写入一个固定区域,每轮循环都会作为前缀重新注入。
4.4 排查技巧整理
我把这些排查经验整理成了一张速查表,方便在实际使用中快速定位问题:
| 问题现象 | 可能原因 | 排查思路 | 解决措施 |
|---|---|---|---|
| 操作落空,元素找不到 | 页面快照与真实DOM不一致 | 检查执行层是否做了元素预检 | 启用指令-快照绑定机制 |
| 指令报参数非法 | 模型生成了幻觉参数 | 在决策输出后增加参数校验 | 严格校验每类动作的必填参数和值域 |
| 任务中途上下文溢出 | 页面快照写入太多 | 统计每轮上下文的token消耗 | 开启增量更新,只发送变化区域 |
| Agent反复执行相同动作 | 状态感知里缺少“结果确认”环节 | 检查上一动作执行后是否更新了状态 | 执行完成后必须确认DOM变化再进入下轮 |
| 长任务早期信息遗忘 | 上下文压缩策略不合理 | 检查里程碑记忆是否正常写入 | 使用三级记忆架构保持核心约束 |
| 页面动态加载导致首次提取失败 | 语义ID提取器提取过快 | 确认是否开启了元素等待补偿逻辑 | 开启轮询等待并二次提取快照 |
4.5 我在实际使用中的几个贴心建议
最后分享几个不在架构图里的、但实战中特别有用的小经验。
第一,Agent的浏览器环境尽量保持干净。我在跑任务之前会自动清理Cookie、LocalStorage、IndexedDB,避免上一次任务的残留状态干扰下一次任务。别小看这一步,很多诡异的前后依赖问题其实都源于状态污染。
第二,人工干预入口一定要做得足够显眼。无论你的Agent多智能,总会有需要人拍板的瞬间。我在控制台上做了一个“任务心跳”设计:如果Agent连续三次决策都没有推进任务进度(比如一直在同一个页面打转),控制台会把任务挂起,弹出一条人工介入请求。刚开始我担心这会增加人工负担,实际跑下来发现这个设计反而大大提升了人工对Agent的信任度——因为你知道系统在关键节点会请你确认,而不是自己横冲直撞。
第三,操作日志要带“重放功能”。我把Agent每轮的页面快照、决策指令、执行结果全部落盘存储,并且可以在控制台像播放录像一样重放整个操作过程。这个功能在调试时帮了大忙——不用靠猜,直接回放日志,一眼就能看出Agent在哪一步出了问题。
5. 这套架构后续还能怎么扩展
写到这里,我自己回头看了一下这套架构的成长路径:第一版只能做一个站点的简单点击任务,第二版能跑复杂表单流程,第三版能适配多系统协作,到现在已经成了一个能独立承接“交钥匙任务”的通用底座。这个过程里变化最大的,其实不是模型能力的提升,而是外围架构对模型能力的利用效率。
后续我还计划在这套架构上做三件事。第一件事是引入主动校验机制,让Agent在关键操作(提交、删除、支付)之后主动向界面“提问”——通过计算机视觉或者DOM状态对比,确认操作是否真的生效了,而不是只关注操作本身是否执行成功。第二件事是前端新增一种“策略热更新”能力,让语义ID的提取规则可以动态下发,这样遇到页面改版时,运维人员只改配置、不动代码,就能让Agent继续工作。第三件事是尝试把知识沉淀做得更强一些,让Agent能真正从过去的失败操作中学习,自动总结出“这类表单不要直接回车提交”之类的隐性经验,而不是每次都在同一个地方栽跟头。
根据我多次从零搭Agent架构的经验,有一个体会特别深:不要一上来就追求模型最强、功能最全,先跑通最小闭环,再逐步加模块。因为Agent系统的复杂度是指数上升的,一次加太多模块,出了问题你根本不知道是模型决策问题还是架构问题。先让一个最简单的“看-想-做”循环跑通,再往上面叠加记忆、调度、协作、校验,每加一层都回归一遍基准任务,这套节奏会让你省下很多排查时间。这套“外挂式”架构对我个人来说目前足够顺手,也希望这篇笔记里的思路和细节,能给你在构建自己的Web Agent时提供一些参考。