前面几篇文章,我们已经把 UI-TARS 的执行链路基本串起来了:

截图
  ↓
模型推理
  ↓
Thought + Action
  ↓
parse_action_to_structure_output
  ↓
结构化 action dict
  ↓
parsing_response_to_pyautogui_code
  ↓
pyautogui 执行
  ↓
重新截图

从技术上看,这条链路已经能让模型“看屏幕、想动作、点按钮、输入文字、滚动页面”。

但真正做本地桌面自动化产品时,最重要的问题不是:

能不能执行?

而是:

能不能安全执行?

因为 GUI Agent 和普通聊天机器人不同。

普通模型答错了,最多是文字错误。

GUI Agent 一旦答错,可能真的会:

点错按钮
删除文件
发送消息
提交表单
关闭窗口
付款
操作敏感页面
绕过访问限制

所以这一篇专门讲安全边界和失败场景。


一、为什么 GUI Agent 的安全风险更高?

UI-TARS 官方 README 在 Limitations 中明确提到,UI-TARS-1.5 可能出现 GUI 元素误识别、生成不准确描述、基于错误推断采取次优动作等问题;同时也提到,GUI 自动化能力可能被滥用于访问保护内容或认证挑战,包括 CAPTCHA 场景。

这说明官方也承认两个核心风险:

第一,模型可能看错、想错、点错。

第二,模型能力可能被滥用。

这两个风险在桌面自动化中非常现实。

比如模型看到一个按钮,Thought 写得很合理:

Thought: 我需要点击确认按钮继续下一步。
Action: click(point='<point>820 620</point>')

但真实界面上这个位置可能是:

删除
付款
发送
确认覆盖
关闭不保存

只要执行层不拦截,就可能造成实际损失。

所以,UI-TARS 二次开发时必须加安全层。


二、安全层应该放在哪里?

很多人会把安全写在 Prompt 里,例如:

不要点击危险按钮。
不要删除文件。
不要付款。

这有用,但不够。

因为 Prompt 只是提醒模型,不是硬约束。

真正的安全边界应该放在执行前:

模型输出 Action
  ↓
Parser 解析 Action
  ↓
Safety 检查
  ↓
通过才执行
  ↓
否则暂停或拒绝

也就是说,安全层应该位于:

parse_action_to_structure_output
和
pyautogui 执行
之间

UI-TARS 官方 ui-tars 包的职责是把 VLM 生成的 GUI 动作解析成结构化动作,并生成 pyautogui 脚本;它本身不是完整安全执行沙箱。

所以二次开发时,我们要自己补上:

动作白名单
坐标边界检查
高风险关键词识别
敏感页面识别
人工确认
失败重试
强制停止
日志审计

三、先建立一个安全执行原则

做 GUI Agent,建议遵守一个原则:

模型只能建议动作,不能直接拥有最终执行权。

也就是说:

模型负责:
我觉得下一步应该点击这里。

系统负责:
这个动作能不能执行?
是否需要用户确认?
是否越界?
是否高风险?
执行后是否成功?

这比“模型输出什么就执行什么”安全得多。

更合理的分层是:

Model:
根据截图和任务生成 Thought + Action。

Parser:
把 Action 转成结构化 dict。

Safety:
判断动作是否允许。

Executor:
只执行 Safety 放行的动作。

Verifier:
执行后重新截图,判断是否成功。

四、风险一:误点击

误点击是 GUI Agent 最常见的失败场景。

原因可能有很多:

模型识别错了按钮;
坐标缩放错了;
窗口位置偏移;
DPI 缩放导致坐标错位;
截图和执行时界面变化;
弹窗突然出现;
页面加载导致布局移动;
滚动区域不对。

UI-TARS 的 parse_action_to_structure_output 会处理 point/start_box/end_box 的格式转换和坐标归一化;parsing_response_to_pyautogui_code 会再把归一化坐标乘以 image_width/image_height,还原成 pyautogui 坐标。

但这只解决了“坐标如何换算”的问题。

它不能保证:

模型选的目标一定对;
当前界面没有变化;
这个按钮可以安全点击。

