1. 项目概述:什么是“可执行的自主记忆”?
最近在跟几个做GUI自动化测试和RPA的朋友聊天,大家普遍头疼一个问题:现在的智能体(Agent)操作图形界面,比如自动填表单、点按钮,看起来挺酷,但换个页面布局或者软件版本一更新,脚本就全废了。每次都得重新录制或者写规则,累得够呛。这让我想起了我们正在折腾的一个东西——Executable Agentic Memory for GUI Agent,直译过来就是“面向GUI智能体的可执行自主记忆”。
这名字听起来有点学术,但内核其实很接地气。你可以把它理解为一个GUI智能体的“肌肉记忆”和“经验库”。传统的自动化脚本是死的,它只知道“在坐标(X, Y)点击‘提交’按钮”。而我们的“可执行自主记忆”是活的,它记录的是智能体在操作过程中的决策逻辑、感知到的界面状态、执行的动作以及最终的结果,并且这个记忆本身可以被检索、推理并再次“执行”出来。
举个例子,一个新手智能体第一次学习“在电商网站完成下单”,它会摸索:先找到搜索框,输入关键词,从商品列表中识别目标,点击进入详情页,找到“加入购物车”的按钮……这个过程会被完整地记录成一段“记忆”。下次再遇到“下单”任务时,智能体不是机械回放坐标,而是去记忆库里寻找类似场景的成功经验(比如“在包含商品图片和价格的列表页,执行选择操作”),然后结合当前页面的实际情况(比如按钮文案变成了“立即购买”),动态生成新的操作序列。这就像一个有经验的司机,遇到修路不会死磕导航,而是根据经验选择绕行。
这个项目的核心价值,就是让GUI智能体摆脱对固定坐标和脆弱定位符(如XPath)的深度依赖,通过积累和复用“记忆”,获得真正的环境适应性和任务泛化能力。它适合任何需要与图形界面打交道的自动化场景开发者,无论是做软件测试、业务流程自动化(RPA),还是构建复杂的数字助手。
2. 核心设计思路:从“录屏回放”到“经验驱动”
为什么传统的GUI自动化路子走不通了?根本原因在于它将GUI视为一个静态的、结构化的文档。而现实中的图形界面是动态、模糊且充满变化的。一个按钮今天叫“提交”,明天可能叫“确认”;一个列表的渲染顺序可能因数据加载而改变。基于“可执行自主记忆”的设计,正是要直面这些挑战。
2.1 记忆的构成:不止记录“做了什么”,更要记录“为什么这么做”
一段有价值的自主记忆,是一个结构化的四元组,我习惯称之为“感知-决策-行动-结果”循环。
感知状态:智能体“看到”了什么?这不仅仅是截图,而是对当前界面的一种结构化理解。我们会使用多模态模型(如视觉语言模型VLM)将屏幕图像转化为一个丰富的语义描述,例如:“这是一个登录页面,包含两个文本输入框,上方标签分别为‘用户名’和‘密码’,一个蓝色背景的按钮,文字为‘登录’,右下角有‘忘记密码?’链接。”同时,还会提取界面元素的层次结构、相对位置和关键视觉特征。
决策上下文:智能体“在想”什么?这部分记录了触发当前动作的意图和目标。例如,任务目标是“登录系统”,当前子目标是“填写用户名”。决策上下文还包括对当前状态的判断,比如“用户名输入框处于可编辑状态且为空”。
执行动作:智能体“做了”什么?记录具体的动作类型(点击、输入文本、滚动等)和动作参数。关键点在于,参数不是绝对的坐标,而是与感知状态绑定的语义化描述。比如,不是
click(坐标(320, 480)),而是click(元素描述: “文本为‘登录’的按钮”)。动作本身也会被编码成可执行的指令模板。结果反馈与验证:动作带来了什么“变化”?执行后,智能体会再次感知界面,记录状态变化。例如,点击“登录”后,新页面出现“欢迎,[用户名]”的标题。这个反馈用于验证动作的成功与否,并作为下一次决策的输入。
注意:记忆的存储不是简单的日志堆砌。我们采用向量数据库对“感知状态”和“决策上下文”进行编码和索引。这样,当遇到新场景时,智能体可以通过语义相似度搜索,快速找到历史上最相关的成功记忆片段,而不是做字符串匹配。
2.2 记忆的可执行性:如何让“经验”动起来?
这是本项目最精妙的部分。记忆的可执行性体现在两个方面:泛化与组合。
泛化是指记忆中的动作能适应界面的局部变化。我们为每个记录的动作开发了“适配器”。比如,记忆中有一条click(“登录按钮”)。在新的界面上,按钮可能变成了绿色,或者文案变成了“Sign In”。我们的动作执行引擎不会失效,它会:
- 首先,基于当前的“感知状态”,利用VLM重新理解界面,找出所有可能是按钮的元素。
- 然后,将记忆中“登录按钮”的语义描述(功能:提交凭证;常见文案:登录、Sign In、Log in;常见位置:表单底部)与当前候选元素进行匹配。
- 最后,选择匹配度最高的元素执行点击。这背后是视觉特征匹配、文本语义相似度计算和布局逻辑推理的综合应用。
组合是指智能体能够将多个原子记忆片段串联起来,完成复杂任务。记忆库中存储的往往是“填写单个输入框”、“点击某个按钮”这样的原子操作。当面临“用户注册”这样的复合任务时,规划模块会将任务分解为“输入用户名”、“输入密码”、“确认密码”、“点击注册”等子目标,然后依次从记忆库中检索并实例化对应的记忆片段,形成可执行的工作流。
# 一个简化的概念性代码示例,展示记忆的检索与执行 class ExecutableMemoryAgent: def __init__(self, memory_store): self.memory = memory_store # 连接向量化记忆库 self.executor = ActionExecutor() # 动作执行引擎 def perform_task(self, task_description, current_screen): # 1. 感知当前状态 current_state = self._perceive(current_screen) # 2. 基于任务和当前状态,检索相关记忆 # 查询:“当状态类似当前,且目标是‘输入用户名’时,成功做了什么?” relevant_memories = self.memory.search( query_state=current_state, query_intent="输入用户名" ) # 3. 选取最相关的记忆,并使其适应当前界面 best_memory = relevant_memories[0] executable_action = self._generalize_action(best_memory.action, current_state) # 4. 执行动作并观察结果 result = self.executor.execute(executable_action) new_state = self._perceive(self.capture_screen()) # 5. 将本次经历作为新记忆存储(学习) new_memory = Memory(current_state, “输入用户名”, executable_action, new_state) self.memory.store(new_memory) return result这个设计思路,将GUI自动化从“脚本录制与回放”的范式,提升到了“经验学习与复用”的范式。智能体在不断实践中丰富自己的记忆库,变得越来越“聪明”和“熟练”。
3. 关键技术栈与实现要点拆解
要实现这样一个系统,需要融合计算机视觉、自然语言处理、强化学习与软件工程等多个领域的技术。下面我拆解几个核心模块的实现选型和实操要点。
3.1 界面感知与理解:让智能体“看得懂”
这是记忆的源头。我们需要把像素图像转换成机器可理解的结构化信息。
核心技术选型:目前的主流方案是使用多模态大模型。例如,使用GPT-4V、Gemini Pro Vision或开源的Qwen-VL等模型,通过提示词工程,让模型输出界面的结构化描述。一个更专业的方案是结合目标检测(如YOLO)和OCR(如PaddleOCR)先提取基础元素,再用LLM进行关系推理和功能归类。
实操提示词设计:给VLM的提示词至关重要。不能简单地问“描述这张图片”。我们的提示词模板会明确要求分层输出:
“你是一个GUI分析专家。请分析此截图:1. 列出所有可交互元素(按钮、输入框、链接等),描述其视觉特征(颜色、形状、大致位置)和文本内容。2. 推断这些元素可能的功能(如‘提交表单’、‘导航到设置页’)。3. 描述界面的整体布局和可能的工作流程。”
注意事项:
- 成本与延迟:调用商用VLM API有成本和速度问题。对于需要高频感知的场景,可以考虑使用轻量化的本地VLM,或在非关键路径上使用传统的基于Accessibility Tree(无障碍树)的方法作为补充。Windows的UI Automation、苹果的AX API、Linux的AT-SPI都能提供部分结构信息。
- 描述的一致性:确保不同时间对相似界面的描述尽可能一致,这关系到记忆检索的准确性。需要对VLM的输出进行标准化处理,比如统一元素类别的命名(“btn” vs “button”)。
3.2 记忆的存储与检索:构建智能体的“海马体”
记忆库的设计直接决定了智能体的“智商”。
- 存储结构:我们使用NoSQL数据库(如MongoDB)存储记忆的完整元数据,同时使用向量数据库(如Chroma、Weaviate、Qdrant)存储“感知状态”和“决策上下文”的向量嵌入。这样既能做精确查询,也能做高效的语义相似度搜索。
- 向量化模型:选择适合短文本和结构化描述的嵌入模型,如
text-embedding-3-small、BGE-M3或sentence-transformers系列。对于界面状态,可以将VLM生成的描述文本进行向量化。 - 检索策略:检索时,我们将当前状态和目标任务共同编码为一个查询向量。记忆的检索不是找“一模一样”的界面,而是找“情境相似”的经历。例如,当前目标是“保存文档”,那么历史上“点击‘保存’按钮”、“选择‘文件->另存为’”的记忆都可能被检索出来,供规划模块参考。
- 记忆的衰减与整理:不是所有记忆都同等重要。频繁成功使用的记忆应加强,而过时或失败的记忆应被降权或归档。可以引入类似“记忆强度”的权重概念,并定期进行清理。
3.3 动作的泛化与执行:让记忆“活”过来
这是将存储的记忆转化为实际操作的关键。
- 动作抽象层:定义一套与具体自动化工具无关的原子动作原语,例如:
Click(ElementDescriptor),TypeText(ElementDescriptor, Text),Scroll(Direction),WaitFor(StateDescriptor)。记忆中存储的就是这些原语。 - 元素描述符:
ElementDescriptor是核心,它不能是易变的XPath或坐标,而应是鲁棒的语义化描述。一个描述符可能包含:元素类型、预测的文本内容、邻近文本(上下文)、视觉特征(颜色、图标哈希)、在布局中的相对位置关系(如“在‘密码’输入框下方”)。 - 执行时解析:当需要执行
Click(描述符)时,执行引擎会:- 获取当前屏幕的感知结果(元素列表)。
- 将描述符与每个候选元素进行多维度匹配打分(文本相似度、视觉相似度、位置一致性)。
- 选择综合得分最高的元素,通过底层驱动(如Playwright、Appium)执行点击操作。
- 容错与重试:匹配可能失败或匹配到错误元素。执行引擎必须包含容错逻辑,例如:如果点击后未达到预期状态(通过结果验证),则尝试匹配得分第二的元素,或触发重新感知和规划。
3.4 任务规划与记忆组合:智能体的“前额叶皮层”
智能体需要决定“现在该用什么记忆”。
规划器实现:可以采用基于LLM的规划器。将当前状态、任务目标和记忆库检索到的相关片段作为上下文,提示LLM生成下一步动作序列。例如:
“当前界面是一个空白笔记页面。你的目标是‘创建一份包含标题和正文的笔记’。你过去的相关经验有:1. 在输入框内点击并输入文字。2. 点击‘加粗’按钮格式化文本。请规划接下来的具体动作步骤。”
分层任务网络:对于复杂且流程固定的任务,可以预定义高级别的HTN。规划器负责将高层任务分解为子任务,而子任务的具体实现则由检索记忆来填充。这结合了规则的可控性和记忆的灵活性。
4. 实战构建:一个简易可执行记忆系统的搭建流程
理论说了这么多,我们来动手搭一个最简单的原型,验证核心流程。这里我们以自动化一个桌面计算器应用为例。
4.1 环境准备与工具选型
我们选择Python作为开发语言,因为它有丰富的AI和自动化库。
- 界面感知:使用
pyautogui截图,结合OpenAI GPT-4V API进行图像理解。对于轻量级本地方案,可以用pytesseract(OCR) +easyocr+opencv(模板匹配) 组合,但泛化能力弱。 - 自动化执行:使用
pywinauto(Windows) 或appium(跨平台) 作为底层驱动来操控计算器。 - 记忆存储:使用
chromadb作为轻量级向量数据库,存储记忆片段。 - 核心逻辑:自行编写记忆管理、检索和执行适配代码。
安装基础包:
pip install openai chromadb pyautogui pillow pywinauto4.2 核心模块代码实现
我们来创建几个核心类。
1. 感知模块
import base64 from openai import OpenAI import pyautogui class GUIPerceiver: def __init__(self, api_key): self.client = OpenAI(api_key=api_key) def perceive(self, region=None): # 1. 截屏 screenshot = pyautogui.screenshot(region=region) # region可指定应用窗口区域 screenshot_path = “temp_screenshot.png” screenshot.save(screenshot_path) # 2. 调用VLM进行理解 with open(screenshot_path, “rb”) as img_file: encoded_image = base64.b64encode(img_file.read()).decode(‘utf-8’) response = self.client.chat.completions.create( model=“gpt-4-vision-preview”, messages=[ { “role”: “user”, “content”: [ {“type”: “text”, “text”: “请详细描述此GUI界面。列出所有看起来可点击或可输入的元素,说明其上的文字或明显特征,并推断其可能的功能。请用简洁的结构化语言描述。”}, {“type”: “image_url”, “image_url”: {“url”: f“data:image/png;base64,{encoded_image}”}}, ], } ], max_tokens=500, ) description = response.choices[0].message.content # 此处应添加解析代码,将描述文本转为结构化的元素列表 # 例如: elements = [{“type”: “button”, “text”: “5”, “position_approx”: “center”}, …] elements = self._parse_description(description) return {“raw_description”: description, “elements”: elements} def _parse_description(self, desc): # 简化处理:实际需要更复杂的NLP解析或固定格式输出 # 这里仅为示例,返回模拟数据 return [{“type”: “button”, “text”: “5”}, {“type”: “button”, “text”: “+”}, {“type”: “button”, “text”: “3”}, {“type”: “display”, “text”: “0”}]2. 记忆存储模块
import chromadb from chromadb.utils import embedding_functions class MemoryStore: def __init__(self, path=“./memory_db”): self.client = chromadb.PersistentClient(path=path) # 使用一个轻量级嵌入模型,生产环境建议用更好的 self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=“all-MiniLM-L6-v2”) self.collection = self.client.get_or_create_collection( name=“gui_memories”, embedding_function=self.embedding_fn ) def store(self, state_desc, intent, action, result_state_desc): # 将一次经历存入记忆 # 记忆的“内容”是状态+意图,用于后续检索 memory_content = f“State: {state_desc}. Intent: {intent}” # 记忆的“元数据”包含动作和结果,用于执行和学习 metadata = {“action”: action, “result_state”: result_state_desc} # 生成一个ID,可以用时间戳+哈希 import uuid mem_id = str(uuid.uuid4()) self.collection.add( documents=[memory_content], metadatas=[metadata], ids=[mem_id] ) def search(self, query_state, query_intent, n_results=3): # 检索相关记忆 query_content = f“State: {query_state}. Intent: {query_intent}” results = self.collection.query( query_texts=[query_content], n_results=n_results ) # 返回记忆内容和元数据 return results3. 动作执行与泛化模块
class ActionExecutor: def __init__(self, app): self.app = app # 例如 pywinauto.Application 连接的计算器对象 def execute_generalized(self, action_template, current_elements): # action_template 例如: {“type”: “click”, “target”: {“text”: “5”}} # current_elements 是当前感知到的元素列表 if action_template[“type”] == “click”: target_desc = action_template[“target”] # 在 current_elements 中寻找最匹配的元素 matched_element = self._find_best_match(target_desc, current_elements) if matched_element: # 这里需要将匹配的元素映射到具体的控件并点击 # 假设我们通过 pywinauto 找到对应按钮 self.app.Dialog.child_window(title=matched_element[“text”], control_type=“Button”).click() return True return False def _find_best_match(self, target_desc, elements): # 简单的文本匹配示例,实际应综合文本、类型、位置等 best_score = -1 best_elem = None for elem in elements: score = 0 if elem.get(“text”) == target_desc.get(“text”): score += 10 # 文本完全匹配,高分 # 可以添加更多匹配规则(类型、相对位置等) if score > best_score: best_score = score best_elem = elem return best_elem4. 智能体主循环
class SimpleMemoryAgent: def __init__(self, perceiver, memory_store, executor): self.perceiver = perceiver self.memory = memory_store self.executor = executor self.current_task = None def learn_and_perform(self, task_intent): self.current_task = task_intent while not self._is_task_complete(): # 1. 感知当前状态 state_info = self.perceiver.perceive() current_state_desc = state_info[“raw_description”] current_elements = state_info[“elements”] # 2. 检索记忆:在类似状态下,为了完成当前任务意图,成功做过什么? search_results = self.memory.search(current_state_desc, task_intent) if search_results[‘documents’]: # 使用最相关的记忆 best_memory_content = search_results[‘documents’][0][0] best_memory_meta = search_results[‘metadatas’][0][0] action_to_try = best_memory_meta[“action”] # 例如 {“type”: “click”, “target”: {“text”: “5”}} else: # 没有记忆,需要探索(例如,通过启发式规则或LLM生成动作) action_to_try = self._explore_action(current_elements, task_intent) # 3. 执行泛化后的动作 success = self.executor.execute_generalized(action_to_try, current_elements) # 4. 感知结果状态 new_state_info = self.perceiver.perceive() new_state_desc = new_state_info[“raw_description”] # 5. 存储记忆(无论成功与否) self.memory.store(current_state_desc, task_intent, action_to_try, new_state_desc) # 6. 根据结果决定下一步(简化处理) if success and self._goal_achieved(new_state_desc): break def _is_task_complete(self): # 判断任务是否完成,基于状态描述或特定目标 pass def _explore_action(self, elements, intent): # 探索策略:例如,随机点击一个按钮或使用简单规则 pass def _goal_achieved(self, state_desc): # 检查目标是否达成,例如计算器显示特定结果 pass4.3 运行与迭代
启动计算器应用,并用pywinauto连接。然后初始化各个模块,让智能体开始学习任务,例如“计算5+3”。
# 连接计算器应用 (Windows示例) from pywinauto import Application app = Application(backend=“uia”).start(‘calc.exe’) dlg = app[‘计算器’] # 初始化组件 perceiver = GUIPerceiver(api_key=“your_openai_key”) memory = MemoryStore() executor = ActionExecutor(dlg) # 需要适配以使用pywinauto控件 agent = SimpleMemoryAgent(perceiver, memory, executor) agent.learn_and_perform(“计算5+3”)第一次运行时,由于记忆库为空,智能体会进入探索模式,可能会随机点击或按预设规则操作。这个过程可能会失败多次,但每次尝试都会作为记忆存储。随着运行次数增加,记忆库丰富,智能体检索到成功路径的概率会大大增加,执行会越来越稳定。
5. 常见问题、挑战与优化方向实录
在实际开发和测试中,我们遇到了不少坑。这里分享一些典型问题和解决思路。
5.1 感知不准:VLM“胡说八道”或漏检元素
- 问题:VLM可能将背景图案误认为按钮,或者无法识别自定义控件。
- 排查与解决:
- 提示词工程:优化提示词,明确指令。例如,要求模型“只关注标准的GUI控件”,或提供控件类型的例子。
- 多模型投票:对于关键判断,可以调用多个VLM(如GPT-4V和Gemini Vision)并比较结果,取共识。
- 混合感知策略:优先使用操作系统提供的无障碍树信息获取精确的控件类型和名称,仅当无障碍树信息缺失或不足时,才调用VLM进行补充理解。将VLM作为“视觉后备”而不是唯一信息来源。
- 缓存与差分感知:不要每次都全屏感知。记录上次操作后的界面状态,只对可能发生变化或感兴趣的区域进行局部感知和差分分析,减少VLM调用量和干扰。
5.2 记忆检索“张冠李戴”:找到不相关的记忆
- 问题:当前界面是“保存文件对话框”,却检索到了“登录对话框”的记忆,因为两者都有“确认”按钮和输入框。
- 排查与解决:
- 丰富查询上下文:检索时,不仅要嵌入当前界面状态,还要嵌入更广泛的任务上下文(如前序步骤、最终目标)。例如,查询向量由“当前屏幕描述 + ‘我正在完成文档编辑后的保存流程’”共同生成。
- 使用元数据过滤:在向量检索前,先用传统数据库对记忆进行一层过滤。例如,只检索“应用名=Word”且“任务类型=文件操作”的记忆。
- 引入失败记忆:不仅要存储成功记忆,也要存储失败记忆及其原因。检索时,可以同时检索成功和失败记忆,避免重蹈覆辙。失败记忆的元数据中可以标记“导致失败的元素特征”。
5.3 动作执行失败:匹配到了元素但操作无效
- 问题:成功匹配并点击了“提交”按钮,但页面无反应。可能原因是元素被遮挡、状态禁用(如灰显)或需要双击。
- 排查与解决:
- 执行前状态验证:在执行动作前,对目标元素进行二次状态检查。例如,通过无障碍树属性检查
IsEnabled,或通过视觉特征检查颜色是否灰暗。 - 动作后结果验证:这是最关键的一环。执行动作后,必须等待一个合理时间,然后感知新状态,与预期结果对比。如果不符合预期,则标记本次执行失败,触发重试或回退策略。
- 丰富动作类型:除了
Click,定义DoubleClick,RightClick,Hover,Drag等。记忆中也应记录精确的动作类型。 - 引入等待与重试机制:在动作执行前后加入显式等待(
WaitFor),确保界面稳定。对于关键操作,配置自动重试次数。
- 执行前状态验证:在执行动作前,对目标元素进行二次状态检查。例如,通过无障碍树属性检查
5.4 系统性能与成本瓶颈
- 问题:频繁调用VLM导致响应慢、API费用高。
- 优化方向:
- 记忆驱动的感知缓存:对于完全相同的界面状态(可通过界面元素的哈希值判断),直接复用之前的感知结果,无需调用VLM。
- 分层感知模型:使用一个快速、小型的模型(如轻量级目标检测网络)进行常规元素检测,只在小型模型置信度低或遇到未知控件时,才启用重型VLM。
- 离线与本地化:探索使用完全本地的多模态模型(如LLaVA-Next)进行界面理解,虽然精度可能稍逊,但能极大降低成本并提升速度。
- 批量处理与异步调用:将多个连续的非紧急感知请求合并或异步化,优化请求流程。
5.5 长期运行的记忆爆炸与管理
- 问题:运行数月后,记忆库庞大,检索速度下降,且包含大量过时或无用的记忆。
- 优化方向:
- 记忆压缩与抽象:将一系列连续的成功操作压缩成一个更高级别的“技能”记忆。例如,将“点击A、输入B、点击C”抽象为“完成表单F1填写”。
- 基于效用的记忆管理:为每条记忆附加“效用值”。每次成功检索并使用该记忆完成任务,则增加其效用;每次检索后导致失败,则降低效用。定期清理效用值低于阈值的记忆。
- 聚类与去重:对记忆进行聚类,将描述相似状态和意图的记忆归为一类,只保留该类中最具代表性或成功率最高的一条作为“原型记忆”。
构建一个健壮的“可执行自主记忆”系统是一个持续迭代的过程。从简单的原型开始,聚焦一个垂直场景(如操作计算器或某个特定网站),逐步解决上述问题,再扩展到更复杂的领域。这个过程中最大的体会是,没有一劳永逸的银弹,必须让智能体具备从错误中学习、管理自身经验的能力,这才是“自主”二字的真谛。