数据截至 (上游 commit 2689884a6257)
精华、边界与横向对比
这一章讲什么: 前面六章是「它怎么做的」。这一章是「你该带走什么」「它在哪会崩」「它和别人比选了什么不同的路」。
1. 巧妙之处(可以直接抄走的七条)
1.1 用接口设计约束模型,而不是用 prompt
ActionEvent.to_prompt_dict(legacy/openadapt/models.py:517-520):只要这个动作有了元素描述,就把 mouse_x/y/dx/dy 从给模型看的字典里删掉。
模型看不到坐标,就不可能照抄坐标,只能在描述层面推理。这比在系统提示词里写「请不要输出坐标」可靠一个数量级——能删的字段就别靠嘴说。
1.2 把异常喂回下一次 prompt
三处都用了同一个模式:
| 场景 | 位置 |
|---|---|
| 改写流程失败 | strategies/visual.py:114(@utils.retry_with_exceptions()) |
| 描述数量对不上掩膜数量 | strategies/visual.py:516-530 |
| 描述在新屏幕上匹配不到 | strategies/visual.py:232-234 |
把 exceptions 列表传进模板渲染,模型下一轮能看到自己上次错在哪。重试不只是重来,是带着错误信息重来。
1.3 录制期就把系统参数快照下来
Recording.double_click_interval_seconds / double_click_distance_pixels(models.py:56-57)。
「两次点击算不算双击」必须按录制那台机器的设置判定。归并逻辑 优先读录制上的值,读不到才回退当前机器并打 warning(events.py:391-399)。
推广一句:任何依赖环境的判定阈值,都应该和数据一起存下来。
1.4 归并不删数据,只建父子树
make_parent_event(events.py:151)+ ActionEvent.parent_id 自引用外键。
收益在三个地方兑现:回放时对键盘事件递归播子事件(playback.py:97-100)、脱敏时只需处理顶层(scrub.py:189-192)、调试时能一路回溯到原始事件。
1.5 空间信息一起塞进 DTW 向量
browser.py:517-548:对齐浏览器 DOM 事件和操作系统事件时,不只用时间戳,而是构造 [时间, x, y, dx, dy] 五维向量做多维 DTW。
时钟会偏,坐标不会。 加一个正交维度,对齐的鲁棒性显著上升。
1.6 判定逻辑做成纯函数,就能穷举测试
check_release_health.py 把「 采集状态」(collect_state,要联网)和「根据状态判定」(evaluate,:265,纯函数)彻底分开。于是 300 多行的 self_test() 可以完全离线跑遍所有状态组合,--dump-state 还能存下线上真实状态复现。
1.7 用 AST 检查那些永远不会被执行的 import
tests/test_import_integrity.py:懒加载的 import 藏在函数体里,普通测试跑不到。解法是不运行、直接解析 AST(抽象语法树,源码被解析成的树状结构),跨包比对符号是否存在,连关键字参数都比对(test_no_phantom_kwargs)。
任何用了大量惰性导入的项目都该抄这一条。
2. 边界与局限(诚实清单)
2.1 关于这个仓库本身
- 产品核心不在这里。 编译器、确定性回放、效果验证、失败即停、受管修复,全部在
openadapt-flow。本仓库的CLAUDE.md和README.md都明确要求「不要在这个仓库实现第二个引擎」。凡是 README 里关于VERIFIED/halt 的描述,在本克隆里只能看到声明,看不到实现。 docs/architecture.md已经过时。 它画的还是「meta-package + capture/ml/evals/viewer」的图,完全没有openadapt-flow。仓库自己的CLAUDE.md把docs/标注为「非规范的历史仓库文档」。
2.2 关于 legacy 代码
| 问题 | 位置 | 后果 |
|---|---|---|
| 交互式调试器留在生产路径 | strategies/base.py:112、playback.py:110、events.py:936、adapters/prompt.py:37 | 无人值守时挂死 |
| 时间戳乱序只记 log 不处理 | record.py:196-198 | 下游所有归并逻辑的前提被破坏 |
| 描述匹配靠精确字符串相等 | strategies/visual.py:229-231 | 模型措辞一变就找不到 |
| 重试循环无上限 | strategies/visual.py:223 | 可能不退出 |
| 分割结果只在内存 | strategies/visual.py:59 | 进程退出全丢,# TODO: store to db |
| 相似片段无法区分 | strategies/visual.py:426 | 表格类界面直接 raise ValueError |
| SoM(Set-of-Mark)适配器不可用 | adapters/som.py:87-90 | 服务端压缩破坏了「颜色即掩膜」的假设 |
| 归并只跑一轮 | events.py:14(MAX_PROCESS_ITERS = 1) | 「反复直到收敛」的框架实际未生效 |
| 屏幕采集无节流 | record.py:707-710 | # TODO: throttle 仍在 |
| 脱敏默认关闭、仅英文、视频未实现 | config.py:183、presidio.py:59、privacy/base.py:90 | 默认录制不脱敏 |
| 模块 import 有副作用 | record.py:68(截屏)、presidio.py:34-39(下模型) | 无头/离线环境炸 |
| Python 版本已冻结 | docs/LEGACY_FREEZE.md | legacy 只支持 3.10–3.11,不再更新 |
2.3 关于方法本身
OpenAdapt 的路线有一个结构性前提:必须先有人演示一遍。 它不解决「没演示过的新任务」。README 也把这条边界写在门口——用得上 API 的时候就用 API,只在界面绕不过去时才用它。
3. 横向对比:同货架的 computer-use 项目
分歧点只有一个:模型在执行路径上吗
模型介入程度 低 ├────[1]────┼────[2]────┤ 高
[1] OpenAdapt(今天)
演示 → 编译成可重复执行的程序
健康路径零模型调用
确定、可审计,代价是必须先有人演示一遍
[2] OpenAdapt(legacy)
演示 + 一句自然语言指令 → 模型改写整条动作序列
每步一次模型调用
灵活,但慢且不确定
[3] 通用 GUI agent
自然语言任务 → 模型每步决策
每步一到多次模型调用
最灵活、最慢、最不确定
同一个项目的两个时期,正好站在这条轴的两端。 这是 OpenAdapt 最有教学价值的地方——它自己走完了从「全交给模型」到「模型只用在编译期」的路。
和兄弟项目的取舍对照
| 项目 | 任务从哪来 | 定位元素靠什么 | 执行期要模型吗 |
|---|---|---|---|
| OpenAdapt(今天) | 人的一次演示 | 编译期留下的结构/a11y/视觉/OCR 多路证据 | 健康路径不要 |
| OpenAdapt(legacy) | 演示 + 一句自然语言修改 | 分割片段的自然语言描述 | 每步都要 |
| agent-s | 自然语言任务 | 规划 + 经验记忆 | 每步都要 |
| ui-tars-desktop | 自然语言任务 | 端到端多模态模型直接出坐标 | 每步都要 |
| midscene | 自然语言描述 | 视觉理解 + 缓存过的定位结果 | 首次要,命中缓存可省 |
| microsoft-ufo | 自然语言任务 | Windows UI Automation 控件树 | 每步都要 |
| self-operating-computer | 自然语言任务 | 截图 + 模型直出坐标 | 每步都要 |
三条可以拿去比较任何 GUI agent 的判据
- 锚点是什么? 坐标(最脆)→ 视觉特征 → 无障碍/DOM 结构 → 多路证据交叉(最稳)。
- 谁承担不确定性? 每步问模型 = 每步都可能错;编译成程序 = 错误集中在编译期,可以人工审。
- 怎么知道自己成功了? 大多数项目止步于「动作发出去了」。OpenAdapt 今天的主张是用独立通道确认业务结果——这一条是它和整排兄弟项目最大的差异,虽然实现不在本克隆里。
一句话总结这个项目在货架上的位置
别的 GUI agent 在回答「模型能不能自己操作电脑」;OpenAdapt 今天在回答「怎么让一件已经被演示过的事,可重复、可验证地办成」。前者是能力问题,后者是可靠性问题。
延伸阅读:货架总览见 ../index.md。