所以,误点击必须靠 Safety 和 Verification 处理。


五、如何降低误点击风险?

建议至少做五层保护。

1. 坐标边界检查

任何 click、drag、scroll 坐标都必须在屏幕范围内。

def validate_point(x, y, width, height):
    if not (0 <= x <= width and 0 <= y <= height):
        raise ValueError(f"坐标越界: ({x}, {y})")

2. 点击前可视化

执行前在截图上画出红点,让用户确认。

模型准备点击的位置
必须能被人类肉眼看到

这对早期调试非常重要。

3. 低风险动作自动执行,高风险动作确认

例如:

scroll:可以自动执行
click 普通区域:可以半自动
type:建议确认
hotkey:建议确认
删除/付款/发送:必须确认

4. 执行后重新截图

不能认为 pyautogui 执行完就成功。

必须重新截图判断:

页面是否变化?
输入是否成功?
按钮点击后是否进入下一步?
是否出现错误弹窗?

5. 连续失败自动停止

如果连续 3 次界面没有变化,就停止。

if unchanged_count >= 3:
    raise RuntimeError("连续多次执行后界面无变化,停止自动操作")

六、风险二:模型幻觉

UI-TARS 官方 README 提到,模型可能偶尔生成不准确描述、误识别 GUI 元素,或基于错误推断采取次优动作,尤其是在模糊或陌生环境中。

这就是 GUI Agent 里的“幻觉”。

文本模型的幻觉通常表现为:

编造事实;
说错知识;
引用不存在内容。

GUI Agent 的幻觉则表现为:

以为页面上有某个按钮;
以为当前已经登录;
以为文件已经保存;
以为弹窗已经关闭;
以为任务已经完成;
以为某个区域可以点击。

这种幻觉更危险。

因为它会转化成真实动作。


七、如何处理 GUI 幻觉?

最有效的方法不是让模型“别幻觉”,而是增加验证机制。

1. Thought 不能作为事实

模型说:

Thought: 当前已经打开设置页面。

不代表真的打开了。

要重新截图验证。

2. finished 必须验证

模型输出:

Action: finished(content='任务已完成')

不能直接相信。

应该检查:

目标页面是否出现?
目标文本是否存在?
文件是否生成?
表单是否提交成功?

3. 引入状态检查器

可以用简单规则,也可以用另一个视觉模型判断。

def verify_task_state(task, screenshot_path):
    """
    判断当前截图是否真的满足任务目标。
    MVP 阶段可以先人工确认。
    """
    return "unknown"

4. 失败后不要无限重试

模型连续误判时,继续循环只会越错越远。

建议:

最多 10 到 15 步;
连续 3 次无明显变化;
连续 2 次解析失败;
连续 2 次点击相近坐标无效果;
立即暂停。

八、风险三:验证码 CAPTCHA

验证码是必须单独处理的场景。

UI-TARS 官方 Limitations 中明确提到,GUI 任务能力可能被滥用于认证挑战,包括 CAPTCHA 场景。

所以在二次开发中,验证码不应该作为“自动化能力展示点”。

正确策略是:

检测到验证码
  ↓
暂停自动化
  ↓
提示用户手动处理
  ↓
用户完成后继续

不要设计自动绕过验证码的逻辑。

因为验证码的目的就是区分真人和自动化程序,自动绕过可能违反网站规则,也可能带来账号风险。


九、如何检测验证码?

可以先用简单策略。

1. 关键词检测

如果模型 Thought 或截图 OCR 中出现:

验证码
captcha
CAPTCHA
人机验证
我不是机器人
滑块验证
选择所有包含
验证你是真人
security check

就暂停。

示例:

CAPTCHA_KEYWORDS = [
    "验证码",
    "captcha",
    "人机验证",
    "我不是机器人",
    "滑块验证",
    "security check",
    "verify you are human",
]

def detect_captcha(text: str) -> bool:
    lower = text.lower()
    return any(k.lower() in lower for k in CAPTCHA_KEYWORDS)

2. Action 风险检测

如果模型输出:

drag(start_point=..., end_point=...)

