的代码(附代码及详细解释)`)
1. 真机跑通通义千问 Mobile Agent 到底卡在哪通义千问 Mobile Agent 是一套用多模态大模型看懂手机屏幕、再输出 ADB 动作指令的智能体方案适合做自动化测试、无障碍辅助、批量操作演示的开发者。它最吸引人的地方在于你不需要训练模型只要把截图喂给通义千问 VL模型就会返回一段 JSON告诉你点哪里、滑哪里、输入什么。听起来很美好但真机落地时卡点往往不在模型本身而在“截图怎么传、坐标怎么对、动作怎么回传”这条链路上。我第一次跑这套代码时遇到的第一个问题不是模型答错而是截图传上去后模型返回的坐标点在了屏幕外。后来才发现通义千问 VL 在 prompt 里被明确告知屏幕分辨率是 1092x2408而我的测试机是 1080x2400两个分辨率不一致模型按 1092x2408 算出来的坐标直接偏了几十像素。这种问题在模拟器上不明显一到真机就暴露。第二个卡点是动作回传。模型返回的是 JSON但 JSON 里的action字段需要映射成真实的 ADB 命令。比如click要转成adb shell input tap x yswipe要转成adb shell input swipe x1 y1 x2 y2 durationopen要转成adb shell am start。如果映射逻辑写错模型再聪明也执行不了。第三个卡点是循环验证。Mobile Agent 不是一次调用就结束它需要“截图 → 模型决策 → 执行动作 → 再截图 → 再决策”这样循环直到模型返回terminate。这个循环里如果截图时机不对比如动作还没执行完就截图模型会看到旧画面导致重复点击。所以这篇文章不打算只贴一段调用代码就完事而是把整条链路拆开环境怎么准备、模型怎么接入、配置怎么写、动作怎么回传、报错怎么排查。你跟着做应该能在自己的真机上跑通同一套通义千问 Mobile Agent 链路。2. 接入前的环境准备与 TaoToken 配置在写代码之前先把两件事搞定一是手机端的 ADB 调试环境二是模型调用通道。ADB 部分比较标准装好 platform-tools手机开启开发者选项和 USB 调试用adb devices能看到设备就行。如果是无线调试用adb connect IP:端口连上后面所有命令都走这个通道。模型调用通道这块我这次用的是 TaoToken 的 API 接入方式。它的好处是兼容 OpenAI SDK 的调用格式通义千问 VL 这类多模态模型可以直接通过chat.completions.create调用不需要额外改协议。你需要在 TaoToken 控制台创建一个 API Key然后拿到 Base URL。Base URL 是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 OpenAI 客户端的base_url使用。创建 Key 的入口在控制台的 API Keys 页面进去之后点新建复制出来的 Key 只显示一次记得存好。如果你后面还要用 Coding Plan 做长期编码任务可以顺便了解一下套餐但这次跑 Mobile Agent 用按量调用的 Key 就够了。环境变量建议这样设置避免把 Key 硬编码进代码export TAOTOKEN_API_KEYsk-你的实际Key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiPython 依赖只需要两个pip install openai pillowopenai用来调模型pillow用来做截图压缩和格式转换。ADB 部分不需要 Python 库直接用subprocess调命令行就行。这里有个细节要注意通义千问 VL 对图片格式有要求base64 编码时要用 JPEG 或 PNG并且data:image/jpeg;base64,这个前缀要和实际格式一致。我试过用 PNG 图片但前缀写成 JPEG模型直接返回空内容。所以截图后统一转成 JPEG质量压到 85 左右既能减小传输体积又不会让模型看不清文字。另外真机截图建议用adb exec-out screencap -p screen.png比adb shell screencap再 pull 出来快很多少一次文件传输。截图分辨率如果和模型 prompt 里写的分辨率不一致要么改 prompt要么在代码里做坐标缩放。我选择的是在代码里做缩放这样换设备不用改 prompt。3. 可复制的配置片段与核心代码结构这一节给出可以直接复制的配置和代码骨架。先看配置文件我用的是 JSON 格式放在项目根目录的config.json{ api: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model: qwen-vl-max, timeout: 30 }, device: { adb_serial: , screen_width: 1080, screen_height: 2400, model_screen_width: 1092, model_screen_height: 2408 }, agent: { max_steps: 15, screenshot_format: jpeg, jpeg_quality: 85, wait_after_action: 1.5 } }这里model_screen_width和model_screen_height是 prompt 里告诉模型的分辨率screen_width和screen_height是你真机的分辨率。代码里会做一个比例换算把模型返回的坐标映射到真机坐标。核心代码结构分四个模块截图模块、模型调用模块、动作执行模块、主循环。截图模块负责调 ADB 拿图并转 base64import subprocess import base64 from io import BytesIO from PIL import Image def capture_screen(serialNone): cmd [adb] if serial: cmd [-s, serial] cmd [exec-out, screencap, -p] raw subprocess.check_output(cmd) img Image.open(BytesIO(raw)).convert(RGB) buf BytesIO() img.save(buf, formatJPEG, quality85) return base64.b64encode(buf.getvalue()).decode(utf-8)模型调用模块用 OpenAI SDK注意base_url指向 TaoTokenimport os from openai import OpenAI client OpenAI( base_urlos.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api), api_keyos.environ[TAOTOKEN_API_KEY] ) def ask_model(prompt, image_b64, user_query): resp client.chat.completions.create( modelqwen-vl-max, messages[ {role: system, content: [{type: text, text: prompt}]}, {role: user, content: [ {type: image_url, image_url: {url: fdata:image/jpeg;base64,{image_b64}}}, {type: text, text: user_query} ]} ], timeout30 ) return resp.choices[0].message.content动作执行模块把 JSON 映射成 ADB 命令这里要处理坐标缩放import json import re def scale_coord(x, y, cfg): sx cfg[device][screen_width] / cfg[device][model_screen_width] sy cfg[device][screen_height] / cfg[device][model_screen_height] return int(x * sx), int(y * sy) def execute_action(action_json, cfg): data json.loads(action_json) name data.get(name) args data.get(arguments, {}) act args.get(action) serial cfg[device][adb_serial] or None base [adb] ([-s, serial] if serial else []) if act click: x, y scale_coord(args[coordinate][0], args[coordinate][1], cfg) subprocess.run(base [shell, input, tap, str(x), str(y)]) elif act swipe: x1, y1 scale_coord(args[coordinate][0], args[coordinate][1], cfg) x2, y2 scale_coord(args[coordinate2][0], args[coordinate2][1], cfg) dur int(args.get(time, 0.5) * 1000) subprocess.run(base [shell, input, swipe, str(x1), str(y1), str(x2), str(y2), str(dur)]) elif act open: subprocess.run(base [shell, am, start, -n, args[text]]) elif act type: subprocess.run(base [shell, input, text, args[text]]) elif act wait: time.sleep(args.get(time, 1)) elif act terminate: return True return False主循环就是截图、调模型、执行、判断终止def run_agent(task, cfg): prompt open(prompt.txt, r, encodingutf-8).read() for step in range(cfg[agent][max_steps]): img_b64 capture_screen(cfg[device][adb_serial]) raw ask_model(prompt, img_b64, task) match re.search(rtool_call(.*?)/tool_call, raw, re.S) if not match: print(模型未返回 tool_call:, raw) break done execute_action(match.group(1), cfg) if done: print(任务完成) break time.sleep(cfg[agent][wait_after_action])prompt 文件直接沿用原方案里那段工具定义把分辨率改成和 config 里model_screen_width一致即可。这样整套配置和代码就齐了接下来跑一次验证。4. 验证请求与一次完整的手机控制任务验证分两步先单独验证模型调用通不通再跑完整任务。单独验证可以用一段最小脚本只发一张截图和一个简单指令看模型返回什么img_b64 capture_screen() raw ask_model(open(prompt.txt).read(), img_b64, 打开设置) print(raw)如果返回类似下面的内容说明模型链路通了tool_call {name: mobile_use, arguments: {action: open, text: com.android.settings, wait: 3}} /tool_call注意open动作的text字段模型可能返回应用包名也可能返回应用名。包名可以直接被am start -n使用应用名则需要你先做一个名称到包名的映射表。我建议在 prompt 里明确要求模型返回包名减少后续处理。完整任务验证我用的是“打开设置进入关于手机查看 Android 版本”这个三步任务。运行命令python agent.py --task 打开设置进入关于手机查看Android版本 --config config.json实际跑下来模型第一步返回open设置第二步截图后返回click坐标点在“关于手机”上第三步返回terminate并报告成功。整个过程大约 12 秒其中模型调用占大头ADB 执行很快。这里有个成功结果判断的技巧不要只看模型说terminate success还要在代码里加一个最终截图保存人工确认画面确实到了目标页面。因为模型有时会误判明明还在设置首页就说完成了。我在主循环结束后加了一行capture_screen()保存为final.png方便回看。如果你要验证滑动类动作可以用“在设置列表里向下滑动找到电池”这个任务。模型会返回swipe坐标从下往上。执行后截图如果画面确实滚动了说明 swipe 映射正确。滑动动作最容易出问题的是 duration 参数太短会变成快速甩动太长会变成慢拖一般 500ms 比较稳。验证通过后你可以把任务换成“打开微信搜索文件传输助手发送一条消息”这种更复杂的链路。但建议先把三步以内的任务跑稳再逐步加长。Mobile Agent 的循环步数越多误差累积越明显max_steps设 15 是保守值复杂任务可以调到 25但要配合更好的错误重试。5. 本篇常见报错与排查对照跑这套代码时我遇到过几类典型报错这里按现象、原因、解决方式列出来你对照着排查。第一类是401 Unauthorized。现象是模型调用直接抛异常提示认证失败。原因通常是 API Key 没设置对或者环境变量名写错。检查echo $TAOTOKEN_API_KEY是否有值再确认代码里读的环境变量名和导出的一致。如果 Key 是从控制台复制的注意不要带多余空格。另外base_url如果写成https://taotoken.net/api/带尾斜杠某些 SDK 版本会拼出双斜杠导致 404建议去掉尾斜杠。第二类是local proxy failed或连接超时。现象是请求发不出去报网络错误。原因可能是本机网络环境对taotoken.net的访问不稳定或者代码里设置了系统代理但代理不可用。检查env | grep -i proxy如果有HTTP_PROXY或HTTPS_PROXY临时 unset 掉再试。另外timeout设 30 秒如果模型响应慢可以调到 60。第三类是reading choices报错提示KeyError: choices或response.choices为空。现象是模型返回了内容但结构不对。原因通常是模型返回的是纯文本而不是预期的 JSON或者返回内容被截断。检查resp的完整结构打印resp.model_dump()看choices是否存在。如果模型返回的是 markdown 代码块包裹的 JSON需要先用正则把json和去掉再解析。第四类是 OAuth 相关报错比如invalid_grant或token expired。这类一般出现在你用 OAuth 方式获取临时凭证的场景。如果你用的是 API Key 方式不应该出现 OAuth 报错。如果出现了检查是不是误用了其他认证方式的配置。TaoToken 的 API Key 方式是静态 Key不需要刷新 token。第五类是坐标偏移现象是点击位置和预期差几十像素。原因就是前面说的分辨率不一致。解决方式是在 config 里正确填写screen_width、screen_height和model_screen_width、model_screen_height让scale_coord做换算。如果换设备只改 config 不用改代码。第六类是adb: device not found。现象是截图或执行动作时报找不到设备。检查adb devices是否列出设备如果是无线调试确认adb connect还有效。有时设备休眠后会断开重新 connect 即可。另外adb_serial如果填了但设备序列号不匹配也会报这个错留空让它自动选第一个设备更省事。第七类是模型返回空内容。现象是raw为空字符串。原因可能是图片 base64 太大被截断或者图片格式前缀和实际格式不符。检查截图转 JPEG 的质量参数85 一般没问题。再确认data:image/jpeg;base64,前缀和img.save(formatJPEG)一致。6. 继续跑通更多任务的接入建议这套通义千问 Mobile Agent 链路跑通之后你可以把它当成一个基础框架往上叠更多能力。比如把单次任务改成任务队列从文件里读多条指令依次执行或者在动作执行后加一个断言用截图对比判断是否真的到了目标页面而不是只信模型的terminate。如果你要长期做编码类或 Agent 类任务可以考虑用 Coding Plan 来管理调用额度避免按量计费时频繁充值。模型对话页面可以用来单独测试通义千问 VL 对某张截图的识别效果不用每次都跑完整循环。接入文档里有更详细的参数说明遇到 SDK 层面的问题可以先查文档。API Key 的管理建议按项目分开建Mobile Agent 用一个其他用途用另一个这样某个 Key 出问题不影响全局。控制台的 API Keys 页面可以随时禁用和重建。最后说一个实用技巧把每次运行的截图和模型返回的 JSON 都存到logs/目录按时间戳命名。这样任务失败时你可以回看每一步模型看到了什么、决策了什么比单纯看终端输出高效得多。我靠这个日志定位过好几次坐标偏移和重复点击的问题。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。