2026 年 8 月 22 日
为什么 Barkan 读屏幕,而不是读文档
文档描述的是泛泛而言的产品,DOM 描述的是此时此刻、这个用户眼前的产品。带你看看 Barkan 如何把一个实时页面,变成模型可以据此行动的信息。

刚开始做 Barkan 时,最顺理成章的架构,就是别人都已经上线的那一套:把产品文档做成向量嵌入,针对问题检索相关片段,再让模型写出回答。我们最先做的也是这个。它好到足以拿去演示,又差到没法上线,而且失败的方式总是一样——回答对产品来说是对的,对用户来说却是错的。这篇文章讲的是随后的决定:改为让每个回答都以实时渲染出的界面为依据,以及真正做到这一点需要什么。
那次改变了架构的失败
让基于文档训练的版本栽跟头的,是一个再平常不过的问题。一位测试人员问:“怎么再加一个席位?”得到的是一个干净利落、分六步的回答,直接照搬自帮助中心。第四步说点击添加成员。可在测试人员的屏幕上,这个按钮是灰的,旁边的提示框写着:Launch 套餐最多只能有一个席位。
这个回答不是幻觉。一般而言,它是对的;具体到眼前,它毫无用处。模型根本不知道这个按钮被禁用了,因为文档里没有任何内容能告诉它,这个账户的屏幕此刻是什么样子。
这就是所有靠产品说明文字获取知识的助手的核心局限:它了解的是设计中的产品,而不是为这个用户实际渲染出来的产品。而两者之间的落差,恰恰就是用户卡住的地方。
如果一个回答取决于用户看得见的东西,模型也必须看得见。文档可以解释为什么;只有实时界面有资格说明在哪里。
“读屏幕”到底是什么意思
它不是截图,也不是把 HTML 一股脑塞进提示词。两者都很诱人,也都行不通——截图会丢掉模型采取行动所需的结构;而在现代应用里,原始 HTML 是几百 KB 的框架噪音,有用的信号就埋在里面。
我们的做法是:用户提问时,组件会为渲染后的文档捕获一份增强快照,连同问题一起发送给 API。大致来说,它包含:
层级 · 包含的内容 · 为什么重要
可交互元素 · 按钮、链接、输入框,附带稳定的引用标识和无障碍标签 · 让模型能够指向某个具体控件,并对它执行操作
关联关系 · 哪个标签对应哪个输入框,哪个按钮属于哪个表单 · 把“邮箱那一栏”变成一个具体的元素
界面事实 · 禁用状态、选中的标签页、角标、计数、校验错误 · 文档里从来没有的“在 Launch 套餐下是灰的”这类信息
内容块 · 可见的标题和文字,经过去重和截断 · 足以理解页面的上下文,而不是整个页面
表单摘要 · 哪些已填、哪些为空、哪些无效 · 让模型能接着完成做到一半的工作流
活动界面层与滚动状态 · 打开的弹窗、抽屉,当前视口 · 区分“不在屏幕上”和“根本不存在”
页面元信息 · 路由、标题、白名单内的 data 属性 · 成本低、可靠的定位信息
整套设计追求的是小、稳、真。小,才能放进上下文预算,还给思考留有余地。稳,同一个元素在多轮对话中才会拿到同一个引用标识,指向和多步操作正是靠这一点才得以实现。真,模型才永远不会看到用户看不到的控件。
由此带来的三个工程难题
以 DOM 为依据,解决了“对用户来说是错的”这个问题,却立刻带来了另外三个问题。这些代价都值得,但问题实实在在。
1. 界面是会动的
文档索引只在有人编辑文档时才会变。DOM 则随时都在变:下拉菜单展开、弹出一条 toast 提示、列表加载完毕。早一秒拍下的快照,描述的就是一个已经不存在的页面。
我们用两种方式应对。第一,快照在页面稳定之后才捕获——我们会等进行中的网络请求和布局变化都安静下来,同时设一个上限,免得一个动个不停的页面让回答无限期卡住。第二,在代操作模式(Do Mode)下,每次操作之后、决定下一步之前,都会静默地重新捕获一次,这样模型面对的始终是页面现在的样子,而不是过去的样子。