同时 Thought 中出现“滑块”“验证”“captcha”,就停止。

3. 页面状态检测

如果连续出现验证页面,也停止。


十、验证码处理策略

推荐策略是:

1. 不自动解验证码;
2. 不调用第三方打码服务;
3. 不模拟滑块破解;
4. 不绕过访问控制;
5. 转人工处理;
6. 用户完成验证后,重新截图继续任务。

可以设计成:

def handle_captcha():
    print("检测到验证码或人机验证,请手动完成。")
    input("完成后按 Enter 继续...")

这样既保留了自动化流程,也避免越过安全边界。


十一、风险四:高风险操作

高风险操作包括:

删除文件
覆盖文件
清空数据
提交表单
发送消息
发送邮件
付款
下单
转账
授权登录
修改密码
安装软件
卸载软件
关闭安全软件
运行命令
上传隐私文件

这些动作不应该全自动执行。

即使模型很确信,也必须暂停确认。

因为 GUI Agent 一旦点错,后果可能不可逆。


十二、高风险动作如何识别?

可以从三层识别。

1. Action 类型风险

某些动作天然更危险:

hotkey
type
right_single
drag

例如:

hotkey(key='alt f4')
hotkey(key='shift delete')
type(content='...\n')
right_single(...)

这些都应该谨慎。

2. Thought 文本风险

模型 Thought 中如果出现:

删除
确认
提交
发送
付款
购买
覆盖
授权
密码
验证码

就标记为高风险。

3. 屏幕内容风险

如果截图中有高风险按钮:

Delete
Remove
Submit
Pay
Send
Confirm
Transfer
Authorize

也应该暂停。

MVP 阶段可以先依赖模型输出文本和 OCR,后续再做按钮区域识别。


十三、设计一个 SafetyPolicy

可以把安全策略封装成类:

class SafetyPolicy:
    def __init__(self):
        self.allowed_actions = {
            "click",
            "left_single",
            "type",
            "hotkey",
            "scroll",
            "wait",
            "finished",
        }

        self.risky_hotkeys = {
            "alt f4",
            "ctrl w",
            "ctrl q",
            "shift delete",
        }

        self.risky_words = {
            "删除",
            "清空",
            "付款",
            "支付",
            "转账",
            "发送",
            "提交",
            "授权",
            "密码",
            "验证码",
            "delete",
            "remove",
            "pay",
            "transfer",
            "send",
            "submit",
            "authorize",
            "password",
            "captcha",
        }

    def check(self, action: dict) -> dict:
        action_type = action.get("action_type")
        action_inputs = action.get("action_inputs", {})
        text = str(action)

        if action_type not in self.allowed_actions:
            return {
                "allow": False,
                "reason": f"不允许的动作类型: {action_type}",
                "need_confirm": False,
            }

        for word in self.risky_words:
            if word.lower() in text.lower():
                return {
                    "allow": False,
                    "reason": f"检测到高风险关键词: {word}",
                    "need_confirm": True,
                }

        if action_type == "hotkey":
            key = action_inputs.get("key") or action_inputs.get("hotkey") or ""
            if key.lower() in self.risky_hotkeys:
                return {
                    "allow": False,
                    "reason": f"高风险快捷键: {key}",
                    "need_confirm": True,
                }

        return {
            "allow": True,
            "reason": "安全检查通过",
            "need_confirm": False,
        }

这只是最小版本。

真实产品里,SafetyPolicy 应该更细。


十四、风险五:直接执行 pyautogui 代码

UI-TARS 的 parsing_response_to_pyautogui_code 会返回 pyautogui 脚本字符串,官方 README 也说明这个函数会把结构化 actions 转换成 pyautogui script string。

这对学习和调试很方便。

但产品化时要谨慎:

exec(pyautogui_code)

这种方式风险很高。

原因有两个。

第一,字符串执行本身风险高。

第二,源码中部分坐标解析使用了 eval(start_box) / eval(end_box)。例如 drag、scroll、click 等分支会通过 eval 把字符串形式的 box 转成坐标列表。

研究代码里这样写很方便,但真实产品中,模型输出属于不可信输入,不建议直接 eval。


