跳到主要内容

数据截至 (上游 commit 96983c73ed09)

UFO — 架构与原理

30 秒导读: UFO 是微软开源的 Windows 桌面 agent。你用中文/英文说一句「把这份 Word 里的表格截图发到邮件里」,它自己截屏、认出屏幕上有哪些按钮、决定点哪个、真的去点。第三代(UFO³ Galaxy)在此之上加了一层:把一句话拆成跨多台设备的任务 DAG,并行调度、边跑边改图。


1. 这是什么(零基础也能懂)

一句话定义

UFO 是一个把自然语言变成真实鼠标键盘操作的 Windows agent 框架。

解决什么问题

假设你要做一件跨好几个应用的琐事:打开 Excel 找到某列数字 → 算个总和 → 打开 Outlook 写封邮件把结果发出去。

这件事对人来说不难,只是烦。对 LLM 来说难在两处:

  • 它看不见你的屏幕。 模型只会读文本,不知道「保存」按钮在哪。
  • 它伸不出手。 就算它想清楚了要点哪里,也没有鼠标。

UFO 补的就是这两样:一只眼睛(截屏 + 枚举 Windows 控件),一双(pywinauto 点击 / Office COM 调用)。

给谁用

用户拿它干什么
想自动化 Windows 日常操作的工程师直接命令行跑,让它替你点 Office、资源管理器、浏览器
研究 GUI agent 的人一套完整的「感知 + 决策 + 执行 + 记忆 + 评测」参考实现
做跨设备编排的人Galaxy 层:把 Windows / Linux / Android 当成算力节点排 DAG

它能做什么

  • 枚举桌面上所有窗口,自己挑一个打开、切换。
  • 在选中的应用里枚举可交互控件,点击、输入、拖拽、滚轮、快捷键。
  • 对 Office 三件套走 COM API 而不是点鼠标(更快更稳)。
  • 敏感动作(发送、删除、关窗)先停下来问人。
  • 把每一步的截图、思考、动作、结果写成结构化日志,还能自我评测。
  • Galaxy:把请求拆成任务 DAG,分配到多台设备并行跑,并根据中间结果改图。

用起来什么样

最小用法就是一条命令(依据:ufo/ufo.py:51-74main,以及 ufo/__main__.py):

# 单机模式:UFO² 直接接管这台 Windows
python -m ufo -t my_task

# 请求也可以直接写在命令行上,省掉交互式提问
python -m ufo -t my_task -r "打开记事本,写一句 hello 并保存到桌面"

# 跨设备模式:UFO³ Galaxy
python -m galaxy "从两台 Linux 服务器收集错误日志,汇总成 Excel 发邮件"

不带 -r 时,它会打印一个欢迎面板再问你要做什么(ufo/module/interactor.py:26 first_request)。

一句话直觉

把屏幕当成一张贴满了编号便利贴的图。

UFO 每一步都干同一件事:截图 → 把每个可点的控件框起来编上号(1、2、3…)→ 把「这张编号图 + 控件清单」丢给模型 → 模型回一句「点 7 号」→ 程序把 7 号翻译回真实控件,点下去。

模型永远只报编号,从不报坐标。编号这层间接寻址,是整个框架最核心的一招。

本节到此为止不涉及任何代码细节。下面开始讲它内部怎么转。


2. 顶层全景(它大概怎么转)

2.1 先分清两层产品

仓库里其实住着两个东西,共用一套底层设施:

UFO²(目录 ufo/)UFO³ Galaxy(目录 galaxy/)
管什么一台 Windows 机器多台异构设备
任务模型顺序循环:一步一动作DAG:任务节点 + 依赖边
主要 agentHostAgent + AppAgentConstellationAgent
并发无(单线主线)有(asyncio 并行跑就绪节点)
入口python -m ufopython -m galaxy

Galaxy 不是替代品,它把一台跑着 UFO² 的 Windows 机器当作 DAG 里的一个执行设备

