<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>无头浏览器 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/tags/%E6%97%A0%E5%A4%B4%E6%B5%8F%E8%A7%88%E5%99%A8/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/tags/%E6%97%A0%E5%A4%B4%E6%B5%8F%E8%A7%88%E5%99%A8/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Lightpanda 源码级拆解：无头 Chrome 对 agent 贵在哪，以及「不渲染」换来和丢掉了什么</title>
        <link>https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/</link>
        <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/cover.svg" alt="Featured image of post Lightpanda 源码级拆解：无头 Chrome 对 agent 贵在哪，以及「不渲染」换来和丢掉了什么" /&gt;&lt;p&gt;一个用 Zig 从零写的浏览器，没有 Chromium 分支，没有 WebKit 补丁，官方口径是比无头 Chrome 省 16 倍内存、快 9 倍。仓库 36.9k star，AGPL-3.0。&lt;/p&gt;
&lt;p&gt;Lightpanda 的背景故事很直接：创始团队做了多年大规模网页抓取，撞上无头 Chrome 的成本和稳定性上限。实例开几百上千个，内存和 CPU 都吃不住，还得频繁重启才能稳住。&lt;/p&gt;
&lt;p&gt;但如果你只把它读成「更省资源的无头 Chrome」，会漏掉它真正动刀的位置。这篇拆的是那一刀。&lt;/p&gt;
&lt;h2 id=&#34;一贵在哪被像素定义的交互协议&#34;&gt;一、贵在哪：被像素定义的交互协议&lt;/h2&gt;
&lt;p&gt;这是我认为整篇里最重要的一段，因为它把因果顺序说反了就会理解错这个项目。&lt;/p&gt;
&lt;p&gt;常见说法是「因为不渲染所以快」。真实顺序是反的：&lt;strong&gt;因为先把交互从像素上解耦了，才敢不渲染。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lightpanda 官方文档自己点出了 CDP 的结构性缺陷。CDP 能在 DOM 上直接查询和修改，但&lt;strong&gt;它没有 DOM 级的用户交互命令&lt;/strong&gt;。要点击一个元素，客户端必须：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查出这个元素的渲染像素坐标&lt;/li&gt;
&lt;li&gt;把虚拟鼠标移到那个坐标&lt;/li&gt;
&lt;li&gt;派发一个点击事件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三次往返，外加一个渲染依赖，去做一件概念上只有一个动作的事。文档的原话是，CDP 是为人坐在显示器前调试浏览器设计的，不是为「一个已经知道要操作哪个 DOM 节点的脚本」设计的。&lt;/p&gt;
&lt;p&gt;结论就出来了：&lt;strong&gt;无头 Chrome 之所以必须把 style resolution、layout、paint、compositing 整条流水线跑起来，是因为它的交互协议是用像素定义的。&lt;/strong&gt; 没有像素坐标，Playwright 点不了按钮。&lt;/p&gt;
&lt;p&gt;Lightpanda 的做法是把交互改成 DOM 级，于是渲染流水线成了可删项。&lt;/p&gt;
&lt;h2 id=&#34;二删掉之后剩下什么&#34;&gt;二、删掉之后剩下什么&lt;/h2&gt;
&lt;p&gt;架构上留下的是自动化真正用到的那部分：网络层、HTML 解析器、JavaScript 引擎、Web API 和 DOM。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;组件&lt;/th&gt;
          &lt;th&gt;实现&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;控制面&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;serve&lt;/code&gt;（CDP 或 WebDriver classic/BiDi）、&lt;code&gt;agent&lt;/code&gt;、&lt;code&gt;mcp&lt;/code&gt;、云端 HTTP API&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;网络层&lt;/td&gt;
          &lt;td&gt;libcurl、robots.txt、WebSocket&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;HTML 解析&lt;/td&gt;
          &lt;td&gt;把取回的 HTML 变成 DOM&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;JavaScript 引擎&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;V8，和 Chrome 同一个引擎&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Web API 与 DOM&lt;/td&gt;
          &lt;td&gt;脚本读写和修改的内存文档&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一行值得停一下：它没有自己造 JS 引擎，用的是 V8。这意味着&lt;strong&gt;跑不了的原因不会是「JS 引擎不支持某个语法」&lt;/strong&gt;，而是缺某个 Web API、或者某个行为依赖真实布局。这个区别在排查问题的时候很有用。&lt;/p&gt;
