
用途AI Agent 工程方向项目解析博客代码级 Agent runtime 深度拆解原则讲为什么这样设计结论锚定真实源码browser-use 仓库类名/行号可查主角是 browser-use 本身仓库browser-use/browser-use本文基于browser-use-main源码定位接续 LangGraph、OpenHands从「Workflow Agent → Coding Agent」走向「Browser Agent」第三种架构视角零、为什么看完 LangGraph 和 OpenHands要来看 browser-use前两篇项目解析我们收获了两个视角LangGraph任务怎么编排State / Node / Edge / Checkpoint / HITL。OpenHandsAgent 怎么和真实世界交互Agent / Event / Workspace / Security / Sandbox。但这两个项目有一个共同点它们的执行环境里网页是一个黑盒。OpenHands 能用 CDP 拉起浏览器、能用 Playwright 截图但它不关心这张网页长什么样、哪里可以点——它把浏览器当成一个普通的执行工具用坐标或 API 去操作。而 browser-use 问了一个更本质的问题Agent 要像人一样用浏览器它得先看懂网页还得能点网页——这件事架构上到底怎么做这就是第三种完全不同的视角。如果说 LangGraph 回答任务怎么编排、OpenHands 回答Agent 怎么执行真实动作那么 browser-use 回答的是Agent 怎么感知一个高动态、超复杂的真实世界界面网页并在上面做出精确操作。browser-use 是当前最火的浏览器自动化 Agent 库之一GitHub 数万 star自己的测评 Odessey 榜上 87.4% 均值压过 OpenAI / Anthropic / Google / Microsoft 的 computer-use 方案。但它真正值得学习的不是它能操作浏览器这个结果而是它为了解决LLM 看懂网页这件事造出的那一整套工程机制。这一篇就拆这套机制。一、项目定位它解决什么工程问题解决什么让 LLM 驱动的 Agent 像人一样使用浏览器——打开页面、点击、输入、填表、滚动、提取数据、多标签页切换完成任务。为什么难网页不是给机器看的。它是一棵动辄几千节点的 DOM 树其中绝大多数节点对完成任务毫无意义而真正可点击的元素又被 CSS、脚本、shadow DOM、iframe 层层包裹。直接把整个 DOM 或截图丢给 LLM上下文瞬间爆炸模型也分不清哪个元素是哪个。两条技术路线一类是 OpenAI / Anthropic 的computer-use视觉路线——给模型看截图让模型输出坐标去点。browser-use 走的是另一条——DOM 文本化路线把网页可交互的部分压缩成一段带索引的文本让模型输出我要点[17]而不是我要点坐标 (312, 88)。项目一句话介绍browser-use 是一个浏览器自动化 Agent 框架核心是把真实网页 DOM 序列化成带索引的可操作文本让 LLM 通过文本感知 结构化动作输出 事件驱动执行完成浏览器任务而不是靠截图猜坐标。它和 computer-use 路线之争是这篇赏析最值得品味的背景——后文会看到DOM 文本化带来的是精确、可回放、上下文可控代价是实现复杂度极高要处理树、布局、遮挡、语义、跨 iframe 一堆脏活。二、先记住 browser-use 最重要的循环它依然是一个 reasoning-action loop但循环的两个端点都是浏览器和 ChatBot 的本质区别在这里变成了一件事LLM 看到的不是你发的消息而是一张实时的、经过精心压缩的网页棋盘。每一步棋盘都会因为上一次操作而更新。三、核心数据流一个任务从进来到结束拿 README 里的经典例子找到 browser-use 这个 repo 的 star 数走一遍完整链路用户: agent Agent(task找到 browser-use repo 的 star 数, llm...) await agent.run(max_steps50) ──────────────────────────────────────────────────────────────── Agent.run() agent/service.py:2506 ├─ 启动浏览器browser_session.start() │ └─ LocalBrowserWatchdog 拉起 Chrome --remote-debugging-port ├─ 初始动作_extract_start_url(task) 提取出 github.com/browser-use/... │ → navigate → CDP 导航 → 页面加载完成 └─ while n_steps max_steps: agent/service.py:2603 └─ _execute_step() agent/service.py:2441 └─ step() agent/service.py:1029 ├─ [1] _prepare_context() agent/service.py:1081 │ ├─ get_browser_state_summary() browser/session.py:1587 │ │ └─ 派发 BrowserStateRequestEvent │ │ └─ DOMWatchdog 响应 browser/watchdogs/dom_watchdog.py │ │ ├─ DomService.get_serialized_dom_tree() dom/service.py │ │ │ ├─ 抓 4 路原始数据DOMSnapshot/DOM/AX/JS监听 │ │ │ └─ DOMTreeSerializer.serialize_accessible_elements() │ │ │ → SerializedDOMState(_root, selector_map) │ │ └─ take_screenshot() │ └─ MessageManager 组装成【一条】状态消息 │ → browser_state[1]a...[17]button.../browser_state ├─ [2] _get_next_action() agent/service.py:1170 │ └─ llm.ainvoke(messages, output_formatAgentOutput) │ → 输出 AgentOutput(action[click(index17)]) ├─ [2] _execute_actions() agent/service.py:1205 │ └─ multi_act() agent/service.py:2733 │ → tools.act() → registry.execute_action() │ → click(index17) │ ├─ selector_map[17] 查到 backendNodeId │ ├─ 发 ClickElementEvent → DefaultActionWatchdog │ └─ CDP Input.dispatchMouseEvent → 真的点下去 └─ [3] _post_process() agent/service.py:1213 └─ _finalize() agent/service.py:1350 └─ history.add_item(AgentHistory)model_output result 状态 截图 └─ 直到 LLM 输出 done(successTrue) → 结束 Agent.run() finally: ├─ token_cost_service 汇总 usage / cost → history.usage └─ close() → 关闭浏览器 / CDP注意这条链路上两个派发事件的节点——状态的获取和执行动作都不是直接函数调用而是通过事件总线派发由 watchdog 响应。这是后文的核心闪光点之一。四、闪光点一DOM 序列化——把网页变成 LLM 的棋盘招牌这是 browser-use 最核心的工程创造也是它区别于 computer-use 路线的地方。问题网页对 LLM 来说是天书一个真实网页的 DOM 可能有上万节点其中 99% 不可点击、不可见、对任务毫无意义。直接丢给 LLM要么上下文爆炸要么模型在海量噪音里找不到该点哪个。方案六步流水线压缩成带索引的可操作文本DOMTreeSerializer.serialize_accessible_elements()dom/serializer/serializer.py:110的源码本身就是一篇很好的工程文章async def serialize_accessible_elements(self) - tuple[SerializedDOMState, dict]: # Reset state self._interactive_counter 1 self._selector_map {} # Step 1: Create simplified tree (includes clickable element detection) simplified_tree self._create_simplified_tree(self.root_node) # Step 2: Remove elements based on paint order去掉被遮挡的元素 if self.paint_order_filtering and simplified_tree: PaintOrderRemover(simplified_tree).calculate_paint_order() # Step 3: Optimize tree (remove unnecessary parents)剪掉无用父节点 optimized_tree self._optimize_tree(simplified_tree) # Step 3: Apply bounding box filtering处理嵌套可点容器 filtered_tree self._apply_bounding_box_filtering(optimized_tree) # Step 4: Assign interactive indices to clickable elements self._assign_interactive_indices_and_mark_new_nodes(filtered_tree) return SerializedDOMState(_rootfiltered_tree, selector_mapself._selector_map), self.timing_info最终产出的文本长这样serializer.py的serialize_tree()静态方法[Start of page] [1]a href/browser-use/browser-use browser-use / browser-use /a |scroll element[9]div rolecombobox [15]input typetext placeholderSearch... compound_components(rolecombobox,count4,optionsA|B|C|D) [End of page]最巧的设计[N]索引 ↔ backendNodeId 的双向映射注意那个[17]——它同时是两样东西给 LLM 看的文本标记 我要点击 [17] 给系统用的执行句柄 selector_map[17] → EnhancedDOMTreeNode → backendNodeId → CDP Input.dispatchMouseEvent(x, y) 真的点下去这就是文本 ↔ 可操作的闭环SerializedDOMState同时携带两份东西——_root给 LLM/渲染看的简化树和selector_map给执行层用的索引表。一次序列化同时服务理解与行动。LLM 不需要知道坐标、不需要知道 DOM id它只需要说点 [17]剩下的系统全部搞定。还有两个细节值得注意is_new标记*[N]序列化时和上一轮的selector_map对比页面新增的元素标上*提示 LLM这轮页面变了出现了新东西——这是观察页面变化的廉价实现。只在可见且可点击的元素上编号不可见、不可点击的元素根本不会进编号池从源头砍掉上下文噪音。五、闪光点二三路数据融合的 DOM 快照哪些元素可点击这个问题比想象中难得多。因为网页有四种信息源单独任何一个都不够数据源提供什么单独用的问题DOMSnapshot.captureSnapshot布局 bounds、computed styles不知道哪些有语义、可交互DOM.getDocument节点层级、shadow DOM、iframe纯结构无语义Accessibility.getFullAXTree无障碍名称可点击语义的重要来源会漏掉 JS 绑定的点击JS 注入getEventListeners()谁挂了 click/mousedown 监听覆盖不全还有 role/tag 判断DomService._get_all_trees()dom/service.py:403一次会话内并发拿这四路数据再按 backendNodeId 融合成一颗EnhancedDOMTreeNodedom/service.py:703支持 iframe / shadow DOM / 跨源 iframe。这个设计解决了框架渲染的 div 没有语义这个老大难一个div onclick...AX 树可能不认为它可点击但 JS 监听器探测会抓到它一个buttonAX 树会给它语义名但 bounds 可能被遮挡。只有融合才能既不漏、也不错。一句话总结可点击性不是一个布尔值而是一个需要四种证据交叉验证的判断。六、闪光点三事件驱动 watchdog 插件架构这是 browser-use 浏览器层最优雅的设计和 OpenHands 的 Event System 有异曲同工之妙但实现得更轻。问题浏览器层有十几个横切关注点拉浏览器、采 DOM、执行点击、监控崩溃、处理验证码、跟踪下载、关弹窗、持久化 cookie、录屏、抓 HAR、防 CSRF……如果每个都塞进BrowserSession类这个类会膨胀到不可维护现在session.py已经 4133 行了再塞会更糟。方案事件总线 单一职责 watchdog核心抽象是BaseWatchdogbrowser/watchdog_base.py:15它有一个非常巧的注册机制——用方法名反射自动订阅事件for method_name in dir(self): if method_name.startswith(on_) and callable(getattr(self, method_name)): event_name method_name[3:] # 去掉 on_ 前缀 event_class event_classes[event_name] self.attach_handler_to_session(self.browser_session, event_class, handler)也就是说一个 watchdog 里写on_ClickElementEvent()它就自动成为点击事件的响应者。于是每个关注点一个文件职责单一Watchdog职责LocalBrowserWatchdog启动/关闭本机 ChromeDOMWatchdog响应BrowserStateRequestEvent建树 序列化 截图DefaultActionWatchdog执行 click/type/scroll 等真实 CDP 交互CrashWatchdog监控 target crash触发重连CaptchaWatchdog等验证码求解完成DownloadsWatchdog自动下载跟踪PopupsWatchdog自动关 JS 弹窗SecurityWatchdog域名白名单安全限制StorageStateWatchdogcookie/localStorage 持久化而且BaseWatchdog会给每个 handler 统一注入熔断逻辑CDP WebSocket 断开时跳过、自动重连等待、异常后重建 CDP 会话。这意味着每个功能天然健壮是框架免费送的而不是每个 watchdog 自己写。一句话总结动作执行的路径是工具层 → 事件 → watchdog → CDP横切关注点被组织成一排可独立增删的插件。七、闪光点四动态判别联合 ActionModel——每加一个工具LLM 的 schema 自动更新browser-use 的动作输出走的是强类型结构化输出而不是让 LLM 自由生成 JSON。关键在Registry.create_action_model()tools/registry/service.py:517它用pydantic.create_model运行时动态生成一个判别联合ActionModel把当前所有已注册的工具内置的 用户自定义的 页面特定的 skills合并成一个类型class AgentOutput(BaseModel): # agent/views.py:388 thinking: str | None None evaluation_previous_goal: str | None None memory: str | None None next_goal: str | None None current_plan_item: int | None None plan_update: list[str] | None None action: list[ActionModel] Field(..., json_schema_extra{min_items: 1})然后AgentOutput作为output_format传给llm.ainvoke(...)各家模型走结构化输出OpenAI 用response_formatjson_schema。这个设计的收益是惊人的每新增一个工具LLM 能输出的动作类型就自动多一种零手工同步。你registry.action(...)注册一个upload_to_s3下一次 LLM 的 JSON schema 里就有upload_to_s3这个选项了。配套的还有工具注册的两个细节特殊参数注入_normalize_action_function_signature()registry/service.py:75用inspect.signature把动作函数拆成用户参数和特殊注入参数——browser_session、page_url、cdp_client、page_extraction_llm、file_system、sensitive_data会自动注入动作函数签名里写了就给你不用手动传。terminates_sequence标记 页面变更双保险navigate/search/go_back 这些动作会让页面大变标记后自动截断剩余动作序列另外multi_act()还会动态检测 URL/focus 变化来截断service.py:2815-2831——防止 LLM 一口气输出一串动作执行到一半页面变了后面的动作全失效。八、闪光点五上下文经济——每一步的 token 都被精确算计浏览器 Agent 是 Token 消耗大户每步都要带网页文本 截图。browser-use 的上下文管理几乎把能省的都省了单状态消息不是追加MessageManager维护三类消息槽agent/message_manager/service.py:104其中state_message只有一个——每步覆盖式替换而不是把历史追加进对话。历史被压缩成agent_history文本块并按max_history_items截断保留首条 最近 N 条。历史压缩compaction长会话用maybe_compact_messages()service.py:216让 LLM 自己把旧历史总结成compacted_memory。前缀缓存状态消息标cacheTrue吃 OpenAI 前缀缓存重复前缀不重复计费。精确截断max_clickable_elements_length40000限制网页文本URL 会被缩短截图use_visionauto只在需要时带图。Token 成本追踪tokens/service.py的TokenCost直接包装llm.ainvoke每次调用后记录 usage定价数据远程拉取 本地缓存run()结束输出完整成本账单。一句话总结让一个每步都要看网页的 Agent 跑 50 步不爆上下文、不烧穿预算不是靠运气是靠一整套上下文经济学。九、闪光点六工具系统 Registry 双层架构browser-use 的历史包袱很有意思早期有个Controller类browser_use/controller/__init__.py现在它只剩 75 字节——一个兼容别名from browser_use.tools.service import Controller __all__ [Controller]旧的 Controller 已被ToolsRegistry双层取代Registrytools/registry/service.py:33通用注册中心。registry.action(...)装饰器注册动作execute_action()统一执行create_action_model()生成判别联合get_prompt_description()按域名过滤生成工具描述文本。Toolstools/service.py:441预置的浏览器动作集合 对外入口。内置search/navigate/go_back/wait/click/input/upload_file/switch/close/extract/scroll/screenshot/done等二十多个动作。用户自定义工具极其简单README 官方示例from browser_use import Tools tools Tools() tools.action(descriptionDescription of what this tool does.) def custom_tool(param: str) - str: return fResult: {param}一个装饰器注册、schema 生成、LLM 可见、可执行全齐了。这就是第七节动态判别联合的收益在用户侧的体现。十、底层技术选型不是 Playwright是 CDP 直连一个反直觉的事实browser-use不用 Playwright而是直接基于 CDPChrome DevTools Protocol和事件总线库bubus# browser/session.py:15-18 from cdp_use import CDPClient from cdp_use.cdp.fetch import AuthRequiredEvent, RequestPausedEvent from bubus import BaseEvent, EventBusBrowserSessionsession.py:134负责连接管理connect/reconnect/_auto_reconnect、CDP 会话管理每个 tab/iframe 一个 session由SessionManager维护映射、页面操作navigate_to/take_screenshot/get_element_by_index、状态聚合get_browser_state_summary。为什么选 CDP 而不是 Playwright从源码看理由是控制和可观测性CDP 让你拿到DOMSnapshot、Accessibility.getFullAXTree、getEventListeners这些 Playwright 抽象不暴露的原始数据——而第五节的四路数据融合恰恰需要这些。这是为了核心创新敢于选更底层的基础设施的典型案例。注browser_use/actor/目录里其实还藏着一个Playwright 替代品——底层 CDP 自动化库提供Page/Element/Mouse给高级用户和 Agent 内部复用。十一、LLM 抽象层为什么有十几个 providerbrowser_use/llm/下有 openai/anthropic/google/deepseek/ollama/groq/mistral/cerebras/aws/azure/litellm/openrouter/vercel/browser_use 十几个子目录每个都有chat.py和serializer.py。核心是一个 Protocolllm/base.py:18runtime_checkable class BaseChatModel(Protocol): model: str async def ainvoke(self, messages, output_format: type[T] | None None, **kwargs) - ChatInvokeCompletion[T]: ...统一返回ChatInvokeCompletion[T]completion usage stop_reason。Agent 完全不关心底层是哪个厂商。为什么每家一个适配器因为每家的方言不同消息结构、结构化输出方式OpenAI 用response_formatjson_schema、Anthropic 用 tool forcing、Google 用google-genai、推理参数都不同。而第七节的结构化输出是 browser-use 的命根子所以每家都必须把AgentOutput的 schema 正确翻译成自家格式——这个适配成本只能每个 provider 单独付。这是抽象层值得做的正面案例因为 Agent 核心强依赖结构化输出所以 provider 适配器不是可有可无的装饰而是核心链路的必要一环。十二、三个不足成熟项目 商业公司的代价赏析不能只说好话。browser-use 有三个很实在的不足而且它们暴露了开源库 云商业公司双重身份下的张力。不足一历史包袱和兼容层很多controller/只剩一个兼容别名actor/是一个独立的 CDP 自动化库和browser/层职责重叠sync/把事件流同步到云端beta/是试验代码。一个 4166 行的agent/service.py、4133 行的browser/session.py——功能复杂度最终变成了架构复杂度这和 OpenHands 一样是成熟项目的宿命。不足二云服务耦合开始渗入开源代码ChatBrowserUse是一个闭源托管模型入口browser_use/sandbox/的sandbox装饰器会把你的函数cloudpickle 后丢到 browser-use 云端执行sync/默认把事件同步到云端平台。对于想完全离线的用户这些是藏在开源代码里的云依赖需要额外注意。不足三对个人学习者认知门槛高DOM 序列化四路融合 paint order bbox 索引编号、事件总线 watchdog、动态判别联合、上下文经济学——每一个单拎出来都是不小的概念。如果只是想用浏览器自动化的能力直接用它是最快的但如果想读懂它学习曲线比 LangGraph 和 OpenHands 都陡。十三、和 LangGraph / OpenHands 的对比三种视角补完这是这次赏析最重要的一个位置。三个项目恰好覆盖了 Agent 工程的三层LangGraph 任务怎么编排 State / Node / Edge / Checkpoint / HITL OpenHands Agent 怎么执行真实任务 Agent / Event / Workspace / Security / Sandbox browser-use Agent 怎么操作真实界面 DOM 序列化 / EventWatchdog / 结构化动作维度LangGraphOpenHandsbrowser-use核心问题多步任务怎么组织Agent 怎么动手Agent 怎么看懂并操作界面状态方案类型化 Channel Checkpoint事件历史Stateless Agent单状态消息 历史压缩事件非核心核心公共语言核心watchdog 插件和 LLM 交互普通调用普通调用强结构化输出判别联合环境感知无文件/Shell/浏览器粗粒度DOM 级细粒度感知招牌检查点 HITLRuntime 抽象 SecurityDOM 序列化 索引闭环关键洞察browser-use 补上了前三者都刻意忽略的一环——感知。LangGraph 和 OpenHands 把看东西交给你的代码去实现而 browser-use 把看懂网页本身做成了一门工程。这就是为什么它是研究 Agent 感知层最好的范本。十四、源码阅读路线不要从头读到尾第一层 Agent 循环agent/service.py的step():1029——先看懂三阶段prepare → act → post-process。第二层 感知dom/service.pydom/serializer/serializer.py——这是全项目最该抠透的核心六步流水线 索引闭环。第三层 动作tools/registry/service.py注册/校验/判别联合tools/service.py内置动作。第四层 执行browser/session.pybrowser/watchdogs/default_action_watchdog.py事件 → CDP 的真实交互。第五层 事件与 watchdogbrowser/watchdog_base.py反射注册 熔断包装——理解横切关注点怎么组织。第六层 上下文经济agent/message_manager/tokens/service.py——看它怎么让长任务跑得下去。最后actor/底层 CDP 库、mcp/、sync/、sandbox/——看生态层但别被它们带偏。十五、工程启示从 browser-use 能学到什么10 点感知是一等工程Agent 面对真实世界第一难的不是决策而是把世界压缩成 LLM 能用的输入。DOM 序列化就是最好的例子。文本索引闭环给 LLM 的标记[17]必须是系统可执行的句柄backendNodeId——理解和行动共享同一份序列化产物。多源证据融合单一数据源AX 树、DOM、布局都有盲区关键判断可点击性要用多路证据交叉验证。事件驱动组织横切关注点十几个关注点崩溃/下载/验证码/弹窗各自一个 watchdog用方法名反射自动订阅可独立增删。动态 schema 生成工具注册后自动并入 LLM 的判别联合新增工具零手工同步——这是工具系统的终极形态之一。上下文经济学单状态消息 历史压缩 前缀缓存 精确截断让每步都要看世界的 Agent 跑得下去、烧得少。为创新敢选底层为了拿到getEventListeners这类原始数据直接上 CDP 而不是 Playwright——技术选型服务于核心创新。结构化作输出让 LLM 输出强类型判别联合Pydantic 校验而不是自由 JSON——校验在生成时就完成。页面变更要防呆terminates_sequence 动态 URL 检测防止一串动作执行到一半页面变了——这是浏览器 Agent 特有的竞态问题。统一异常入口step()里所有异常都进_handle_step_error()所有收尾都进finally: _finalize()——不优雅的异常也要有章法。十六、选型视角browser-use 适合什么、边界在哪适合需要像人一样用浏览器的自动化任务填表、抓数据、监控、QA想研究Agent 感知层怎么做的学习者作为自定义 Agent 的浏览器能力底座。边界它是框架不是平台生产级的大规模并行、防检测、代理轮换官方推荐用云端这恰恰是它云耦合的体现。学习曲线陡DOM 序列化 watchdog 判别联合概念密度高于 LangGraph/OpenHands。视觉类任务不是它的强项它靠 DOM 文本如果某个按钮是纯 canvas 绘制的DOM 路线会失效——这是和 computer-use 路线的根本分界。状态与 HITL 不是它的主场它没有像 LangGraph 那样的检查点中断模型长任务靠 compaction 续命人工介入要自己接。十七、如果让我给 browser-use 做一次工程评价维度评价DOM 序列化感知层★★★★★全行业标杆索引闭环文本↔执行★★★★★事件 watchdog 架构★★★★★动态判别联合结构化输出★★★★★上下文经济学★★★★★多 provider 适配★★★★☆代码可读性 / 学习成本★★☆☆☆历史包袱 / 职责重叠★★☆☆☆离线纯净度云耦合★★☆☆☆工程参考价值★★★★★最大的优点把Agent 怎么感知并操作真实网页这件没人做好的事做成了教科书级的工程。最大的缺点复杂度高、云依赖渗入、对学习者和离线用户不友好。十八、不要抄 browser-use要学 browser-use你不需要在自己的项目里复刻六步 DOM 流水线、十几个 watchdog。但你要带走它的核心判断你的 Agent 面对的真实世界需要先被压缩成 LLM 能用的输入——这比调模型参数重要得多。给 LLM 的标记最好同时是可执行的句柄——理解与行动共享同一份表示。横切关注点多的时候事件驱动比加方法优雅。能动态生成的东西工具 schema不要手工维护。真正优秀的 Agent 工程永远是只在问题真正出现时引入对应的工程机制——但 browser-use 给你展示了当感知真实界面这个问题足够大时机制可以深到什么程度。十九、最终总结browser-use 真正值得赏析的不是它让 LLM 会点按钮而是它围绕感知 执行这两个端点长出了一整套工程感知四路数据融合 → DOM 序列化 → [N] 索引文本 截图 决策判别联合 ActionModel → 结构化输出Pydantic 校验 执行事件总线 → watchdog → CDP 真实交互 观察页面变更检测is_new / URL 变化 / 遮挡过滤 上下文单状态消息 压缩 前缀缓存 token 成本追踪它和 computer-use 走的是两条路一条让模型看图猜坐标一条把世界翻译成可操作文本。browser-use 证明了第二条路的工程深度——也顺带证明了在 Agent 时代如何把现实世界喂给模型正在成为一个比如何调模型更硬的工程问题。下一篇预告至此「Workflow Agent → Coding Agent → Browser Agent」三种视角已经齐了。下一步建议去看一个Research / SuperAgent 项目如 DeerFlow从Agent 感知网页/操作文件走向Agent 多阶段研究、检索、写作拿到第四种架构视角——也可以回到你自己的 Repo Doctor 项目把这三篇的启示真正落地。