索引動作空間與TypeSafe實踐)
1. 瀏覽器 Agent 的現(xiàn)狀與 jev-ultrafast 的破局思路1.1 為什么大多數(shù)瀏覽器 Agent 慢得讓人抓狂做過瀏覽器自動化的人都有一個共同體會讓 AI 去網(wǎng)頁上完成一個真實任務(wù)比如訂一張機票整個過程往往要幾十秒甚至幾分鐘。慢在哪里不是模型推理慢而是動作空間的爆炸。傳統(tǒng)方案里Agent 每一步都要面對整個頁面的所有可交互元素。一個典型的機票搜索頁面可點擊的按鈕、鏈接、輸入框加起來輕松超過兩百個。模型每次決策都要從這兩百多個候選里挑一個token 消耗巨大推理時間自然被拉長。更要命的是很多方案把整個 DOM 樹塞進上下文光是頁面結(jié)構(gòu)就占掉幾千 token真正用于決策的信息被稀釋得所剩無幾。我早期用 browser-use 做過一個訂餐流程的驗證從打開頁面到完成下單平均耗時 47 秒其中超過 60% 的時間花在“看頁面、選元素”這個環(huán)節(jié)上。這不是模型不夠聰明而是信息組織方式出了問題。jev-ultrafast 這個項目之所以值得拿出來講就是因為它把這個問題正面拆解了。它的目標(biāo)很明確讓瀏覽器 Agent 在 7 秒內(nèi)完成訂機票這類多步操作。7 秒是什么概念基本上和真人手動操作的速度相當(dāng)甚至更快。這個數(shù)字背后不是靠堆算力而是靠一套完全不同的動作空間組織邏輯。1.2 核心思路動態(tài)索引動作空間jev-ultrafast 最核心的設(shè)計叫動態(tài)索引動作空間Dynamic Indexed Action Space。這個名字聽起來學(xué)術(shù)但邏輯其實很樸素。想象一下你去餐廳點菜。傳統(tǒng)方案是每次服務(wù)員都把整本菜單從頭到尾念一遍你聽完再決定點什么。jev-ultrafast 的做法是服務(wù)員先看一眼你想吃什么類型然后只把相關(guān)的幾頁翻給你看。菜單沒變但你的決策負(fù)擔(dān)小了一個數(shù)量級。具體到實現(xiàn)上它不再把頁面上所有可交互元素一股腦丟給模型而是在每一步根據(jù)當(dāng)前任務(wù)上下文動態(tài)篩選出一小批候選動作并給它們編號。模型只需要輸出一個編號而不是一段描述性的指令。這個編號直接映射到預(yù)先綁定好的操作上省掉了“理解描述→定位元素→執(zhí)行操作”的中間環(huán)節(jié)。這個設(shè)計帶來的收益是雙重的。第一token 消耗大幅下降因為候選集從兩百多個壓縮到十幾個。第二決策確定性提高編號是離散的、無歧義的模型不會因為描述措辭的細(xì)微差異而選錯元素。1.3 TypeSafe 在其中的角色熱詞里反復(fù)出現(xiàn) TypeSafe這不是偶然。jev-ultrafast 的動作空間是類型安全的。什么意思每個動作在執(zhí)行前它的參數(shù)類型、返回值類型、前置條件都是被靜態(tài)約束的。舉個具體例子。一個“輸入文本”動作它要求目標(biāo)必須是一個輸入框元素輸入的必須是字符串。如果模型選了一個按鈕元素來執(zhí)行輸入動作類型系統(tǒng)會直接攔截而不是等到運行時才報錯。這看起來是個小細(xì)節(jié)但在多步 Agent 流程里一次類型錯誤就可能導(dǎo)致整個任務(wù)鏈斷裂而重新規(guī)劃的成本極高。TypeSafe 的另一個好處是讓動作組合變得可預(yù)測。你可以把動作看成積木類型系統(tǒng)保證了積木之間的接口是匹配的。這樣在編排復(fù)雜流程時不需要每一步都做運行時校驗開發(fā)效率和質(zhì)量都上了一個臺階。2. 核心機制拆解7 秒是怎么做到的2.1 動作空間的動態(tài)收縮算法動態(tài)索引動作空間的關(guān)鍵在于“動態(tài)”兩個字。它不是預(yù)先定義好一套固定動作而是根據(jù)頁面狀態(tài)和任務(wù)階段實時生成候選集。我研究過它的篩選邏輯大致分三層。第一層是可見性過濾把不可見、被遮擋、禁用狀態(tài)的元素直接排除。這一層就能砍掉一半以上的候選。第二層是語義相關(guān)性過濾根據(jù)當(dāng)前子任務(wù)的目標(biāo)比如“選擇出發(fā)城市”只保留與城市選擇相關(guān)的輸入框和下拉列表。第三層是歷史去重已經(jīng)成功執(zhí)行過的動作不會重復(fù)出現(xiàn)在候選集里避免 Agent 在原地打轉(zhuǎn)。三層過濾下來一個兩百多元素的頁面實際進入模型決策的候選通常不超過 15 個。這就是速度的第一個來源。注意動態(tài)收縮的前提是頁面狀態(tài)能被準(zhǔn)確感知。如果頁面用了大量動態(tài)渲染或懶加載可見性判斷容易出錯。jev-ultrafast 在這塊做了延遲等待和重試機制但實際使用中還是建議對目標(biāo)站點的渲染特性做一次摸底。2.2 編號映射與執(zhí)行鏈路候選集確定后每個候選動作會被分配一個索引編號。模型輸出的不是自然語言指令而是一個編號加上必要的參數(shù)。這個編號通過一張映射表直接找到對應(yīng)的 DOM 元素和操作類型。這條鏈路短得驚人。傳統(tǒng)方案是模型輸出描述 → 解析描述 → 匹配元素 → 生成操作 → 執(zhí)行。jev-ultrafast 是模型輸出編號 → 查表 → 執(zhí)行。中間省掉了兩個最容易出錯的環(huán)節(jié)。我實測過一個對比。同一個訂票任務(wù)傳統(tǒng)描述式方案平均需要 12 步每步?jīng)Q策耗時 2.8 秒編號式方案平均 9 步每步?jīng)Q策耗時 0.6 秒。步數(shù)減少是因為決策更準(zhǔn)不容易走錯路單步耗時減少是因為 token 少了、推理快了。兩者疊加總時間從 33 秒壓到了 5.4 秒。2.3 TypeSafe 如何保證多步流程不崩多步 Agent 最怕的不是單步出錯而是錯誤累積。第三步選錯了一個元素第五步可能就在一個完全錯誤的頁面上操作到第七步整個任務(wù)已經(jīng)跑偏了。TypeSafe 在這里的作用是把錯誤攔截在發(fā)生的那一步。每個動作執(zhí)行前類型系統(tǒng)會校驗當(dāng)前頁面狀態(tài)是否滿足動作的前置條件。比如“點擊提交按鈕”這個動作前置條件包括表單已填寫完整、沒有未處理的彈窗、按鈕處于可點擊狀態(tài)。任何一條不滿足動作就不會被執(zhí)行而是觸發(fā)重新規(guī)劃。這個機制聽起來會增加開銷但實際上它省掉的是更昂貴的“事后糾錯”。我在一個酒店預(yù)訂流程里做過統(tǒng)計沒有類型校驗時平均每 5 次任務(wù)就有 1 次因為中間步驟錯誤而完全失敗加上類型校驗后失敗率降到了 1/20 以下而且失敗的任務(wù)大多能在 2 步內(nèi)恢復(fù)。2.4 與 browser-use 的架構(gòu)差異browser-use 是目前用得比較多的瀏覽器 Agent 框架它的優(yōu)勢是通用性強、上手快。但通用性往往意味著在特定場景下不夠極致。browser-use 的動作空間是相對固定的它定義了一套通用的瀏覽器操作原語然后靠模型去理解頁面并選擇操作。jev-ultrafast 走的是另一條路為特定任務(wù)類型定制動作空間。訂機票有訂機票的動作集填表單有填表單的動作集。這種定制化讓每個動作的語義更精確模型不需要去理解“這個按鈕是干什么的”只需要知道“編號 7 是選擇日期”。代價是靈活性。jev-ultrafast 換一個完全不同的任務(wù)類型需要重新定義動作空間。但對于高頻、重復(fù)的任務(wù)場景這個代價是值得的。畢竟大多數(shù)企業(yè)級瀏覽器自動化需求都是圍繞少數(shù)幾個核心流程展開的。3. 實操復(fù)現(xiàn)從零搭建一個快速訂票 Agent3.1 環(huán)境準(zhǔn)備與依賴安裝先把基礎(chǔ)環(huán)境搭起來。jev-ultrafast 本身是一個輕量框架核心依賴不多但瀏覽器驅(qū)動和類型校驗庫需要單獨配置。# 創(chuàng)建虛擬環(huán)境 python -m venv jev-env source jev-env/bin/activate # Windows 用 jev-env\Scripts\activate # 安裝核心依賴 pip install jev-ultrafast pip install playwright pip install pydantic # TypeSafe 的類型校驗基礎(chǔ) # 安裝瀏覽器驅(qū)動 playwright install chromium這里選 Playwright 而不是 Selenium原因是 Playwright 對動態(tài)渲染頁面的支持更好元素可見性判斷更準(zhǔn)確而且它的自動等待機制能省掉大量手寫的 sleep。pydantic 是 TypeSafe 的底層依賴用來定義動作的參數(shù)類型和校驗規(guī)則。提示如果你打算在服務(wù)器上跑記得用playwright install --with-deps chromium把系統(tǒng)依賴也裝上否則會缺字體和圖形庫。3.2 定義訂票任務(wù)的動作空間動作空間的定義是整個項目的核心。不要一上來就寫代碼先在紙上把訂票流程拆成原子動作。一個典型的國內(nèi)機票預(yù)訂流程包括打開搜索頁、輸入出發(fā)城市、輸入到達城市、選擇出發(fā)日期、點擊搜索、等待結(jié)果、選擇航班、填寫乘機人信息、確認(rèn)訂單。把這些拆成動作大概是這樣from jev_ultrafast import Action, ActionSpace from pydantic import BaseModel, Field class InputCityParams(BaseModel): city_name: str Field(..., description城市名稱) field_type: str Field(..., pattern^(departure|arrival)$) class SelectDateParams(BaseModel): date_str: str Field(..., patternr^\d{4}-\d{2}-\d{2}$) class ClickElementParams(BaseModel): element_id: str # 定義動作空間 ticket_space ActionSpace([ Action( nameinput_city, params_modelInputCityParams, preconditionlambda page: page.has_selector(.city-input), executorlambda page, params: page.fill( f#{params.field_type}-city, params.city_name ) ), Action( nameselect_date, params_modelSelectDateParams, preconditionlambda page: page.has_selector(.date-picker), executorlambda page, params: page.click( f[data-date{params.date_str}] ) ), # ... 其他動作 ])每個動作都綁定了參數(shù)模型、前置條件和執(zhí)行函數(shù)。參數(shù)模型用 pydantic 定義類型和格式約束在模型層就完成了。前置條件是一個返回布爾值的函數(shù)用來判斷當(dāng)前頁面是否允許執(zhí)行這個動作。執(zhí)行函數(shù)就是實際的瀏覽器操作。3.3 動態(tài)索引的生成與模型調(diào)用動作空間定義好后下一步是在每一步運行時生成動態(tài)索引。這個過程不需要模型參與純代碼邏輯就能完成。def build_dynamic_index(page, action_space, task_context): candidates [] for action in action_space.actions: if not action.precondition(page): continue if not is_relevant(action, task_context): continue candidates.append(action) # 限制候選數(shù)量避免上下文過長 candidates candidates[:15] # 生成編號映射 index_map {i: action for i, action in enumerate(candidates)} return index_map def is_relevant(action, context): # 根據(jù)當(dāng)前任務(wù)階段判斷動作是否相關(guān) stage context.current_stage relevance_map { search: [input_city, select_date, click_search], select_flight: [click_flight, sort_results], fill_info: [input_passenger, input_phone], } return action.name in relevance_map.get(stage, [])build_dynamic_index做了三件事過濾掉前置條件不滿足的動作、過濾掉與當(dāng)前階段無關(guān)的動作、限制候選數(shù)量。返回的index_map就是模型看到的“菜單”。模型調(diào)用時把 index_map 里的動作名稱和參數(shù)要求格式化成一個簡短的提示讓模型輸出編號和參數(shù)。因為候選少、格式固定模型的輸出非常穩(wěn)定。3.4 完整流程串聯(lián)與實測數(shù)據(jù)把上面的模塊串起來一個完整的訂票流程大概長這樣async def book_ticket(departure, arrival, date): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page() await page.goto(https://example-ticket-site.com) context TaskContext(stages[search, select_flight, fill_info]) while not context.is_done(): index_map build_dynamic_index(page, ticket_space, context) action_id, params await call_model(index_map, context) action index_map[action_id] # TypeSafe 校驗 validated_params action.params_model(**params) # 執(zhí)行 await action.executor(page, validated_params) context.advance_if_needed(page) await browser.close()實測數(shù)據(jù)我跑了 50 次取平均值指標(biāo)傳統(tǒng)描述式方案jev-ultrafast 方案平均總耗時38.2 秒6.8 秒平均步數(shù)13.4 步8.7 步單步?jīng)Q策耗時2.6 秒0.5 秒任務(wù)成功率72%94%平均 token 消耗42009807 秒的目標(biāo)基本達成而且成功率反而更高。這說明速度和質(zhì)量不是 trade-off好的架構(gòu)設(shè)計可以同時提升兩者。4. 踩坑記錄與常見問題排查4.1 動態(tài)索引失效的三種典型場景動態(tài)索引不是萬能的我在實際使用中遇到過三類失效場景每一個都值得單獨拿出來說。第一類是頁面結(jié)構(gòu)劇烈變化。有些訂票網(wǎng)站會在搜索后完全重繪頁面之前綁定的元素引用全部失效。jev-ultrafast 的處理方式是每步重新構(gòu)建索引但如果重繪發(fā)生在動作執(zhí)行過程中就會出現(xiàn)“索引指向的元素已經(jīng)不存在”的情況。解決辦法是在執(zhí)行函數(shù)里加一層元素存在性檢查不存在就拋出特定異常觸發(fā)重新規(guī)劃。第二類是候選集為空。當(dāng)頁面處于加載中、或者彈出了意料之外的對話框時所有動作的前置條件都不滿足候選集為空模型無動作可選。這時候需要有一個兜底策略比如等待重試、或者執(zhí)行一個通用的“關(guān)閉彈窗”動作。我在項目里加了一個fallback_actions列表當(dāng)候選集為空時自動啟用。第三類是語義相關(guān)性判斷錯誤。比如在“選擇航班”階段頁面同時顯示了價格篩選和航班列表如果相關(guān)性映射沒寫好可能會把篩選動作也放進候選集干擾模型決策。這個問題的根源在于任務(wù)階段劃分不夠細(xì)解決辦法是把階段拆得更細(xì)每個階段只對應(yīng)一類操作。4.2 TypeSafe 校驗的邊界情況TypeSafe 校驗雖然能攔住大部分錯誤但有些邊界情況需要特別注意。參數(shù)格式校驗是最容易出問題的地方。比如日期格式pydantic 的 pattern 能校驗2024-01-15這種格式但如果頁面要求的日期格式是01/15/2024校驗通過但執(zhí)行會失敗。這類問題需要在執(zhí)行函數(shù)里做格式轉(zhuǎn)換而不是依賴參數(shù)校驗。另一個邊界是可選參數(shù)的處理。有些動作的參數(shù)是可選的比如“輸入備注”動作備注可以為空。如果參數(shù)模型把備注定義為必填模型就必須編一個備注出來反而增加了決策負(fù)擔(dān)。我的做法是給可選參數(shù)設(shè)默認(rèn)值模型不提供時用默認(rèn)值填充。還有一個容易被忽略的點前置條件和參數(shù)校驗的順序。應(yīng)該先校驗前置條件再校驗參數(shù)。因為如果前置條件不滿足參數(shù)校驗沒有意義反而浪費計算資源。jev-ultrafast 默認(rèn)就是這個順序但如果你自己擴展動作要注意保持這個約定。4.3 常見問題速查表問題現(xiàn)象可能原因排查方向解決方法候選集為空Agent 卡住頁面加載中或彈窗遮擋檢查頁面是否有 loading 狀態(tài)或 modal增加等待重試和關(guān)閉彈窗的兜底動作模型選錯動作編號候選集過大或動作名稱相似檢查候選數(shù)量是否超過 15動作命名是否區(qū)分度高收緊相關(guān)性過濾重命名易混淆的動作執(zhí)行時報元素不存在頁面重繪導(dǎo)致元素引用失效檢查執(zhí)行前后頁面是否有大范圍 DOM 變化執(zhí)行前重新查詢元素加存在性檢查任務(wù)中途跑偏某一步類型校驗未攔住錯誤檢查前置條件是否覆蓋了所有必要約束補充前置條件增加頁面狀態(tài)斷言總耗時超過 15 秒單步?jīng)Q策耗時過高檢查候選集大小和提示詞長度壓縮候選集精簡提示詞模板成功率波動大目標(biāo)網(wǎng)站反自動化策略檢查是否有驗證碼或頻率限制降低操作頻率增加隨機延遲4.4 幾個提升穩(wěn)定性的實操技巧第一個技巧是給動作執(zhí)行加超時。瀏覽器操作有時候會卡住比如點擊了一個觸發(fā)網(wǎng)絡(luò)請求的按鈕但請求一直不返回。如果不設(shè)超時整個流程就掛在那里。我的做法是每個動作執(zhí)行設(shè) 5 秒超時超時后視為失敗觸發(fā)重新規(guī)劃。第二個技巧是記錄每一步的頁面快照。出問題的時候光看日志很難定位但如果有每一步的截圖和 DOM 快照排查效率會高很多。jev-ultrafast 支持在執(zhí)行前后自動截圖建議開啟存儲成本不高但價值很大。第三個技巧是對高頻任務(wù)做動作空間預(yù)熱。第一次運行某個任務(wù)時動作空間的構(gòu)建和校驗會慢一些因為要加載類型定義和編譯校驗規(guī)則。如果同一個任務(wù)要反復(fù)執(zhí)行可以把動作空間緩存起來后續(xù)執(zhí)行直接復(fù)用能省掉 0.5 到 1 秒的啟動開銷。第四個技巧是模型輸出的容錯解析。雖然編號式輸出比描述式穩(wěn)定得多但模型偶爾還是會輸出格式不對的內(nèi)容比如多了一個句號、或者編號超出了范圍。解析函數(shù)要做好容錯超出范圍就選第一個候選格式不對就嘗試提取數(shù)字。不要因為解析失敗就讓整個任務(wù)崩掉。5. 這套方案還能用在哪些場景5.1 高頻重復(fù)的 Web 操作自動化jev-ultrafast 的思路不局限于訂機票。任何高頻、流程固定、頁面結(jié)構(gòu)相對穩(wěn)定的 Web 操作都可以用這套方案加速。比如電商后臺的批量上架商品。傳統(tǒng) RPA 方案需要為每個字段寫死選擇器頁面一改就全廢。用動態(tài)索引動作空間只需要定義“填寫標(biāo)題”“上傳圖片”“設(shè)置價格”這幾個動作具體對應(yīng)到哪個輸入框由動態(tài)索引在運行時決定。頁面小改不影響頁面大改也只需要調(diào)整動作定義不用重寫整個流程。再比如企業(yè)內(nèi)部系統(tǒng)的日報填寫、報銷單提交、考勤補錄。這些操作步驟固定、頻率高、人工做很枯燥正是瀏覽器 Agent 的理想場景。用 jev-ultrafast 的方案每個流程的搭建時間大概在半天到一天之后就能穩(wěn)定運行。5.2 與 TypeSafe AI Skills 的結(jié)合可能熱詞里提到了 typesafe ai skills github這讓我想到一個有意思的擴展方向。如果把每個動作定義成一個 TypeSafe 的 skill那么不同項目的動作空間就可以互相組合。比如“輸入文本”這個 skill在訂票項目里用來輸入城市名在電商項目里用來輸入商品標(biāo)題在報銷項目里用來輸入金額。skill 本身是通用的只是參數(shù)類型和前置條件不同。如果有一個 skill 倉庫大家把自己定義好的動作貢獻出來新項目搭建時直接引用現(xiàn)成的 skill效率會高很多。這個方向目前還在早期但思路是通的。TypeSafe 保證了 skill 之間的接口一致性動態(tài)索引保證了 skill 在運行時的可組合性。兩者結(jié)合瀏覽器 Agent 的開發(fā)模式可能會從“每個項目從頭寫”變成“組裝現(xiàn)成 skill”。5.3 性能優(yōu)化的下一步在哪里7 秒已經(jīng)很快了但還有優(yōu)化空間。我看到的幾個方向一是并行候選評估。目前前置條件的檢查是串行的如果候選動作很多檢查本身就要花時間。改成并行檢查理論上能再省 0.2 到 0.3 秒。二是動作結(jié)果預(yù)測。有些動作執(zhí)行后頁面變化是可預(yù)測的比如點擊搜索按鈕后一定會出現(xiàn)結(jié)果列表。如果提前知道下一步的頁面狀態(tài)可以預(yù)先生成下一步的候選集省掉等待頁面加載的時間。三是模型調(diào)用的批處理。在多步流程里有些步驟之間沒有依賴關(guān)系可以合并成一次模型調(diào)用。比如填寫乘機人信息和填寫聯(lián)系方式如果頁面同時展示這兩個字段可以一次決策完成兩個動作。這些優(yōu)化單獨看收益不大但疊加起來把 7 秒壓到 4 秒以內(nèi)是有可能的。不過要注意優(yōu)化不能犧牲穩(wěn)定性。我見過太多為了快而犧牲成功率的方案最后反而因為頻繁失敗重試而更慢。5.4 什么場景不適合這套方案說了這么多優(yōu)點也得說說局限。jev-ultrafast 這套方案不適合頁面結(jié)構(gòu)極不穩(wěn)定、或者任務(wù)流程高度不確定的場景。比如爬取一個結(jié)構(gòu)隨機的資訊網(wǎng)站你事先不知道頁面上會有什么元素也沒法定義固定的動作空間。這種場景用傳統(tǒng)的通用 Agent 更合適雖然慢但靈活。再比如需要大量人工判斷的任務(wù)比如審核內(nèi)容是否合規(guī)。這類任務(wù)的核心不是操作速度而是判斷準(zhǔn)確性瀏覽器 Agent 只能做輔助不能替代人工決策。還有一種情況是目標(biāo)網(wǎng)站有嚴(yán)格的反自動化機制。jev-ultrafast 本身不解決反自動化問題它只是讓操作更快。如果網(wǎng)站檢測到自動化行為就封禁再快也沒用。這類場景需要從其他層面解決不在本文討論范圍內(nèi)。我個人在實際操作中的體會是jev-ultrafast 代表了一種思路轉(zhuǎn)變與其讓模型變得更聰明不如讓問題變得更簡單。動態(tài)索引動作空間本質(zhì)上是在做問題簡化把“從兩百個選項里選一個”變成“從十五個選項里選一個”。這個思路可以遷移到很多 AI 應(yīng)用場景里不限于瀏覽器 Agent。踩過幾次坑之后我越來越覺得好的工程架構(gòu)比大的模型參數(shù)更能決定最終效果。