数据截至 (上游 commit 5d92feea9f1e)
运行时底座:VLM 多后端引擎、消息组装与主循环
30 秒导读: 前四章讲的是「智能」——统一动作空间、两种推理模式、坐标定位、跨平台执行。 但这些要真跑起来,底下得有一层枯燥但关键的管道:怎么调用一个视觉语言模型、怎么把 截图和文字拼成模型认得的消息格式、失败了怎么重试、整个 agent 怎么一步步转起来。这一章 就补这层管道。读完你能:接自己的模型后端、看懂消息为什么长这样、照着源码把主循环跑通。
本章面向要读源码、要接自己模型的人。它不重复第 2 章的动作解析、 也不重复第 3 章的坐标反归一化——只讲「让这一切能运行」的骨架。
代码全部在 playground/ 下:
| 关切 | 文件 |
|---|---|
多后端引擎(统一 generate) | playground/core/engine.py |
| 消息组装(拼图文、选后端) | playground/core/mllm.py |
| 模块基类(造引擎的口子) | playground/core/module.py |
| 安全调用(3 次重试) | playground/utils/common_utils.py |
| 交互式主循环(predict→step) | playground/agent_run_interactive.py |
1. 这是什么(零基础也能懂)
一句话定义: 这是 ScaleCUA 的「电源和线路」——把「一个 GUI agent」拆成三段可替换的管道: 造引擎 → 拼消息 → 转循环。
先建立一个直觉。一个 computer-use agent 每一步都在做同一件事:
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌──────────┐
│ 截当前屏 │ → │ 拼成 消息 │ → │ 问 模型 │ → │ 执行动作 │ ── 循环 ──┐
│ (截图) │ │ (图+文+历史) │ │ (VLM) │ │ (点/输入) │ │
└──────────┘ └─────────────┘ └──────────┘ └──────────┘ │
▲ │
└──────────────────────────────────────────────────────────────────┘
中间那三步(拼消息 / 问模型 / 循环)就是本章。它们刻意做得跟具体模型无关:
- 你可以换成 OpenAI、Claude、Azure、本地 vLLM、HuggingFace——上层代码一个字不改。
- 换后端只改一份 YAML 配置里的
engine_type字段(见 §5)。
给谁用: 想把 ScaleCUA 接到自己私有模型上的人;想照着复现主循环的人;想搞清「为什么 同一段代码能同时喂 GPT 和 Claude」的人。
一句话类比: 把它想成一个多口充电器。不管你插的是 OpenAI 头还是 Claude 头,面板上
按钮(generate)长得一样;内部再各自转换电压(每家 API 的图片格式不同)。
2. 顶层全景(它大概怎么转)
三个类分工明确,自底向上叠成一根管子:
playground/agent_run_interactive.py
main() / run_agent() ← 主循环:20 步 predict→step
│
│ 调用 agent.predict()(NativeAgent / AgenticWorkflow)
▼
┌──────────────────── core/module.py ────────────────────┐
│ BaseModule._create_vlm_api() ← agent 造引擎的唯一口子 │
└──────────────────────────┬──────────────────────────────┘
│ new
▼
┌──────────────────── core/mllm.py ──────────────────────┐
│ VLMEngine ← 会话状态 + 消息组装 │
│ · 按 engine_type 挑一个底层引擎 │
│ · add_message() 按后端差异拼「图 + 文」 │
│ · get_response() → 转交底层引擎 │
└──────────────────────────┬──────────────────────────────┘
│ self.engine.generate(messages)
▼
┌──────────────────── core/engine.py ────────────────────┐
│ LMMEngine 家族(统一 generate 接口 + backoff 重试): │
│ OpenAI · Anthropic · AzureOpenAI · vLLM · HuggingFace │
└──────────────────────────────────────────────────────────┘
怎么读这张图: 从上到下是「谁调用谁」。上层 agent 只认识 VLMEngine;VLMEngine 只认识
一个抽象的 self.engine;真正跟某家 API 说话的是 core/engine.py 里那五个具体引擎。换模型
= 换最底下那一层,上面两层不动。 这就是「模型可插拔」的物理含义。
各部件一句话职责:
| 部件 | 干什么 | 符号 / 文件 |
|---|---|---|
| 多后端引擎 | 每家 API 一个类,都暴露同名 generate | core/engine.py LMMEngineOpenAI 等 |
| 消息组装器 | 存会话、按后端拼图文负载 | core/mllm.py VLMEngine |
| 造引擎口子 | agent 拿配置生一个 VLMEngine | core/module.py BaseModule._create_vlm_api |
| 安全调用 | 包一层 3 次重试 | common_utils.py call_llm_safe |
| 主循环 | 20 步 predict→step→录屏 | agent_run_interactive.py run_agent |
3. 多后端引擎:一个 generate,五种后端
这节讲什么: core/engine.py 如何用「同名方法、各自实现」把五家不同的模型 API 抹平成
一个接口。
3.1 统一接口:面板 一样,内部各异
思路很直接:定义一个空基类 LMMEngine(core/engine.py:17),然后每家后端一个子类,
都实现一个签名相近的 generate(messages, temperature, max_new_tokens, ...)。上层永远只
调 engine.generate(...),不关心底下是谁。
五个后端和它们的差异:
| 引擎类 | 底层 SDK | 鉴权来源(环境变量) | 备注 |
|---|---|---|---|
LMMEngineOpenAI | openai.OpenAI | OPENAI_API_KEY / OPENAI_BASE_URL | 标准 chat completions |
LMMEngineAnthropic | anthropic.Anthropic | ANTHROPIC_API_KEY | system 单独传;另有思考模式 |
LMMEngineAzureOpenAI | openai.AzureOpenAI | AZURE_OPENAI_API_KEY + endpoint | 额外累计 self.cost |
LMMEnginevLLM | openai.OpenAI(指向本地) | vLLM_ENDPOINT_URL | 本地自托管,默认采样参数不同 |
LMMEngineHuggingFace | openai.OpenAI(TGI 端点) | HF_TOKEN | 模型名硬编码 "tgi" |
有意思的细节:vLLM 和 HuggingFace 都复用 OpenAI 的 SDK——因为它们都提供了 OpenAI 兼容的
HTTP 接口,只是 base_url 指到本地或 TGI。见 core/engine.py:239(vLLM)和 :278(HF)都是
OpenAI(base_url=..., api_key=...)。
3.2 最简后端长什么样(OpenAI)
真实源码,core/engine.py:44-53 LMMEngineOpenAI.generate:
def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs):
"""Generate the next message based on previous messages"""
result = self.llm_client.chat.completions.create(
model=self.model,
messages=messages,
max_tokens=max_new_tokens if max_new_tokens else 4096,
temperature=temperature,
**kwargs,
)
return result.choices[0].message.content
一句话:收一串 messages,调 SDK,把首个候选的文本返回。temperature 默认 0.0——GUI
agent 要的是确定性、可复现,不是创造力。这是全家族的通用默认。