&lt;p&gt;进程模型上，它编译成单个原生二进制，运行时&lt;strong&gt;只有一个进程&lt;/strong&gt;。这个进程持有一份跨整个运行周期共享的状态（代码里叫 &lt;code&gt;App&lt;/code&gt;），浏览器实例按需创建，每个实例拥有自己的一个 V8 isolate。仓库结构直接对应这套分层：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;路径&lt;/th&gt;
          &lt;th&gt;对应组件&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/cdp/&lt;/code&gt;、&lt;code&gt;src/agent/&lt;/code&gt;、&lt;code&gt;src/mcp/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;控制面&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/main.zig&lt;/code&gt;、&lt;code&gt;src/App.zig&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;App&lt;/code&gt; 与进程级服务&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/network/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;App&lt;/code&gt; 的网络层（libcurl、robots.txt、WebSocket）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/browser/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Browser、Session、Page、Frame&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一个引擎后面挂了五个入口，这是它设计上比较聪明的地方：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda fetch --dump html https://example.com   &lt;span style=&#34;color:#75715e&#34;&gt;# 一次性取页，输出 HTML 或 Markdown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda serve --obey-robots                     &lt;span style=&#34;color:#75715e&#34;&gt;# 起服务，默认 CDP，127.0.0.1:9222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda mcp                                     &lt;span style=&#34;color:#75715e&#34;&gt;# MCP server（stdio）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda agent                                   &lt;span style=&#34;color:#75715e&#34;&gt;# 交互式 agent REPL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda run script.js                           &lt;span style=&#34;color:#75715e&#34;&gt;# 重放已保存的脚本，不需要 LLM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;serve&lt;/code&gt; 的协议选择值得单独说：默认讲 CDP，加 &lt;code&gt;--protocol webdriver&lt;/code&gt; 改讲 WebDriver（classic 走 HTTP，BiDi 走 WebSocket），而 &lt;code&gt;--protocol cdp --protocol webdriver&lt;/code&gt; 可以在&lt;strong&gt;同一个端口上同时&lt;/strong&gt;提供两套。对已经有一套 Selenium 测试栈的团队，这个兼容层省掉了重写。&lt;/p&gt;
