跳到主要内容

数据截至 (上游 commit 96983c73ed09)

第 7 章 · 巧妙之处、边界与横向对比

这章讲什么: 前六章讲了「它怎么转」。这一章讲「哪些地方值得抄」「它会在哪崩」「跟别人比取舍在哪」。


7.1 值得带走的十处设计

① 错误信息是写给模型的

UFO 里几乎所有异常文本都直接面向模型措辞。三处例子:

位置文本
ufo/module/dispatcher.py:55... please retry or execute a different command.
ufo/automator/action_execution.py:108... please refresh the application state to get the latest interactable control information.
ufo/client/computer.py:550Tool {tool_key} is not registered in current computer: {self._name}

妙在哪: agent 系统里,异常的最终读者是 LLM 而不是人。把「下一步该怎么办」写进错误消息,等于免费获得一次纠错引导。

② id + name 双保险,不一致时执行且告知

模型必须同时报编号和名字;不一致时以编号为准执行,再把真实名字写进返回值告诉模型(ufo/client/mcp/local_servers/ui_mcp_server.py:312-327,_verify_id:170)。

妙在哪: 拒绝执行会让屏幕毫无变化,模型无从判断发生了什么,只会原样重试。执行 + 告知,让模型下一步能看见后果并纠偏。

③ 声明式段间依赖 + 三处校验

@depends_on / @provides 把「我要什么、我给什么」写在类头上,构造期查一次链、运行期执行前后各查一次(ufo/agents/processors/core/strategy_dependency.py:481:510;三个校验方法在 processor_framework.py:238:265:312)。

妙在哪: 用字典解耦管线段落,通常的代价是打错 key 只能在运行时炸。这套装饰器把契约显式化,把偶发 bug 变成确定性报错,却不牺牲可插拔性。

④ 一张 YAML 决定谁能用什么工具

config/ufo/mcp.yaml 用「agent 名 × 应用 root 名」两级键决定工具集。打开 Word 就多出 COM 工具,打开记事本就没有。

妙在哪: 上下文相关的工具集,通常要写一堆条件分支。这里退化成一次字典查找 + 一次 default 回退(ufo/client/computer.py:640-647)。加应用支持不需要改 Python。

⑤ 改 DAG 与点鼠标共用一条管线

Galaxy 的七个改图操作是七个 MCP 工具(ufo/client/mcp/local_servers/constellation_mcp_server.py:29),经过和 GUI 动作完全相同的 ActionCommandInfo → Command → dispatcher → Computer 链路。

妙在哪: 「编辑一个数据结构」和「操作一个 GUI」表面上毫不相干,但抽象到「LLM 发起一次带 schema 的工具调用」这一层就完全同构。复用换来的是自动获得 schema 注入、错误回喂、日志、记忆一整套设施。

⑥ 运行态直接写进 prompt

拼改图 prompt 时,每个 DAG 节点后面标 ✏️ [MODIFIABLE]🔒 [READ-ONLY](galaxy/agents/prompters/base_constellation_prompter.py:120-170)。

妙在哪: 与其让模型乱改再拒绝(浪费一轮 + 模型不知道为什么被拒),不如先把规则用它能读懂的方式告诉它。这是把并发约束翻译成自然语言。

⑦ 状态合并按「谁更靠前」而不是「谁更新」

_is_state_more_advanced 给状态排等级,只有编排器那份等级更高才覆盖 agent 那份(galaxy/session/observers/constellation_sync_observer.py:452-478)。

妙在哪: 两份数据合并的常见做法是比时间戳,但分布式下时钟不可靠。这里用领域语义的单调性(任务状态只会前进不会后退)来定优先级,比时间戳稳。

⑧ 阻塞代码整体隔离到线程池

MCP 工具调用被丢进线程池,并在线程内新建事件循环(ufo/client/computer.py:198-230);LLM 同步调用同样 run_in_executor(app_agent_processing_strategy.py:1236-1244)。两处注释都写明理由是防止阻塞主循环导致 WebSocket 断连。

妙在哪: GUI 自动化生态(pywinauto、COM)天生同步阻塞。与其把它们全改异步,不如承认现实、整体隔离。这是务实而非优雅的选择,但它对。

