1. 从 Siri AI 系统级 App 操作测试切入:先把 Key 注入链路跑通
最近 Apple 把新一代 Apple Intelligence 的 Siri AI 推进测试版,系统级 App 操作、屏幕感知和个人语境理解成了测试重点。如果你正在写操作能力测试脚本,第一件要解决的不是断言,而是模型调用链的稳定性——我通常直接把 Key 和 Base URL 统一到 TaoToken(官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script),Base URL 固定为 https://taotoken.net/api。这样脚本里的“决策模型”和“截图断言模型”都走同一条链路,出问题也容易定位。
这类测试脚本和普通接口测试不太一样。普通接口测试只要构造请求、比对响应即可;而 Siri AI 的系统级 App 操作测试,需要让模型根据当前屏幕内容决定下一步动作,再由本地驱动去点击、滑动、输入,最后用操作前后的截图做差异断言。常见报错并不是“找不到控件”这么直观,而是脚本刚跑起来就返回401 invalid api key,或者请求发出去后返回404 model not found。前者通常是环境变量没注入,后者多半是 Base URL 或模型名写错。所以本文不先聊业务断言,而是从可复制的 Key 注入、Base URL 配置、Claude Code / Codex / CC Switch 三件套配置讲起,再落到测试脚本和操作前后截图对照。
本文的目标产出很明确:一份能跑的测试脚本骨架、一组 Key 注入参数、一套操作前后截图对照方法,以及排障清单。所有命令都在本地终端执行,不连接生产库,也不要把测试 Key 提交到 Git 仓库。
2. 测试脚本分层设计:用例、决策、执行、截图、断言
写操作能力测试脚本最忌讳把所有逻辑塞进一个文件。模型调用、动作执行、截图采集、差异断言混在一起,一旦401就要全文件找原因,一旦截图空白又要怀疑模型。建议拆成五层:
siri_app_test/ ├── cases/ │ └── siri_app_ops.yaml ├── runner.py ├── llm_client.py ├── driver.py ├── capture.py ├── diff_assert.py ├── shots/ └── reports/cases/放用例,描述指令、目标 App、最大步数、截图路径、断言阈值。llm_client.py只负责调用模型,把当前屏幕描述和指令转成下一步动作 JSON。driver.py负责本地执行动作,比如通过 Appium、UiAutomator、XCUITest 或你自己的自动化框架执行tap、swipe、input。capture.py负责操作前后截图。diff_assert.py负责比较操作前后截图,判断是否真的发生了变化。reports/存每次运行的 JSON 报告。
用例文件可以先用 YAML 描述,重点是让“操作前截图”和“操作后截图”成对出现:
cases: - id: siri_open_settings_wifi instruction: "打开设置并进入 Wi-Fi 页面" app: "Settings" max_steps: 8 before: "shots/siri_open_settings_wifi_before.png" after: "shots/siri_open_settings_wifi_after.png" assert: min_diff_ratio: 0.008 final_text: "Wi-Fi" - id: siri_create_note instruction: "打开备忘录,新建一条内容为 test-siri-ops 的笔记" app: "Notes" max_steps: 10 before: "shots/siri_create_note_before.png" after: "shots/siri_create_note_after.png" assert: min_diff_ratio: 0.010 final_text: "test-siri-ops"模型决策层的输出必须是严格 JSON,不要让它自由发挥。推荐动作白名单:tap、swipe、input、back、home、wait、assert。这样即使模型判断偏了,也不会执行越界动作。下一步动作示例:
{ "action": "tap", "target": "Wi-Fi", "reason": "设置首页可见 Wi-Fi 入口,点击后进入 Wi-Fi 页面", "expect": "页面标题变为 Wi-Fi" }这一层跑通之后,再接入模型 Key。不要在代码里硬编码 Key,统一走环境变量。
3. 在 TaoToken 拿 Key:Base URL、环境变量与可复制注入参数
创建 Key 的入口在 TaoToken 控制台。你可以先从官网进入:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script,登录后在 API Keys 页面创建新 Key。为了方便直接跳转,也可以使用这个地址:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script。创建时建议按用途命名,比如siri-app-ops-test,方便后续轮换。
拿到 Key 后,不要写进 Python 文件。用环境变量注入,脚本只读环境变量。下面这组参数可以直接复制到本地终端:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="YOUR_MODEL_ID"如果你使用.env文件,也要确保它被.gitignore排除。测试脚本启动时先检查变量是否存在,缺失就直接报错,避免跑到一半才出现401:
# config_check.py import os REQUIRED = ["TAOTOKEN_API_KEY", "TAOTOKEN_BASE_URL", "TAOTOKEN_MODEL"] missing = [name for name in REQUIRED if not os.environ.get(name)] if missing: raise SystemExit(f"缺少环境变量: {', '.join(missing)},请先注入 Key 和 Base URL")接着写模型客户端。Base URL 使用https://taotoken.net/api,Key 使用YOUR_API_KEY占位符替换后的真实值。下面的llm_client.py负责把屏幕描述和测试指令转成动作 JSON:
# llm_client.py import os import json import time from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) SYSTEM_PROMPT = """你是移动端 App 操作测试的决策器。 只看当前屏幕描述和用户指令,输出下一步动作 JSON。 可用动作:tap、swipe、input、back、home、wait、assert。 输出格式: {"action":"tap","target":"设置","reason":"...","expect":"..."} 不要输出多余文字。""" def plan_action(instruction, screen_desc, history): user_payload = { "instruction": instruction, "screen": screen_desc, "history": history, } last_error = None for _ in range(3): resp = client.chat.completions.create( model=os.environ["TAOTOKEN_MODEL"], messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)}, ], temperature=0, ) text = resp.choices[0].message.content.strip() try: return json.loads(text) except json.JSONDecodeError as exc: last_error = exc time.sleep(1.5) raise RuntimeError(f"模型未返回合法 JSON: {last_error}")这里有一个容易踩的坑:Base URL 不要凭感觉补/v1。本文统一使用产品配置里给出的https://taotoken.net/api。如果你把它写成https://taotoken.net/api/v1,而服务端路由并不匹配,就会得到404。先按https://taotoken.net/api跑通,再根据控制台文档调整。
4. Claude Code、Codex、CC Switch 三件套:把模型调用统一到 TaoToken
测试脚本本身用环境变量就够了,但很多开发者会在 Claude Code、Codex 或 CC Switch 里预演提示词、生成动作规划、检查 JSON 格式。这里要把配置分开写,尤其注意:ANTHROPIC_*只用于 Claude Code,不要套到 Codex;Codex 用config.toml。
4.1 Claude Code:settings.json 与 ANTHROPIC_*
Claude Code 的配置放在~/.claude/settings.json。把 Base URL 指向 TaoToken,Key 用你的YOUR_API_KEY替换:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID", "ANTHROPIC_SMALL_FAST_MODEL": "YOUR_SMALL_MODEL_ID" } }如果你的 Claude Code 版本支持项目级配置,也可以放在项目目录的.claude/settings.json,但不要把真实 Key 提交到仓库。更推荐本机全局配置加环境变量。配置完成后重启终端或 Claude Code,让它重新读取。
4.2 Codex:config.toml 与 model_providers
Codex 使用~/.codex/config.toml。不要写ANTHROPIC_*,而是通过model_providers指定供应商:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"这里的env_key = "TAOTOKEN_API_KEY"表示 Codex 会从环境变量读取 Key。所以运行 Codex 之前,确保先执行:
export TAOTOKEN_API_KEY="YOUR_API_KEY"如果你在 Codex 里看到401,先检查这个环境变量是否在当前 shell 生效。可以用printenv TAOTOKEN_API_KEY确认。
4.3 CC Switch 三件套:Base URL、API Key、Model
CC Switch 用来切换不同配置时,核心就是三件套:Base URL、API Key、Model。新增供应商时填:
名称:TaoToken Base URL:https://taotoken.net/api API Key:YOUR_API_KEY Model:YOUR_MODEL_ID保存后切换到这个供应商,再启动对应的 CLI 或编辑器。如果你同时维护多个环境,建议把三件套写成备注,例如“TaoToken / siri-app-ops-test / 测试专用 Key”,避免和日常配置混在一起。切换后先用一句简单提示验证连通性,不要直接跑完整测试用例。
5. 操作能力测试脚本核心:动作白名单、前后截图对照与差异断言
现在回到测试脚本。操作能力测试的关键不是让模型“描述页面”,而是让它输出可执行动作,并且每个动作执行前后都有截图。下面是一个简化 runner:
# runner.py import os import json import time import subprocess from llm_client import plan_action from diff_assert import assert_changed CASE = { "id": "siri_open_settings_wifi", "instruction": "打开设置并进入 Wi-Fi 页面", "app": "Settings", "max_steps": 8, "before": "shots/siri_open_settings_wifi_before.png", "after": "shots/siri_open_settings_wifi_after.png", "min_diff_ratio": 0.008, } def screenshot(path): os.makedirs(os.path.dirname(path), exist_ok=True) # 本地执行:Android 示例 with open(path, "wb") as f: subprocess.run(["adb", "exec-out", "screencap", "-p"], stdout=f, check=True) def run_case(case): history = [] screenshot(case["before"]) for step in range(case["max_steps"]): # 这里的 screen_desc 可以来自 OCR、无障碍树或视觉模型 screen_desc = "当前屏幕:设置首页,可见 Wi-Fi、蓝牙、蜂窝网络等入口" action = plan_action(case["instruction"], screen_desc, history) history.append(action) # 把 action 交给本地驱动执行 # 不要连接生产库,不要执行未白名单动作 if action["action"] not in {"tap", "swipe", "input", "back", "home", "wait", "assert"}: raise RuntimeError(f"越界动作: {action['action']}") # driver.execute(action) time.sleep(1.0) if action["action"] == "assert": break screenshot(case["after"]) ratio = assert_changed( case["before"], case["after"], min_ratio=case["min_diff_ratio"], ) report = { "case": case["id"], "steps": len(history), "history": history, "diff_ratio": ratio, "before": case["before"], "after": case["after"], } os.makedirs("reports", exist_ok=True) with open(f"reports/{case['id']}.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) return report if __name__ == "__main__": result = run_case(CASE) print(json.dumps(result, ensure_ascii=False, indent=2))差异断言不要只看“是否完全相等”,因为截图里可能有时间、动画、光标闪烁。更实用的做法是计算变化像素比例:
# diff_assert.py from PIL import Image, ImageChops import numpy as np def assert_changed(before_path, after_path, min_ratio=0.008): before = Image.open(before_path).convert("RGB") after = Image.open(after_path).convert("RGB") if before.size != after.size: raise AssertionError(f"截图尺寸不一致: {before.size} vs {after.size}") diff = ImageChops.difference(before, after) arr = np.asarray(diff).astype("int16") changed_mask = arr.sum(axis=2) > 30 ratio = float(changed_mask.mean()) if ratio < min_ratio: raise AssertionError(f"操作前后变化过小: {ratio:.4f} < {min_ratio}") return ratio跑完后你可以得到一张操作前后截图对照表:
| 用例 ID | 指令 | 步骤数 | before 截图 | after 截图 | diff 比例 | 判定 |
|---|---|---|---|---|---|---|
| siri_open_settings_wifi | 打开设置并进入 Wi-Fi 页面 | 3 | shots/siri_open_settings_wifi_before.png | shots/siri_open_settings_wifi_after.png | 0.0213 | 通过 |
| siri_create_note | 新建 test-siri-ops 笔记 | 5 | shots/siri_create_note_before.png | shots/siri_create_note_after.png | 0.0157 | 通过 |
| siri_toggle_bluetooth | 打开蓝牙开关 | 2 | shots/siri_toggle_bluetooth_before.png | shots/siri_toggle_bluetooth_after.png | 0.0041 | 失败,变化过小 |
失败用例不要急着改断言阈值。先看before和after截图:如果两张图几乎一样,可能是动作没执行、控件没点中、或者页面还没加载完。可以把wait动作加入白名单,并在动作后固定等待 1 到 2 秒。若截图全黑,多半是设备锁屏或没有前台权限。
还有一点:模型输出动作时,最好把target限制在当前屏幕描述里出现过的文本。否则模型可能编出“点击右上角头像”这类当前页面不存在的目标。你可以在plan_action后加一层本地校验:
def validate_action(action, screen_desc): if action["action"] in {"tap", "input"}: target = action.get("target", "") if target and target not in screen_desc: raise ValueError(f"目标不在当前屏幕描述中: {target}") return action这一层校验能显著降低误点概率,也能减少截图对照失败后的排查成本。
6. 排障清单:401、404、模型名不匹配、截图为空、动作越界
接入 TaoToken 跑测试脚本时,下面这些报错最常出现。建议先按表格排查,再改代码。
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
401 invalid api key | 环境变量是否生效 | printenv TAOTOKEN_API_KEY确认;重新export;Claude Code / Codex 分别检查配置 |
404 model not found | Base URL 与模型名 | Base URL 先用https://taotoken.net/api;模型名以控制台列表为准,不要凭记忆写 |
429 too many requests | 并发与重试 | 脚本降低并发;给模型调用加指数退避;不要在同一秒发大量用例 |
| 模型返回非 JSON | 提示词与解析 | 强化“只输出 JSON”;失败重试 3 次;必要时加response_format |
| 截图全黑或空白 | 设备权限与前台状态 | 解锁设备;保持目标 App 在前台;检查截屏权限 |
| 前后截图几乎一样 | 动作是否执行 | 检查 driver 是否真的执行了tap;加入wait;确认控件文本匹配 |
| 动作越界 | 白名单与本地校验 | 只允许tap/swipe/input/back/home/wait/assert;校验 target 是否在当前屏幕描述中 |
| Claude Code 不生效 | ANTHROPIC_*配置 | 确认写在~/.claude/settings.json的env内;重启终端 |
| Codex 不生效 | config.toml与env_key | 确认model_provider指向taotoken;确认TAOTOKEN_API_KEY已 export |
这里再强调一次配置隔离:Claude Code 使用ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL;Codex 使用config.toml里的base_url、env_key、wire_api。不要把ANTHROPIC_*写进 Codex 配置,也不要把 Codex 的model_providers段落到 Claude Code 的settings.json里。混用会导致“配置看起来对,但请求就是不通”的问题。
另外,测试脚本里的 Key 只用于本地测试环境。不要把 Key 写进用例 YAML、不要提交到仓库、不要打印到日志。报告里如果需要记录模型名,只记录模型 ID 和 Base URL,不要记录 Key。
7. 把回归跑起来:文末 CTA 与下一步
到这里,测试脚本的主链路已经完整:用例描述指令,llm_client调模型输出动作 JSON,driver本地执行,capture保存操作前后截图,diff_assert计算差异并给出判定。你可以在本地先跑一条最简单用例,比如“打开设置并进入 Wi-Fi 页面”,确认before.png和after.png能正常生成,并且 diff 比例大于阈值。然后再扩展蓝牙开关、新建备忘录、返回桌面等用例。
如果你还没有 Key,可以从官网进入控制台创建:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script。创建后把YOUR_API_KEY替换掉,Base URL 保持https://taotoken.net/api,再按本文的settings.json、config.toml和 CC Switch 三件套分别配置。跑通模型调用后,再回到测试脚本调整动作白名单和截图阈值。
高转化路径建议按下面顺序走:
- 模型对话:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script
- 创建 Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script
- Claude Code 文档:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=siri_app_test_script
先把 Key 注入链路跑通,再把操作前后截图对照加进回归。这样即使 Siri AI 的系统级 App 操作能力继续扩展,你的测试脚本也只需要增加用例和动作校验,而不需要重写模型调用层。