&lt;p&gt;日志是结构化的，一眼能看出它在干什么：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335600862 $scope=app $level=note $msg=&amp;#34;server running&amp;#34; address=127.0.0.1:9222
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335543068 $scope=browser $level=info $msg=&amp;#34;executing script&amp;#34; src=.../script.js kind=javascript
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335543139 $scope=http $level=info $msg=&amp;#34;request complete&amp;#34; source=xhr status=200 len=4770
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;三代价一组看起来有实际没有的能力&#34;&gt;三、代价：一组「看起来有、实际没有」的能力&lt;/h2&gt;
&lt;p&gt;这一节是我认为用它之前必须知道的，因为它的失败方式很特殊。&lt;/p&gt;
&lt;p&gt;不渲染意味着&lt;strong&gt;没有真实布局&lt;/strong&gt;。官方文档写得很清楚：元素尺寸是从 CSS 和 HTML 属性做的合理近似，而&lt;strong&gt;元素位置是从 DOM 深度和兄弟顺序模拟出来的&lt;/strong&gt;（对应 &lt;code&gt;calculateDocumentPosition&lt;/code&gt;），不是真实布局算出来的。&lt;/p&gt;
&lt;p&gt;更要紧的是这一条：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Page.captureScreenshot&lt;/code&gt; 返回一张&lt;strong&gt;占位图&lt;/strong&gt;，不是真实渲染。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;原文的收尾是一句提醒：需要真实像素的时候，请改用完整浏览器。&lt;/p&gt;
&lt;p&gt;把这两条放在一起看，会得到一个不太妙的性质：&lt;strong&gt;它在这类能力上不会报错，它会返回一个看起来正常的替代品。&lt;/strong&gt; 截图请求成功返回了 200 和一张图片，只是图里没有你的页面。&lt;/p&gt;
&lt;p&gt;对 agent 来说这比报错危险。一个拿到占位图的 agent 不会停下来，它会继续基于那张图往下推理，然后给你一个基于空白内容得出的结论。&lt;/p&gt;
&lt;p&gt;所以边界要自己划清。以下这些能力不在它的范围内：视觉回归测试、布局断言（元素到底在哪、有没有重叠）、截图给多模态模型看、验证码图像识别、PDF 生成。&lt;/p&gt;
&lt;h2 id=&#34;四省了多少数字和它的适用边界&#34;&gt;四、省了多少：数字和它的适用边界&lt;/h2&gt;
&lt;p&gt;官方基准是在 AWS m5.large 上请求 933 个真实网页，通过真实网络：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;指标&lt;/th&gt;
          &lt;th&gt;Lightpanda&lt;/th&gt;
          &lt;th&gt;无头 Chrome&lt;/th&gt;
          &lt;th&gt;差距&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;峰值内存（100 页）&lt;/td&gt;
          &lt;td&gt;123 MB&lt;/td&gt;
          &lt;td&gt;2 GB&lt;/td&gt;
          &lt;td&gt;约 16 倍&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;执行时间（100 页）&lt;/td&gt;
          &lt;td&gt;5 秒&lt;/td&gt;
          &lt;td&gt;46 秒&lt;/td&gt;
          &lt;td&gt;约 9 倍&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这两个数字的意义不一样，我想分开说。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存那 16 倍才是决定性的。&lt;/strong&gt; 它决定的是「一台机器上能同时跑多少个实例」，也就是你的单页成本。做大规模抓取或者给每个 agent 会话开一个浏览器进程时，这个数字直接换算成账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行时间那 9 倍在单页任务上你基本感知不到。&lt;/strong&gt; 46 秒和 5 秒都是 100 页的累计值，摊到单页是零点几秒的差别，而网络往返往往比这更慢。这个数字真正起作用的地方是批量任务。&lt;/p&gt;