⑨ UIA 枚举的四连击优化

FindAllBuildCache 一次批量取 + 手动回填 _cached_* + 硬上限 500 + 用 name 顶替昂贵的 rich_text(ufo/automator/ui_control/inspector.py:206-311)。

妙在哪: 跨进程 COM 调用是这里唯一的性能瓶颈,四个手法全冲着「减少调用次数」去。硬上限 500 尤其现实——它承认「控件太多时,给模型 500 个和给 5000 个一样没用」。

⑩ 纯函数加缓存

标签图片和字体的构造函数加了 functools.lru_cache(ufo/automator/ui_control/screenshot.py:573-611)。编号与配色的组合有限,命中率极高。

妙在哪: 教科书级的用法。渲染函数是纯的、参数空间小、调用频繁——三个条件全中。

额外一条:失败也是数据

_execute_task_on_device 的文档写着 always returns, never raises(galaxy/client/device_manager.py:600-617);CommandRouter 也一样不抛异常——失败的那条命令返回 FAILURE(ufo/client/computer.py:768-779),被早退跳过的后续命令返回 SKIPPED(:733-747)。两种状态都原样回到模型眼前(详见第 4 章 §4.7)。

妙在哪: agent 系统里,「失败」是模型必须看到的观测值。异常一抛,这个观测就丢了。而把「哪条真失败了、哪几条只是被连带跳过」分成两个状态回报,模型才知道该从哪一步重来。


7.2 边界与局限(诚实版)

平台绑死 Windows

核心 GUI 能力依赖 pywinautouiautomationpywin32comtypes,requirements.txt 里全部标了 sys_platform == 'win32'ui_mcp_server.py 开头直接检测平台,非 Windows 就 sys.exit(0) 退出模块加载(ufo/client/mcp/local_servers/ui_mcp_server.py:16-24)。

Linux / Android 走的是另一套 HTTP MCP 服务器,能力集完全不同——不是「跨平台的同一个 agent」,而是「同一套骨架下的不同 agent」

一台设备同时只能跑一个任务

assign_task_to_device 遇到 BUSY 就排队(galaxy/client/device_manager.py:585-595)。DAG 的并行度上限是设备数,不是节点数。四个节点分到两台机器,实际只有两路并行。

改图同步有超时后放行的口子

wait_for_pending_modifications 超时后会打警告、清空 pending、继续跑(galaxy/session/observers/constellation_sync_observer.py:305-312)。注释写明是为了避免永久死锁。

这意味着极端情况下,编排器可能在 agent 改图未完成时就放行了下游节点。 代码选择了活性优先于一致性。

控件上限 500 是硬截断

ufo/automator/ui_control/inspector.py:265-268min(com_elem_array.Length, 500)。控件超过 500 的窗口(大型表格、复杂 IDE),后面的控件直接看不见,而且没有任何降级提示。

编号只在当前步有效

TargetInfo.id 的注释写明 only valid at current step(ufo/agents/processors/schemas/target.py:25)。控件树每步重建,同一个按钮在不同步可能是不同编号。这对模型是持续的负担——prompt 里因此反复强调「只用当前清单」。

视觉检测依赖外部服务

OmniParser 路径需要一个外部 HTTP 端点(config/ufo/system.yamlOMNIPARSER.ENDPOINT),没配就静默降级成纯 UIA(_init_omniparser_service 只打一条 warning,app_agent_processing_strategy.py:472-474)。仓库不含模型本体。

控件过滤器未接入主线

ufo/automator/ui_control/control_filter.py 里三种过滤器在当前 AppControlInfoStrategy.execute 里看不到调用点。代码没有说明原因。(inferred)

大量硬编码超时

Computer._tool_timeout = 6000(注释却写「5 分钟」,ufo/client/computer.py:70)、dispatcher 默认 timeout=6000、TaskStar 默认 1000.0 秒。这些值不在配置文件里,改要动代码。

安全模型是「问人」

SAFE_GUARD 开着时,敏感动作靠终端里问一句。这依赖:模型正确识别出敏感、且有人在终端前。无人值守场景下把它关掉,就没有任何护栏了。DISCLAIMER.md 也明确要求在受控环境运行。

