构建自进化网页智能体:从认知感知到持续学习的自动化实践
1. 项目概述从“执行脚本”到“认知学徒”的进化最近在跟几个做RPA和自动化测试的朋友聊天大家普遍有个痛点现在的网页自动化工具无论是传统的Selenium、Playwright还是新兴的基于LLM的Agent都太“脆”了。一个页面结构微调、一个弹窗、一个验证码甚至只是加载慢了几秒整个流程就崩了。我们花大量时间写异常处理、维护XPath选择器本质上是在用静态规则对抗动态世界。这让我想起了自己最近在琢磨的一个方向也是今天想跟大家深入聊聊的具备认知感知探索能力的自进化网页智能体。这个项目标题“Learning to Adapt: Self-Improving Web Agent via Cognitive-Aware Exploration”听起来很学术但内核非常务实。它要解决的就是上面那个“脆”的问题。简单说我们不再试图编写一个能应对所有情况的“完美脚本”而是创造一个能像人类一样在陌生网页环境中观察、思考、试错、学习并最终适应的智能体。它知道自己“知道什么”比如如何点击按钮更关键的是它知道自己“不知道什么”比如这个新出现的滑块验证该怎么过然后它会主动、有策略地去探索未知把探索得来的新知识固化下来让自己下次遇到同类问题时能处理得更好。这就像给一个新手程序员配了一个永不疲倦的、能自己写文档和调试代码的“认知学徒”。最近“Pi Agent Web”这个概念挺火很多人问它到底是做什么的。在我看来它正是这个自进化网页智能体理念的一个具体实践或雏形。其核心价值不在于替代某个具体工具而在于引入了一套持续学习和适应的机制。传统的Web Agent是“开环”的输入指令执行预设动作序列输出结果结束。而自进化的、具备认知感知能力的Agent是“闭环”的执行观察结果与预期的差异认知基于差异规划探索策略感知执行探索从探索结果中学习更新自身知识库然后继续执行或应对新任务。这个闭环就是“Self-Improving”的精髓。那么这样一个智能体适合谁来关注或构建呢我认为有三类人一是业务自动化开发者厌倦了无止境的脚本维护希望构建更健壮、生命周期更长的自动化流程二是AI应用工程师希望将大语言模型LLM的能力从“聊天”切实落地到“操作”解决具身智能Embodied AI在数字世界的一个关键问题三是研究型开发者对强化学习、元学习、探索-利用权衡Exploration-Exploitation Trade-off等课题在真实环境中的应用感兴趣。接下来我将拆解构建这样一个智能体所需的核心思路、技术栈、实操难点以及我趟过的一些坑。2. 核心架构设计构建认知闭环的四大支柱要打造一个能自我改进的网页智能体我们不能只堆砌模型和API。需要一个稳固的、支持持续学习的系统架构。经过多次迭代我认为一个有效的架构应该围绕以下四个支柱展开它们共同构成了智能体的“认知循环”。2.1 支柱一多层次环境感知与表征智能体要学习首先得“看清”世界。对于网页环境感知不是简单截图而是构建一个既能被机器高效处理又能保留丰富语义信息的内部表征。1. 原始信号层这是基础包括DOM树通过浏览器开发工具协议如CDP获取完整的HTML结构。光有静态DOM不够还需要监听DOM的突变MutationObserver实时捕捉动态加载的内容。视觉信息屏幕截图或页面局部截图。这对于理解图标、验证码、非标准控件如Canvas绘制的图表至关重要。我们通常使用轻量化的目标检测模型如YOLO系列或视觉语言模型如GPT-4V的API来解析截图。辅助信息浏览器控制台日志、网络请求记录XHR/Fetch、性能指标加载时间。这些信息能帮助智能体判断页面状态是否加载完成、发现异步加载的数据或诊断错误。2. 语义抽象层这是将原始信号转化为智能体能理解的“概念”的关键。我们构建一个可操作的UI元素清单。每个元素包含基础属性从DOM中提取的tagName, attributes (id, class, name, type, aria-*), textContent, bounding box坐标。视觉特征从对应区域截图提取的视觉嵌入Embedding用于相似性匹配。功能预测使用一个小型分类模型或Prompt LLM预测该元素可能的交互类型如CLICKABLE_BUTTON,INPUT_TEXT,SELECT_DROPDOWN,LINK,SCROLLABLE_AREA等。关系上下文该元素在DOM树中的位置XPath/CSS Selector备用、邻近元素的文本作为上下文提示。实操心得初期我们试图用纯LLM解析整个HTML来生成这个清单成本高且延迟大。后来优化为混合策略先用轻量级规则如检查onclick属性、标签类型和简单模型过滤出候选交互元素再对候选元素用LLM进行精细化的功能预测和描述生成。这样在保证精度的同时将单次感知的Token消耗降低了70%以上。2.2 支柱二认知核心与决策引擎这是智能体的大脑负责根据任务、历史和环境状态做出决策下一步做什么。它通常由一个大语言模型驱动但需要精心设计提示词Prompt和决策框架。决策流程如下状态整合将“语义抽象层”产生的UI元素清单、当前URL、任务目标、已执行的动作历史整合成一段结构化的自然语言描述作为模型的“当前观察”。推理与规划模型基于观察输出下一个动作。动作应是一个结构化的指令例如{ action: click, target: { description: 蓝色的、写着‘提交’的按钮, confidence: 0.95 }, reasoning: 表单已填写完毕根据页面布局和常见模式下一步应提交表单。 }探索指令注入这是“Cognitive-Aware Exploration”的关键。当模型对下一步行动信心不足confidence低或遇到从未见过的UI模式时决策引擎应能主动发出探索指令而非盲目猜测。例如{ action: explore, goal: 弄清这个滑动验证块的操作方式, strategy: systematic_trial, // 或 random, model_guided parameters: { element: slider_container, max_steps: 5 } }踩坑记录直接让LLM输出“explore”动作它往往表现得很犹豫或干脆不输出。我们的解决方案是在Prompt中显式定义探索作为一种首要的动作类型并给出清晰的条件和格式示例。例如“如果你不确定如何完成当前子任务或者遇到了一个不熟悉的界面组件你可以选择‘explore’动作。请描述你的探索目标和初步策略。”2.3 支柱三探索策略执行与学习模块当决策引擎发出“探索”指令后这个模块负责接管执行具体的探索行为并将探索结果转化为可重用的知识。1. 策略执行器根据explore指令中的策略字段调用不同的探索例程。系统性尝试对于未知控件尝试一系列基础交互点击、输入文本、拖拽、悬停观察页面状态变化DOM突变、网络请求、URL变化。模型引导探索使用一个经过训练的强化学习策略网络或者用一个专门的“探索建议”LLM根据当前状态生成更有可能获得信息增益的动作序列。随机探索在限定范围内随机交互用于发现潜在的可操作区域或功能。2. 结果评估与知识提取探索不是瞎逛必须有目标。我们需要定义“信息增益”或“任务进展”的衡量标准。变化检测比较探索前后的页面状态DOM哈希、关键元素、URL。显著变化可能意味着成功交互。目标相关性评估当前状态是否更接近子任务目标可用另一个LLM或规则判断。知识固化如果探索成功例如发现滑动验证需要拖拽滑块到指定位置就将这个“状态-动作-结果”三元组作为一条新经验存储到知识库中。存储时需要抽象化不是存储具体的像素坐标而是存储“对于具有[某些视觉/属性特征]的滑块元素正确的操作是‘drag_and_drop’”。2.4 支柱四动态知识库与经验回放这是智能体能够“Self-Improving”的记忆中枢。它不是一个简单的数据库而是一个支持快速检索、类比和更新的经验库。1. 知识表示每条经验是一个案例Case包含情境触发探索前的页面状态特征关键UI元素的抽象描述、任务上下文。问题遇到的具体障碍如“未知验证方式”。解决方案探索得到的有效动作序列。效果执行后的结果状态变化。元数据成功率、使用次数、最后更新时间。2. 检索与复用当智能体再次遇到相似情境时优先从知识库中检索历史经验。这里的关键是相似性计算。我们使用UI元素的视觉嵌入和属性嵌入的加权组合来计算当前页面与历史案例中“情境”的相似度。找到相似案例后可以直接复用解决方案或将其作为强力提示Few-shot Example注入决策引擎的Prompt中。3. 持续优化知识库需要维护。冲突解决如果同一情境下发现了更优解决方案则更新。衰减与淘汰长期未使用或成功率低的经验权重降低或归档。泛化将多个具体案例总结成更通用的规则或提示词提升未来应对新变体的能力。这个四支柱架构形成了一个从感知到决策从探索到学习再从学习反馈到决策的完整闭环。接下来我们深入到每个环节的实操细节中。3. 关键技术实现与实操要点有了架构蓝图我们来聊聊具体怎么实现。这里会涉及不少工具选型和代码片段我会重点讲清楚为什么这么选以及实际做的时候要注意什么。3.1 环境感知的实现平衡精度与效率我们选择Playwright作为浏览器自动化底层库因为它对现代Web特性支持好CDP协议暴露充分且能同时支持多浏览器引擎。核心感知代码结构示例async def perceive_page(page): # 1. 获取基础DOM和视觉信息 dom_snapshot await page.evaluate(() document.documentElement.outerHTML) screenshot await page.screenshot(typepng, full_pageFalse) # 通常不需要全页 viewport_size page.viewport_size # 2. 提取可交互元素候选集 (轻量级过滤) candidate_elements [] all_elements await page.query_selector_all(*:not(html):not(head):not(body):not(script):not(style)) for element in all_elements: # 规则1检查是否可见且在视口内 is_visible await element.is_visible() bounding_box await element.bounding_box() if not is_visible or not bounding_box: continue # 规则2检查是否具有交互属性 tag_name await element.evaluate(el el.tagName.toLowerCase()) is_clickable await element.evaluate( el { return el.hasAttribute(onclick) || el.tagName BUTTON || el.tagName A || el.role button || window.getComputedStyle(el).cursor pointer; } ) is_input tag_name in [input, textarea, select] if not (is_clickable or is_input): continue # 非交互候选跳过精细分析节省资源 # 对候选元素进行精细特征提取 element_info await extract_element_details(element, bounding_box, screenshot) candidate_elements.append(element_info) # 3. 使用LLM对候选元素进行功能分类与描述生成 (批量处理以节省Token) semantic_elements await llm_classify_elements(candidate_elements) return semantic_elementsextract_element_details函数需要精心设计async def extract_element_details(element, bbox, page_screenshot): # 提取文本和属性 text await element.inner_text() attributes await element.evaluate( el { const attrs {}; for (const {name, value} of el.attributes) { attrs[name] value; } return attrs; } ) # 裁剪元素视觉区域 (用于生成视觉嵌入) from PIL import Image import io element_screenshot await element.screenshot() pil_img Image.open(io.BytesIO(element_screenshot)) # 使用视觉编码器生成嵌入 (例如CLIP) visual_embedding clip_model.encode_image(preprocess(pil_img)) return { selector: await element.evaluate(el el.cssSelector()), # Playwright内置 bbox: bbox, tag: await element.evaluate(el el.tagName), text: text[:200], # 截断长文本 attributes: {k: v for k, v in attributes.items() if k in [id, class, name, type, role, aria-label]}, visual_embedding: visual_embedding.tolist(), is_candidate: True }注意事项视觉嵌入的生成是计算瓶颈。对于实时性要求高的场景可以考虑使用更轻量的模型如MobileCLIP。仅在元素没有清晰文本或属性标识时如图标按钮才启用视觉嵌入匹配。对嵌入进行降维PCA和量化提升检索速度。3.2 决策引擎的Prompt工程引导LLM成为稳健的规划者Prompt的设计直接决定LLM是“天才”还是“疯子”。我们的目标是让LLM输出结构化、可解释、且包含探索意识的决策。一个经过多次迭代的Prompt模板示例你是一个专业的网页操作智能体。你的目标是通过操作浏览器来完成用户任务。 ## 当前任务 {task_description} ## 当前页面状态 - URL: {current_url} - 页面标题: {page_title} - 可交互元素列表 (按视觉位置从上到下从左到右排序): {element_list_json} ## 动作历史 (最近5步) {action_history} ## 可用动作类型 1. click: 点击一个元素。需要提供元素描述。 2. type: 向输入框输入文本。需要提供元素描述和文本内容。 3. select: 从下拉框选择选项。需要提供元素描述和选项值。 4. scroll: 滚动页面。需要提供方向(up/down/left/right)和幅度(small/medium/large)。 5. navigate: 跳转到新URL。 6. wait: 等待特定条件如元素出现、加载完成。需要提供条件描述。 7. explore: **当你对如何推进任务不确定或遇到一个不熟悉、无法识别的界面组件时使用此动作。** 你需要说明探索的目标和你打算尝试的策略如systematic_trial-对特定元素尝试一系列基础交互find_alternative-寻找其他可能达成目标的路径。 ## 输出格式 请严格按以下JSON格式输出只输出JSON对象 { action: 动作类型, target: {description: 对目标元素的清晰描述需与上方元素列表中的信息对应, confidence: 0.0到1.0之间的数值}, parameters: {}, // 根据动作类型填充如type动作的textselect动作的value reasoning: 简要解释为什么选择这个动作基于当前状态和任务目标。, exploration_trigger: false // 如果是常规动作则为false如果是因为不确定性而触发探索则为true并在reasoning中说明。 } ## 特别提醒 - 优先使用有明确标识如id、独特文本的元素。 - 如果当前页面没有明显能推进任务的元素考虑使用scroll或wait。 - **当信心不足confidence 0.7或遇到未知组件时不要强行猜测主动使用explore动作。这是你学习和适应新环境的关键能力。** - 保持动作简单一步只做一个操作。实操心得confidence字段和exploration_trigger字段是引导LLM“承认无知”的关键。我们会在后续模块中监控这两个字段。如果confidence持续低于阈值或exploration_trigger为真则会触发更主动的探索模式甚至暂时切换到一个更“好奇”的LLM参数配置如提高temperature。3.3 探索策略的具体实现从随机到系统化探索模块是智能体智慧的体现。我们实现了几个层次的探索策略。1. 系统性尝试策略async def systematic_exploration(page, target_element_description, max_trials5): 对特定元素进行一系列基础交互尝试 trials [] element await find_element_by_description(page, target_element_description) if not element: return {success: False, error: Element not found} common_actions [ (click, {}), (double_click, {}), (right_click, {}), (hover, {}), (type, {text: test}), (clear_and_type, {text: test123}), ] initial_state await get_page_state_hash(page) for action_name, params in common_actions[:max_trials]: try: # 执行动作前保存状态 before_state await get_page_state_hash(page) await perform_action(page, element, action_name, params) await page.wait_for_timeout(1000) # 等待响应 after_state await get_page_state_hash(page) # 判断状态是否发生变化 if before_state ! after_state: trials.append({ action: action_name, params: params, state_changed: True, new_url: page.url, # 可以加入更精细的变化检测如弹窗出现、特定元素消失等 }) # 如果变化是积极的如跳转到下一步可以提前结束探索 if await is_positive_change(page, initial_state): return {success: True, discovered_action: action_name, trials: trials} else: trials.append({action: action_name, state_changed: False}) # 如果不是点击类动作导致页面跳转尝试回退或重置 if action_name not in [click, double_click] and before_state ! after_state: await page.go_back() # 或执行其他重置逻辑 await page.wait_for_load_state(networkidle) except Exception as e: trials.append({action: action_name, error: str(e)}) continue return {success: False, trials: trials, message: No effective interaction found}2. 基于好奇心的模型引导探索当系统性尝试无效时我们可以启用一个更高级的探索模式。例如训练一个简单的强化学习RL智能体其状态是页面元素的抽象表示动作是预定义的操作集合点击某个位置、输入等奖励函数设计为正奖励页面状态发生显著变化1任务进度被推进5。负奖励触发错误-1进入死循环或无关页面-2。小正奖励访问了之前未访问过的新状态0.1鼓励探索新区域。这个RL智能体可以在一个网页模拟环境如基于DOM的简单游戏或表单中进行预训练学习基本的探索策略。在实际运行时当主LLM决策引擎发出explore指令且策略为model_guided时就调用这个RL策略网络来生成一系列探索动作。踩坑记录纯粹的随机探索在复杂网页中效率极低容易陷入无意义循环如反复点击logo返回首页。“系统性尝试”对于表单、按钮等标准控件非常有效但对于像“滑动拼图验证”这种复杂交互就力不从心。这时需要结合视觉模型来理解组件的类型“这是一个滑块”然后调用预先为“滑块”设计好的专用探索例程尝试拖拽这属于“基于模型的探索”。因此一个成熟的探索模块应该是分层和可插拔的先匹配已知组件类型调用专用策略再降级到系统性尝试最后才考虑随机探索。3.4 知识库的构建与检索让经验流动起来知识库我们选用向量数据库如ChromaDB, Weaviate来存储和检索基于嵌入的经验案例。经验存储def store_experience(knowledge_base, situation_embedding, problem, solution, outcome): situation_embedding: 由当前页面关键元素的视觉/语义嵌入融合而成。 problem: 字符串描述遇到的问题。 solution: JSON结构记录有效的动作序列。 outcome: 字符串描述结果。 experience_id knowledge_base.add( embeddings[situation_embedding], documents[json.dumps({ problem: problem, solution: solution, outcome: outcome, timestamp: time.time(), success_count: 1 })], metadatas[{type: exploration_experience}] )相似经验检索当智能体进入一个新页面/状态时会实时计算当前状态的嵌入并从知识库中检索Top-K个相似案例。def retrieve_similar_experiences(knowledge_base, current_state_embedding, top_k3): results knowledge_base.query( query_embeddings[current_state_embedding], n_resultstop_k ) similar_experiences [] for doc, metadata, distance in zip(results[documents][0], results[metadatas][0], results[distances][0]): exp json.loads(doc) exp[similarity_score] 1 - distance # 假设使用余弦相似度 similar_experiences.append(exp) return similar_experiences检索到的经验有两个主要用途直接复用如果当前问题与历史案例高度相似相似度0.9且解决方案通用可以直接执行历史动作序列。提示增强将相似案例作为Few-shot Examples插入到决策引擎的Prompt中帮助LLM更好地理解当前情境。例如在Prompt的“可交互元素列表”部分之后加入一个“相关历史经验”章节展示过去如何解决类似问题。注意事项知识库容易积累垃圾数据或过时经验。我们建立了简单的经验权重机制每次成功使用一条经验其success_count加1并更新last_used_time。定期清理长时间未使用且成功率低的经验。同时对于检索到的经验如果直接复用失败会触发一次新的探索并将新旧经验进行对比和融合可能生成一条更通用或更准确的新经验。4. 系统集成与工作流编排将以上模块串联起来形成一个自治的工作流是项目从原型到可用的关键一步。4.1 主控制循环设计智能体的核心是一个无限循环直到任务完成或失败退出。class SelfImprovingWebAgent: def __init__(self, llm_client, kb_vector_store, exploration_policy): self.llm llm_client self.kb kb_vector_store self.explorer exploration_policy self.action_history [] self.known_component_patterns [] # 已知UI模式缓存 async def run_task(self, initial_task, start_url): current_url start_url task_stack [initial_task] # 支持子任务分解 while task_stack: current_subtask task_stack[-1] # 1. 感知 page_state await self.perceive(current_url) # 2. 检索历史经验 (认知感知) state_embedding self._encode_state(page_state) similar_exps retrieve_similar_experiences(self.kb, state_embedding) # 3. 决策 (注入历史经验作为上下文) llm_decision await self.make_decision( page_state, current_subtask, self.action_history[-5:], similar_exps ) # 4. 执行与学习 if llm_decision[action] explore: # 进入探索模式 exploration_result await self.explorer.execute( llm_decision[goal], llm_decision[strategy] ) if exploration_result[success]: # 探索成功提取知识并存储 new_knowledge self._extract_knowledge( page_state, llm_decision[goal], exploration_result[discovered_action_sequence] ) self.kb.store(new_knowledge) # 继续执行任务可能使用新发现的动作 await self.execute_actions(exploration_result[discovered_action_sequence]) else: # 探索失败记录教训可能尝试其他策略或请求人工帮助 self._record_failure(llm_decision[goal], exploration_result) else: # 执行常规动作 success await self.execute_single_action(llm_decision) self.action_history.append((llm_decision, success)) # 如果动作失败且信心度原本不高可能自动转为探索 if not success and llm_decision.get(confidence, 1) 0.7: # 自动触发对目标元素的探索 await self.trigger_auto_exploration(llm_decision[target]) # 5. 评估任务进度可能分解出新子任务或标记完成 new_subtasks, is_complete await self.evaluate_progress(current_subtask, page_state) if is_complete: task_stack.pop() elif new_subtasks: task_stack.extend(new_subtasks) # 更新当前URL/状态 current_url await self.get_current_url() return {status: completed, history: self.action_history}4.2 子任务分解与规划复杂任务如“在电商网站下单购买某商品”需要被分解。我们让LLM在任务开始时进行一次性分解或在遇到复杂页面时进行动态分解。动态分解示例当决策引擎发现当前页面包含一个多步骤表单而任务目标是填写该表单时它可以主动发起一个“分解子任务”的请求。用一个专门的Prompt让LLM分析表单结构输出如[填写收货地址, 选择配送方式, 支付]这样的子任务序列。然后主循环将当前任务压栈转而处理这些子任务。实操心得子任务分解的粒度很重要。太粗如“完成购物”没用太细如“点击姓名输入框”会导致规划开销巨大。一个好的经验法则是一个子任务应对应一个明确的、可验证的页面状态转变比如“从商品详情页进入购物车页”、“从空白表单到所有必填项标绿的表单”。5. 常见问题、调试与性能优化在实际开发和测试中你会遇到各种各样的问题。下面是我总结的一些典型问题及其解决思路。5.1 感知一致性难题问题同一页面两次感知得到的UI元素列表顺序或描述可能有细微差异导致后续匹配失败。解决元素唯一标识优先使用稳定的选择器如id或>