2.2 单机主线:一步是怎么走完的

怎么读这张图:从上到下就是一个 Step 的时间顺序,右侧是它产出的东西。

用户请求 "把这张表发邮件"


┌──────────────────┐
│ ① 总管 HostAgent │ 看整个桌面,挑一个应用/子 agent
└────────┬─────────┘ 产出:子任务 + 选中的窗口
│ 状态 ASSIGN

┌──────────────────┐
│ ② 干活 AppAgent │ 只在这一个应用窗口里干活
└────────┬─────────┘ 产出:一步动作(点/输入/调 API)
│ 状态 FINISH

回到 ① 换下一个应用,或整轮结束

为什么要拆成两个 agent? 因为「该用哪个应用」和「在这个应用里点哪个按钮」是两种完全不同的判断,给它们不同的 prompt、不同的工具集、不同的截图(桌面全景 vs 单窗口),各自都更准。

2.3 一个 Step 内部:四阶段管线

怎么读这张图:从左到右依次执行,前一段的产出进入后一段的输入。

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ ① 收集 │──▶│ ② 问模型 │──▶│ ③ 动手 │──▶│ ④ 记账 │
│ 截图+控件│ │ 拼 prompt│ │ 执行动作 │ │ 写记忆 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
data_collection llm_interaction action_execution memory_update

这四段在代码里是四个可替换的策略对象,由一个模板方法按固定顺序驱动(ufo/agents/processors/core/processor_framework.py:336 ProcessorTemplate.process)。换平台(Linux / Android)就换策略,骨架不动。

2.4 动作怎么落地:一切皆 MCP

UFO 最与众不同的一步在这里:模型选的动作不会直接调 pywinauto,而是被包成一个统一的 Command 信封,交给一条路由链。

模型输出 {"function":"click_input","arguments":{"id":"7"}}


Command 信封 (tool_name / parameters / tool_type)


CommandDispatcher ── 本地直连 ┐
│ ├──▶ Computer(按 agent+应用选一套工具)
└── WebSocket 远程 ──┘ │

MCP 服务器(FastMCP)


pywinauto 点击 / Word COM / 改 DAG

这条链带来两个直接后果:

  • 本地跑和远程跑,上层代码一模一样,只换一个 dispatcher 实现。
  • Galaxy 的「改 DAG」也是一个 MCP 工具,跟「点鼠标」走同一条管线。

2.5 部件一句话职责

部件干什么在哪个文件
BaseSession / BaseRound会话与轮次骨架,驱动状态机直到结束ufo/module/basic.py:456 / :98
HostAgent看桌面全景,挑应用、派子任务、创建 AppAgentufo/agents/agent/host_agent.py:143
AppAgent在单个应用窗口内一步步操作ufo/agents/agent/app_agent.py:43
AgentStateManager状态名 → 状态类的注册表(单例)ufo/agents/states/basic.py:49
ProcessorTemplate四阶段管线的模板方法ufo/agents/processors/core/processor_framework.py:45
PhotographerFacade截图、SoM 标注、IoU 合并控件框ufo/automator/ui_control/screenshot.py:938
ControlInspectorFacade用 UIA/Win32 枚举窗口与控件ufo/automator/ui_control/inspector.py:466
BasicCommandDispatcherCommand 送出去、等 Result 回来ufo/module/dispatcher.py:25
Computer / CommandRouter按 agent+应用挑一套 MCP 工具并调用ufo/client/computer.py:22 / :679
AppPuppeteer / ReceiverManager命令模式:命令名 → 接收者(GUI 或 COM)ufo/automator/puppeteer.py:22 / :184
TaskConstellation任务 DAG 本体:节点、边、拓扑序、环检测galaxy/constellation/task_constellation.py:31
ConstellationAgent生成 DAG、并在执行中改 DAGgalaxy/agents/constellation_agent.py:45
TaskConstellationOrchestrator异步调度就绪节点、等完成、合并改动galaxy/constellation/orchestrator/orchestrator.py:31
AIPProtocol设备间消息层(WebSocket 之上)aip/protocol/base.py:22

