1870 lines
43 KiB
Markdown
1870 lines
43 KiB
Markdown
# ChatGPT Share: 网站操作项拓扑设计
|
||
|
||
- Source URL: https://chatgpt.com/share/6a30e583-a7d0-83ea-88bd-1299eb78d086
|
||
- Extracted at: 2026-06-16T06:17:36.331Z
|
||
- Extraction method: parsed `window.__reactRouterContext.streamController.enqueue(...)` from the shared page HTML.
|
||
- Project use: source discussion for a focused deterministic action topology engine.
|
||
|
||
## Conversation
|
||
|
||
### User Message 2044
|
||
|
||
如何建立一个网站中所有可操作项的拓扑,包含可操作项本身(如按钮,输入框),它的可定位信息(不同框架似乎不一样),前置打开项,所调用函数,相关参数等等。目的是能够在浏览器自动化控制中能够快速实现
|
||
|
||
|
||
### Assistant Message 2002
|
||
|
||
可以建立,而且建议把它设计成一个 **Action Topology Graph,可操作项拓扑图**。核心思想是:**不要只索引 DOM 元素,而是索引“在某个页面状态下,对某个可操作元素执行某个动作,会导致什么结果”**。
|
||
|
||
也就是说,真正有用的基本单元不是:
|
||
|
||
```text
|
||
button#save
|
||
```
|
||
|
||
而是:
|
||
|
||
```text
|
||
State A -- click(button: Save) --> State B
|
||
```
|
||
|
||
其中 `State A` 可能是“首页已登录、左侧菜单已展开、弹窗未打开”,`State B` 可能是“保存成功提示出现”或“发出了 POST /api/save 请求”。
|
||
|
||
---
|
||
|
||
## 1. 拓扑图应该分四层建模
|
||
|
||
### 第一层:页面状态 State
|
||
|
||
一个网站不是一个静态页面,而是一组状态。状态可以由以下信息共同定义:
|
||
|
||
```json
|
||
{
|
||
"stateId": "s_home_menu_open",
|
||
"url": "https://example.com/dashboard",
|
||
"route": "/dashboard",
|
||
"title": "Dashboard",
|
||
"framePath": [],
|
||
"domHash": "sha256:...",
|
||
"axHash": "sha256:...",
|
||
"visibleTextHash": "sha256:...",
|
||
"modalStack": ["settings-dialog"],
|
||
"openedBy": "edge_click_settings",
|
||
"preconditionPath": [
|
||
"login",
|
||
"click:user-menu",
|
||
"click:settings"
|
||
]
|
||
}
|
||
```
|
||
|
||
这里最重要的是 `preconditionPath`。它表示:要操作这个状态下的元素,浏览器自动化必须先执行哪些前置打开动作。
|
||
|
||
例如:
|
||
|
||
```text
|
||
点击用户头像
|
||
→ 打开下拉菜单
|
||
→ 点击 Settings
|
||
→ 打开设置弹窗
|
||
→ 才能看到 Save 按钮
|
||
```
|
||
|
||
这个 `Save` 按钮的前置路径就不是空的,而是:
|
||
|
||
```json
|
||
[
|
||
"click:user-avatar",
|
||
"click:settings-menu-item"
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
### 第二层:可操作元素 Actionable Element
|
||
|
||
每个可操作项都应该绑定到某个状态,而不是全局唯一。
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_save_button_001",
|
||
"stateId": "s_settings_dialog_open",
|
||
"kind": "button",
|
||
"tag": "button",
|
||
"role": "button",
|
||
"accessibleName": "Save",
|
||
"text": "Save",
|
||
"label": null,
|
||
"placeholder": null,
|
||
"attributes": {
|
||
"data-testid": "save-settings",
|
||
"type": "submit",
|
||
"aria-disabled": "false"
|
||
},
|
||
"framePath": [],
|
||
"shadowPath": [],
|
||
"bbox": {
|
||
"x": 824,
|
||
"y": 612,
|
||
"width": 96,
|
||
"height": 36
|
||
},
|
||
"visibility": {
|
||
"visible": true,
|
||
"enabled": true,
|
||
"editable": false,
|
||
"receivesEvents": true
|
||
},
|
||
"componentHint": {
|
||
"framework": "react",
|
||
"componentName": "SettingsDialog"
|
||
}
|
||
}
|
||
```
|
||
|
||
建议优先用 **Accessibility Tree + DOM Snapshot** 来抽取元素。Playwright 的定位器体系也建议优先使用面向用户的定位方式,例如 role、label、text、placeholder、test id,而不是脆弱的长 CSS/XPath 链;其文档明确指出 locator 是自动等待和重试能力的核心,并推荐 `getByRole()`、`getByLabel()`、`getByText()`、`getByTestId()` 等方式。citeturn773971view0
|
||
|
||
---
|
||
|
||
### 第三层:定位器 Locator Candidates
|
||
|
||
不要只存一个 selector。应该为每个元素存一组候选定位器,并打分。
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_save_button_001",
|
||
"locators": [
|
||
{
|
||
"type": "playwright",
|
||
"strategy": "role",
|
||
"expression": "page.getByRole('button', { name: 'Save' })",
|
||
"score": 0.95,
|
||
"reason": "role + accessible name stable"
|
||
},
|
||
{
|
||
"type": "playwright",
|
||
"strategy": "testId",
|
||
"expression": "page.getByTestId('save-settings')",
|
||
"score": 0.92,
|
||
"reason": "explicit test id"
|
||
},
|
||
{
|
||
"type": "css",
|
||
"strategy": "data-testid",
|
||
"expression": "[data-testid=\"save-settings\"]",
|
||
"score": 0.88
|
||
},
|
||
{
|
||
"type": "xpath",
|
||
"strategy": "text",
|
||
"expression": "//button[normalize-space()='Save']",
|
||
"score": 0.55
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
推荐的定位器优先级:
|
||
|
||
```text
|
||
1. role + accessible name
|
||
2. label / placeholder / alt / title
|
||
3. data-testid / data-pw / data-qa
|
||
4. 稳定业务属性,例如 name、aria-label、href、type
|
||
5. 局部 CSS
|
||
6. XPath
|
||
7. 坐标点击,仅作为兜底
|
||
```
|
||
|
||
Playwright 文档也特别提醒,CSS 和 XPath 容易跟 DOM 结构绑定,长链 selector 在 DOM 变化时很容易失效,因此应该尽量用更接近用户感知的定位方式,例如 role 或 test id。citeturn773971view0
|
||
|
||
---
|
||
|
||
### 第四层:动作边 Action Edge
|
||
|
||
动作边是拓扑的核心。
|
||
|
||
```json
|
||
{
|
||
"edgeId": "edge_click_save_settings",
|
||
"fromState": "s_settings_dialog_open",
|
||
"toState": "s_settings_saved_toast",
|
||
"elementId": "el_save_button_001",
|
||
"action": {
|
||
"type": "click",
|
||
"params": {}
|
||
},
|
||
"preconditions": [
|
||
"state.url matches /dashboard",
|
||
"dialog Settings visible",
|
||
"button Save enabled"
|
||
],
|
||
"effects": {
|
||
"urlChanged": false,
|
||
"domChanged": true,
|
||
"network": [
|
||
{
|
||
"method": "POST",
|
||
"url": "/api/settings",
|
||
"bodySchema": {
|
||
"theme": "string",
|
||
"notifications": "boolean"
|
||
}
|
||
}
|
||
],
|
||
"storageChanged": [],
|
||
"visibleTextAdded": ["Settings saved"]
|
||
},
|
||
"handlerTrace": {
|
||
"event": "click",
|
||
"functionName": "handleSave",
|
||
"source": "src/components/SettingsDialog.tsx:88",
|
||
"callStack": [
|
||
"handleSave",
|
||
"onClick",
|
||
"dispatchEvent"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
自动化控制时,你真正需要查询的是:
|
||
|
||
```text
|
||
我要执行“保存设置”
|
||
→ 找到对应 Action Edge
|
||
→ 取它的 preconditionPath
|
||
→ 打开所需状态
|
||
→ 用最高分 locator 执行动作
|
||
→ 验证 expected effects
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 采集流程:静态快照 + 动态探索 + 运行时追踪
|
||
|
||
推荐管线如下:
|
||
|
||
```text
|
||
启动浏览器
|
||
↓
|
||
注入 instrumentation
|
||
↓
|
||
进入起始 URL
|
||
↓
|
||
采集 State Snapshot
|
||
↓
|
||
抽取可操作元素
|
||
↓
|
||
生成定位器候选
|
||
↓
|
||
逐个执行动作
|
||
↓
|
||
记录状态变化、网络请求、函数调用
|
||
↓
|
||
生成 Action Edge
|
||
↓
|
||
进入新状态继续 BFS/DFS
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 元素发现:不要只靠 querySelectorAll
|
||
|
||
基础选择器可以这样开始:
|
||
|
||
```js
|
||
const actionableSelector = [
|
||
'button',
|
||
'a[href]',
|
||
'input',
|
||
'textarea',
|
||
'select',
|
||
'[role="button"]',
|
||
'[role="link"]',
|
||
'[role="menuitem"]',
|
||
'[role="checkbox"]',
|
||
'[role="radio"]',
|
||
'[role="tab"]',
|
||
'[role="option"]',
|
||
'[contenteditable="true"]',
|
||
'[tabindex]:not([tabindex="-1"])',
|
||
'[onclick]',
|
||
'[aria-haspopup]',
|
||
'[aria-expanded]'
|
||
].join(',');
|
||
```
|
||
|
||
但这还不够。现代网站里大量元素是:
|
||
|
||
```html
|
||
<div role="button">Submit</div>
|
||
```
|
||
|
||
或者:
|
||
|
||
```html
|
||
<span tabindex="0" @click="...">Open</span>
|
||
```
|
||
|
||
所以建议同时使用三种信号:
|
||
|
||
```text
|
||
DOM 信号:
|
||
tag、attributes、onclick、tabindex、contenteditable、form 结构
|
||
|
||
Accessibility 信号:
|
||
role、accessible name、checked、expanded、disabled、selected
|
||
|
||
Layout/visual 信号:
|
||
是否可见、是否被遮挡、是否有 bounding box、是否能接收事件
|
||
```
|
||
|
||
Chrome DevTools Protocol 的 `Accessibility.getFullAXTree` 可以拿到完整 Accessibility Tree;`DOMSnapshot.captureSnapshot` 可以返回 DOM、layout、style、iframe、template、shadow DOM flatten 后的信息,并且可以包含 DOM rects。citeturn592949view0 citeturn592949view1
|
||
|
||
---
|
||
|
||
## 4. iframe、Shadow DOM、不同前端框架的定位统一
|
||
|
||
不要让定位器直接依赖具体框架。应该定义一个中间层 `LocatorSpec`,再编译成 Playwright、Selenium 或 CDP 指令。
|
||
|
||
例如:
|
||
|
||
```json
|
||
{
|
||
"locatorSpec": {
|
||
"framePath": [
|
||
{
|
||
"strategy": "url",
|
||
"value": "https://pay.example.com/*"
|
||
}
|
||
],
|
||
"shadowPath": [
|
||
{
|
||
"host": "payment-widget"
|
||
}
|
||
],
|
||
"target": {
|
||
"role": "button",
|
||
"name": "Pay now",
|
||
"testId": "pay-now"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
编译成 Playwright:
|
||
|
||
```ts
|
||
page
|
||
.frameLocator('iframe[src*="pay.example.com"]')
|
||
.getByRole('button', { name: 'Pay now' });
|
||
```
|
||
|
||
对于 iframe,必须把 `framePath` 存下来。对于 Shadow DOM,也要保存 `shadowHost` 路径。Playwright 的 locator 默认可以穿透 open Shadow DOM,但 XPath 不会穿透 Shadow Root,closed Shadow Root 也不支持直接定位;这类限制要在拓扑里标注。citeturn773971view0
|
||
|
||
---
|
||
|
||
## 5. 如何拿到“所调用函数”
|
||
|
||
这里要分三种层次。
|
||
|
||
### 层次一:DOM event listener
|
||
|
||
可以用 CDP 的 `DOMDebugger.getEventListeners` 查询某个对象上的 event listeners;也可以用 `DOMDebugger.setEventListenerBreakpoint` 在特定事件上暂停。CDP 的 Debugger domain 会在暂停时给出 call stack,也就是 `callFrames`。citeturn592949view3 citeturn592949view4 citeturn633002view0
|
||
|
||
这可以得到类似:
|
||
|
||
```json
|
||
{
|
||
"event": "click",
|
||
"listener": "bound dispatchDiscreteEvent",
|
||
"scriptUrl": "static/js/main.abc123.js",
|
||
"line": 12345,
|
||
"column": 67
|
||
}
|
||
```
|
||
|
||
但在 React、Vue、Angular 里,这一层通常只能看到框架的事件分发函数,不一定直接看到业务函数。
|
||
|
||
---
|
||
|
||
### 层次二:运行时 call stack
|
||
|
||
执行动作时,打开 event listener breakpoint:
|
||
|
||
```text
|
||
click Save
|
||
→ debugger paused
|
||
→ 读取 callFrames
|
||
→ 跳过 react-dom / vue / angular / zone.js 等框架层
|
||
→ 找到第一个业务代码 frame
|
||
→ 结合 sourcemap 映射到源码
|
||
```
|
||
|
||
最终得到:
|
||
|
||
```json
|
||
{
|
||
"businessHandler": {
|
||
"functionName": "handleSave",
|
||
"sourceFile": "src/features/settings/SettingsForm.tsx",
|
||
"line": 88,
|
||
"confidence": 0.82
|
||
}
|
||
}
|
||
```
|
||
|
||
如果代码被压缩、没有 sourcemap,函数名可能只能得到:
|
||
|
||
```text
|
||
n
|
||
t
|
||
anonymous
|
||
```
|
||
|
||
这时要把置信度降下来,并更多依赖网络请求、DOM 变化和业务文本来描述动作语义。
|
||
|
||
---
|
||
|
||
### 层次三:API / 副作用追踪
|
||
|
||
对自动化更有价值的通常不是“函数名”,而是这个动作导致了什么副作用:
|
||
|
||
```text
|
||
发起了什么请求?
|
||
请求参数是什么?
|
||
路由是否变化?
|
||
localStorage/sessionStorage/cookie 是否变化?
|
||
是否打开弹窗?
|
||
是否新增 toast?
|
||
是否触发表单校验?
|
||
```
|
||
|
||
建议 hook 这些 API:
|
||
|
||
```text
|
||
fetch
|
||
XMLHttpRequest
|
||
HTMLFormElement.prototype.submit
|
||
history.pushState
|
||
history.replaceState
|
||
localStorage.setItem
|
||
sessionStorage.setItem
|
||
window.open
|
||
navigator.clipboard.writeText
|
||
```
|
||
|
||
Playwright 的 `page.addInitScript()` 可以在文档创建后、页面自身脚本运行前注入脚本,适合提前 patch `addEventListener`、`fetch`、XHR 等运行时行为。citeturn487582search0
|
||
|
||
---
|
||
|
||
## 6. 参数建模:输入框、下拉框、表单、API body
|
||
|
||
输入类元素需要单独建 `ParameterSpec`。
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_email_input",
|
||
"kind": "input",
|
||
"parameter": {
|
||
"name": "email",
|
||
"inputType": "email",
|
||
"required": true,
|
||
"pattern": null,
|
||
"minLength": null,
|
||
"maxLength": 255,
|
||
"examples": [
|
||
"user@example.com"
|
||
],
|
||
"validationMessages": [
|
||
"Please enter a valid email address"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
对于表单,可以把多个输入参数合并成一个动作参数:
|
||
|
||
```json
|
||
{
|
||
"edgeId": "edge_submit_login",
|
||
"action": {
|
||
"type": "submitForm",
|
||
"paramsSchema": {
|
||
"email": {
|
||
"type": "string",
|
||
"format": "email",
|
||
"required": true
|
||
},
|
||
"password": {
|
||
"type": "string",
|
||
"minLength": 8,
|
||
"required": true
|
||
}
|
||
}
|
||
},
|
||
"effects": {
|
||
"network": [
|
||
{
|
||
"method": "POST",
|
||
"url": "/api/login",
|
||
"bodyFields": ["email", "password"]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
参数来源可以按优先级推断:
|
||
|
||
```text
|
||
1. input type、name、placeholder、label、aria-label
|
||
2. required、pattern、min、max、maxlength
|
||
3. select/options/radio group/checkbox group
|
||
4. 前端校验错误文本
|
||
5. 请求 body 中的字段名
|
||
6. 服务端返回的 validation error
|
||
7. 历史成功样本
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 状态探索算法
|
||
|
||
不要无限爬。应该做有边界的状态搜索。
|
||
|
||
```text
|
||
Queue = [initialState]
|
||
|
||
while Queue not empty:
|
||
state = Queue.pop()
|
||
snapshot = capture(state)
|
||
elements = extractActionables(snapshot)
|
||
elements = rank(elements)
|
||
|
||
for element in elements:
|
||
if shouldSkip(element):
|
||
continue
|
||
|
||
before = captureFingerprint()
|
||
trace = startTrace()
|
||
|
||
performAction(element)
|
||
|
||
after = captureFingerprint()
|
||
effect = diff(before, after, trace)
|
||
|
||
edge = buildEdge(state, element, effect)
|
||
save(edge)
|
||
|
||
if effect.newState and not seen(effect.newState):
|
||
Queue.push(effect.newState)
|
||
```
|
||
|
||
`shouldSkip` 很关键,否则状态爆炸。建议跳过:
|
||
|
||
```text
|
||
外链
|
||
登出 / 删除 / 支付 / 提交订单 等高风险动作
|
||
重复列表项中的同构按钮
|
||
分页无限翻页
|
||
文件上传
|
||
下载
|
||
破坏性操作
|
||
需要验证码或 MFA 的动作
|
||
```
|
||
|
||
对于重复结构,例如表格中 100 行都有 “Edit” 按钮,不要存 100 个完全独立节点,而是抽象成模板:
|
||
|
||
```json
|
||
{
|
||
"template": "table-row-action",
|
||
"action": "Edit",
|
||
"rowSelector": "row by primary key",
|
||
"params": {
|
||
"rowId": "dynamic"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 状态指纹:如何判断“新状态”
|
||
|
||
状态不能只看 URL,因为 SPA 经常 URL 不变但弹窗、菜单、tab 已经变化。
|
||
|
||
建议状态指纹由这些组成:
|
||
|
||
```json
|
||
{
|
||
"canonicalUrl": "/dashboard",
|
||
"visibleRoleNamePairs": [
|
||
["button", "Save"],
|
||
["button", "Cancel"],
|
||
["heading", "Settings"]
|
||
],
|
||
"modalSignature": "settings-dialog",
|
||
"expandedControls": [
|
||
"user-menu"
|
||
],
|
||
"formSignature": [
|
||
"email",
|
||
"password"
|
||
],
|
||
"networkIdle": true
|
||
}
|
||
```
|
||
|
||
可以生成:
|
||
|
||
```text
|
||
stateHash = hash(
|
||
canonicalUrl +
|
||
visible interactive AX tree +
|
||
modal/dialog/menu stack +
|
||
selected tab +
|
||
major form fields
|
||
)
|
||
```
|
||
|
||
DOM hash 不能太细,否则每次时间戳、随机 id、广告位变化都会制造伪新状态。
|
||
|
||
---
|
||
|
||
## 9. 最小可行实现架构
|
||
|
||
可以分成五个模块:
|
||
|
||
```text
|
||
Crawler
|
||
负责打开页面、登录、BFS 探索状态
|
||
|
||
Snapshotter
|
||
负责采集 DOM、AX Tree、layout、frame、shadow 信息
|
||
|
||
Locator Generator
|
||
负责给每个元素生成多种 locator,并评分
|
||
|
||
Tracer
|
||
负责追踪 event listener、call stack、network、storage、route change
|
||
|
||
Graph Store
|
||
负责保存 State、Element、Locator、Action Edge、Parameter、Effect
|
||
```
|
||
|
||
推荐存储:
|
||
|
||
```text
|
||
开发期:SQLite / Postgres JSONB
|
||
复杂查询:Neo4j / ArangoDB
|
||
向量检索:把元素语义、文本、动作描述 embedding 后存入 vector DB
|
||
```
|
||
|
||
查询例子:
|
||
|
||
```sql
|
||
-- 找到“保存设置”相关动作
|
||
SELECT *
|
||
FROM action_edges
|
||
WHERE semantic_label ILIKE '%save%'
|
||
AND effects::text ILIKE '%settings%';
|
||
```
|
||
|
||
图查询例子:
|
||
|
||
```cypher
|
||
MATCH p = shortestPath(
|
||
(:State {name: "dashboard"})-[:ACTION*]->(:Element {accessibleName: "Save"})
|
||
)
|
||
RETURN p;
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Playwright 执行时如何利用拓扑
|
||
|
||
执行一个任务时:
|
||
|
||
```text
|
||
用户目标:修改通知设置并保存
|
||
|
||
1. 语义检索 action graph
|
||
2. 找到目标 edge: click Save Settings
|
||
3. 反查 shortest preconditionPath
|
||
4. 逐步打开菜单、弹窗、tab
|
||
5. 对目标元素使用最高分 locator
|
||
6. 如果失败,尝试下一个 locator
|
||
7. 执行动作
|
||
8. 用 expected effects 验证结果
|
||
9. 如果状态漂移,局部重新探索
|
||
```
|
||
|
||
伪代码:
|
||
|
||
```ts
|
||
async function executeEdge(page, graph, edgeId) {
|
||
const edge = graph.getEdge(edgeId);
|
||
const path = graph.shortestPathTo(edge.fromState);
|
||
|
||
for (const step of path) {
|
||
await runAction(page, graph.getEdge(step.edgeId));
|
||
await verifyState(page, step.toState);
|
||
}
|
||
|
||
const element = graph.getElement(edge.elementId);
|
||
const locator = chooseBestLocator(page, element.locators);
|
||
|
||
await perform(page, locator, edge.action);
|
||
await verifyEffects(page, edge.effects);
|
||
}
|
||
```
|
||
|
||
Playwright 在执行 `click()`、`fill()` 等动作前会做 actionability 检查,例如元素是否唯一、可见、稳定、可接收事件、enabled/editable 等;这类检查应当成为你的拓扑质量验证标准。citeturn773971view1
|
||
|
||
---
|
||
|
||
## 11. 一个简化版采集代码骨架
|
||
|
||
```ts
|
||
import { chromium, Page } from 'playwright';
|
||
|
||
type Actionable = {
|
||
tag: string;
|
||
role: string | null;
|
||
text: string;
|
||
name: string | null;
|
||
id: string | null;
|
||
testId: string | null;
|
||
placeholder: string | null;
|
||
ariaLabel: string | null;
|
||
bbox: { x: number; y: number; width: number; height: number };
|
||
};
|
||
|
||
async function installInstrumentation(page: Page) {
|
||
await page.addInitScript(() => {
|
||
const store = {
|
||
listeners: [],
|
||
fetches: [],
|
||
xhrs: [],
|
||
storage: [],
|
||
history: []
|
||
};
|
||
|
||
Object.defineProperty(window, '__actionTopoTrace', {
|
||
value: store,
|
||
configurable: false
|
||
});
|
||
|
||
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
||
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
||
try {
|
||
store.listeners.push({
|
||
type,
|
||
listenerName: listener && (listener as any).name,
|
||
targetTag: (this as Element).tagName || null,
|
||
targetId: (this as Element).id || null,
|
||
stack: new Error().stack
|
||
});
|
||
} catch {}
|
||
|
||
return originalAddEventListener.call(this, type, listener, options);
|
||
};
|
||
|
||
const originalFetch = window.fetch;
|
||
window.fetch = async function (...args) {
|
||
try {
|
||
store.fetches.push({
|
||
input: String(args[0]),
|
||
init: args[1] || null,
|
||
stack: new Error().stack
|
||
});
|
||
} catch {}
|
||
|
||
return originalFetch.apply(this, args as any);
|
||
};
|
||
|
||
const originalPushState = history.pushState;
|
||
history.pushState = function (...args) {
|
||
try {
|
||
store.history.push({
|
||
type: 'pushState',
|
||
args,
|
||
stack: new Error().stack
|
||
});
|
||
} catch {}
|
||
|
||
return originalPushState.apply(this, args as any);
|
||
};
|
||
});
|
||
}
|
||
|
||
async function extractActionables(page: Page): Promise<Actionable[]> {
|
||
return await page.evaluate(() => {
|
||
const selector = [
|
||
'button',
|
||
'a[href]',
|
||
'input',
|
||
'textarea',
|
||
'select',
|
||
'[role="button"]',
|
||
'[role="link"]',
|
||
'[role="menuitem"]',
|
||
'[role="checkbox"]',
|
||
'[role="radio"]',
|
||
'[role="tab"]',
|
||
'[contenteditable="true"]',
|
||
'[tabindex]:not([tabindex="-1"])',
|
||
'[onclick]',
|
||
'[aria-haspopup]',
|
||
'[aria-expanded]'
|
||
].join(',');
|
||
|
||
function isVisible(el: Element) {
|
||
const rect = el.getBoundingClientRect();
|
||
const style = window.getComputedStyle(el);
|
||
return (
|
||
rect.width > 0 &&
|
||
rect.height > 0 &&
|
||
style.visibility !== 'hidden' &&
|
||
style.display !== 'none' &&
|
||
Number(style.opacity) > 0
|
||
);
|
||
}
|
||
|
||
return Array.from(document.querySelectorAll(selector))
|
||
.filter(isVisible)
|
||
.map((el) => {
|
||
const rect = el.getBoundingClientRect();
|
||
|
||
return {
|
||
tag: el.tagName.toLowerCase(),
|
||
role: el.getAttribute('role'),
|
||
text: (el.textContent || '').trim().slice(0, 200),
|
||
name: el.getAttribute('name'),
|
||
id: el.id || null,
|
||
testId:
|
||
el.getAttribute('data-testid') ||
|
||
el.getAttribute('data-test') ||
|
||
el.getAttribute('data-qa'),
|
||
placeholder: el.getAttribute('placeholder'),
|
||
ariaLabel: el.getAttribute('aria-label'),
|
||
bbox: {
|
||
x: rect.x,
|
||
y: rect.y,
|
||
width: rect.width,
|
||
height: rect.height
|
||
}
|
||
};
|
||
});
|
||
});
|
||
}
|
||
|
||
async function main() {
|
||
const browser = await chromium.launch({ headless: false });
|
||
const context = await browser.newContext();
|
||
const page = await context.newPage();
|
||
|
||
await installInstrumentation(page);
|
||
|
||
await page.goto('https://example.com');
|
||
|
||
const cdp = await context.newCDPSession(page);
|
||
|
||
const axTree = await cdp.send('Accessibility.getFullAXTree');
|
||
const domSnapshot = await cdp.send('DOMSnapshot.captureSnapshot', {
|
||
computedStyles: [
|
||
'display',
|
||
'visibility',
|
||
'opacity',
|
||
'pointer-events'
|
||
],
|
||
includeDOMRects: true,
|
||
includePaintOrder: true
|
||
});
|
||
|
||
const elements = await extractActionables(page);
|
||
|
||
console.log({
|
||
elements,
|
||
axNodeCount: axTree.nodes.length,
|
||
documentCount: domSnapshot.documents.length
|
||
});
|
||
|
||
await browser.close();
|
||
}
|
||
|
||
main();
|
||
```
|
||
|
||
Playwright 的 `CDPSession` 可以直接调用 Chrome DevTools Protocol 方法,并订阅协议事件;这适合把 Playwright 的高层自动化能力和 CDP 的底层采集能力结合起来。citeturn576235search0
|
||
|
||
---
|
||
|
||
## 12. 拓扑图中的 Locator 评分公式
|
||
|
||
可以给每个 locator 算一个稳定性分数:
|
||
|
||
```text
|
||
score =
|
||
semanticScore
|
||
+ uniquenessScore
|
||
+ visibilityScore
|
||
+ stabilityScore
|
||
+ frameSpecificityScore
|
||
- brittlenessPenalty
|
||
```
|
||
|
||
示例:
|
||
|
||
```text
|
||
getByRole('button', { name: 'Save' })
|
||
semanticScore: 0.35
|
||
uniquenessScore: 0.25
|
||
stabilityScore: 0.20
|
||
total: 0.80
|
||
|
||
[data-testid="save-settings"]
|
||
contractScore: 0.40
|
||
uniquenessScore: 0.30
|
||
total: 0.90
|
||
|
||
#root > div:nth-child(2) > div > button:nth-child(3)
|
||
uniquenessScore: 0.25
|
||
brittlenessPenalty: -0.45
|
||
total: 0.10
|
||
```
|
||
|
||
同时要定期做 locator health check:
|
||
|
||
```text
|
||
locator 是否还能命中?
|
||
是否唯一?
|
||
命中元素的 role/name/text 是否和原元素一致?
|
||
bbox 是否大致相近?
|
||
点击后 effect 是否一致?
|
||
```
|
||
|
||
---
|
||
|
||
## 13. “前置打开项”应该怎么存
|
||
|
||
对于每个可操作项,存两种前置信息:
|
||
|
||
### 结构前置
|
||
|
||
表示它依赖哪个 UI 容器:
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_save_button",
|
||
"requiresVisibleAncestors": [
|
||
{
|
||
"kind": "dialog",
|
||
"name": "Settings"
|
||
},
|
||
{
|
||
"kind": "tabPanel",
|
||
"name": "Notifications"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 路径前置
|
||
|
||
表示从入口状态怎么走到这里:
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_save_button",
|
||
"shortestOpenPath": [
|
||
{
|
||
"action": "click",
|
||
"target": "User menu"
|
||
},
|
||
{
|
||
"action": "click",
|
||
"target": "Settings"
|
||
},
|
||
{
|
||
"action": "click",
|
||
"target": "Notifications tab"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
实际自动化执行时优先用 `shortestOpenPath`;如果路径失效,再用结构前置做局部恢复。
|
||
|
||
---
|
||
|
||
## 14. 最容易踩的坑
|
||
|
||
第一,**“全站所有可操作项”在严格意义上不可穷尽**。搜索框、分页、权限、A/B 实验、用户数据、无限滚动都会产生无穷状态。工程上应当做 bounded crawling、状态归并和模板化。
|
||
|
||
第二,**React/Vue/Angular 的真实业务函数不一定挂在 DOM 节点上**。很多事件是框架委托到根节点的,所以 `getEventListeners(button)` 未必能直接得到 `handleClick`。更可靠的方法是:动作执行时抓 call stack、network、DOM diff、storage diff。
|
||
|
||
第三,**不要把坐标当主定位方式**。坐标可以作为兜底,但 viewport、响应式布局、滚动位置、遮罩层都会让坐标失效。
|
||
|
||
第四,**不要把每次 DOM diff 都当新状态**。动态时间戳、随机 id、广告、埋点元素都会制造噪声。状态归并应基于“可见可交互语义树”,而不是完整 DOM。
|
||
|
||
第五,**不要盲点危险动作**。删除、支付、提交订单、发送邮件、改权限这类动作需要沙盒账户、mock 后端或人工白名单。
|
||
|
||
---
|
||
|
||
## 15. 推荐的最终数据结构
|
||
|
||
一个比较完整的 schema 可以是:
|
||
|
||
```text
|
||
State
|
||
stateId
|
||
url
|
||
route
|
||
title
|
||
frameContext
|
||
stateHash
|
||
visibleAxSignature
|
||
modalStack
|
||
openedByEdgeId
|
||
createdAt
|
||
|
||
Element
|
||
elementId
|
||
stateId
|
||
tag
|
||
role
|
||
accessibleName
|
||
text
|
||
label
|
||
placeholder
|
||
attributes
|
||
bbox
|
||
framePath
|
||
shadowPath
|
||
componentHint
|
||
interactability
|
||
|
||
Locator
|
||
locatorId
|
||
elementId
|
||
framework
|
||
strategy
|
||
expression
|
||
score
|
||
lastVerifiedAt
|
||
failureCount
|
||
|
||
ActionEdge
|
||
edgeId
|
||
fromState
|
||
toState
|
||
elementId
|
||
actionType
|
||
actionParams
|
||
preconditions
|
||
effects
|
||
riskLevel
|
||
confidence
|
||
|
||
HandlerTrace
|
||
traceId
|
||
edgeId
|
||
eventType
|
||
listeners
|
||
callFrames
|
||
businessFunction
|
||
sourceMappedLocation
|
||
confidence
|
||
|
||
NetworkEffect
|
||
edgeId
|
||
method
|
||
url
|
||
requestHeaders
|
||
requestBodySchema
|
||
responseStatus
|
||
responseBodySchema
|
||
|
||
ParameterSpec
|
||
elementId or edgeId
|
||
name
|
||
type
|
||
required
|
||
enum
|
||
pattern
|
||
min
|
||
max
|
||
examples
|
||
validationMessages
|
||
```
|
||
|
||
---
|
||
|
||
## 16. 实用落地顺序
|
||
|
||
最小可用版本可以这样做:
|
||
|
||
```text
|
||
第 1 步:只做已登录状态下的页面状态采集
|
||
第 2 步:抽取 button/link/input/select/textarea
|
||
第 3 步:生成 Playwright locators
|
||
第 4 步:点击低风险元素,记录 URL/DOM/AX/network diff
|
||
第 5 步:把“打开菜单/弹窗/tab”的路径存成 preconditionPath
|
||
第 6 步:接入 fetch/XHR/history/storage tracing
|
||
第 7 步:加入 call stack 和 sourcemap
|
||
第 8 步:做 locator 自愈和状态局部重探索
|
||
```
|
||
|
||
最终你会得到一个可以被自动化系统快速查询的图:
|
||
|
||
```text
|
||
目标:点击“导出报表”
|
||
→ 查询 Action Graph
|
||
→ 发现按钮在 Reports 页面 More 菜单里
|
||
→ 自动执行:
|
||
1. goto /reports
|
||
2. click More
|
||
3. click Export
|
||
→ 验证:
|
||
- 出现 download
|
||
- 或请求 GET /api/reports/export
|
||
```
|
||
|
||
这就是比传统 Page Object Model 更强的一点:它不是手写固定脚本,而是把网站的可操作结构、前置路径、定位器、函数/API 副作用都建成可检索、可验证、可恢复的拓扑。
|
||
|
||
|
||
### User Message 1449
|
||
|
||
在整个架构里面,你是否建议在里面使用大语言模型。如果是在局域网环境下,那么至少需要多少参数的模型才有效果?因为我们必须考虑,在模型性能不足的情况下,有模型效果会低于没有模型的效果
|
||
|
||
|
||
### Assistant Message 1379
|
||
|
||
建议使用大语言模型,但**不要把它放在浏览器自动化的执行核心里**。在这个架构中,LLM 应该是一个“语义增强层”和“候选生成器”,而不是“事实来源”或“最终执行器”。
|
||
|
||
我会给一个明确结论:
|
||
|
||
> **局域网环境下,如果只是做可操作项语义标注,7B/8B 可以试用;如果要参与任务到动作的映射,建议至少 14B;如果要参与多步路径规划、失败恢复、参数推断,建议 32B 起步;如果要做跨网站、长链路、低人工干预的通用浏览器代理,70B 级别才比较稳。**
|
||
|
||
如果只能选一个生产起步点:
|
||
|
||
> **14B 是最低可用线,32B 是更合理的工程线。低于 7B/8B,不建议让模型参与任何会影响点击、填写、提交的决策。**
|
||
|
||
---
|
||
|
||
## 1. 我建议使用 LLM,但只放在这些位置
|
||
|
||
在你前面描述的“网站可操作项拓扑”架构中,确定性系统应该负责:
|
||
|
||
```text
|
||
DOM / AX Tree 采集
|
||
iframe / shadow DOM 路径
|
||
locator 生成
|
||
actionability 检查
|
||
网络请求追踪
|
||
storage / route / DOM diff
|
||
状态图构建
|
||
动作执行
|
||
效果验证
|
||
风险控制
|
||
```
|
||
|
||
LLM 适合负责:
|
||
|
||
```text
|
||
元素语义理解
|
||
业务意图归类
|
||
自然语言任务到 Action Edge 的匹配
|
||
参数含义推断
|
||
表单字段解释
|
||
相似动作合并
|
||
失败后的候选修复建议
|
||
页面状态摘要
|
||
风险等级初判
|
||
```
|
||
|
||
也就是说,LLM 可以回答:
|
||
|
||
```text
|
||
“这个按钮大概率是导出报表”
|
||
“这个输入框应该对应客户编号”
|
||
“用户说的‘下载账单’可能对应 edge_export_invoice”
|
||
“这个失败可能是因为菜单没有展开”
|
||
```
|
||
|
||
但不应该直接拥有最终权限去做:
|
||
|
||
```text
|
||
click(x, y)
|
||
fill(selector, value)
|
||
submit form
|
||
delete record
|
||
confirm payment
|
||
change permission
|
||
```
|
||
|
||
最终执行必须由确定性执行器完成,并且要通过 locator 唯一性、可见性、enabled、effect verification 等校验。
|
||
|
||
---
|
||
|
||
## 2. 为什么不能完全不用 LLM?
|
||
|
||
因为纯规则系统在这些地方会很吃力:
|
||
|
||
```text
|
||
“Export” / “Download” / “导出” / “下载报表” 是同一类动作
|
||
“客户编号” 和 “Customer ID” 是同一类字段
|
||
一个菜单项虽然叫 “More”,但里面真正有 “Export”
|
||
按钮文案是图标,没有 text,但周围上下文暗示它是“编辑”
|
||
请求 POST /api/report/export 和 UI 上的“下载”语义相关
|
||
```
|
||
|
||
这些都是语义问题,不是 DOM 问题。
|
||
|
||
Web agent 研究也说明,仅把完整 HTML 丢给模型并不好;Mind2Web 论文明确指出真实网页 HTML 经常过大,先过滤候选元素能显著改善 LLM 在网页任务上的效果和效率。你的拓扑图方案本质上就是在做这种“先结构化、再让模型理解”的架构。citeturn694831search1
|
||
|
||
---
|
||
|
||
## 3. 为什么也不能过度依赖 LLM?
|
||
|
||
因为浏览器自动化的错误成本很高。LLM 在网页上最危险的错误通常不是“不知道”,而是**看起来很合理地选错**。
|
||
|
||
典型错误包括:
|
||
|
||
```text
|
||
把“取消”理解成“确认”
|
||
把隐藏菜单里的旧按钮当成当前按钮
|
||
把列表第一行的 Edit 当成目标行的 Edit
|
||
把 Disabled 按钮当成可点击按钮
|
||
把“导出模板”当成“导出数据”
|
||
误以为某个 modal 已经打开
|
||
幻觉出页面上不存在的按钮
|
||
```
|
||
|
||
WebArena 的实验也说明真实网页任务对自动化代理仍然很难;论文中的最佳 GPT-4-based agent 端到端任务成功率只有 14.41%,远低于人类 78.24%。这不是说 LLM 没用,而是说明**不能把 LLM 当成无约束网页执行器**。citeturn694831academia38
|
||
|
||
所以你的担心是对的:
|
||
|
||
> **在模型能力不足、验证不足、错误动作成本较高时,有模型确实可能低于没有模型。**
|
||
|
||
---
|
||
|
||
## 4. 建议的 LLM 放置方式
|
||
|
||
我建议把 LLM 做成独立服务:
|
||
|
||
```text
|
||
Topology Engine
|
||
↓
|
||
Candidate Retriever
|
||
↓
|
||
LLM Semantic Layer
|
||
↓
|
||
Validator / Verifier
|
||
↓
|
||
Deterministic Executor
|
||
```
|
||
|
||
LLM 只输出候选,不直接执行。
|
||
|
||
例如用户目标是:
|
||
|
||
```text
|
||
下载上个月销售报表
|
||
```
|
||
|
||
系统应该这样工作:
|
||
|
||
```text
|
||
1. 图数据库检索候选 Action Edge
|
||
2. LLM 只在候选边中排序,不允许自由编造动作
|
||
3. Validator 检查:
|
||
- edge 是否存在
|
||
- locator 是否唯一
|
||
- 前置路径是否可达
|
||
- action 是否低风险
|
||
- 参数是否满足 schema
|
||
4. 执行器按图执行
|
||
5. effect verifier 验证是否真的触发下载或请求
|
||
```
|
||
|
||
LLM 输出应该类似:
|
||
|
||
```json
|
||
{
|
||
"selectedEdgeId": "edge_export_sales_report",
|
||
"reason": "用户目标中的下载销售报表与该 edge 的 accessibleName、network effect、页面上下文匹配",
|
||
"confidence": 0.84,
|
||
"requiredPreconditions": [
|
||
"进入 Reports 页面",
|
||
"打开 Sales tab",
|
||
"设置日期范围为上个月"
|
||
],
|
||
"abstain": false
|
||
}
|
||
```
|
||
|
||
而不是输出:
|
||
|
||
```json
|
||
{
|
||
"click": "#app > div:nth-child(3) > button"
|
||
}
|
||
```
|
||
|
||
后者非常危险。
|
||
|
||
---
|
||
|
||
## 5. 参数量建议:按任务分级
|
||
|
||
### 总表
|
||
|
||
| 使用场景 | 是否建议用 LLM | 最低参数量 | 推荐参数量 | 低于这个规模的风险 |
|
||
|---|---:|---:|---:|---|
|
||
| 元素语义标注 | 建议 | 7B/8B | 14B | 标签粗糙,但可控 |
|
||
| 表单字段含义推断 | 建议 | 7B/8B | 14B/32B | 容易误判业务字段 |
|
||
| 自然语言目标 → 已有 Action Edge | 建议 | 14B | 32B | 容易选错相似动作 |
|
||
| 多步路径规划 | 谨慎使用 | 32B | 70B | 容易漏前置状态 |
|
||
| 自动失败恢复 | 谨慎使用 | 32B | 70B | 容易越修越错 |
|
||
| 自主探索未知网页 | 只限沙盒 | 32B | 70B+ | 状态爆炸、误点风险高 |
|
||
| 基于截图理解 UI | 需要 VLM | 8B VLM | 32B VLM | 视觉 grounding 不稳 |
|
||
| 危险动作:删除、付款、授权 | 不建议全自动 | 70B 也需人工/规则门控 | 70B+ | 不接受模型单独决策 |
|
||
|
||
---
|
||
|
||
## 6. 一个实用判断:14B 是“最低可用线”,32B 是“工程舒适线”
|
||
|
||
对于你的场景,我会这样定:
|
||
|
||
### 7B/8B:只能做辅助标注
|
||
|
||
可以用于:
|
||
|
||
```text
|
||
给按钮生成中文/英文语义标签
|
||
把相似按钮聚类
|
||
生成候选 description
|
||
解释 input 字段
|
||
把 URL / API / 文本摘要成业务对象
|
||
```
|
||
|
||
不建议用于:
|
||
|
||
```text
|
||
选择最终动作
|
||
复杂路径规划
|
||
失败恢复
|
||
危险操作判断
|
||
```
|
||
|
||
原因是 7B/8B 模型在简单语义上通常够用,但在多约束、多步推理、相似项区分时容易出错。对你的系统来说,7B/8B 的正确用法是:
|
||
|
||
```text
|
||
LLM enriches graph metadata, but does not control browser.
|
||
```
|
||
|
||
---
|
||
|
||
### 14B:可以参与“候选排序”和“意图映射”
|
||
|
||
14B 是我认为局域网部署的最低可用线。它可以用于:
|
||
|
||
```text
|
||
用户目标 → 候选 edge 排序
|
||
元素语义归类
|
||
参数 schema 推断
|
||
菜单/弹窗前置路径解释
|
||
把 trace / network effect 翻译成业务动作
|
||
```
|
||
|
||
但仍然要加硬验证:
|
||
|
||
```text
|
||
候选 edge 必须来自图数据库
|
||
不能生成不存在的 locator
|
||
不能跳过 actionability 检查
|
||
不能执行高风险动作
|
||
confidence 低时必须 abstain
|
||
```
|
||
|
||
当前开源模型里,Qwen3-14B 这类 14B 级别模型已经具备比较强的中文、多语言、工具调用和推理能力。Qwen3-14B 模型卡给出的参数量是 14.8B,并且原生上下文长度为 32,768 tokens,可通过 YaRN 扩展到 131,072 tokens。citeturn968736view0
|
||
|
||
---
|
||
|
||
### 32B:更适合生产自动化
|
||
|
||
如果你希望模型真正参与:
|
||
|
||
```text
|
||
复杂页面状态理解
|
||
多候选动作排序
|
||
失败恢复建议
|
||
表单参数推断
|
||
跨页面 workflow 规划
|
||
工具调用
|
||
```
|
||
|
||
我会建议直接上 32B。
|
||
|
||
Qwen3-32B 模型卡给出的参数量是 32.8B,并强调其 reasoning、instruction-following、agent capabilities、多语言能力和工具集成能力;它同样原生支持 32,768 tokens 上下文,并可通过 YaRN 扩展。citeturn255948view0
|
||
|
||
在你的架构里,32B 的价值不是“能不能聊天”,而是它更可能稳定完成这种任务:
|
||
|
||
```text
|
||
给定:
|
||
- 用户目标
|
||
- 当前 State
|
||
- 20 个候选 Action Edge
|
||
- 每个 edge 的 accessibleName、role、effect、preconditionPath、riskLevel
|
||
|
||
要求:
|
||
- 选择最可能的 edge
|
||
- 解释选择依据
|
||
- 给出缺失参数
|
||
- 在证据不足时 abstain
|
||
```
|
||
|
||
这个任务对 7B/8B 来说容易不稳定,对 14B 勉强可用,对 32B 更像工程上可接受。
|
||
|
||
---
|
||
|
||
### 70B:用于跨网站通用代理和复杂恢复
|
||
|
||
如果目标不是单个业务系统,而是很多不同网站、很多未知页面、长链路任务、低人工干预,那么 70B 级别更合适。
|
||
|
||
70B 级别模型的缺点是部署成本、吞吐、显存压力都更高。以 Llama 3.3 70B 为例,其模型卡显示它是 70B text-only multilingual instruction-tuned model,并支持 128k context。citeturn557352search1
|
||
|
||
但我仍然不建议让 70B 模型绕过验证直接控制浏览器。70B 只是降低错误概率,不改变架构原则。
|
||
|
||
---
|
||
|
||
## 7. 如果涉及截图或视觉 UI,单独考虑 VLM
|
||
|
||
如果你能拿到 DOM、Accessibility Tree、layout、network trace,那么**优先不要依赖截图模型**。DOM/AX 通常更可验证、更稳定、更便宜。
|
||
|
||
但有些场景需要视觉模型:
|
||
|
||
```text
|
||
canvas UI
|
||
图标按钮没有文本
|
||
复杂图表区域
|
||
截图式后台系统
|
||
远程桌面或 Citrix
|
||
closed shadow root
|
||
无法读取语义 DOM 的第三方控件
|
||
```
|
||
|
||
这时需要 VLM。Qwen3-VL 技术报告中提到该系列包含 dense 2B/4B/8B/32B 和 MoE 30B-A3B/235B-A22B 等规模,面向图文/视频和长上下文多模态任务。citeturn621338academia42
|
||
|
||
我的建议是:
|
||
|
||
```text
|
||
简单截图理解:8B VLM 可以试
|
||
生产级 UI grounding:32B VLM 起步
|
||
高风险点击:不要只靠 VLM 坐标,必须结合 DOM/AX/截图标注/验证
|
||
```
|
||
|
||
---
|
||
|
||
## 8. “模型低于没有模型”的具体触发条件
|
||
|
||
你可以用下面这个标准判断是否应该禁用模型。
|
||
|
||
如果模型出现这些情况,就不应该进入执行链路:
|
||
|
||
```text
|
||
1. 经常选择不存在的元素
|
||
2. 经常把相似按钮混淆
|
||
3. 无法稳定输出 JSON schema
|
||
4. 不会 abstain,证据不足也强行回答
|
||
5. 对危险动作风险识别不稳定
|
||
6. 生成的前置路径不可执行
|
||
7. 延迟过高,导致整体自动化吞吐下降
|
||
8. 修复建议导致更多重试和状态污染
|
||
```
|
||
|
||
尤其是第 4 点很关键。
|
||
|
||
一个弱模型最大的问题不是“不知道”,而是:
|
||
|
||
```text
|
||
不知道,但仍然给出看起来合理的答案。
|
||
```
|
||
|
||
这种模型对自动化系统是负资产。
|
||
|
||
---
|
||
|
||
## 9. 正确的上线策略:先 shadow mode
|
||
|
||
我建议按这四个阶段上线。
|
||
|
||
### 阶段 1:完全不用 LLM,建立 baseline
|
||
|
||
先做纯确定性版本:
|
||
|
||
```text
|
||
采集拓扑
|
||
生成 locator
|
||
记录前置路径
|
||
记录 network effect
|
||
人工给部分 action edge 打标签
|
||
执行固定 workflow
|
||
统计成功率
|
||
```
|
||
|
||
这一步很重要,因为你需要知道“没有模型时”到底能做到什么程度。
|
||
|
||
---
|
||
|
||
### 阶段 2:LLM 只做旁路标注,不影响执行
|
||
|
||
LLM 只生成:
|
||
|
||
```text
|
||
semanticLabel
|
||
businessObject
|
||
riskLevelCandidate
|
||
parameterMeaning
|
||
edgeDescription
|
||
```
|
||
|
||
但这些字段不能影响执行。
|
||
|
||
例如:
|
||
|
||
```json
|
||
{
|
||
"edgeId": "edge_btn_8273",
|
||
"llmSemanticLabel": "导出销售报表",
|
||
"llmRiskCandidate": "read_only",
|
||
"llmConfidence": 0.78
|
||
}
|
||
```
|
||
|
||
然后人工或规则抽样评估。
|
||
|
||
---
|
||
|
||
### 阶段 3:LLM 参与候选排序,但执行器强验证
|
||
|
||
用户说:
|
||
|
||
```text
|
||
导出销售报表
|
||
```
|
||
|
||
系统先用向量检索/关键词/图查询找出 20 个候选 edge,再让 LLM 排序。
|
||
|
||
LLM 不允许生成新 edge,只能从候选中选:
|
||
|
||
```json
|
||
{
|
||
"allowedEdgeIds": [
|
||
"edge_export_sales",
|
||
"edge_export_template",
|
||
"edge_download_invoice",
|
||
"edge_open_report_menu"
|
||
]
|
||
}
|
||
```
|
||
|
||
如果 LLM 选了不存在的 edge,直接拒绝。
|
||
|
||
---
|
||
|
||
### 阶段 4:LLM 参与失败恢复,但只能提出修复候选
|
||
|
||
例如执行失败:
|
||
|
||
```text
|
||
locator 命中 0 个元素
|
||
```
|
||
|
||
LLM 可以建议:
|
||
|
||
```text
|
||
可能需要先打开 More 菜单
|
||
可能当前 tab 不对
|
||
可能页面语言切换为英文
|
||
可能按钮被移动到右上角菜单
|
||
```
|
||
|
||
但系统仍然要验证:
|
||
|
||
```text
|
||
这个菜单是否存在?
|
||
打开菜单后目标按钮是否出现?
|
||
新 locator 是否唯一?
|
||
点击后 effect 是否符合原 edge?
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 建议的模型门控机制
|
||
|
||
每次 LLM 输出后都做 gate:
|
||
|
||
```text
|
||
LLM Output
|
||
↓
|
||
Schema Validator
|
||
↓
|
||
Candidate Existence Check
|
||
↓
|
||
Locator Uniqueness Check
|
||
↓
|
||
Actionability Check
|
||
↓
|
||
Risk Policy Check
|
||
↓
|
||
Effect Verifier
|
||
↓
|
||
Execute or Reject
|
||
```
|
||
|
||
可以给每个候选动作计算综合分:
|
||
|
||
```text
|
||
finalScore =
|
||
retrievalScore * 0.25
|
||
+ locatorHealthScore * 0.20
|
||
+ effectMatchScore * 0.25
|
||
+ llmReasoningScore * 0.15
|
||
+ historicalSuccessScore * 0.15
|
||
- riskPenalty
|
||
```
|
||
|
||
注意:`llmReasoningScore` 不能占太高。我的建议是不超过 15% 到 25%。
|
||
|
||
LLM 应该是加分项,不是决定项。
|
||
|
||
---
|
||
|
||
## 11. 关键原则:LLM 不写入事实,只写入解释和候选
|
||
|
||
图数据库中应该区分:
|
||
|
||
```text
|
||
verified fields
|
||
derived fields
|
||
llm fields
|
||
```
|
||
|
||
例如:
|
||
|
||
```json
|
||
{
|
||
"elementId": "el_123",
|
||
"verified": {
|
||
"role": "button",
|
||
"accessibleName": "Export",
|
||
"locator": "page.getByRole('button', { name: 'Export' })",
|
||
"framePath": [],
|
||
"networkEffect": "GET /api/reports/export"
|
||
},
|
||
"llm": {
|
||
"semanticLabel": "导出报表",
|
||
"businessObject": "report",
|
||
"riskGuess": "read_only",
|
||
"confidence": 0.82
|
||
}
|
||
}
|
||
```
|
||
|
||
其中 `verified` 来自 DOM、AX、CDP、Playwright、network trace。
|
||
|
||
`llm` 只能用于检索、排序、解释、候选生成,不能直接覆盖 verified facts。
|
||
|
||
---
|
||
|
||
## 12. 局域网部署时的粗略硬件判断
|
||
|
||
只按推理权重估算,4-bit 量化大致可以这么看:
|
||
|
||
| 模型规模 | 4-bit 权重粗略体积 | 工程建议 |
|
||
|---:|---:|---|
|
||
| 7B/8B | 4–6 GB | 单卡轻量辅助 |
|
||
| 14B | 8–10 GB | 最低可用生产线 |
|
||
| 32B | 18–24 GB | 推荐生产线 |
|
||
| 70B | 40–50 GB+ | 高质量但成本高 |
|
||
|
||
实际还要加:
|
||
|
||
```text
|
||
KV cache
|
||
batch size
|
||
context length
|
||
并发数
|
||
推理框架开销
|
||
显存碎片
|
||
量化格式
|
||
```
|
||
|
||
对于局域网服务,我会优先考虑:
|
||
|
||
```text
|
||
14B:低成本语义服务
|
||
32B:主力语义/规划服务
|
||
70B:复杂任务、人工助手、疑难恢复
|
||
```
|
||
|
||
可以用统一的 OpenAI-compatible API 封装,例如 vLLM、SGLang、Ollama、llama.cpp server 等。Qwen3 模型卡也给出了 vLLM、SGLang、本地应用和 OpenAI-compatible endpoint 的部署方式。citeturn255948view0
|
||
|
||
---
|
||
|
||
## 13. 我建议的实际组合
|
||
|
||
### 方案 A:保守生产方案
|
||
|
||
```text
|
||
确定性拓扑引擎:必须有
|
||
LLM:14B
|
||
用途:标注、候选排序、参数解释
|
||
执行:完全由规则和图完成
|
||
```
|
||
|
||
适合:
|
||
|
||
```text
|
||
企业内网系统
|
||
固定业务流程
|
||
风险较低
|
||
预算有限
|
||
```
|
||
|
||
---
|
||
|
||
### 方案 B:推荐生产方案
|
||
|
||
```text
|
||
确定性拓扑引擎:必须有
|
||
LLM:32B
|
||
Embedding 模型:单独部署
|
||
可选 reranker:小模型
|
||
用途:
|
||
- 目标到 action edge 匹配
|
||
- 多步 workflow 排序
|
||
- 失败恢复建议
|
||
- 参数 schema 推断
|
||
执行:
|
||
- 仍由 deterministic executor 完成
|
||
```
|
||
|
||
适合:
|
||
|
||
```text
|
||
多个业务系统
|
||
页面变化频繁
|
||
需要自然语言驱动
|
||
需要较高成功率
|
||
```
|
||
|
||
---
|
||
|
||
### 方案 C:高能力方案
|
||
|
||
```text
|
||
32B 作为在线主力
|
||
70B 作为疑难任务 fallback
|
||
8B/14B 用于批量离线标注
|
||
VLM 仅用于 DOM 不可读页面
|
||
```
|
||
|
||
适合:
|
||
|
||
```text
|
||
跨系统自动化平台
|
||
多租户
|
||
复杂后台
|
||
未知页面较多
|
||
```
|
||
|
||
---
|
||
|
||
## 14. 必须做自己的评测集
|
||
|
||
不要只看通用 benchmark。你的任务很特殊,应该建立自己的 browser automation benchmark。
|
||
|
||
至少准备:
|
||
|
||
```text
|
||
100–300 个元素语义标注样本
|
||
100–300 个自然语言目标到 edge 的匹配样本
|
||
50–100 个多步 workflow 样本
|
||
50 个失败恢复样本
|
||
20–50 个高风险拒绝样本
|
||
```
|
||
|
||
评估指标:
|
||
|
||
```text
|
||
schema_valid_rate
|
||
top1_edge_accuracy
|
||
top3_edge_recall
|
||
wrong_action_rate
|
||
abstain_precision
|
||
abstain_recall
|
||
locator_repair_success_rate
|
||
effect_verification_success_rate
|
||
latency_p50
|
||
latency_p95
|
||
tokens_per_second
|
||
human_intervention_rate
|
||
```
|
||
|
||
我会设这样的上线门槛:
|
||
|
||
```text
|
||
schema_valid_rate >= 99%
|
||
top3_edge_recall >= 95%
|
||
top1_edge_accuracy >= 90% 低风险任务
|
||
wrong_action_rate <= 0.5% 低风险任务
|
||
高风险动作 wrong_action_rate 必须接近 0
|
||
abstain 行为必须稳定
|
||
LLM 方案整体成功率至少比 no-LLM baseline 高 5–10 个百分点
|
||
```
|
||
|
||
如果达不到这些指标,就不要让模型进入执行链路。
|
||
|
||
---
|
||
|
||
## 15. 最重要的工程结论
|
||
|
||
你的系统里,LLM 的正确角色是:
|
||
|
||
```text
|
||
语义层
|
||
检索增强层
|
||
候选排序层
|
||
解释层
|
||
修复建议层
|
||
```
|
||
|
||
不是:
|
||
|
||
```text
|
||
DOM 事实来源
|
||
locator 事实来源
|
||
浏览器执行器
|
||
风险最终裁决器
|
||
状态图唯一构建者
|
||
```
|
||
|
||
如果局域网部署,我的建议是:
|
||
|
||
```text
|
||
MVP:不用 LLM 或只用 7B/8B 做旁路标注
|
||
第一版生产:14B 起步
|
||
推荐生产:32B
|
||
复杂通用代理:70B 级别,但仍需强验证
|
||
```
|
||
|
||
所以,针对你的问题,最直接的回答是:
|
||
|
||
> **要用 LLM,但要把它关在“可验证候选生成器”的笼子里。14B 是最低可用线,32B 是我会推荐的局域网生产线。低于 7B/8B 的模型,不如不用;低于 14B 的模型,不要参与动作选择;低于 32B 的模型,不要承担复杂路径规划和失败恢复。**
|
||
|