【GUI-Agent】阶跃星辰 GUI-MCP 解读---(2)---决策层
【GUI-Agent】阶跃星辰 GUI-MCP 解读---(2)---决策层
0x00 摘要
25年底,阶跃星辰升级发布了全新的AI Agent系列模型Step-GUI,包括云端模型Step-GUI、首个面向GUI Agent的MCP协议:GUI-MCP(Graphical User Interface - Model Context Protocol),这是首个专为图形用户界面自动化而设计的 MCP 实现,兼顾标准化与隐私保护。因此,我们就来解读这个MCP协议,顺便看看端侧Agent的实现架构。
本文是第二篇,主要是介绍决策层,本层在任何情况下(是/非MCP)都会用到。
因为是反推解读,而且时间有限,所以可能会有各种错误,还请大家不吝指出。
0x01 LocalServer
LocalServer 是本地 GUI Agent 服务器实现,作为系统核心组件之一,负责协调模型推理、环境管理和任务执行。总的来说,LocalServer 是 GUI Agent 系统的大脑,负责将高层任务请求转化为具体的模型推理和设备操作序列。
1.1 核心功能概述
- 会话管理
- 模型推理调度,即负责与 LLM 交互,解析模型输出为可执行动作
- 环境状态跟踪
- 日志记录与持久化
1.2 详细功能说明
会话管理(get_session 方法)
- 创建新的任务会话并分配唯一 session_id(UUID 保证唯一性)
- 初始化会话配置(任务描述、任务类型、模型配置等)
- 记录会话开始日志
自动化步骤执行(automate_step 方法):这是 LocalServer 的核心方法,负责执行单步自动化操作:
① 环境状态读取:读取历史日志,获取之前的环境和动作记录
- 解析当前观察(主要是屏幕截图)
- 支持图像缩放预处理
② 消息构建:
- 使用 Parser 将任务、环境和历史动作转换为模型输入消息
- 支持图像缩放预处理
③ 模型调用:调用 ask_llm_anything 函数与 LLM 交互,并记录模型推理时间和响应
④ 动作解析:使用 Parser 将模型响应解析为可执行动作
⑤ 日志记录:
- 保存完整的交互日志(环境、动作、模型输入输出等)
- 支持调试模式下的详细输出
日志管理系统:使用 LocalServerLogger 管理会话日志,提供:
- 日志与图像文件分离存储
- 支持日志读取和图像保存
1.3 在系统架构中的位置
LocalServer 在整个系统中起到承上启下的协调作用:
MCP 接口 / 命令行工具
↓
[LocalServer](协调层)
↓ ↓
Parser(环境转换) LLM 模型(推理决策)
↓
设备执行层(act_on_device 等)
1.4 实现
automate_step
automate_step 将 GUI 交互的观测信息(截图 + 文本)转换为可执行的动作指令,是 GUI-Agent 决策的核心环节。通过解析器适配不同任务类型,图片预处理适配不同模型,日志全流程记录保证可溯源;
流程图
-
数据预处理阶段:核心是将用户的截图 + 查询转换为 LLM 可识别的格式,同时通过日志器保存原始数据,保证可溯源;
-
适配优化阶段:通过图片预处理(缩放)适配不同 LLM 的输入要求,深拷贝原始消息避免预处理修改日志数据;
-
LLM 交互阶段:封装模型调用参数,记录调用全生命周期耗时,便于性能监控和调优;
-
结果输出阶段:将 LLM 的文本响应解析为结构化动作,同时返回步骤数,适配 Copilot 服务的多步执行逻辑;