2.6 主线走一遍(高层,不进代码)

  1. SessionFactory--mode 造出一个 Session,SessionPool 把它跑起来(ufo/ufo.py:64-74)。
  2. Session 取一条用户请求,开一个 Round
  3. Round 进循环:让当前 agent handle 一次,再问状态机「下一个状态是谁、下一个 agent 是谁」(ufo/module/basic.py:156-180)。
  4. HostAgent 那一次 handle 会跑完整条四阶段管线:截桌面 → 问模型「用哪个应用」→ 调 select_application_window → 记账。
  5. 状态变成 ASSIGN,状态机换人:创建并切到 AppAgent(ufo/agents/states/host_agent_state.py:175 AssignHostAgentState)。
  6. AppAgent 循环跑管线,每次输出一步动作,直到它自己报 FINISH
  7. 控制权交回 HostAgent,继续下一个子任务;全部完成则 Round 结束,可选地做评测、存经验。

3. 阅读地图

建议顺序(由浅入深):

顺序章节读完你会知道
101-session-and-states.md任务是怎么被切成三层的,状态机怎么在两个 agent 间倒手
202-processor-pipeline.md一个 Step 内部四段管线怎么串,依赖怎么被静态+运行期校验
303-perception.md屏幕怎么变成「编号便利贴图 + 控件清单」
404-action-mcp.md一句 click_input(id=7) 怎么最终变成真实的一次鼠标点击
505-galaxy-constellation.md跨设备 DAG 怎么生成、怎么并行跑、怎么边跑边改
606-aip-and-devices.md本地/远程两种部署共用同一套上层代码的机制
707-deep-dive.md值得抄走的设计、它会在哪崩、跟兄弟项目怎么比

只想抓要点的话:读 §2 + 第 3、4 章,就已经能讲清 UFO 的核心。


4. 承重术语表(一词一义,后文不再解释)

术语含义(本文档内固定这一个意思)
Session(会话)一次完整的对话式任务,可含多轮
Round(轮次)一条用户请求的完整处理过程
Step(步)一次「截图→问模型→执行一个动作」
Subtask(子任务)HostAgent 派给 AppAgent 的一段活
UIAWindows UI Automation,系统提供的控件树 API
SoMSet-of-Mark,把控件框出来编号画在截图上
MCPModel Context Protocol,这里用 fastmcp 实现,是所有动作的统一出口
Receiver(接收者)命令模式里真正执行动作的对象(GUI 控件包装 或 Office COM 对象)
TaskStar(任务星)Galaxy DAG 的一个节点
TaskStarLine(星线)Galaxy DAG 的一条依赖边
Constellation(星座)整张任务 DAG
AIPAgent Interaction Protocol,设备间的 WebSocket 消息协议

5. 代码地图(顶层入口)

主题文件路径符号名
单机 CLI 入口ufo/ufo.pymain
Galaxy CLI 入口galaxy/galaxy.pymainparse_args
会话工厂 / 会话池ufo/module/session_pool.pySessionFactorySessionPool
会话与轮次骨架ufo/module/basic.pyBaseSessionBaseRound
Windows 会话实现ufo/module/sessions/session.pySessionFollowerSession
Agent 抽象基类ufo/agents/agent/basic.pyBasicAgentAgentRegistry
管线模板ufo/agents/processors/core/processor_framework.pyProcessorTemplate
动作出口ufo/module/dispatcher.pyBasicCommandDispatcher
MCP 路由配置config/ufo/mcp.yaml(配置文件,按 agent × 应用 root 分层)
系统参数config/ufo/system.yamlCONTROL_BACKENDMAX_STEPSAFE_GUARD
Galaxy DAGgalaxy/constellation/task_constellation.pyTaskConstellation
设备清单配置config/galaxy/devices.yaml(配置文件,device_id/capabilities)