十五、如何替代 exec 和 eval?

建议自己写安全执行器。

不要执行模型生成的 Python 代码,而是执行结构化 action dict。

示例:

import ast
import pyautogui


def safe_parse_box(box: str):
    try:
        value = ast.literal_eval(box)
    except Exception as e:
        raise ValueError(f"坐标解析失败: {box}") from e

    if not isinstance(value, (list, tuple)):
        raise ValueError("坐标必须是 list 或 tuple")

    if len(value) not in (2, 4):
        raise ValueError("坐标长度必须是 2 或 4")

    nums = [float(x) for x in value]

    for n in nums:
        if not (0 <= n <= 1):
            raise ValueError(f"归一化坐标越界: {n}")

    if len(nums) == 2:
        x, y = nums
        return [x, y, x, y]

    return nums


def execute_click(action, image_width, image_height):
    box = action["action_inputs"].get("start_box")
    x1, y1, x2, y2 = safe_parse_box(box)

    x = ((x1 + x2) / 2) * image_width
    y = ((y1 + y2) / 2) * image_height

    pyautogui.click(x, y, button="left")

这样你就可以保留 UI-TARS 的 Parser,但把执行层改成更安全的版本。


十六、风险六:type 自动输入敏感内容

UI-TARS 的 type 分支默认 input_swap=True,会生成 pyperclip.copy(...)pyautogui.hotkey('ctrl', 'v'),如果内容以换行结尾,还会生成 pyautogui.press('enter')

这对中文、长文本、特殊符号很友好。

但也带来风险:

可能覆盖用户剪贴板;
可能把内容输入到错误窗口;
可能自动提交表单;
可能输入密码、验证码、付款信息;
可能发送错误消息。

尤其是:

type(content='xxx\n')

末尾 \n 通常意味着输入后按 Enter。

这可能触发:

发送消息
提交搜索
提交表单
确认命令

所以 type 要比普通 click 更谨慎。


十七、type 的安全策略

建议:

1. 默认显示待输入内容;
2. 输入前确认当前焦点位置;
3. 如果 content 以 \n 结尾,必须额外确认;
4. 不允许自动输入密码、验证码、银行卡、支付信息;
5. 剪贴板输入前保存原剪贴板;
6. 输入后恢复剪贴板;
7. 输入后截图验证。

示例:

def validate_type_action(action):
    content = action.get("action_inputs", {}).get("content", "")

    sensitive_words = ["密码", "验证码", "password", "captcha", "银行卡", "支付"]
    for word in sensitive_words:
        if word.lower() in content.lower():
            raise ValueError(f"禁止自动输入敏感内容: {word}")

    if content.endswith("\\n") or content.endswith("\n"):
        return {
            "need_confirm": True,
            "reason": "输入内容包含回车,可能触发表单提交或消息发送"
        }

    return {
        "need_confirm": False,
        "reason": "普通文本输入"
    }

十八、风险七:hotkey 破坏当前状态

快捷键非常强大,也非常危险。

例如:

ctrl a:全选
ctrl x:剪切
ctrl s:保存
ctrl w:关闭标签页
alt f4:关闭窗口
shift delete:永久删除

UI-TARS 的 Prompt 要求 hotkey(key='ctrl c') 这类格式,按键用空格分隔且小写;源码执行层会把它拆成多个键并生成 pyautogui.hotkey(...)

所以 hotkey 必须做风险分级。

建议:

低风险:
ctrl c
ctrl v
ctrl f
ctrl l
esc

中风险:
ctrl a
ctrl s
enter

高风险:
alt f4
ctrl w
ctrl q
shift delete

执行策略:

低风险:可自动执行
中风险:半自动确认
高风险:默认禁止

十九、风险八:scroll 和 drag 导致状态不可控

scroll 和 drag 看起来不像 delete、pay 那么危险,但它们也容易导致任务失控。

parsing_response_to_pyautogui_code 中,drag/select 会读取 start_boxend_box,计算起点终点后生成 pyautogui.moveTo(...)pyautogui.dragTo(...);scroll 会根据 direction 生成 pyautogui.scroll(5)pyautogui.scroll(-5),有 start_box 时会在指定位置滚动。

