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