截图会离开本机

每一步的桌面/窗口截图都要发给 LLM。DISCLAIMER.md 第 3 条把责任明确划给用户:确保执行期间屏幕上没有敏感信息。


7.3 横向对比:同货架的 computer-use 兄弟

本货架 computer-use 这一支收了多个项目。核心分歧就一条:模型怎么指认屏幕上的目标?

项目模型输出什么定位靠什么代价 / 收益
UFO控件编号系统 UIA 控件树(可选叠加视觉检测)准、不会点歪;但自绘界面看不见,且绑死 Windows
agent-s自然语言描述「点蓝色保存按钮」独立的视觉定位模型翻成坐标平台无关;但多一次模型调用,定位模型质量决定上限
ui-tars-desktop直接吐归一化坐标单个 VLM 一步到位,SDK 只做反缩放链路最短;但完全押注模型的定位能力
scalecua归一化 [0,1] 坐标 + 统一动作空间单模型原生 或 规划器+定位器双模式跨 Ubuntu/Android/Web;抽象层更厚
self-operating-computerJSON 动作OCR / YOLO 标注 / 百分比 三种定位法极简好读;定位精度靠外部工具兜
skyvern元素 unique_idDOM 里打 id,再翻回 Playwright 定位器与 UFO 同构的思路,但战场在浏览器 DOM 而非桌面控件树

表里唯一不属于 computer-use 这一支的是 skyvern,它在货架里归 browser-agents(docs/skyvern/index.mdarea)。列进来是因为它的指认方式与 UFO 同构,不是因为货架位置相同。

看这张表最该注意的一点: UFO 和 Skyvern 走的是同一条路——让模型只报 id,程序负责翻译。区别只在 id 的来源:一个来自 Windows 控件树,一个来自 DOM。而 UI-TARS 系走的是相反的路:让模型直接给坐标。

两条路的分野在于你信任谁:UFO/Skyvern 信任平台的可访问性 API,UI-TARS 信任模型的视觉定位能力。前者上限受 API 覆盖度限制,后者上限受模型能力限制。

UFO 独有的两点

对比同支项目,UFO 有两处别人普遍没有的:

  1. GUI 与原生 API 混合。 Word/Excel/PowerPoint 走 COM,不点鼠标(第 4 章 §4.9)。这是「深度绑定单一平台」换来的红利。
  2. 跨设备 DAG 编排。 其余项目基本都是单机单循环;Galaxy 那一层(第 5 章)在这支里是独一份。

与运行时框架的关系

cua 属于 agent-runtime,解决的是「不同模型怎么路由到不同 loop、动作怎么落到任意 OS 沙箱」。UFO 不做模型路由(它有自己的 ufo/llm/ 多后端层,但不是可插拔 loop),而是把宽度花在了 Windows 深度和跨设备编排上。

上游领域地图见货架总览 ../index.md,分支 B「对世界采取动作」下的 computer-use 一支。


7.4 仓库里还有什么(前六章没覆盖的)

目录干什么入口
dataflow/任务实例化 + 执行的数据生成流水线dataflow/__main__.pydataflow/data_flow_controller.py
record_processor/把人的操作录制解析成可复用的演示record_processor/record_processor.py
learner/把离线文档建成向量索引learner/learner.pylearner/indexer.py
ufo/experience/把成功轨迹总结成「经验」存库ufo/experience/summarizer.py
ufo/rag/四种检索器:离线文档 / 在线搜索 / 经验 / 演示ufo/rag/retriever.py:16 RetrieverFactory
ufo/trajectory/轨迹解析,给可视化和评测用ufo/trajectory/parser.py
galaxy/webui/Galaxy 的 Web 界面python -m galaxy --webui
galaxy/visualization/终端里画 DAGgalaxy/visualization/dag_visualizer.py

RAG 这一支值得单独说一句

四种检索器全由配置开关控制,在 AppAgent.context_provision 里按需构建(ufo/agents/agent/app_agent.py:454-495):

配置项检索什么
rag.offline_docs应用的离线帮助文档
rag.online_searchBing 搜索结果
rag.experience自己过去成功的轨迹
rag.demonstration人类录制的演示