代码
automate_step 的代码如下:
def automate_step(self, payload: dict) -> dict:
"""
Automate a step in the Copilot service.
核心功能:执行Copilot服务的单步自动化逻辑,输入观测信息(截图+查询),输出下一步操作指令
"""
# 从请求载荷中提取会话ID(用于日志隔离和溯源)
session_id = payload["session_id"]
# 初始化本地服务器日志器:指定日志目录、图片目录、会话ID,用于保存步骤截图和日志
logger = LocalServerLogger({
"log_dir": self.server_config["log_dir"],
"image_dir": self.server_config["image_dir"],
"session_id": session_id
})
# 计算当前步骤数:日志列表长度-1(logs为全局/类内维护的历史日志,0号为配置日志)
current_ste = len(logs) - 1
# 读取0号日志(配置日志),解析核心配置信息
config_log = logs[0]
config_dict = config_log['message']
# 提取任务类型、模型配置、核心任务描述
task_type = config_dict['task_type']
model_config = config_dict['model_config']
task = config_dict['task']
# 从观测信息中获取当前步骤的核心输入:截图+用户查询
observation = payload['observation']
# 提取截图的URL并读取图片数据
image_url = observation['screenshot']['image_url']['url']
image = read_from_url(image_url)
# 保存图片到本地,生成内部可访问的图片URL(用于日志和LLM输入)
image_inner_url = logger.save_image(image, f"step_{current_ste+1}")
# 提取用户查询(无则为空字符串)
query = observation.get('query', '')
# 定义内部函数:从历史日志中提取环境信息和动作序列(logs[1:]为非配置日志)
def get_envs_acts_from_logs(logs):
environments = [] # 存储历史环境信息(截图+用户输入)
actions = [] # 存储历史动作指令
for log in logs[1:]:
msg = log['message']
environments.append(msg['environment'])
actions.append(msg['action'])
return environments, actions
# 提取历史环境和动作,构建上下文
environments, actions = get_envs_acts_from_logs(logs)
# 构建当前步骤的环境信息(本地图片URL+用户查询)
current_env = {
"image": image_inner_url,
"user_comment": query
}
# 将当前环境加入历史列表,用于LLM上下文构建
environments.append(current_env)
# 根据任务类型获取对应的解析器(适配不同任务的消息构建/动作解析逻辑)
parser = get_parser(task_type)
# 调用解析器:将任务、历史环境/动作转换为LLM可识别的消息格式
messages_to_ask = parser.env2messages4ask(
task = task,
environments = environments,
actions = actions,
)
# 深拷贝原始消息(用于日志记录,避免后续图片预处理修改原始数据)
asked_messages = deepcopy(messages_to_ask)
# 提取模型配置:模型名称、提供商(默认eval)
model_name = model_config['model_name']
model_provider = model_config.get('model_provider', 'eval')
# 提取LLM调用参数,设置默认值(保证参数完整性)
args = model_config.get('args', {
"temperature": 0.1, # 低温度保证输出稳定
"top_p": 1.0, # 采样策略
"frequency_penalty": 0.0, # 重复惩罚
"max_tokens": 512, # 最大生成令牌数
})
# 提取图片预处理配置(None则不处理)
image_preprocess = model_config.get('image_preprocess', None)
# 图片预处理:若指定目标尺寸,缩放消息中的图片
if image_preprocess is not None:
if "target_image_size" in image_preprocess:
target_image_size = image_preprocess["target_image_size"]
# 定义内部函数:遍历消息,缩放所有图片URL
def resize_image_in_messages(messages, target_size):
for msg in messages:
# 跳过纯文本消息
if type(msg['content']) == str:
continue
# 遍历消息内容,处理图片URL
for content in msg['content']:
if content['type'] == "text":
continue
# 提取图片URL并生成缩放后的Base64 URL
image_url = content['image_url']['url']
image_resize_url = make_b64_url(image_url, resize_config={
"is_resize": True,
"target_image_size": target_size
})
# 替换为缩放后的URL(适配LLM的图片输入要求)
content['image_url']['url'] = image_resize_url
# 执行图片缩放:修改待发送给LLM的消息中的图片URL
resize_image_in_messages(messages_to_ask, target_image_size)
# 记录LLM调用开始时间(监控耗时)
llm_start_time = time.time()
# 调用LLM:传入提供商、模型名、消息、参数,获取响应
response = ask_llm_anything(
model_provider=model_provider,
model_name=model_name,
messages=messages_to_ask,
args=args
)
# 记录LLM调用结束时间
llm_end_time = time.time()
# 调用解析器:将LLM文本响应解析为结构化的动作指令
action = parser.str2action(response)
# 构建当前步骤的日志消息(完整记录上下文、模型响应、耗时等)
log_message = {
"environment": current_env, # 当前环境信息
"action": action, # 解析后的动作指令
"asked_messages": asked_messages, # 原始LLM请求消息(未预处理)
"model_response": response, # LLM原始响应
"model_config": model_config, # 模型配置
"llm_cost": { # LLM调用耗时统计
"llm_time": llm_end_time - llm_start_time,
"llm_start_time": llm_start_time,
"llm_end_time": llm_end_time
},
}
# 返回核心结果:下一步动作指令、当前步骤数
return {
"action": action,
"current_step": current_ste + 1
}
ask_llm_anything
ask_llm_anything 是阶跃星辰 GUI-Agent 中通用的 LLM 调用核心函数,专门负责将 GUI 场景下的多模态消息(文本 + 图片)标准化处理后调用指定 LLM 并返回结果;其核心特色包括:
- ① 多源图片适配,自动将 URL 形式的图片转换为 Base64 编码,兼容 PNG/JPEG 格式,同时支持
image_b64格式自动转换为image_url格式,统一 LLM 输入规范; - ② 灵活的图片预处理能力,支持按指定尺寸缩放图片,且缩放后统一转换为 JPEG 格式并压缩质量,适配不同 LLM 的图片输入限制;
- ③ 模型配置解耦,通过读取
model_config.yaml文件动态加载不同提供商的 API 地址和密钥,无需硬编码; - ④ 完整的调用监控,记录 LLM 推理耗时和请求 ID,便于性能分析和问题排查;
- ⑤ 兼容推理过程返回,若 LLM 返回
reasoning_content则拼接至结果前,保留模型思考过程,提升 GUI-Agent 决策的可解释性; - ⑥ 鲁棒的参数处理,为温度、最大令牌数等核心参数设置默认值,避免调用异常;
流程图
流程图核心说明
-
初始化阶段:优先加载模型配置文件,校验提供商合法性,避免无效 API 调用;
-
消息预处理核心:统一图片格式:将
image_url(URL/Base64)、image_b64全部转换为标准image_url(Base64 格式),将图片按需缩放至目标尺寸,统一转换为 JPEG 格式压缩,适配 LLM 输入限制; -
LLM 交互阶段:记录调用全生命周期耗时,打印关键监控信息(耗时 / 请求 ID),便于问题排查;
-
结果处理:兼容
reasoning_content(模型思考过程),拼接后返回,提升 GUI-Agent 决策的可解释性;