风险包括:

拖错文件;
拖错滑块;
滚动到错误区域;
页面滚过目标;
拖动窗口导致布局变化;
选中文本后误覆盖;
触发隐藏菜单。

所以 drag 建议先不要自动执行。

scroll 可以自动执行,但要限制次数。

例如:

MAX_SCROLLS_PER_TASK = 5

如果滚动多次仍找不到目标,就暂停。


二十、风险九:wait 和 finished 被滥用

Prompt 中包含:

wait()
finished(content='xxx')

wait() 表示睡眠 5 秒并重新截图检查变化,finished 表示任务完成。

这两个动作也需要检查。

wait 风险

模型可能反复 wait,导致任务卡住。

处理方式:

连续 wait 超过 2 次,暂停。

finished 风险

模型可能误以为任务完成。

处理方式:

finished 不能直接结束;
必须验证任务目标是否真的满足。

例如任务是:

下载文件

不能因为模型说 finished 就结束。

应该检查:

文件是否存在;
下载是否完成;
页面是否显示成功;
是否有错误提示。

二十一、失败场景分类

可以把失败场景分成 8 类。

1. Parse Failure:
模型输出格式不合法,无法解析。

2. Coordinate Failure:
坐标越界、缩放错误、窗口偏移、DPI 错误。

3. Target Failure:
模型找错目标元素。

4. State Failure:
执行后界面没有变化,或变化不符合预期。

5. Loop Failure:
反复点击、反复滚动、反复 wait。

6. Safety Failure:
动作涉及删除、付款、发送、验证码等高风险内容。

7. Environment Failure:
窗口不在前台、远程桌面异常、剪贴板失效、权限不足。

8. Model Failure:
幻觉、误识别、任务理解错误。

每类失败都应该有不同处理策略。


二十二、失败处理策略

建议做一个统一的失败处理表。

Parse Failure:
停止执行,保存原始模型输出,提示需要调整 Prompt。

Coordinate Failure:
停止执行,保存截图和坐标可视化图。

Target Failure:
重新截图,让模型反思一次;连续失败则停止。

State Failure:
允许重试一次;仍无变化则暂停人工接管。

Loop Failure:
超过最大步数或重复动作,停止。

Safety Failure:
拒绝执行或请求人工确认。

Environment Failure:
提示用户检查窗口、权限、DPI、远程桌面。

Model Failure:
降低自动化级别,切换半自动确认模式。

这比简单地 try-catch 更有用。

因为 GUI Agent 的失败不是一个异常,而是一个状态。


二十三、设计一个 Step Guard

可以给每一步执行增加 Guard。

class StepGuard:
    def __init__(self, max_steps=15, max_waits=2, max_scrolls=5):
        self.max_steps = max_steps
        self.max_waits = max_waits
        self.max_scrolls = max_scrolls
        self.wait_count = 0
        self.scroll_count = 0
        self.last_actions = []

    def check_step_count(self, step):
        if step > self.max_steps:
            raise RuntimeError("超过最大执行步数")

    def check_repeated_action(self, action):
        signature = str(action)
        self.last_actions.append(signature)
        self.last_actions = self.last_actions[-3:]

        if len(self.last_actions) == 3 and len(set(self.last_actions)) == 1:
            raise RuntimeError("检测到连续重复动作,停止执行")

    def check_action_limit(self, action):
        action_type = action.get("action_type")

        if action_type == "wait":
            self.wait_count += 1
            if self.wait_count > self.max_waits:
                raise RuntimeError("连续 wait 过多,停止执行")

        if action_type == "scroll":
            self.scroll_count += 1
            if self.scroll_count > self.max_scrolls:
                raise RuntimeError("scroll 次数过多,停止执行")

这个 Guard 可以防止 Agent 陷入循环。


二十四、建议的安全执行流程

综合起来,一个安全执行流程应该是:

