前言
在前两篇文章中,我们给 UFO 装上了“眼睛”(UI 截图与标注)和“双手”(Pywinauto 鼠标控制)。但还有一个终极问题没解决:
面对一张满是按钮的截图,UFO 怎么知道该点哪一个?它是如何规划“先点菜单 -> 再点保存 -> 输入文件名”这一系列步骤的?
这就涉及到了 Agent 的大脑——Prompt 构建模块。
在 UFO 的源码中,ufo/prompter 目录藏着微软调教 GPT-4V 的核心秘籍。今天我们就来拆解 agent_prompter.py,看看一个工业级的 Agent Prompt 是如何动态生成的。
🏗️ 1. Prompt 的“三明治”架构
打开 ufo/prompter/agent_prompter.py,你会发现 UFO 并不是简单地把一句话扔给 GPT。它构建了一个非常厚的“三明治”结构,包含以下几层:
-
System Prompt (系统人设):定义它是谁(Windows 操控专家)。
-
Few-Shot Examples (少样本示例):教它怎么做(给几个操作演示)。
-
Observation (当前观测):当前的截图 + 控件列表。
-
History (记忆/历史):之前做了什么(防止死循环)。
-
User Request (任务):用户到底想干嘛。
🎭 2. 系统人设 (System Prompt)
UFO 把提示词模板放在了外部文件中(通常是 ufo/prompter/templates/ 下的 .yaml 文件)。这种代码与提示词分离的设计非常值得学习。
让我们看看核心 System Prompt 大概长什么样(简化版):
YAML
system: "You are a helpful assistant that interacts with Windows applications..."
instructions:
- "You will receive a screenshot with numeric labels on UI elements."
- "Your goal is to choose the correct label to click or type text."
- "Return your response in strict JSON format."
源码解析:
在 AgentPrompter 类的 construct_system_prompt() 方法中,它会加载这个模板。
关键点:微软在这里极其强调 JSON 格式的约束。它不仅要求 JSON,还详细定义了 JSON 的字段(如 Thought, ControlLabel, Function),确保 GPT 输出的内容能被 Python 代码直接解析。
💡 3. 核心秘籍:动态 Few-Shot (示例学习)
这是 UFO 变聪明的关键。如果你只给规则,GPT 可能会发懵;但如果你给它看几个例子,它学得飞快。
UFO 实现了一套基于检索的示例加载机制 (RAG-like)。
源码逻辑 (build_examples_prompt):
Python
def build_examples_prompt(self, current_app_name):
# 1. 加载示例库
examples = load_examples()
# 2. 筛选
# 如果当前操作的是 Notepad,就优先加载 Notepad 的操作示例
relevant_examples = [ex for ex in examples if ex['app'] == current_app_name]
# 3. 构造 Prompt
example_text = "Here are some examples of how to interact:\n"
for ex in relevant_examples:
example_text += f"User: {ex['user_query']}\n"
example_text += f"Assistant: {ex['response_json']}\n"
return example_text
为什么要动态加载?
因为 GPT 的上下文窗口(Context Window)是有限的(虽然在变大,但也要省着用)。
-
当你操作 Word 时,UFO 会塞入“设置字体”、“保存文档”的示例。
-
当你操作 PowerPoint 时,UFO 会塞入“插入幻灯片”、“播放”的示例。
这种动态上下文切换,让 UFO 在不同软件下都能表现得像个专家。
📸 4. 融合视觉与文本 (Multimodal Context)
我们在第二篇讲过 SoM(Set-of-Marks)。在 prompter 模块中,这两种模态被正式组装在一起。
Python
# 构造 User Message
user_content = [
# 1. 文本部分:告诉 GPT 所有的控件列表
# 这样 GPT 即使看不清图片上的小字,也能通过列表查到 "ID 5: Save Button"
{
"type": "text",
"text": f"Current UI Structure:\n{ui_tree_text_description}"
},
# 2. 视觉部分:带红框标注的截图
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
}
]
微软的巧思:
单纯依靠视觉(GPT-4V)容易出现幻觉(Hallucination)。
UFO 通过在 Prompt 里附带一份纯文本的控件清单(包含 ID、名称、类型),构成了**“双重校验”**。
GPT 思考过程变成了:
-
看图:“我觉得右上角那个红框 #5 像保存。”
-
查清单:“清单上写着 #5 是 Button 'Save'。”
-
结论:“没错,就是 #5!”
🧠 5. 记忆链 (History)
Agent 最怕陷入死循环:点了一个按钮 -> 没反应 -> 再点一次 -> 还没反应...
UFO 的 prompter 会维护一个 history 列表,记录过去 N 步的操作和结果。
Python
# 简化逻辑
prompt += "History of actions:\n"
for step in history[-5:]: # 只看最近5步
prompt += f"Step {step.index}: Clicked #5 (Save). Result: Success.\n"
自我反思机制:
如果在 History 中,GPT 发现上一步点击了 #5 但界面没变化(截图没变),它会在新的 Prompt 中收到这个反馈。
GPT-4V 就会在 Thought 中推理:“Previous click failed, maybe I need to click the menu first.”(上次失败了,也许我得先点菜单)。
📝 总结
通过拆解 prompter 模块,我们可以把 Microsoft UFO 的“大脑”公式总结为:
$$\text{Prompt} = \text{Role (System)} + \text{RAG Examples} + \text{Visual (Image)} + \text{Data (UI Tree)} + \text{Memory (History)}$$
这对我们开发自己的 Agent 有什么启示?
-
结构化输出是王道:一定要强制 LLM 输出 JSON,不要输出自然语言,否则代码没法接。
-
不要吝啬 Context:能给控件清单就给清单,不要只依赖截图。
-
动态示例 (RAG):针对不同场景动态替换 Few-Shot 示例,是提升准确率性价比最高的手段。
下期预告:
现在的 UFO 只能用官方的代码跑。如果你想给它加一个“语音控制”功能,或者想让它支持 Mac 系统(虽然很难),该怎么改?
下期文章:《UFO 源码实战 (5):实战修改源码,打造属于你的专属桌面助理》,我们将进行一次代码魔改实战!
想看更多硬核源码拆解?关注博主,不迷路! 🚀

2409

被折叠的 条评论
为什么被折叠?