代码
ask_llm_anything 的代码如下:
def ask_llm_anything(model_provider, model_name, messages, args= {
"max_tokens": 256,
"temperature": 0.5,
"top_p": 1.0,
"frequency_penalty": 0.0,
}, resize_config=None):
"""
通用LLM调用函数:处理多模态消息(文本+图片),调用指定LLM并返回结果
:param model_provider: 模型提供商(如openai/self-host等)
:param model_name: 模型名称(如gpt-4v/llama3-vision等)
:param messages: 待发送的多模态消息列表
:param args: LLM调用参数(温度、最大令牌数等)
:param resize_config: 图片缩放配置(是否缩放、目标尺寸)
:return: LLM返回的文本结果(含可选的推理过程)
"""
# 读取模型配置文件:加载不同提供商的API地址和密钥
with smart_open("model_config.yaml", "r") as f:
model_config = yaml.safe_load(f)
# 配置当前提供商的API基础地址和密钥
if model_provider in model_config:
openai.api_base = model_config[model_provider]["api_base"]
openai.api_key = model_config[model_provider]["api_key"]
else:
# 未知提供商则抛出异常,避免无效调用
raise ValueError(f"Unknown model provider: {model_provider}")
# 定义内部函数:预处理消息中的图片,统一转换为Base64格式的image_url
def preprocess_messages(messages):
# 遍历每条消息
for msg in messages:
# 跳过纯文本消息(content为字符串)
if type(msg['content']) == str:
continue
# 断言content为列表(多模态内容格式)
assert type(msg['content']) == list
# 遍历消息内的每个内容块(文本/图片)
for content in msg['content']:
# 跳过文本内容块
if content['type'] == "text":
continue
# 处理image_url类型的图片
if content['type'] == "image_url":
url = content['image_url']['url']
# 已为Base64格式则跳过
if url.startswith("data:image/"):
continue
else:
# 从URL读取图片字节数据
image_bytes = smart_open(url, mode="rb").read()
# 转换为Base64编码
b64 = base64.b64encode(image_bytes).decode('utf-8')
# 判断图片格式,设置对应的Base64前缀
if image_bytes[0:4] == b"\x89PNG":
content['image_url']['url'] = "data:image/png;base64," + b64
elif image_bytes[0:2] == b"\xff\xd8":
content['image_url']['url'] = "data:image/jpeg;base64," + b64
else:
# 未知格式默认按PNG处理
content['image_url']['url'] = "data:image/png;base64," + b64
else:
# 处理image_b64类型,转换为统一的image_url格式
assert content['type'] == "image_b64"
b64 = content['image_b64']['b64_json']
# 删除原image_b64字段
del content['image_b64']
# 新增image_url字段,统一格式
content['image_url'] = {"url": "data:image/png;base64," + b64}
content['type'] = "image_url"
# 图片缩放处理:若配置了缩放且开启
if resize_config is not None and resize_config.get("is_resize", False) == True:
# 提取Base64编码部分(去掉前缀)
image_url = content['image_url']['url']
image_b64_url = image_url.split(",", 1)[1]
# 解码Base64为图片字节数据
image_data = base64.b64decode(image_b64_url)
# 打开图片并缩放至目标尺寸
image = Image.open(io.BytesIO(image_data))
image = image.resize(size= resize_config['target_image_size'])
# 转换为RGB并保存为JPEG格式(压缩质量85)
image_data = io.BytesIO()
image = image.convert('RGB')
image.save(image_data, format="JPEG", quality=85)
image_data = image_data.getvalue()
# 重新编码为Base64并更新URL
b64_image = base64.b64encode(image_data).decode('utf-8')
content['image_url']['url'] = f"data:image/jpeg;base64,{b64_image}"
return messages
# 执行消息预处理:统一图片格式和尺寸
messages = preprocess_messages(messages)
# 记录LLM调用开始时间
start_time = time.time()
# 调用OpenAI兼容的ChatCompletion接口
completion = openai.ChatCompletion.create(
api_key=openai.api_key, # 传入API密钥
api_base = openai.api_base, # 传入API基础地址
model=model_name, # 指定模型名称
messages=messages, # 预处理后的多模态消息
temperature=args.get("temperature", 0.5), # 采样温度(默认0.5)
top_p=args.get("top_p", 1.0), # 核采样(默认1.0)
frequency_penalty=args.get("frequency_penalty", 0.0), # 重复惩罚(默认0.0)
max_tokens=args.get("max_tokens", 100), # 最大生成令牌数(默认100)
)
# 记录LLM调用结束时间
end_time = time.time()
# 打印推理耗时(便于性能监控)
print(f"LLM {model_name} inference time: {end_time - start_time:.2f} seconds")
# 提取LLM返回的核心结果
result = completion.choices[0].message['content']
# 打印请求ID(便于问题排查)
print("llm ask id:", completion['id'])
# 提取可选的推理过程内容(reasoning_content)
reasoning = completion.choices[0].message.get("reasoning_content", "")
# 若有推理过程,拼接至结果前(格式:推理内容\n结果)
if reasoning is not None and len(reasoning) > 0:
result = "" + reasoning + "<|FunctionCallEnd|>
0x02 Parser0920Summary 的功能
如上一篇所言,GUI-MCP 提供一套跨平台的标准协议,将设备能力抽象为少量原子级和复合级工具。其分层双栈架构把“低层 MCP”与“高层 MCP”结合起来:前者提供细粒度操作(如点击、滑动、文本输入);后者则把整个任务委派给本地部署的 GUI 专家模型(如 Step-GUI-4B)。该设计让主 LLM 只需关注高层规划,而将常规 GUI 操作卸载给本地模型。尤为关键的是,GUI-MCP 支持高隐私执行模式:原始截图与敏感状态始终留在设备端,仅将语义摘要发送给外部 LLM,从而在利用云端推理能力的同时有效保护用户隐私。
因此,图像摘要/文本摘要/任务描述 是一个及其重要的功能。
Parser0920Summary 是一个专用于 GUI Agent 交互的解析器类, 在 GUI Agent 体系中充当关键桥梁,负责把自然语言任务和环境信息转换成模型可理解的消息,并将模型输出解析为可执行的动作。
2.1 核心功能概览
Parser0920Summary 的核心功能如下。
- 构造提供给 LLM 的提示信息:环境信息 → 消息格式,即 环境信息 + 任务描述 → [Parser0920Summary] → 模型输入消息
- 解析 LLM 返回的动作指令:模型输出 → 结构化动作,即 模型输出字符串 → [Parser0920Summary] → 结构化动作 → 设备执行
- 动作格式标准化
- 历史交互摘要维护
通过上述双向转换,Parser0920Summary 实现自然语言与设备操作之间的无缝衔接,是 GUI Agent 理解并执行复杂任务的核心组件。
2.2 详细功能说明
构建提示词(make_status_prompt ):该函数是整个 GUI Agent 系统中人机交互的关键环节,确保模型能够获得足够的上下文信息来做出正确的操作决策。
- 整合任务上下文信息
- 当前用户任务描述
- 历史操作摘要(summary_history)
- 用户评论/指令(user_comment)
- 当前屏幕截图
- 格式化提示内容
- 将所有信息组织成结构化的文本格式
- 添加必要的引导说明,指导模型如何响应
- 包含动作空间定义和输出格式要求
- 处理历史对话
- 整合之前的问答历史(historical_qa)
- 特别强调用户的最新指令
环境转消息(env2messages4ask):将任务描述、历史环境和动作转换为可供模型理解的消息格式
- 整合任务描述与用户指令
- 添加历史操作摘要
- 包含当前屏幕截图
- 处理用户与 Agent 的问答历史
- 生成符合模型输入要求的多模态消息
字符串到动作解析(str2action):将模型输出的字符串解析为结构化的动作对象
- 解析
- 提取动作类型和参数
- 兼容多种动作格式
- 标准化坐标点
- 容错错误格式
动作到字符串转换(action2str):将结构化动作转换为标准化字符串格式
- 生成含思考过程的完整输出
- 格式化各类动作参数
- 添加动作摘要信息
动作标准化(action2action):确保动作对象符合标准格式
- 根据动作类型(CLICK、TYPE、AWAKE、INFO、WAIT、COMPLETE、ABORT、SLIDE、LONGPRESS)验证特定字段
- 统一不同动作类型的参数
- 处理字段命名兼容性问题
2.3 重点函数分析
我们要把几个函数单独拿出来分析。
2.3.1 str2action
str2action 函数负责把模型生成的原始字符串解析成可执行的结构化指令,使系统能够理解并执行模型的决策。
核心步骤
核心步骤如下:
- 提取思考过程(CoT)
- 捕获
内容; - 容错修正标签拼写(如
)
- 捕获
- 解析动作参数
- 按制表符分隔提取键值对;
- 获取 action、explain、point/point1/point2 等关键字段;
- 处理制表符分隔的键值对格式。
- 数据类型转换
- 将坐标字符串 "100,200" 转为整数列表 [100, 200];
- 对其他参数执行相应类型转换。
- 容错处理
- 应对模型输出格式不规范;
- 返回错误信息,便于调试解析失败。
使用场景
在 LocalServer 的 automate_step 方法中调用:
response = ask_llm_anything(...) # 获取模型响应
action = parser.str2action(response) # 解析为结构化动作
输入(模型字符串):
<THINK>我需要点击搜索按钮来查找相关信息</THINK>
explain:点击搜索按钮以开始搜索|taction:CLICK|tpoint:500,320|tsummary:已点击搜索按钮
输出(结构化字典):
OrderedDict([
("cot", "我需要点击搜索按钮来查找相关信息"),
("explain", "点击搜索按钮以开始搜索"),
("action", "CLICK"),
("point", [500, 320]),
("summary", "已点击搜索按钮")
])
代码
def str2action(self, command_str):
command_str = command_str.strip()
# Normalize THINK tags: fix typos, case, and spacing
command_str = (
command_str
.replace("<TINK>", "<THINK>").replace("</TINK>", "</THINK>")
.replace("<think>", "<THINK>").replace("</think>", "</THINK>")
)
command_str = re.sub(r"<\s*/?THINK\s*>", lambda m: "<THINK>" if "/" not in m.group() else "</THINK>", command_str, flags=re.IGNORECASE)
# Extract CoT and key-value parts
# Expected format: <THINK> cot </THINK>\nexplain:xxx\taction:xx\tvalue:xxx\tsummary:xxx
try:
cot_part = command_str.split("<THINK>")[1].split("</THINK>")[0].strip()
kv_part = command_str.split("</THINK>")[1].strip()
except IndexError:
print(f"[Parser Warning] Missing <THINK> tags, treating entire response as kv")
kv_part = command_str
cot_part = ""
action = OrderedDict()
action['cot'] = cot_part
# Error split by \n, should split by tab separator
kvs = [kv.strip() for kv in kv_part.split("\t") if kv.strip()]
for kv in kvs:
if ":" not in kv:
continue
key = kv.split(":", 1)[0].strip()
value = kv.split(":", 1)[1].strip()
if key == "action":
action['action'] = value
elif key == "summary":
action['summary'] = value
elif "point" in key:
# Parse point format: "x,y" or "x y"
try:
# Replace comma with space for unified processing
coords = value.replace(",", " ").split()
if len(coords) < 2:
raise ValueError(f"Expected 2 coordinates, got {len(coords)}")
x, y = int(coords[0]), int(coords[1])
action[key] = [x, y]
except (ValueError, IndexError) as e:
raise ValueError(
f"[Parser Error] Failed to parse point '{value}' for key '{key}': {str(e)}. "
f"Expected format: 'x,y' or 'x y' with integer values"
) from e
else:
action[key] = value
return action
2.3.2 make_status_prompt
该函数在 Parser0920Summary 类的 env2messages4ask 方法中被调用,用于生成发送给 LLM 的消息内容:
# 在 parser_0920_summary.py 中
conversations = [
{
"type": "text",
"text": task_define_prompt # 任务定义和动作空间说明
+ make_status_prompt( # 当前状态信息
task,
current_env['image'],
hints,
summary_history,
qa_prompt
)
}
]
函数返回一个包含以下元素的列表:
- 任务描述和历史操作:用户任务与已完成操作的文本描述
- 当前截图:以图像 URL 形式提供的当前屏幕截图
- 操作指导:指导模型如何思考和回应的说明文本
代码如下:
def make_status_prompt(task, current_image, hints, summary_history="", user_comment=""):
if len(hints) == 0:
hint_str = ""
else:
hint_str = "\n".join([f"- {hint}" for hint in hints])
hint_str = f"### HINT:\n{hint_str}\n"
if user_comment == "":
history_display = summary_history if summary_history.strip() else "暂无历史操作"
else:
history_display = summary_history + user_comment if summary_history.strip() else "暂无历史操作"
user_instruction = f'''\n\n{user_comment}\n\n''' if user_comment != "" else ""
task = task + user_instruction + "指令结束\n\n"
status_conversation = [
{
"type": "text",
"text": f'''
已知用户指令为:{task}
已知已经执行过的历史动作如下:{history_display}
当前手机屏幕截图如下:
'''
},
{
"type": "image_url",
"image_url": {"url": current_image}
},
{
"type": "text",
"text": f'''
在执行操作之前,请务必回顾你的历史操作记录和限定的动作空间,先进行思考和解释然后输出动作空间和对应的参数:
1. 思考(THINK):在 <THINK> 和 </THINK> 标签之间。
2. 解释(explain):在动作格式中,使用 explain: 开头,简要说明当前动作的目的和执行方式。
在执行完操作后,请输出执行完当前步骤后的新历史总结。
输出格式示例:
<THINK> 思考的内容 </THINK>
explain:解释的内容\taction:动作空间和对应的参数\tsummary:执行完当前步骤后的新历史总结
'''
}
]
return status_conversation
2.4 THINK 标签
既然上面代码中提到了THINK 标签,我们本小节就进行分析。
定义和结构
在 Parser0920Summary 类中定义了 THINK 标签的使用方式
- THINK 标签用于封装 Agent 的思维过程,格式为
<THINK> 思考的内容 </THINK> - 在
make_status_prompt函数中明确要求 Agent 在执行操作前进行思考
解析
在 str2action 方法中解析包含 THINK 标签的响应
- 使用正则表达式提取 THINK 标签中的内容作为COT(Chain of Thought)字段
str2action方法中包含错误处理逻辑,处理标签格式不规范的情况
在 LLM 交互中的应用
- 在
ask_llm_v2.py中,当 LLM 响应包含 THINK 标签时,会提取 `` 之后的内容 ask_llm_anything函数会处理reasoning_content字段,将其包装在 THINK 标签中
在 Agent 决策中的作用
-
在
gui_agent_loop中,每个步骤的action都包含cot字段(即 THINK 标签内容) -
COT内容在日志中输出,便于调试和理解 Agent 的决策过程
在 INFO 动作中的应用
-
当 Agent 执行 INFO 动作时,auto_reply 函数会分析 THINK 标签内容来理解 Agent 的问题
-
auto_reply 使用 THINK 标签中的内容作为上下文,生成合适的回复
2.5 COT 输出与执行流程
GUI-Agent 不是将 COT 输出作为具体执行流程。COT 输出主要作用为:
- COT 不直接参与设备操作的执行,实际执行的动作由 action 字段定义(如 CLICK、TYPE 等)
- COT 记录并展示 Agent 的思考过程,让用户了解 Agent 如何分析界面元素和决策点击位置
- COT 可以协助任务规划
- Agent 需要分析当前屏幕截图和任务,COT 可以帮助 Agent 系统化地分析视觉信息和任务需求
- Agent 需要将复杂任务分解为一系列动作,COT 可以帮助 Agent 规划执行路径
- Agent 需要将视觉信息转换为具体操作,COT 可以帮助 Agent 理解界面元素与目标之间的关系,有助于精确定位点击坐标(x,y 为 0-1000 范围)
- 有助于处理复杂的界面布局和交互场景
- COT 提供决策的可解释性,开源辅助调试和分析
- 当 Agent 做出错误决策时,可以通过 THINK 内容追踪错误原因
- 便于调试和改进 Agent 的决策逻辑
- 在需要时辅助生成对用户问题的回复,即在 INFO 动作中,COT 信息可用于生成对用户的回复
具体如下:
- COT 是在
标签中生成的,用于解释 Agent 的决策逻辑 - 在 str2action 方法中,COT 从 LLM 响应中提取并存储为 COT 字段
- 在 execute_task 的返回结果中,中间日志会包含 COT 信息
- intermediate_logs 中包含每个步骤的 COT 字段
- 在最终结果中,final_action 也包含 COT 信息
2.6 支持的动作类型
动作空间定义
Parser0920Summary 支持完整的 Android GUI 操作集合,例如:
- CLICK:点击屏幕坐标
- OTYPE:输入文本
- COMPLETE:标记任务完成
- WAIT:等待指定时间
- AWAKE:唤醒指定应用
- INFO:向用户询问信息
- ABORT:终止任务
- SLIDE:滑动操作
- LONGPRESS:长按操作
提示词引导
在 task_define_prompt 中明确定义了可用的动作空间和参数格式。
task_define_prompt = """你是一个手机 GUI-Agent 操作专家,你需要根据用户下发的任务、手机屏幕截图和交互操作的历史记录,借助既定的动作空间与手机进行交互,从而完成用户的任务。
请牢记,手机屏幕坐标系以左上角为原点,x轴向右,y轴向下,取值范围均为 0-1000。
# 行动原则:
1. 你需要明确记录自己上一次的action,如果是滑动,不能超过5次。
2. 你需要严格遵循用户的指令,如果你和用户进行过对话,需要更遵守最后一轮的指令
# Action Space:
在 Android 手机的场景下,你的动作空间包含以下9类操作,所有输出都必须遵守对应的参数要求:
1. CLICK:点击手机屏幕坐标,需包含点击的坐标位置 point。
例如:action:CLICK\tpoint:x,y
2. TYPE:在手机输入框中输入文字,需包含输入内容 value、输入框的位置 point。
例如:action:TYPE\tvalue:输入内容\tpoint:x,y
3. COMPLETE:任务完成后向用户报告结果,需包含报告的内容 value。
例如:action:COMPLETE\treturn:完成任务后向用户报告的内容
4. WAIT:等待指定时长,需包含等待时间 value(秒)。
例如:action:WAIT\tvalue:等待时间
5. AWAKE:唤醒指定应用,需包含唤醒的应用名称 value。
例如:action:AWAKE\tvalue:应用名称
6. INFO:询问用户问题或详细信息,需包含提问内容 value。
例如:action:INFO\tvalue:提问内容
7. ABORT:终止当前任务,仅在当前任务无法继续执行时使用,需包含 value 说明原因。
例如:action:ABORT\tvalue:终止任务的原因
8. SLIDE:在手机屏幕上滑动,滑动的方向不限,需包含起点 point1 和终点 point2。
例如:action:SLIDE\tpoint1:x1,y1\tpoint2:x2,y2
9. LONGPRESS:长按手机屏幕坐标,需包含长按的坐标位置 point。
例如:action:LONGPRESS\tpoint:x,y
"""
动作解析
- 通过
str2action方法解析模型输出的动作 - 该方法会从模型响应中提取 action 类型、参数等信息
- 解析时会验证动作类型是否在预定义的动作空间内
动作验证
- 使用
action_assertion函数验证动作格式 - 验证每个动作是否包含必需的字段,如:
- CLICK 动作必须包含 point 参数
- TYPE 动作必须包含 value 参数
- SLIDE 动作必须包含 point1 和 point2 参数
实际执行流程
实际生成动作的流程如下:
- LocalServer 的 automate_step 方法接收 observation 并生成动作
- gui_agent_loop 调用 automate_step 获取动作
- 动作通过 uiTars_to_frontend_action 转换为前端动作
- act_on_device 执行具体的设备操作
0x03 交互
LocalServer.automate_step 与 Parsero920Summary 之间的逻辑关系如下:
- LocalServer.automate_step 负责协调整个流程,从日志读取历史环境和动作
- 通过 get_parser 获取 Parsero920Summary 实例
- 调用 env2messages4ask 构建模型输入消息
Parsero920Summary 内部函数协作
- env2messages4ask 接收任务、环境和动作参数,整合历史动作和当前状态
- 通过 make_status_prompt 构建状态提示,包含任务描述、历史动作和当前截图
- str2action 解析模型输出,action2action 验证动作格式
模型交互流程
- env2messages4ask 返回构建好的消息格式
- LocalServer 通过 ask_llm_anything 调用模型
- 模型返回字符串后,str2action 解析成动作对象
- 处理
- LocalServer 将动作和当前步骤信息记录到日志
数据流向
- 环境数据从 LocalServer 传递给 Parsero920Summary
- Parsero920Summary 构建消息并返回给 LocalServer
- LocalServer 调用模型并接收响应
- Parsero920Summary 解析模型响应并返回动作给 LocalServer
- LocalServer 记录结果并返回给调用者
Parser0920Summary 与 LocalServer 交互流程图如下:

0x04 模型分发
论文中提到:GUI-MCP 提供一套标准化、跨平台的协议,将设备能力抽象为少量原子及组合工具。其分层双栈架构结合:“低层 MCP”提供细粒度操作(点击、滑动、文本输入等),与“高层 MCP”将整个任务委派给本地部署的 GUI 专有模型(如 Step-GUI-4B)。该设计使主语言模型专注于高层规划,同时将常规 GUI 操作卸载至本地模型。尤为关键的是,GUI-MCP 支持高隐私执行模式:原始截图与敏感状态留在设备端,仅语义摘要流向外部语言模型,从而在利用云端推理能力的同时有效保护用户隐私。
本小节就来看看如何进行模型分发(没有实际串联的调用代码,只是反推)。
4.1 模型分发机制
- 统一接口:
ask_llm_anything函数抽象了不同模型提供商的差异 - 配置驱动:通过
model_config.yaml文件配置不同模型提供商 - 灵活切换:在任务配置中指定使用哪个模型提供商

4.2 本地模型流程
模型配置
run_single_task.py中的local_model_config定义了模型配置- 配置中指定
model_provider: "local"表示使用本地模型 model_config.yaml文件中定义了本地模型的 API 基础地址和密钥
模型推理流程
-
evaluate_task_on_device函数启动任务评估 -
LocalServer类作为本地服务器 -
automate_step方法处理单步推理 -
get_parser获取解析器(Parser0920Summary) -
env2messages4ask生成模型输入消息 -
ask_llm_anything函数调用模型 API
本地模型调用链
ask_llm_anything从model_config.yaml读取配置- 设置
openai.api_base和openai.api_key - 通过
ChatCompletion.create发起请求
4.3 云端模型流程
云端模型配置
model_config.yaml文件可配置多个模型提供商- 例如
openai、anthropic等云端服务提供商 - 配置相应的 API 基础地址和密钥
云端模型调用
- 调用流程与本地模型相同,通过
ask_llm_anything函数 - 根据
model_provider参数切换不同模型提供商 - 通过网络请求访问云端模型服务
0x05 过渡到执行层
下面流程展示了完整的从输入到最终执行操作的流程:
- make_status_prompt 整合任务、截图、历史、QA → 多模态消息
- LLM 接收消息 → 返回自然语言动作字符串
- LLM 输出:模型生成包含思考过程和动作指令的文本响应
- 解析响应:使用 str2action 函数解析文本,提取结构化信息
- 动作验证:验证解析出的动作是否符合预定义的动作类型和参数要求
- 前端动作转换:将标准化动作转换为设备可执行的前端动作格式
- 设备执行:通过 ADB 命令在 Android 设备上执行具体操作 (点击、滑动、输入等)
- 循环回到步骤 1,直至任务完成或达到最大步数。
具体参见下图。

浙公网安备 33010602011771号