检索到的内容进 prompt 时被明确标注为「仅供参考,可能不相关、不准确或过时」(ufo/prompts/share/base/app_agent.yaml)。这是很克制的写法——避免模型把检索结果当成事实。


7.5 全库代码地图(总索引)

入口与骨架

主题文件路径符号名
单机入口ufo/ufo.pymain
Galaxy 入口galaxy/galaxy.pymain
会话工厂ufo/module/session_pool.pySessionFactorySessionPool
会话/轮次骨架ufo/module/basic.pyBaseSessionBaseRound
全局上下文键ufo/module/context.pyContextNames

Agent 与状态机

主题文件路径符号名
Agent 基类与注册表ufo/agents/agent/basic.pyBasicAgentAgentRegistry
总管 agentufo/agents/agent/host_agent.pyHostAgentAgentFactoryAgentConfigResolver
应用内 agentufo/agents/agent/app_agent.pyAppAgentOpenAIOperatorAgent
状态注册表ufo/agents/states/basic.pyAgentStateManagerAgentState
两套状态ufo/agents/states/host_agent_state.pyapp_agent_state.py
共享黑板ufo/agents/memory/blackboard.pyBlackboard

管线

主题文件路径符号名
模板方法ufo/agents/processors/core/processor_framework.pyProcessorTemplate
依赖声明ufo/agents/processors/core/strategy_dependency.pydepends_onprovides
AppAgent 四段策略ufo/agents/processors/strategies/app_agent_processing_strategy.pyAppScreenshotCaptureStrategyAppControlInfoStrategyAppLLMInteractionStrategyAppActionExecutionStrategyAppMemoryUpdateStrategy
HostAgent 四段策略ufo/agents/processors/strategies/host_agent_processing_strategy.pyDesktopDataCollectionStrategyHostActionExecutionStrategy
响应模式ufo/agents/processors/schemas/response_schema.pyAppAgentResponseHostAgentResponse
动作模式ufo/agents/processors/schemas/actions.pyActionCommandInfoListActionCommandInfo

感知与执行

主题文件路径符号名
控件枚举ufo/automator/ui_control/inspector.pyUIABackendStrategyControlInspectorFacade
截图与标注ufo/automator/ui_control/screenshot.pyPhotographerFacadeAnnotationDecorator
编号表ufo/agents/processors/schemas/target.pyTargetRegistryTargetInfo
动作出口ufo/module/dispatcher.pyLocalCommandDispatcherWebSocketCommandDispatcher
工具路由ufo/client/computer.pyComputerComputerManagerCommandRouter
命令模式ufo/automator/puppeteer.pyufo/automator/basic.pyAppPuppeteerReceiverManagerCommandBasic
MCP 服务器ufo/client/mcp/local_servers/ui_mcp_server.pyword_wincom_mcp_server.pyconstellation_mcp_server.py

Galaxy 与协议

主题文件路径符号名
DAG 本体galaxy/constellation/task_constellation.pyTaskConstellation
节点与边galaxy/constellation/task_star.pytask_star_line.pyTaskStarTaskStarLine
编排器galaxy/constellation/orchestrator/orchestrator.pyTaskConstellationOrchestrator
Galaxy 大脑galaxy/agents/constellation_agent.pyConstellationAgent
改图命令galaxy/constellation/editor/commands.pyBaseConstellationCommand 及各子类
竞态合并galaxy/session/observers/constellation_sync_observer.pyConstellationModificationSynchronizer
协议核心aip/protocol/base.pyAIPProtocol
消息定义aip/messages.pyServerMessageClientMessage
设备管理galaxy/client/device_manager.pyConstellationDeviceManager

配置与提示词

主题文件路径
系统参数config/ufo/system.yaml
MCP 路由表config/ufo/mcp.yaml
RAG 开关config/ufo/rag.yaml
Galaxy 运行参数config/galaxy/constellation.yaml
设备清单config/galaxy/devices.yaml
AppAgent / HostAgent 提示词ufo/prompts/share/base/app_agent.yamlhost_agent.yaml
Galaxy 提示词galaxy/prompts/constellation/share/