&lt;p&gt;顺便提醒一句这两个数字的适用边界：它是&lt;strong&gt;爬取场景&lt;/strong&gt;的基准（取页、执行 JS、拿 DOM），不是「跑一个多步交互的 agent 任务」的基准。后者的耗时更多取决于页面数量、等待策略和模型推理，浏览器本身省下的那部分会被摊薄。&lt;/p&gt;
&lt;h2 id=&#34;五agent-模式把-llm-放进了浏览器进程&#34;&gt;五、agent 模式：把 LLM 放进了浏览器进程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lightpanda agent&lt;/code&gt; 这个入口的设计和常见的「agent 通过 CDP 驱动浏览器」不一样：&lt;strong&gt;它跑在浏览器同一个进程里，不走 CDP。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它叠了三层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;浏览器&lt;/strong&gt;：加载页面、执行 JavaScript、处理 DOM，和 CDP server 用的是同一个引擎&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时&lt;/strong&gt;：一组原生工具，&lt;code&gt;goto&lt;/code&gt;、&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;fill&lt;/code&gt;、&lt;code&gt;extract&lt;/code&gt;、&lt;code&gt;evaluate&lt;/code&gt;、&lt;code&gt;search&lt;/code&gt;、&lt;code&gt;detectForms&lt;/code&gt;、&lt;code&gt;selectOption&lt;/code&gt;、&lt;code&gt;waitForState&lt;/code&gt; 等，每个都有对应的斜杠命令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM&lt;/strong&gt;：把你的自然语言请求翻译成工具调用，&lt;strong&gt;这一层是可选的&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三层可选这点很有意思。&lt;code&gt;--no-llm&lt;/code&gt; 之后它退化成只有斜杠命令的 REPL，你仍然可以手工驱动浏览器。也就是说 LLM 在这里是「把自然语言编译成工具调用」的便利层，不是必需品。&lt;/p&gt;
&lt;p&gt;provider 的解析顺序是四级，值得记一下因为它决定你第一次跑会不会卡住：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;记住的&lt;/strong&gt;：上次用 &lt;code&gt;/provider&lt;/code&gt; 选的，按目录持久化在 &lt;code&gt;.lp-agent.zon&lt;/code&gt; 里，前提是它的 key 还在&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动探测&lt;/strong&gt;：环境变量里第一个找到的 key，按固定优先级顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地&lt;/strong&gt;：没有云端 key 时，探测本机在跑的 Ollama 或 llama.cpp。本地服务不会被自动探测到，必须显式指定 &lt;code&gt;--provider ollama&lt;/code&gt; 或 &lt;code&gt;--provider llama_cpp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有&lt;/strong&gt;：&lt;code&gt;--no-llm&lt;/code&gt; 退化成基础 REPL，自然语言和 &lt;code&gt;/login&lt;/code&gt;、&lt;code&gt;/logout&lt;/code&gt; 这类由 LLM 驱动的命令不可用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;还有一个反直觉的开关：&lt;code&gt;--effort&lt;/code&gt; 设的是每轮推理预算（&lt;code&gt;none&lt;/code&gt; 到 &lt;code&gt;xhigh&lt;/code&gt;，REPL 里默认 &lt;code&gt;low&lt;/code&gt;，一次性 &lt;code&gt;--task&lt;/code&gt; 模式默认 &lt;code&gt;medium&lt;/code&gt;）。官方给的说明是，&lt;strong&gt;更高的 effort 可能意味着每个任务更少的工具调用&lt;/strong&gt;，因为模型规划得更好。所以它不单纯是「更慢更聪明」的滑块，而是一个真实的取舍。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--attach&lt;/code&gt; 可以把本地文件喂给模型，文本内联（单个上限 512 KiB），图片、音频、PDF 走 base64（单个上限 20 MiB），每个 &lt;code&gt;--attach&lt;/code&gt; 带一个文件，可重复。&lt;/p&gt;
&lt;h2 id=&#34;六为什么-agent-模式要放进同一个进程&#34;&gt;六、为什么 agent 模式要放进同一个进程&lt;/h2&gt;
&lt;p&gt;回到第一节那个 CDP 的缺陷，就能看出 &lt;code&gt;lightpanda agent&lt;/code&gt; 那个设计选择的理由。&lt;/p&gt;
&lt;p&gt;如果 agent 通过 CDP 驱动浏览器，那么每一个动作都要过一次协议：客户端发命令，浏览器执行，结果回来，客户端再决定下一步。第一节里那个点击的例子是三次往返；一个多步任务就是几十次。每次往返都是延迟、都是状态同步的机会、也都是「客户端和服务端对当前状态的理解可能不一致」的窗口。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;agent&lt;/code&gt; 模式把运行时工具放进了浏览器进程里。&lt;code&gt;goto&lt;/code&gt;、&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;fill&lt;/code&gt; 这些工具直接操作内存里的 DOM，&lt;strong&gt;不过协议&lt;/strong&gt;。省下的不只是那几次往返的延迟，更是一个根本性的简化：不再有两个进程各自维护一份「页面现在长什么样」的理解。&lt;/p&gt;
&lt;p&gt;这也解释了为什么它的工具集里有 &lt;code&gt;detectForms&lt;/code&gt;、&lt;code&gt;selectOption&lt;/code&gt;、&lt;code&gt;waitForState&lt;/code&gt; 这种&lt;strong&gt;语义级&lt;/strong&gt;的操作。它们的存在前提就是能直接看到 DOM 结构。用像素坐标的协议做不出 &lt;code&gt;detectForms&lt;/code&gt;，因为「表单」这个概念在像素层面不存在。&lt;/p&gt;
&lt;p&gt;代价是耦合：这套工具只有 &lt;code&gt;lightpanda agent&lt;/code&gt; 能用，你在自己的 agent 框架里通过 CDP 连过去是拿不到的。想用就得把任务交给它的 REPL，或者用 &lt;code&gt;--task&lt;/code&gt; 跑一次性任务。&lt;/p&gt;
&lt;h2 id=&#34;七迁移一份已有的-playwright-脚本要改什么&#34;&gt;七、迁移一份已有的 Playwright 脚本要改什么&lt;/h2&gt;
&lt;p&gt;对已经有自动化栈的团队，Lightpanda 的迁移成本比较低，因为它的策略是「在你已经用的接口上等你」：Puppeteer、Playwright、Chromedp 通过 CDP 连它的方式和连 Chrome 一样，Selenium 走 WebDriver BiDi。把客户端指向它的端点，脚本基本不用动。&lt;/p&gt;
&lt;p&gt;但有几类操作会出问题，这是迁移时该先盘一遍的清单：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;你的脚本里有什么&lt;/th&gt;
          &lt;th&gt;在 Lightpanda 上会怎样&lt;/th&gt;
          &lt;th&gt;处理方式&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;截图 / 视觉断言&lt;/td&gt;
          &lt;td&gt;拿到占位图，不报错&lt;/td&gt;
          &lt;td&gt;这段留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;依赖真实布局的定位（按位置、重叠判断）&lt;/td&gt;
          &lt;td&gt;位置是模拟值&lt;/td&gt;
          &lt;td&gt;改成 DOM 语义定位&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;PDF 生成&lt;/td&gt;
          &lt;td&gt;不支持&lt;/td&gt;
          &lt;td&gt;留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;文件上传&lt;/td&gt;
          &lt;td&gt;不支持&lt;/td&gt;
          &lt;td&gt;留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;多标签页流程&lt;/td&gt;
          &lt;td&gt;一条 CDP 连接一个页面&lt;/td&gt;
          &lt;td&gt;拆成多次运行，或改用 &lt;code&gt;goto_url&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;依赖冷门 Web API 的页面&lt;/td&gt;
          &lt;td&gt;取决于该 API 有没有实现&lt;/td&gt;
          &lt;td&gt;报错就回退完整浏览器&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看这张表的方式是：&lt;strong&gt;能过一遍 DOM 语义完成的部分交出去，剩下需要像素的部分留给自己。&lt;/strong&gt; 这也是混合部署的划分线。&lt;/p&gt;
