Agent 反复填同一个输入框:一个属性读取方式引起的静默循环
WebRunner 里最难查的一个缺陷:观察层读的是 HTML 初始属性,动作改的却是运行时状态,于是模型永远看不到「已经填过」。以及它如何变成一条写进设计里的约束。
- Agent
- 浏览器自动化
- 调试
- 观察层
浏览器 Agent 的循环很朴素:观察 → 决策 → 执行 → 自检。用户给一句目标,Agent 把当前页面提炼成文本快照喂给模型,模型每步返回一个原子动作(工具集只有 8 个:click / type_text / select / navigate / scroll / wait / done / fail),执行结果 —— 包括错误 —— 作为反馈进入下一轮。
在这个循环里,我做了一个基础决定:观察层要压缩,而不是把整页 HTML 交给模型(整页 HTML 100KB+、上万 token,其中约 90% 是样式与结构噪声);所以只把可见交互元素编号与页面大纲作为观察,单步输入压到约 3K token。下面两个真实的坑都出在这一层。事实与测量方式来自项目记录,超出记录的解释会标注为「分析」。
问题一:模型反复输入同一个框
任务进入死循环:模型一遍遍地往同一个输入框里输入内容。它看起来像「模型不聪明」,但项目记录里把它归类为一个隐蔽的「静默错误」——因为根因不在模型,而在快照的数据采集上。
根因:读的是初始属性,改的是运行时状态
我把问题定位到快照层:它当时用 getAttribute('value') 读取输入框。这个调用拿到的是 HTML 初始属性;而 Playwright 的 fill() 修改的是运行时属性。两者不是同一个东西,于是快照永远显示输入框是空的,模型也就「看不到自己已经填过」,只能再填一次。
修复:改成读取运行时值,并写成约束
- 我改的实现:读取元素当前的运行时值(
el.value),而不是getAttribute('value')。 - 留下的约束:把「快照必须反映运行时状态」固化成设计约束,而不是当成一次性的 bugfix。
复盘结论是:属性读取方式的一个细节差异,足以让模型陷入重复操作的循环。这就是「架构约束来自真实缺陷」的样子 —— 这条约束不是凭空写的,是被一个具体问题逼出来的。
问题二:SPA 重渲染之后,序号指向了别的元素
快照里的交互元素编号(bid)只在当次快照内有效。单页应用局部重渲染之后,同一个编号会指向完全不同的元素 —— 模型按编号点击,就点错了目标。
方案:元素指纹 + 歧义时保守报错
- 考虑过的两个方向:继续用快照序号定位;或用元素指纹做跨步骤映射。
- 我最终选择的:FNV-1a 元素指纹(
tag | role | name | aria-label | placeholder | id | 文本前 40 字 | href)持久映射。 - 动作定位改成:编号 → 指纹 → 在当前 DOM 中重新查找。
- 歧义的处理:如果两个元素指纹完全相同,宁可报错让模型重新观察,也不猜着点击。
与这两个坑配套的另一条设计:错误即反馈
既然错误无法避免,我就没让它终止任务:把异常格式化成工具结果回传,模型可以自己调整策略。项目记录里有一个很典型的例子:GitHub 搜索框的 `fill` 超时之后,模型自主降级为直接构造搜索 URL,仍然完成了任务。任务回放页记录了这一步的失败与降级过程。
验证:这些修复有没有真的修好
| 指标 | 结果 | 测量方式 |
|---|---|---|
| 评测成功率 | 13/14(92.9%) | 自建 6 用例评测集(4 个本地 fixture + 2 个外网),跨多次运行累计 |
| 单轮全量通过率 | 6/6 | DeepSeek 单轮全量运行(2026-09-02 记录) |
| 外网用例通过率 | 4/5(80%) | GitHub 外网用例,受外部页面变化影响 |
| 上下文压缩效果 | bing -61%(35,488 → 13,949)、github -35%(33,752 → 21,924) | prompt token 实测前后对比 |
| 截图体积 | 116,455 B → 17,107 B(-85%) | 改为 JPEG q70 后的实测平均单张体积 |
| 后端测试 | 28 项全绿 | pytest 本次实测收集 28 项 |
外网用例只有 4/5,因为外部页面本身会变 —— 这条我如实写在结果里,没有四舍五入成「全通过」。能跑出 4/5 的环境,比一个永远 5/5 的假象更有参考价值。
复盘
- 我把观察层当成 Agent 的地基:模型的行为上限,取决于它看到的东西是否真实。
- 不确定时我选择保守报错:无法区分的歧义场景,让模型重新观察比误操作便宜。
- 约束从缺陷中来:「快照必须反映运行时状态」这条约束的价值,来自一次真实的死循环。
- 错误要当反馈:把异常回传,模型才有机会自己降级。
完整的观察层设计、可靠队列(Redis Streams + XAUTOCLAIM 认领 pending 任务)与实测数据在项目页里:查看 WebRunner 项目详情。
韦坤计算机科学本科生 · 后端开发 / AI 应用开发方向
相关项目
用一句自然语言描述目标,Agent 自主操控真实浏览器完成:把页面提炼成紧凑文本快照喂给模型,模型每步返回一个原子动作,错误作为反馈进入下一轮。
- Python 3.14
- FastAPI
- SQLAlchemy(async)
- 还有 12 项技术
相关文章
我为什么不相信 Agent 说「完成了」
AI / Agent约 4 分钟从分阶段实施到全项目回归检查:Agent 可以承担越来越多的执行工作,但「Agent 说完成了」不能自动等于「工程结果已经完成」。记录我如何把它的执行结果变成可验证的工程结果。
为什么不让大模型给自己打分:CodePilot 的评分引擎与不可信输入边界
AI / Agent约 4 分钟同一份代码多次审计分数会变,所以我把「找问题」和「算分数」拆开:LLM 只输出结构化 Finding,总分由确定性评分引擎按维度权重计算,并给不可信输入划出明确边界。
忠实度 0.25 → 0.99:一次先怀疑评测、再怀疑模型的排查
复盘约 3 分钟忠实度只有 0.25,与人工观察明显不符。这是一次把嫌疑人先放在评测口径上的排查复盘:引用预览被截断污染了判分上下文,以及为什么我坚持给 RAG 项目先建可信的评测,再谈优化。