2. 在不稳定的树上保持稳定的引用
告诉模型“点击第三个按钮”很脆弱。告诉它“点击 id 为 b17 的元素”,也只有在下一轮里 b17 仍指向同一个东西时才管用。现代前端框架重新渲染得非常频繁,所以我们不能依赖 DOM 节点本身的身份。
我们的引用标识,来自人类辨认一个元素时会用到的信息——它的角色、它的标签、它在同级元素中的位置、包裹它的地标区域——同时维护一张短时有效的映射表,把引用标识对应到实时节点。映射一旦过期,操作就会明确报错,模型会重新读取页面,而不是点错东西。一次看得见的失败操作,远胜过一次作用在错误元素上的“成功”操作。
3. 哪些东西不该发送
真实产品的增强快照里,有真实的数据:表格里的客户姓名、发票总额、表单里的邮箱地址。默认把这些全部发给模型是不可接受的,而“我们需要它作为上下文”也不是一个站得住的理由。
快照在离开页面之前,就会在客户端做最小化处理。可见文字会被截断和去重;输入框里的值只概括为已填 / 为空 / 无效,而不会原样复制;data 属性也只转发白名单里的那些。目标是让模型知道有一张 48 行的客户表格、上方有一个搜索框,而不是知道这些客户是谁。
最小化偶尔会让某个问题答不上来。如果用户问“这张发票的总额为什么不对?”,模型是看不到那个数字的。我们认为这是正确的默认做法:它可以把用户指向那个字段,解释总额是怎么算出来的,而这个数字自始至终都不会离开页面。
演示,而不是描述
一旦模型和用户看着同一个界面,就有一件任何文档训练的助手都做不到的事成为可能:它可以不再描述,而是直接指给你看。
当回答提到某个元素时,组件会在真实页面上把光标移到那里,然后等待。在多步工作流中,这个光标会带着用户从一个控件走到下一个控件——跨页面跳转也不例外,因为快照会在新的路由上重新生成。说明和界面变成了同一个东西,而让文档用起来那么累人的“翻译”这一步,就这样消失了。
每个助手都能告诉你按钮在哪里。区别在于,它能不能看出你已经打开了错误的页面。

文档依然重要的地方
这一切并不意味着文档对模型毫无用处,而是说它有不同的分工。文档承载的是意图——一个功能是做什么的、什么时候用、某项设置是什么意思——屏幕承载的则是状态。好的回答往往两者都需要:知识库说明 webhook 会重试三次,屏幕则显示这个 webhook 最近一次推送失败了。
所以 Barkan 确实会检索知识库,但它是在读完屏幕之后才去检索的,而且两者一旦矛盾,以屏幕为准。如果文档说有一个添加成员按钮,而屏幕显示它被禁用了,那么回答要讲的,就是这个被禁用的按钮。
– 以渲染出的界面为依据,而不是文档。“一般而言是对的”,是代价最高的一种错。
– 发送结构,而不是像素或原始 HTML:可交互元素、关联关系、界面事实、概括后的内容。
– 等页面稳定后再捕获,每次操作后重新捕获;DOM 是个移动靶。
– 在客户端做最小化。模型应该知道数据的形态,而不是数据本身。
– 指出来,别描述。只要看得到元素,就能把它指给用户看。
安装依然只要一行
一个合理的担心是:“读取渲染出的界面”听起来意味着深度集成。其实不然。组件只是你现有页面布局里的一个 script 标签;它在 shadow DOM 中挂载自己的根节点,在浏览器内部观察页面,不需要任何路由注解,也不需要组件包装器。
<script async src="https://trybarkan.com/widget.js" data-barkan-site="site_your_key"></script>上面描述的一切,都发生在这个脚本里。安装它的产品,甚至不需要知道 Barkan 的存在。
装上这段代码,打开你的应用,问它一个你的文档答不上来的问题。开通即送 $25 额度,无需绑卡。
“大多数用户并不想再得到一个答案。他们要么想有人指路,要么想有人直接把事办好。这就是产品的全部。”
Gabriel Lancelot
Barkan 联合创始人