Step 1:截图
Step 2:模型输出 Thought + Action
Step 3:解析 Action
Step 4:动作白名单检查
Step 5:验证码检测
Step 6:高风险关键词检测
Step 7:坐标边界检查
Step 8:重复动作检查
Step 9:必要时用户确认
Step 10:执行动作
Step 11:重新截图
Step 12:验证状态变化
Step 13:记录日志

伪代码:

for step in range(max_steps):
    screenshot = capture_screen()

    response = model.infer(task, screenshot, history)

    actions = parse_response_to_actions(response)

    for action in actions:
        safety_result = safety_policy.check(action)
        if not safety_result["allow"]:
            if safety_result["need_confirm"]:
                ask_user_confirm(action, safety_result["reason"])
            else:
                raise RuntimeError(safety_result["reason"])

        step_guard.check_action_limit(action)
        step_guard.check_repeated_action(action)

    execute(actions)

    new_screenshot = capture_screen()
    verify_result = verify_state(task, screenshot, new_screenshot, actions)

    log_step(step, response, actions, verify_result)

这才是适合真实桌面自动化的执行方式。


二十五、日志是安全系统的一部分

很多人把日志当成调试工具。

但对 GUI Agent 来说,日志也是安全系统的一部分。

每一步至少记录:

{
  "step": 5,
  "before_screenshot": "step_005_before.png",
  "after_screenshot": "step_005_after.png",
  "model_response": "Thought: ... Action: ...",
  "parsed_actions": [],
  "safety_result": {
    "allow": true,
    "reason": "安全检查通过"
  },
  "executed": true,
  "execution_result": "success"
}

如果发生误点击,日志可以帮助你追查:

模型为什么点这里?
坐标怎么还原的?
执行前界面是什么?
执行后发生了什么?
安全检查为什么放行?

没有日志,就很难定位责任链。


二十六、半自动模式是最好的起点

如果你刚开始做 UI-TARS 二次开发,不要一上来全自动。

建议默认半自动:

模型输出 Thought + Action
  ↓
显示点击点
  ↓
显示动作说明
  ↓
显示风险评级
  ↓
用户确认
  ↓
执行

可以按风险级别逐步放开:

Level 0:
只展示动作,不执行。

Level 1:
低风险动作自动执行,高风险动作确认。

Level 2:
常见任务自动执行,敏感动作确认。

Level 3:
全自动执行,但保留强制停止和日志审计。

MVP 阶段建议停在 Level 0 或 Level 1。


二十七、安全边界的本质

UI-TARS 的价值在于,它把 GUI 操作抽象成了受控动作空间。

Prompt 中明确要求模型输出 Thought: ... Action: ...,并把桌面端动作限制在 click、drag、hotkey、type、scroll、wait、finished 等动作中。

但这只是第一层安全。

真正的安全边界应该是:

动作空间受控;
Parser 严格解析;
Executor 不直接 eval/exec;
Safety 拦截高风险;
Verifier 验证结果;
用户可随时接管。

换句话说:

安全不是一句 Prompt,而是一整套工程链路。


总结

这篇文章我们从安全角度分析了 UI-TARS 二次开发中的关键风险。

主要失败场景包括:

误点击;
模型幻觉;
验证码;
高风险操作;
坐标偏移;
快捷键破坏状态;
type 自动提交;
scroll/drag 失控;
finished 误判;
重复动作死循环。

对应的处理策略是:

动作白名单;
坐标边界检查;
验证码转人工;
高风险动作确认;
禁止自动输入敏感内容;
不用 exec 直接执行模型生成代码;
用 ast.literal_eval 或严格解析替代 eval;
执行后重新截图验证;
连续失败自动停止;
完整日志审计;
从半自动模式开始。

UI-TARS 官方代码提供了从模型输出到 pyautogui 脚本的基础能力,但真实产品不能只追求“能自动点”。官方也提醒模型可能出现误识别、幻觉、次优动作,以及 CAPTCHA 等滥用风险。

真正可靠的本地桌面 Agent,必须做到:

能执行,
也能暂停;

能点击,
也能拒绝;

能自动化,
也能让用户接管;

能完成任务,
也能在不确定时停下来。

这就是 GUI Agent 从 Demo 走向产品时最重要的一道安全边界。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