# 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()` 等方式。citeturn773971view0 --- ### 第三层:定位器 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。citeturn773971view0 --- ### 第四层:动作边 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
Submit
``` 或者: ```html Open ``` 所以建议同时使用三种信号: ```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。citeturn592949view0 citeturn592949view1 --- ## 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 也不支持直接定位;这类限制要在拓扑里标注。citeturn773971view0 --- ## 5. 如何拿到“所调用函数” 这里要分三种层次。 ### 层次一:DOM event listener 可以用 CDP 的 `DOMDebugger.getEventListeners` 查询某个对象上的 event listeners;也可以用 `DOMDebugger.setEventListenerBreakpoint` 在特定事件上暂停。CDP 的 Debugger domain 会在暂停时给出 call stack,也就是 `callFrames`。citeturn592949view3 citeturn592949view4 citeturn633002view0 这可以得到类似: ```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 等运行时行为。citeturn487582search0 --- ## 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 等;这类检查应当成为你的拓扑质量验证标准。citeturn773971view1 --- ## 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 { 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 的底层采集能力结合起来。citeturn576235search0 --- ## 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 在网页任务上的效果和效率。你的拓扑图方案本质上就是在做这种“先结构化、再让模型理解”的架构。citeturn694831search1 --- ## 3. 为什么也不能过度依赖 LLM? 因为浏览器自动化的错误成本很高。LLM 在网页上最危险的错误通常不是“不知道”,而是**看起来很合理地选错**。 典型错误包括: ```text 把“取消”理解成“确认” 把隐藏菜单里的旧按钮当成当前按钮 把列表第一行的 Edit 当成目标行的 Edit 把 Disabled 按钮当成可点击按钮 把“导出模板”当成“导出数据” 误以为某个 modal 已经打开 幻觉出页面上不存在的按钮 ``` WebArena 的实验也说明真实网页任务对自动化代理仍然很难;论文中的最佳 GPT-4-based agent 端到端任务成功率只有 14.41%,远低于人类 78.24%。这不是说 LLM 没用,而是说明**不能把 LLM 当成无约束网页执行器**。citeturn694831academia38 所以你的担心是对的: > **在模型能力不足、验证不足、错误动作成本较高时,有模型确实可能低于没有模型。** --- ## 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。citeturn968736view0 --- ### 32B:更适合生产自动化 如果你希望模型真正参与: ```text 复杂页面状态理解 多候选动作排序 失败恢复建议 表单参数推断 跨页面 workflow 规划 工具调用 ``` 我会建议直接上 32B。 Qwen3-32B 模型卡给出的参数量是 32.8B,并强调其 reasoning、instruction-following、agent capabilities、多语言能力和工具集成能力;它同样原生支持 32,768 tokens 上下文,并可通过 YaRN 扩展。citeturn255948view0 在你的架构里,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。citeturn557352search1 但我仍然不建议让 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 等规模,面向图文/视频和长上下文多模态任务。citeturn621338academia42 我的建议是: ```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 的部署方式。citeturn255948view0 --- ## 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 的模型,不要承担复杂路径规划和失败恢复。**