GUI Agent記憶層設(shè)計(jì):事實(shí)常駐與技能按需的工程實(shí)踐)
1. 為什么手機(jī) GUI Agent 需要一個(gè)“記憶層”做過手機(jī)端 GUI Agent 的人大概都有過這種體驗(yàn)?zāi)P偷谝淮螏湍泓c(diǎn)開某個(gè) App 完成一個(gè)任務(wù)第二次再讓它做同樣的事它還是像失憶一樣從頭摸索。每一步都要重新截圖、重新理解界面、重新推理下一步點(diǎn)哪里。單次任務(wù)跑下來token 消耗驚人延遲也高得讓人抓狂。這個(gè)問題的本質(zhì)是 Agent 沒有記憶。它每次都在“從零開始理解這個(gè)世界”。我在給自己的 Android GUI Agent 加記憶層之前專門統(tǒng)計(jì)過一組數(shù)據(jù)一個(gè)典型的“打開外賣 App 搜索某家店并下單”的任務(wù)平均需要 18 到 25 步操作每步都要把當(dāng)前屏幕截圖喂給視覺模型做理解單次任務(wù)消耗的 token 在 15000 以上。而如果 Agent 能記住“這個(gè) App 的搜索框在頂部”“下單按鈕在右下角”這類信息至少能省掉三分之一的探索步驟。但記憶層不是簡單地把所有歷史操作都塞進(jìn)上下文就完事了。我踩過的最大坑就是一開始把所有歷史記錄無腦拼接進(jìn) prompt結(jié)果上下文迅速膨脹到幾萬 token模型反而因?yàn)樾畔⑦^載變得遲鈍該記住的沒記住不該記的全塞進(jìn)去了。所以后來我重新設(shè)計(jì)了整個(gè)記憶架構(gòu)核心思路就一句話事實(shí)常駐、技能按需用一條統(tǒng)一的寫入鏈把兩者串起來。事實(shí)類信息比如某個(gè) App 的界面布局特征常駐在系統(tǒng)提示詞里技能類信息比如“如何完成一次搜索操作”按需檢索注入。兩者通過同一條寫入鏈從 Agent 的執(zhí)行軌跡中自動(dòng)提取和沉淀。這套方案跑下來同樣的下單任務(wù)token 消耗降到了 6000 左右步驟數(shù)從平均 21 步降到了 13 步而且隨著使用次數(shù)增加Agent 對(duì)常用 App 的操作越來越熟練。下面我把整個(gè)設(shè)計(jì)和實(shí)現(xiàn)過程拆開講。2. 記憶層的整體架構(gòu)設(shè)計(jì)2.1 兩類記憶的劃分邏輯在動(dòng)手寫代碼之前我先想清楚了一個(gè)問題Agent 到底需要記住什么從實(shí)際執(zhí)行軌跡來看信息可以分成兩大類。第一類是事實(shí)也就是那些相對(duì)穩(wěn)定、不隨任務(wù)變化的描述性知識(shí)。比如“微信的搜索入口在首頁頂部的放大鏡圖標(biāo)”“設(shè)置 App 的返回按鈕在左上角”“這個(gè) App 啟動(dòng)后有 3 秒開屏廣告”。這類信息的特點(diǎn)是跨任務(wù)復(fù)用率高變化頻率低適合常駐在上下文里。第二類是技能也就是完成某類任務(wù)的操作流程。比如“如何在購物 App 里完成一次搜索”“如何關(guān)閉一個(gè)彈窗廣告”“如何從列表中找到目標(biāo)項(xiàng)并點(diǎn)擊”。這類信息的特點(diǎn)是數(shù)量多、按場景觸發(fā)、不需要一直占著上下文。這個(gè)劃分直接決定了記憶的存儲(chǔ)方式和注入策略。事實(shí)常駐意味著它要放在 system prompt 里每次請(qǐng)求都帶著技能按需意味著它要存在外部存儲(chǔ)里通過檢索機(jī)制在需要時(shí)才注入到當(dāng)前輪次的上下文中。這里有個(gè)容易混淆的點(diǎn)有些信息既像事實(shí)又像技能。比如“這個(gè) App 的搜索框在頂部”是事實(shí)“點(diǎn)擊搜索框然后輸入關(guān)鍵詞”是技能。我的處理原則是靜態(tài)描述歸事實(shí)動(dòng)態(tài)操作歸技能。如果一條信息里既有靜態(tài)描述又有操作步驟就拆成兩條分別存儲(chǔ)。2.2 為什么不用向量數(shù)據(jù)庫做全量檢索很多人一提到記憶層第一反應(yīng)就是上向量數(shù)據(jù)庫把所有記憶做 embedding 然后語義檢索。我一開始也是這么想的但實(shí)際跑下來發(fā)現(xiàn)兩個(gè)問題。第一個(gè)問題是延遲。手機(jī)端 Agent 的每一步操作都需要快速響應(yīng)如果每次注入記憶都要做一次向量檢索哪怕只增加 200 毫秒累積到 20 步的操作里就是 4 秒的額外延遲。對(duì)于交互式任務(wù)來說這個(gè)延遲是能感知到的。第二個(gè)問題是精度。向量檢索擅長語義相似但 GUI 操作場景下很多記憶的觸發(fā)條件是高度結(jié)構(gòu)化的。比如“當(dāng)前前臺(tái) App 是某購物 App 且當(dāng)前頁面包含搜索框”這種條件用向量檢索反而容易召回不相關(guān)的記憶。所以我最終采用的方案是事實(shí)直接常駐技能用結(jié)構(gòu)化索引加關(guān)鍵詞匹配做檢索。技能庫用 JSON 存儲(chǔ)每條技能帶有明確的觸發(fā)條件App 包名、頁面特征、任務(wù)類型檢索時(shí)先按結(jié)構(gòu)化條件過濾再用關(guān)鍵詞做二次排序。這樣既保證了速度又保證了精度。2.3 一條寫入鏈的設(shè)計(jì)記憶層的難點(diǎn)不在于讀而在于寫。怎么從 Agent 的執(zhí)行軌跡中自動(dòng)提取出值得記住的事實(shí)和技能這是整個(gè)系統(tǒng)能不能持續(xù)進(jìn)化的關(guān)鍵。我的寫入鏈設(shè)計(jì)是這樣的每次任務(wù)執(zhí)行完成后把完整的操作軌跡包括每步的截圖描述、操作類型、操作目標(biāo)、執(zhí)行結(jié)果送入一個(gè)蒸餾模塊。這個(gè)蒸餾模塊本身也是用大模型做的通過精心設(shè)計(jì)的提示詞讓它從軌跡中提取出兩類輸出新發(fā)現(xiàn)的事實(shí)和可復(fù)用的技能。這里的關(guān)鍵是提示詞的設(shè)計(jì)。我試過很多版本最后穩(wěn)定下來的提示詞核心邏輯是讓模型先判斷這次任務(wù)是否成功成功的任務(wù)才值得提取技能然后讓模型區(qū)分哪些信息是跨任務(wù)穩(wěn)定的事實(shí)哪些是本次任務(wù)特有的操作序列。提取出來的事實(shí)和技能分別寫入對(duì)應(yīng)的存儲(chǔ)事實(shí)更新到常駐區(qū)技能追加到技能庫。3. 事實(shí)常駐層的實(shí)現(xiàn)細(xì)節(jié)3.1 事實(shí)的存儲(chǔ)結(jié)構(gòu)事實(shí)層我用的是一棵簡單的樹形結(jié)構(gòu)按 App 包名做一級(jí)索引下面掛這個(gè) App 的各種事實(shí)條目。每條事實(shí)包含幾個(gè)字段事實(shí)內(nèi)容、置信度、最后驗(yàn)證時(shí)間、來源任務(wù) ID。{ com.tencent.mm: { facts: [ { id: fact_001, content: 搜索入口位于首頁頂部右側(cè)的放大鏡圖標(biāo), confidence: 0.95, last_verified: 2025-01-15T10:30:00, source_task: task_20250115_001 }, { id: fact_002, content: 啟動(dòng)后有約2秒的開屏廣告需等待或點(diǎn)擊跳過, confidence: 0.88, last_verified: 2025-01-14T20:15:00, source_task: task_20250114_003 } ] } }置信度這個(gè)字段很重要。不是所有提取出來的事實(shí)都靠譜有些可能是模型誤判有些可能因?yàn)?App 版本更新而過時(shí)。我給每條事實(shí)設(shè)了初始置信度 0.7每次被成功驗(yàn)證即 Agent 依據(jù)這條事實(shí)操作成功就加 0.05上限 0.99如果依據(jù)這條事實(shí)操作失敗就減 0.15。置信度低于 0.5 的事實(shí)會(huì)被標(biāo)記為待驗(yàn)證低于 0.3 的直接淘汰。3.2 事實(shí)如何注入 system prompt事實(shí)常駐不等于把所有事實(shí)都塞進(jìn)去。我的做法是按當(dāng)前任務(wù)涉及的 App 做過濾只注入相關(guān) App 的事實(shí)。具體來說Agent 在接到任務(wù)后先做一次任務(wù)解析識(shí)別出可能涉及的 App 列表然后從事實(shí)庫里撈出這些 App 的事實(shí)按置信度排序取前 N 條N 根據(jù) token 預(yù)算動(dòng)態(tài)調(diào)整一般不超過 15 條注入到 system prompt 的一個(gè)專門區(qū)域。注入格式我試過幾種最后發(fā)現(xiàn)用簡潔的自然語言描述效果最好不要用 JSON 或表格模型對(duì)自然語言的理解更準(zhǔn)確。比如你已知的關(guān)于當(dāng)前App的信息 - 搜索入口位于首頁頂部右側(cè)的放大鏡圖標(biāo) - 啟動(dòng)后有約2秒開屏廣告 - 底部導(dǎo)航欄有首頁、發(fā)現(xiàn)、通訊錄、我四個(gè)標(biāo)簽實(shí)操心得事實(shí)描述要盡量具體避免模糊表述?!八阉骺蛟陧敳俊辈蝗纭八阉骺蛟谑醉擁敳坑覀?cè)是一個(gè)放大鏡圖標(biāo)”來得有用。前者模型還要猜后者直接就能定位。3.3 事實(shí)的更新與淘汰機(jī)制事實(shí)不是寫完就不管了。App 會(huì)更新界面會(huì)變化舊的事實(shí)會(huì)失效。我的更新機(jī)制分兩種主動(dòng)更新和被動(dòng)更新。主動(dòng)更新是在每次任務(wù)執(zhí)行前如果任務(wù)涉及的 App 有超過 7 天未驗(yàn)證的事實(shí)就先跑一個(gè)輕量的驗(yàn)證流程——讓 Agent 打開這個(gè) App截圖然后讓模型判斷已有事實(shí)是否仍然成立。這個(gè)流程消耗不大但能有效防止用過時(shí)事實(shí)指導(dǎo)操作。被動(dòng)更新就是前面說的置信度機(jī)制。每次操作成功或失敗都會(huì)反饋到相關(guān)事實(shí)的置信度上形成自動(dòng)的優(yōu)勝劣汰。4. 技能按需層的實(shí)現(xiàn)細(xì)節(jié)4.1 技能的粒度控制技能粒度是設(shè)計(jì)中最難拿捏的部分。粒度太粗比如“完成一次購物”作為一個(gè)技能那它跟直接執(zhí)行任務(wù)沒區(qū)別沒有復(fù)用價(jià)值粒度太細(xì)比如“點(diǎn)擊坐標(biāo) (100, 200)”作為一個(gè)技能那它跟具體屏幕分辨率綁定換個(gè)設(shè)備就廢了。我最后確定的粒度是原子操作序列一個(gè)技能對(duì)應(yīng)一個(gè)語義完整的操作單元通常包含 2 到 5 步操作。比如“在搜索框中輸入關(guān)鍵詞并提交”是一個(gè)技能“從彈窗中關(guān)閉廣告”是一個(gè)技能“在列表中滑動(dòng)找到目標(biāo)項(xiàng)并點(diǎn)擊”是一個(gè)技能。這個(gè)粒度下技能既有復(fù)用價(jià)值又不會(huì)跟具體任務(wù)綁定太死。一個(gè)復(fù)雜任務(wù)可以被拆解成多個(gè)技能的串聯(lián)Agent 在執(zhí)行時(shí)按需調(diào)用。4.2 技能的觸發(fā)條件設(shè)計(jì)每條技能都帶有明確的觸發(fā)條件這是實(shí)現(xiàn)精準(zhǔn)檢索的關(guān)鍵。觸發(fā)條件包含三個(gè)維度App 維度這條技能適用于哪個(gè)或哪些 App。有些技能是通用的比如“關(guān)閉彈窗”有些是 App 特有的。頁面維度這條技能適用的頁面特征。比如“頁面包含搜索框元素”“頁面標(biāo)題為‘訂單確認(rèn)’”。任務(wù)維度這條技能適用的任務(wù)類型。比如“搜索類任務(wù)”“支付類任務(wù)”。檢索時(shí)Agent 根據(jù)當(dāng)前的前臺(tái) App、當(dāng)前頁面截圖描述、當(dāng)前任務(wù)類型三個(gè)維度同時(shí)匹配召回符合條件的技能。匹配度用加權(quán)分?jǐn)?shù)計(jì)算App 維度權(quán)重最高頁面維度次之任務(wù)維度最低。4.3 技能庫的存儲(chǔ)與檢索技能庫我用的是 JSON 文件加內(nèi)存索引的方式。JSON 文件持久化存儲(chǔ)Agent 啟動(dòng)時(shí)加載到內(nèi)存構(gòu)建倒排索引加速檢索。{ skill_id: skill_search_001, name: 在搜索框中輸入并提交, trigger: { apps: [com.tencent.mm, com.taobao.taobao], page_features: [包含搜索框, 包含輸入框], task_types: [search, query] }, steps: [ 定位頁面中的搜索框元素, 點(diǎn)擊搜索框使其獲得焦點(diǎn), 輸入目標(biāo)關(guān)鍵詞, 點(diǎn)擊提交按鈕或按回車鍵 ], success_count: 23, fail_count: 2, avg_steps: 4 }檢索時(shí)先按 App 維度過濾再按頁面特征做關(guān)鍵詞匹配最后按任務(wù)類型排序。召回 top 3 的技能注入到當(dāng)前輪次的上下文中。注入時(shí)只注入技能名稱和步驟概要不注入完整細(xì)節(jié)避免占用過多 token。5. 寫入鏈的完整實(shí)現(xiàn)5.1 軌跡采集與預(yù)處理寫入鏈的輸入是 Agent 執(zhí)行任務(wù)的完整軌跡。軌跡采集從 Agent 啟動(dòng)任務(wù)時(shí)開始每一步操作都記錄以下信息時(shí)間戳、操作類型點(diǎn)擊/輸入/滑動(dòng)/返回等、操作目標(biāo)元素描述或坐標(biāo)、操作前的頁面截圖描述、操作結(jié)果成功/失敗/未知。軌跡采集的關(guān)鍵是頁面截圖描述的質(zhì)量。我用的是一個(gè)視覺模型對(duì)截圖做結(jié)構(gòu)化描述輸出包括當(dāng)前 App 包名、頁面標(biāo)題、可見的交互元素列表、頁面布局特征。這個(gè)描述質(zhì)量直接決定了后續(xù)蒸餾的效果。預(yù)處理階段會(huì)把原始軌跡整理成結(jié)構(gòu)化的 JSON去掉冗余信息比如連續(xù)多次滑動(dòng)只保留一次標(biāo)注出關(guān)鍵節(jié)點(diǎn)任務(wù)開始、任務(wù)成功、任務(wù)失敗、遇到異常。5.2 蒸餾提示詞的設(shè)計(jì)蒸餾模塊是整個(gè)寫入鏈的核心它決定了什么信息會(huì)被記住。我用的蒸餾提示詞經(jīng)過多次迭代核心結(jié)構(gòu)如下你是一個(gè)GUI操作軌跡分析專家。請(qǐng)分析以下Agent執(zhí)行軌跡提取出兩類信息 1. 事實(shí)跨任務(wù)穩(wěn)定的描述性知識(shí)。判斷標(biāo)準(zhǔn)這條信息在下次執(zhí)行類似任務(wù)時(shí)仍然成立且不依賴于具體任務(wù)目標(biāo)。 2. 技能可復(fù)用的操作序列。判斷標(biāo)準(zhǔn)這個(gè)操作序列在類似場景下可以被重復(fù)使用且不綁定具體任務(wù)參數(shù)。 輸出格式要求 - 事實(shí)用一句話描述具體到可操作的程度 - 技能包含名稱、觸發(fā)條件、步驟列表 - 只提取本次任務(wù)中成功驗(yàn)證過的信息 - 如果任務(wù)失敗只提取失敗原因相關(guān)的教訓(xùn)不提取技能 軌跡數(shù)據(jù) {trajectory_json}這個(gè)提示詞的關(guān)鍵在于“判斷標(biāo)準(zhǔn)”部分。我試過不給判斷標(biāo)準(zhǔn)讓模型自由發(fā)揮結(jié)果提取出來的東西要么太泛“這個(gè) App 可以用”要么太具體“點(diǎn)擊了坐標(biāo) 350, 800”。給了明確的判斷標(biāo)準(zhǔn)后提取質(zhì)量明顯提升。5.3 寫入沖突的處理寫入鏈會(huì)遇到?jīng)_突新提取的事實(shí)和已有事實(shí)矛盾或者新技能和已有技能重復(fù)。我的處理策略是事實(shí)沖突時(shí)比較置信度和時(shí)間。新事實(shí)置信度更高且時(shí)間更新就替換舊事實(shí)否則保留舊事實(shí)把新事實(shí)標(biāo)記為待驗(yàn)證。如果兩條事實(shí)置信度接近但內(nèi)容矛盾就都保留在注入時(shí)都帶上讓 Agent 在實(shí)際操作中驗(yàn)證哪條正確。技能重復(fù)時(shí)比較步驟相似度。如果新技能和已有技能的步驟相似度超過 80%就合并——更新已有技能的觸發(fā)條件取并集增加成功計(jì)數(shù)。如果相似度在 50% 到 80% 之間就作為獨(dú)立技能保留但在檢索時(shí)做去重。6. 常見問題與排查技巧6.1 記憶注入導(dǎo)致上下文超限這是最常見的問題。事實(shí)和技能都注入后system prompt 可能膨脹到幾千 token加上對(duì)話歷史和截圖描述很容易超過模型的上下文窗口。我的解決方案是動(dòng)態(tài)預(yù)算分配。給記憶注入設(shè)一個(gè) token 上限比如 1500 token事實(shí)和技能各分一半。如果事實(shí)超過預(yù)算按置信度排序截?cái)嗳绻寄艹^預(yù)算按匹配分?jǐn)?shù)排序截?cái)?。同時(shí)監(jiān)控實(shí)際使用量如果連續(xù)多次任務(wù)都觸發(fā)了截?cái)嗑驼f明記憶庫需要做一次清理和壓縮。6.2 技能檢索召回不相關(guān)有時(shí)候 Agent 會(huì)召回一些看起來相關(guān)但實(shí)際上用不上的技能浪費(fèi)上下文空間。排查這類問題我一般按以下順序檢查排查項(xiàng)檢查方法常見原因觸發(fā)條件是否過寬查看召回技能的 trigger 字段apps 列表包含太多 App頁面特征是否模糊對(duì)比當(dāng)前頁面描述和技能頁面特征特征詞太泛如“包含按鈕”匹配權(quán)重是否合理檢查 App/頁面/任務(wù)三維度權(quán)重任務(wù)維度權(quán)重過高導(dǎo)致誤召回技能本身是否過時(shí)查看技能的 fail_countApp 更新導(dǎo)致技能失效6.3 蒸餾提取質(zhì)量不穩(wěn)定蒸餾模塊的輸出質(zhì)量直接決定記憶庫的質(zhì)量。我遇到過的典型問題包括提取的事實(shí)太泛沒有操作價(jià)值、提取的技能步驟不完整、把失敗的操作也提取成技能。解決這些問題的核心是提示詞的約束要足夠強(qiáng)。我后來在提示詞里加了幾個(gè)硬性約束事實(shí)必須包含具體的位置或特征描述技能必須包含完整的起止步驟只提取成功軌跡中的技能。另外我加了一個(gè)后處理校驗(yàn)步驟用規(guī)則過濾掉明顯不合格的輸出比如步驟數(shù)少于 2 的技能、不包含任何位置信息的事實(shí)。避坑技巧蒸餾模塊本身也會(huì)消耗 token如果每個(gè)任務(wù)都跑一次全量蒸餾成本不低。我的做法是只對(duì)成功任務(wù)做蒸餾失敗任務(wù)只提取教訓(xùn)類事實(shí)。另外對(duì)于高度重復(fù)的任務(wù)類型可以設(shè)置蒸餾頻率限制比如同一 App 同一任務(wù)類型每天最多蒸餾 3 次。6.4 記憶庫膨脹導(dǎo)致檢索變慢隨著使用時(shí)間增長記憶庫會(huì)越來越大檢索速度會(huì)下降。我的經(jīng)驗(yàn)是當(dāng)技能庫超過 500 條時(shí)就需要做一次壓縮。壓縮策略是合并相似技能、淘汰低成功率技能、歸檔長期未使用的技能。具體操作上我寫了一個(gè)定時(shí)任務(wù)每周跑一次記憶庫維護(hù)。維護(hù)內(nèi)容包括重新計(jì)算所有技能的相似度矩陣合并相似度超過閾值的技能淘汰 fail_count 超過 success_count 且最近 30 天未使用的技能把 90 天未使用的事實(shí)標(biāo)記為歸檔不再注入但保留在存儲(chǔ)中。7. 實(shí)際效果與后續(xù)優(yōu)化方向這套記憶層跑在我的 Android GUI Agent 上已經(jīng)有一段時(shí)間了。最直觀的變化是常用 App 的任務(wù)執(zhí)行步驟數(shù)平均下降了 35% 到 40%token 消耗下降了約 55%。更重要的是Agent 的行為開始有了“熟練感”——第一次做某個(gè)任務(wù)可能磕磕絆絆第三次第四次就明顯流暢了。不過也有做得不夠好的地方。目前事實(shí)和技能的提取還是依賴大模型蒸餾偶爾會(huì)有誤提取。我下一步想嘗試的是引入一個(gè)輕量的驗(yàn)證機(jī)制讓 Agent 在執(zhí)行任務(wù)時(shí)順便驗(yàn)證一兩條已有記憶形成一個(gè)持續(xù)的自我校驗(yàn)循環(huán)。另外跨 App 的通用技能提取目前效果一般很多技能還是綁定在具體 App 上這塊的泛化能力還有提升空間。如果你也在做類似的手機(jī)端 Agent我的建議是先把寫入鏈跑通哪怕提取質(zhì)量一開始不高也沒關(guān)系重要的是讓記憶流動(dòng)起來。有了流動(dòng)才有優(yōu)化的基礎(chǔ)。事實(shí)常駐和技能按需這個(gè)框架本身不復(fù)雜難的是讓它在真實(shí)使用中持續(xù)沉淀出有價(jià)值的記憶。