&lt;h2 id=&#34;八pandascript被低估的那一环&#34;&gt;八、PandaScript：被低估的那一环&lt;/h2&gt;
&lt;p&gt;这个设计我认为在 agent 工具里不常见，值得单独讲。&lt;/p&gt;
&lt;p&gt;流程是这样的：你用自然语言让 agent 把一件事跑通，然后 &lt;code&gt;/save&lt;/code&gt; 把它导出成一个普通的 JavaScript 重放脚本。之后：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda run script.js    &lt;span style=&#34;color:#75715e&#34;&gt;# 没有 LLM 调用，不需要 API key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它做的事是&lt;strong&gt;把 agent 的探索成本摊薄成脚本的运行成本&lt;/strong&gt;。第一次调用的代价是模型推理加试错，第二次开始是纯执行。&lt;/p&gt;
&lt;p&gt;在 agent 流水线里这是一种少见的「确定性降级」：一个任务一旦被探索明白，就可以从「每次都要模型决策」降级成「一段可重放、可版本管理、可 CI 的代码」。稳定性、成本和可审计性三样一起改善。&lt;/p&gt;
&lt;p&gt;官方也给了对应的警告：&lt;code&gt;.js&lt;/code&gt; 脚本里可以包含 &lt;code&gt;evaluate(...)&lt;/code&gt; 调用，那是&lt;strong&gt;在页面里执行任意 JavaScript&lt;/strong&gt;，只跑你信任的脚本，标准跟对待 shell 脚本一样。&lt;/p&gt;
&lt;h2 id=&#34;九三个会咬人的细节&#34;&gt;九、三个会咬人的细节&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 一条 CDP 连接只持有一个页面。&lt;/strong&gt; 第一次 &lt;code&gt;new_tab(url)&lt;/code&gt; 正常，第二次会失败并报 &lt;code&gt;TargetAlreadyLoaded&lt;/code&gt;。这不只是个 bug，它的会话模型没有跟上「agent 会开很多标签页」这个现实。用的时候要按「一个连接一个页面」来组织，或者用 &lt;code&gt;goto_url&lt;/code&gt; 复用当前页。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. AGPL-3.0 是架构决策，不是法务细节。&lt;/strong&gt; 一个以「替换无头 Chrome」为定位的浏览器选了这个许可，直接决定它能不能被嵌进闭源产品。如果那件事是真的，AGPL-3.0 会关掉这条路径，把采用方式推向「自托管」和「用它的云服务」两端。评估它的时候，这一条应该和内存数字放在同一张表上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. robots.txt 要自己开。&lt;/strong&gt; 官方给了 &lt;code&gt;--obey-robots&lt;/code&gt; 这个参数，同时提了一句「尊重 robots.txt，避免对小站点高频请求」。要注意的是&lt;strong&gt;它默认不开&lt;/strong&gt;，也就是默认行为不遵守 robots.txt。把抓取跑在别人的站点上时，这是一个需要主动处理的合规点，&lt;code&gt;fetch&lt;/code&gt; 和 &lt;code&gt;serve&lt;/code&gt; 都接受这个参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 真实部署形态通常是混合的。&lt;/strong&gt; Hermes Agent 的官方文档里写着 &lt;code&gt;browser.engine: lightpanda&lt;/code&gt;，配的是&lt;strong&gt;自动 Chrome 兜底&lt;/strong&gt;：navigate、snapshot、click、type、scroll、back、press、eval 走 Lightpanda，而&lt;strong&gt;截图、&lt;code&gt;browser_vision&lt;/code&gt;、PDF 生成、文件上传，以及任何在 Lightpanda 上报错的操作，自动重试到 Chrome&lt;/strong&gt;。这等于官方承认了那个边界的存在，并给出了工程上的处理方式。&lt;/p&gt;
&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;我的判断是 Lightpanda 的价值不在「更便宜的无头浏览器」这个定位上，而在于它把无头浏览器的成本结构&lt;strong&gt;归因清楚了&lt;/strong&gt;。过去几年大家默认无头 Chrome 重是必然代价，它指出核心依赖是交互协议定义在像素上，于是渲染成了不可删项。删掉那个依赖，渲染就可以删。&lt;/p&gt;
&lt;p&gt;代价同样清楚：没有真实布局，截图是占位图，而失败方式是返回替代品而不是报错。用它的正确姿势是按能力分层，需要像素的活儿交给完整浏览器。&lt;/p&gt;
&lt;p&gt;最后交代一下本文的证据边界：以上全部来自 Lightpanda 的官方文档、仓库结构和源码目录，&lt;strong&gt;我没有实机跑过&lt;/strong&gt;。要动手验证，最小的一组是三条命令：&lt;code&gt;lightpanda fetch --dump html &amp;lt;url&amp;gt;&lt;/code&gt; 看它取回的是不是最终 DOM；&lt;code&gt;lightpanda serve&lt;/code&gt; 之后用 Playwright 连上去，试一次 &lt;code&gt;captureScreenshot&lt;/code&gt; 看返回的占位图长什么样；以及开第二个 &lt;code&gt;new_tab&lt;/code&gt; 复现 &lt;code&gt;TargetAlreadyLoaded&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;相关阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/&#34; &gt;《Neko 源码级拆解：把浏览器变成一条视频流》&lt;/a&gt;：姊妹篇。这篇讲的是把浏览器压到最轻、砍掉一切与人有关的部分；那篇讲的是反过来，把浏览器整个变成一条给人看的流。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260910-browser-agent-trio-comparison/&#34; &gt;《browser-use 114k⭐ 称王！浏览器 Agent 三强争霸》&lt;/a&gt;：把这篇放进那个坐标系里看，Lightpanda 属于「驱动层」，和三强不是同一层的东西。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261019-opencode-byok-console-credential-breach/&#34; &gt;《OpenCode 被「蒸馏攻击」打穿》&lt;/a&gt;：同样涉及浏览器与凭据，那篇的教训是凭据放在哪比工具选哪个更值钱。&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
    </channel>
</rss>
