资讯详情

在浏览器里跑一个能自主思考的Agent:架构设计与实战

📅 2026/10/13 8:04:18 | 华诺云谱 👁 阅读
在浏览器里跑一个能自主思考的Agent:架构设计与实战
在浏览器里跑一个能自主思考、调用工具、操作页面的 Agent这种想法我惦记了很久。市面上多数 Agent 都包装在服务端你在网页上敲一行命令后台调大模型模型拿到权限返回结果再吐给你。这套模式的好处是安全可控坏处也很明显——Agent 离真实场景太远它看不到你正在操作的标签页也碰不到浏览器里的登录态、表单数据更别提像真人一样“看见”一个网页再决定下一步动作。Aeon Connect 这个项目就是想换个思路把 Agent 的执行现场直接搬进浏览器让它在页面内部启动、运行、观察、操作整个过程用户全程可见、可干预、可中断。如果你做过浏览器插件、搞过 AI 对话应用或者正想给自己的 RPA 脚本加一个会思考的“大脑”这篇文章会非常适合你。我会从项目拆解、架构设计、代码实现一路讲到实战里踩过的坑尽量把“在浏览器里启动 Agent”这条路线讲透让你读完能直接动手搭一个属于自己的版本。1. 项目拆解与设计思路1.1 Aeon Connect 到底解决什么问题标题拆开看是三个部分Aeon、Connect、在浏览器里。Aeon 是给这个项目起的代号核心含义是“长周期、可持续的会话”Connect 指的是连接通路连接大模型、连接浏览器能力、连接用户的每一次点击和输入在浏览器里则点明了执行环境。过去我们做自动化常用方案是服务端起一个无头浏览器代码操控页面元素。这个方案有两个让人头疼的地方第一无头环境和你本机打开的真实浏览器完全隔离Cookie、登录态、二次验证全都要重来一遍第二页面在后台跑出错了很难调试你只能靠看日志猜发生了什么。所以我把执行环境放回用户当前正在使用的这个浏览器里通过扩展或网页脚本注入一个“执行器”让 Agent 能直接拿到真实的页面上下文。核心价值可以浓缩成三点就近执行、全程可见、轻量接入。就近执行意味着 Agent 和你看到的页面是同一个世界它读取的就是你眼前的 DOM操作后也能立刻看到渲染结果。全程可见意味着每一轮“思考—工具调用—观察结果”的循环都可以在前端面板里实时滚动用户随时能打断。轻量接入意味着不需要部署重型服务端模型请求可以直接从前端发出或者走一个很薄的后端做密钥托管。1.2 为什么选择浏览器作为 Agent 运行时选浏览器做运行时不是因为它“新潮”而是它能同时解决信任和上下文两个关键问题。先说信任。传统 Agent 要操作你的邮箱、网盘、后台系统就必须把账号密码、Token 或者至少是一整套自动化接口交给对方。大多数人不敢这么做因为授权出去之后你根本不知道它在干什么。但在浏览器这个环境里权限是可以按需下放的某一时刻只允许它读取当前标签页的标题下一个动作才申请点击某个按钮每次调用都由浏览器层面拦截确认用户可以撤销某个工具权限而不需要把整个系统密钥暴露给云端。这相当于把“信任边界”从服务器搬回了本地。再说上下文。Agent 最大的软肋之一是缺乏真实世界的感知能力语言模型再强也猜不到当前页面里有什么按钮、什么弹窗、什么报错。浏览器提供了现成的上下文管道DOM 结构、屏幕截图、Console 日志都能被采集并传给模型。我试过一个很直观的实验让 Agent 完成“在这个页面里找到所有输入框并填写测试数据”。如果只用 API 文档描述它需要猜但当我直接把页面截图和 DOM 摘要喂给模型它几乎一次就完成了任务。另外浏览器天然自带一套完整的多任务环境。标签页是独立的隔离域推进一个任务时可以同时挂着另一个任务的现场每个 Agent 实例占用的是独立的执行上下文互不干扰。这样对用户来说Agent 更像是一名可以随时调度的“临时员工”而不是一把只能执行固定脚本的工具。1.3 适用场景与目标用户这个方案的典型场景有四类办公自动化助手、数据采集与整理、前端测试与巡检、个人知识工作流。办公自动化助手里最让我有实感的例子是报销单填写。传统 RPA 脚本写起来繁琐页面一改版就要重写规则换成浏览器 Agent 之后它可以根据你屏幕上的真实表单动态调整策略遇到未知字段会停下来询问而不是报错退出。数据采集场景里它能把自己伪装成正常用户操作动态等待列表加载、翻页、抓取摘要然后把整理后的结构化数据发给后端。前端测试就更直接了Agent 代替人去肉眼检查 UI 缺陷和交互异常甚至可以把截图拼接成操作痕迹记录。目标用户我大概分成三拨一是前端开发者想摸一摸 Agent 落地但不想伺候服务器二是业务分析人员希望通过自然语言去驱动浏览器完成重复劳动三是个人效率工具爱好者想给自动化流程加一层“会思考的粘合层”。这篇文章的代码示例会以 Web 项目为主有一定编程基础的读者上手会更顺但即便你只做过一点脚本也能从架构和排查部分得到启发。2. 核心运行机制与技术选型2.1 一条指令从输入到执行的完整链路一个典型的请求流程大概是这样用户在面板输入指令 → 前端会话模块组装上下文 → 请求送达模型网关 → 模型返回工具调用意图 → 前端分发器解析工具名与参数 → 执行器调用浏览器 API → 返回结构化执行结果 → 结果追加进上下文 → 再次请求模型 → 直到模型输出“任务完成”这条链路看起来平平无奇但有两个关键设计点值得展开讲。第一工具调用是状态机而不是一次性脚本。每一次模型返回的可能是“调用某个工具参数是多少”执行器拿到这个指令后必须以同步方式执行、异步方式返回。前端不能把 Promise 直接丢给模型而是要把它转成一个可序列化的结果对象否则会话无法续接。比如点击一个按钮后要等待 Shadow DOM 内部的内容变化那就得用 MutationObserver 来等待而不是简单 sleep 几秒。第二上下文管理不是简单地把所有历史消息堆给模型。浏览器环境里产生的数据量非常大截屏、DOM 快照、Console 输出动辄几万字。我设计了一套轻量级的压缩策略当上下文超过阈值时把旧的观察结果转成摘要摘要只保留最近三轮的完整工具调用记录。这个策略在实际跑长任务时非常管用否则价格高不说模型的注意力也会被稀释导致它“忘了”最开始的目标。2.2 前端与后端之间的通信协议设计在浏览器方案里通信协议是整个系统的“神经”。我没有选择 REST 轮询而是使用 WebSocket 构建双工通道原因主要有两点一是 Agent 任务通常持续几十秒甚至几分钟期间模型返回是流式的用户需要看到每个 token 和工具调用步骤轮询体验会很差二是后端可能随时推送事件给前端比如模型调用了某个工具需要用户确认授权这种“服务端主动找用户”的场景只有全双工通道能自然覆盖。协议格式参考了 JSON-RPC 的规范每条消息包含{ id: msg_01, type: request, method: agent.step, params: { ... } }回复时返回result或error。这套格式的好处是通用、可扩展、调试直观。我建议不要自创一套“高深”格式维护成本会立刻上来。尤其是当你想在手机浏览器里看任务进度时WebSocket 天然兼容不需要额外适配。为了兼容模型网关不是 WebSocket 协议的情况我在后端做了一层适配器把 HTTP 请求和流式响应包在一个 WebSocket 会话里。对前端来说它永远只面对一条稳定连接模型切换、API 格式变化都被后端隔离掉了。2.3 模型网关与工具注册中心模型网关承担的是密钥安全与多模型路由的职责。Aeon Connect 里密钥永远不落到前端所有大模型请求都从后端转发。这样做既避免了把密钥暴露给浏览器上下文也方便切换模型供应商或部署测速。工具注册中心是另一块重头戏。我维护一个统一注册表每个工具声明自己的名称、描述、参数 Schema、权限等级和执行体。比如registerTool({ name: browser.click, description: 点击页面上的指定元素需要传入选择器, params: { selector: { type: string, required: true } }, permission: user-verification, async execute(ctx, params) { await ctx.waitForSelector(params.selector); await ctx.page.evaluate((sel) { document.querySelector(sel)?.click(); }, params.selector); return { ok: true, selector: params.selector }; } });权限等级决定工具被调用前是否需要用户确认。对于高风险操作比如提交表单、购买、发消息必须弹窗征求同意对于低风险操作比如读取标签页标题可直接执行。这个权限图我会在文件里明确声明模型调用工具时如果权限不足执行器返回错误信息并停止本轮动作避免因误操作引发问题。2.4 为什么不直接用现成的浏览器自动化框架写到这里肯定有人问现在浏览器自动化工具已经很多了为什么还要自己造轮子我的答案很简单现成框架解决的是“脚本化执行”问题我想解决的是“模型驱动下的动态任务”问题二者的执行模型根本不同。传统自动化框架强调选择器的稳定性和执行速度一套脚本写一次跑一百次不变。但模型驱动的 Agent 不一样它每一步都有可能根据页面实际情况调整策略所以核心需求变成了给模型提供足够清晰的“感知”、足够灵活的“动作”、足够安全的“边界”。一个轻量级的执行器反而比全家桶更好用因为全家桶很多能力 Agent 用不上还会拖慢启动速度。当然这不意味着完全从零写。DOM 选择、元素定位这类底层能力我认为可以直接复用成熟方案但策略层、上下文层、协议层一定得自己掌控。只有把握住了这三层后续加多模态输入、升级工具集才会顺利。3. 代码实现与核心环节详解3.1 前端入口从输入框到会话面板前端界面不需要花哨一个输入框 一个消息流就够了。我在项目初期十行代码就完成了核心骨架div idapp div idchat-log/div input idcmd-input placeholder输入你想让 Agent 完成的任务 / button idsend-btn发送/button /div对应的逻辑是点击发送后把指令封装成agent.start请求发到后端同时建立会话记录的界面占位。后续所有模型流式输出、工具调用记录都会按顺序渲染到聊天区域里。每个工具调用步骤渲染成一张卡片上面有工具名、传入参数、返回结果和耗时。这样用户能清晰看到 Agent 到底做了什么而不是只看到最终结论。我在实际写的时候还加了两个小细节体验提升明显一是输入框支持CtrlEnter快速发送二是当 Agent 正在执行时输入框进入锁定状态但允许插入“打断指令”。打断不是终止会话而是把一条新指令作为“干预消息”插入上下文模型在下一轮里会看到这条注入讯息并调整策略。3.2 浏览器执行器的实现要点执行器是 Agent 的“手和眼睛”主要负责三件事感知页面、操作页面、等待变化。感知页面我采取两路并行一路采集可见文本把页面里的标题、按钮文案、表单 label 提取成一个精简文本块另一路生成页面截图转成 base64 或缩略图后传给模型做视觉理解。文本块不宜过大我会用密度扫描算法去掉频率过高的导航词只保留主体区域里的关键文本。截图则一定要压缩原图直接传给视觉模型不仅慢而且贵压缩到 800 像素宽以内的 JPG 就够用了。操作页面统一通过execute方法执行内部再做安全校验和参数修正。比如点击某个元素前先判断它是否在视口内不在则滚动到可视区域填表前先清空已有值再触发input事件因为很多现代前端框架只监听原生事件直接赋值不会生效。这些细节不写进去模型的成功率会大幅度下降。等待变化这一块最容易被忽略。Agent 点击“登录”按钮后页面跳转可能耗时数秒不能立刻返回“点击成功”而要持续观察直到出现新页面的标志元素。我用MutationObserver加定时器双重机制既懒又不漏。方案是观察 body 子树的增删并检查目标节点是否出现超时才返回“未检测到变化”。async function waitForChange(targetSelector, timeout 5000) { const startedAt Date.now(); return new Promise((resolve) { const observer new MutationObserver(() { if (document.querySelector(targetSelector)) { observer.disconnect(); resolve({ changed: true }); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() { observer.disconnect(); resolve({ changed: false, message: timeout }); }, timeout); }); }这里有一个容易踩坑的点MutationObserver的回调触发频率极高如果每次页面微小变化都往模型那边送结果会浪费大量上下文。我处理的方式是做一个 debounce只有连续变化停止后的 500 毫秒内才真正提交结果这样既能看到最终的稳定状态又不会刷屏。3.3 后端调度器与流式响应后端调度器要解决的核心问题是“怎么把模型的长响应过程平滑地推给前端”。我用的是stream模式当模型开始返回内容时切分消息块每块按增量推送。# 伪代码假设是 Python 服务端 async def handle_agent_step(ws, params): conversation build_conversation(params) async for chunk in model_client.stream_chat(conversation): await ws.send_json({ type: model.delta, content: chunk.choices[0].delta.content or })这种流式推送带给用户的感知是“Agent 正在思考”而不是卡死。实测下来除了增加一点编码工作量收益非常大——用户干预的意愿和耐心都更好了。原来要等 5 秒才看到第一个字符现在 0.5 秒就开始出字更像是在跟人聊天。在调度器内部我还维护一个任务队列。同一个会话的模型请求会被严格串行化避免乱序。不同会话之间可以并发但每个会话最多同时运行一个任务。这个限制是为了防止用户在某一步解释后模型还拿着旧上下文继续乱跑造成不可预测状态。3.4 参数选择与计算逻辑这里重点说一下几个会直接影响成败的参数。上下文窗口预算。假设模型窗口是 8000 token那么系统提示词占 1000工具定义占 1500最近的对话记录预留 3000留给页面感知和工具返回的只有 2500 左右。我计算完这些数字之后意识到如果不压缩页面文本一次工具返回就能把预算打爆。所以我的策略是页面文本块限制在 800 字以内截图描述限制在 300 字以内工具返回超过 1000 字符的自动截断并按需保留原文。单步超时。Agent 的每一步操作从模型返回工具调用到执行器完成工具执行通常不应超过 30 秒。我设置了三层超时模型请求 60 秒、工具执行 30 秒、网络请求 15 秒。超过任一阈值调度器会把该步骤标记为失败并要求模型给出新的策略或向用户提问。三层超时的好处是任何故障都有明确的责任方排查问题时分外清楚。重试次数与退避策略。模型输出偶尔会不稳定比如返回了不存在的工具名、参数格式错误、或者干脆说了一段废话。我设计了一个“柔性重试”机制第一遍发现解析失败自动把原始输出附加到消息里要求模型自我修正一次修正仍失败则暂停任务并询问用户是否继续。重试间隔采用指数退避从 1 秒开始每次翻倍最多 8 秒。防止在模型服务不稳定时造成雪崩式重试。4. 常见问题与实战排查4.1 页面元素定位失败的五种原因做浏览器 Agent 的人头号敌人就是 element not found。总结下来无非五种原因第一元素在 Shadow DOM 里普通选择器够不着必须使用closed或open状态下的内部分级定位第二元素存在但不在视口内如果没先滚动点击事件会被浏览器忽略第三元素本身是动态渲染出来的比如弹窗、下拉菜单需要等待出现而不是立即查询第四选择器写得太过具体比如依赖了会被框架自动生成的 class 名称页面一改版就失效第五元素被遮挡比如悬浮按钮盖住了目标点击实际落在了遮罩层上。对策分三路并行先用text与role这类语义化选择器定位再用笨办法遍历所有可点击元素匹配文本包含关系最后实在不行就截屏交给模型让模型直接输出坐标点。模型视觉定位对于复杂布局的容错率很高我甚至在表单填写的场景里尝试过默认优先坐标定位成功率反而比纯 DOM 定位更高。4.2 模型幻觉导致工具参数错误这个问题几乎无法避免只能预防。典型的表现是模型想要点击“确认”按钮但它传出来的 selector 是不存在的选择器或者参数里带上了根本不在工具 Schema 里的字段。我用了两层防线。第一层是工具 Schema 的强校验。在注册工具时明确标出必填项、类型、枚举值执行器在传给模型之前就把 Schema 作为系统提示的一部分并在后端同样做一次 JSON Schema 校验。如果校验不通过直接把校验错误作为工具返回结果丢给模型去修正。第二层是参数预清洗。比如模型传入颜色值是#F00页面只认识#FF0000执行器内部做一次规范转换再比如模型可能把文本2024-12-32这种错误日期填进日期控件我也会通过格式校验来拦截。我更建议在提示词层面做约束明确告诉模型“你可以调用哪些工具不可以调用哪些调用每个工具前必须确认参数名称完全来自 Schema 原文”。实测这套约束是有效的幻觉发生率至少能降低一半。4.3 长任务执行中上下文膨胀长任务跑着跑着最直观的感受是模型越到后面越“傻”甚至忘记了最开始要求它完成的步骤。这本质上是因为上下文里塞了太多旧数据把有效信息稀释掉了。我处理的方式是一个三级摘要机制第一级工具返回结果如果超过阈值立刻原地截断只保留前 200 字和末 100 字。第二级每三轮对话结束后把这三轮的关键决策压缩成一句摘要顶替原始对话位置。第三级整个会话超过 30 轮时生成一份全流程的“项目小结”模型每轮开头都会先读这个小结再读取最近两轮细节。这个机制唯一的问题是摘要本身可能丢失重要细节。我的补救措施是保留一个“关键事实表”专门记录用户明确表达过的硬性约束比如“不要提交实盘订单”“不要修改生产环境配置”这些事实不允许被摘要机制压缩掉永远占据上下文固定预算。4.4 用户打断与安全熔断做浏览器 Agent 最怕失控所以安全机制不是后加的功能而是基础协议的一部分。我在三个层次做了熔断保护。第一层是操作权限。前面提到的高风险工具必须用户确认这一层由浏览器扩展的授权弹窗完成。用户一秒钟点掉Agent 就无法继续。第二层是操作频率。同一工具在 10 秒内最多调用 5 次如果超过我会判定“疑似循环”器自动暂停并输出日志让用户决定是否继续。第三层是全局急停。WebSocket 通道上专门留一个agent.abort方法从前端面板、浏览器插件、甚至手机端都可以远程触发终止。这套熔断机制曾经在一次真实演练里救了我。当时 Agent 在做自动填写表单的任务本来应该填 30 条测试数据结果模型突然把循环条件写错开始无限提交重复内容。如果没有频率限制那几百条垃圾数据就会真实发到测试环境。从那以后我把频率限制值设得更保守了宁可误报暂停也不能放任失控。4.5 多标签页与状态隔离浏览器天然支持多个标签页但也意味着 Agent 需要处理“哪个标签页是当前有效上下文”的问题。我为此设计了标签页注册表Agent 启动时主动声明自己占用哪一个 Tab后续所有操作都限定在该 Tab 内。如果用户切换标签页执行器会把 Agent 挂起切回来才继续。这个设计避免了一件很尴尬的事模型想读取某个表格数据结果拿到的是用户正在看视频的标签页标题。我在早期版本里就吃过这个亏上下文里塞进了一堆与任务无关的页面信息。现在我会要求感知层的每一步先检查activeTabId和目标不一致就直接等待或报错不会把错误的上下文喂给模型。5. 进阶扩展与实践心得5.1 从“单步执行”升级到“规划器 执行器”我在 Aeon Connect 早期版本里用的是最朴素的循环模型返回工具调用执行器执行结果回填。这种做法对短任务没问题但对多步骤长任务来说模型的每一步决策都依赖上一步的观察结果链条一长就变得很脆弱。后来我把结构升级成“规划器 执行器”规划器负责拆解任务产出一个步骤清单执行器每个步骤结束后把执行情况反馈给规划器由规划器决定是继续、调整还是终止。这相当于给 Agent 加了一套“项目管理机制”让模型不用在每一步都重新思考全局而可以把主要算力花在细节执行上。实测下来任务的完成率提升了约三成而且日志也更清晰了。但规划器也有代价。它输出的步骤清单一旦过长同样会占用大量上下文。我的做法是让规划器只输出一个 Markdown 风格的缩略清单每个步骤控制在 20 个字以内具体参数交给执行阶段去补全。5.2 持久化记忆与重启恢复浏览器环境有个天然的劣势刷新一下扩展上下文就没了。为了让 Agent 能跨会话工作我设计了一个持久化层把会话关键信息保存在本地存储中。存储的内容分三类任务目标、执行摘要、完成进度。恢复流程很简单每次启动时读存档如果检测到未完成任务先让推理模块根据存档生成“当前状态说明”再让 Agent 决定是从断点继续还是重新规划。这里有个小技巧存档里不需要存原始 DOM 这么大的数据只需要存“在步骤 3 等待弹窗后的确认”真正恢复时重新感知页面即可。因为页面状态是活的上次的 DOM 快照已经没有意义重要的是“意图上下文”而不是“像素快照”。5.3 给小白的三个起步建议如果你也想在浏览器里跑一个自己的 Agent我建议按三步走。第一步别急着做大而全的平台。先用一个聊天面板 一个执行器 一个模型网关跑通最核心的“用户发指令模型调工具执行器操作页面”这个闭环。能做到这一步你就已经超越大多数只会调 API 的 Demo 了。第二步在闭环稳定之后再补安全体系比如权限确认、频率限制、日志审计这些在早期阶段可以粗糙一点但不能没有。第三步当工具数量超过十几个时再考虑给工具写文档、加 Schema 校验、引入工具自动选择排序。我见过不少项目过早地陷入工具治理结果连最基本的任务都跑不稳。5.4 一些真实体感最后分享几条我做 Aeon Connect 这段时间的真实体感。第一模型是 Agent 的核心但绝不是全部。同样的模型工具设计、上下文管理、错误后处理这三样东西做好成功率可以翻倍。第二浏览器环境的调试又爽又痛。爽在你能直观看到每一步、随时打断痛在某些浏览器 API 的限制和更新会让你半夜爬起来修兼容问题。第三不要把 Agent 设计成“无人驾驶”至少在现在这个阶段给用户保留一个“看得见、拦得住”的入口比让 Agent 自己无限循环要重要得多。我目前还在继续迭代这个项目接下来想做的事是让 Agent 能读复杂图表和多维表格以及把面板从浏览器扩展里摘出来做成一个本地桌面端的悬浮窗。如果你读完这篇文章觉得有启发完全可以拿前面的代码原型去搭一个自己的版本上手跑一遍比看十篇讲解都有效。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