
开工前先聊点实际的。最近很多测试团队都在折腾 AI Agent但大多数人只是拿它聊聊天、写写用例真正把 Agent 变成“能干活”的工作搭子的人并不多。问题出在哪出在 Agent 没有接上你的业务、没有接上你的设备、没有一套可以被稳定调用的“技能”上。这篇文章不聊虚的直接围绕 Skill 体系讲清楚怎么把一个 APP 测试流程拆解成 Agent 能理解、能执行、能返回结构化结果的完整链路。读完你可以照着搭出一只能帮你查包名、拉日志、跑冒烟用例、甚至汇报测试结果的 Agent 搭子。1. 背景与核心概念1.1 什么是 Agent Skill为什么测试场景需要它AI Agent 本身是一个“会思考但不会动手”的大脑。它能理解你的问题能拆解任务但如果不能调用外部工具、不能执行脚本、不能读取设备状态那它就只能停留在“建议者”的角色而不是“执行者”。Skill 就是给 Agent 装上“手”的那一层能力扩展。Skill 的本质是一组可以被大模型识别并调用的功能接口通常包含三个要素一段给大模型看的自然语言描述说明这个技能什么时候该用一组定义好的入参和出参结构一段真正干活的代码或 HTTP 接口比如启动 App、查询进程、读取崩溃日志。在 APP 测试场景里Agent 的价值是帮你把重复的、机械的、需要跨工具操作的步骤自动化掉。比如领导让你“看一下这个版本启动有没有崩溃”你需要手动连设备、查日志、看进程。但如果你给 Agent 配了一个check_app_health的 Skill它自己就能完成命令拼装、设备连接、日志抓取和结果判定。1.2 Skill 与普通脚本、自动化用例有什么区别很多同学会问我直接写 Python 脚本不也能做这些事吗为什么要绕一圈经过 Agent这里的区别在于“意图理解”。普通脚本是写死的输入是什么、执行什么流程全部由人决定。而 Skill 是挂在 Agent 上的Agent 可以根据你的自然语言指令动态决定调用哪个 Skill、传入什么参数、如何组合多个 Skill 完成一个复杂任务。举个例子。你写了一个adb logcat的封装脚本它只能做一件事拉日志。但如果你把“拉日志”和“检查崩溃关键字”和“生成测试报告”三个 Skill 组合起来交给 Agent你说一句“帮我看看这个版本有没有崩溃”Agent 会自动决定先拉日志、再匹配关键字、最后整理成报告。这就是 Skill 组合带来的质变。1.3 常见应用场景APP 测试中的 Skill 设计思路基于实际项目经验以下几类测试任务非常适合做 Skill场景Skill 示例预期输出启动检查check_app_startup启动耗时、是否闪退、首帧渲染结果日志分析fetch_crash_log崩溃堆栈、关键异常、发生时间性能采集collect_performanceCPU、内存、FPS、流量数据用例执行run_smoke_test用例通过率、失败详情设备管理list_devices在线设备列表、系统版本、分辨率把这些能力做成 Skill 之后测试人员只需要像聊天一样给 Agent 下指令剩下的调度和工具调用交给 Agent 完成。2. 环境准备与版本说明2.1 做一个 Agent Skill 需要哪些基础环境这里给出一份可以直接照着准备的清单版本请根据你的实际项目调整重点演示配置思路操作系统Windows 10/11、macOS 或 Linux 均可Python 3.9并安装requests、flask如果 Skill 走 HTTP 接口方式Android SDK Platform-Tools确保adb命令可用一台开启 USB 调试的 Android 设备或模拟器Agent 平台账号国内常用的有扣子Coze、Dify也可以使用 OpenAI 的 Function Calling 方案自己搭建接口调试工具Postman 或 Apifox用于单独验证 Skill 后端接口IDEVS Code 或 PyCharm 均可非必须。如果你只想在本地验证流程不需要真实手机也可以用模拟器完成。推荐先跑通最小闭环再接入真实设备。2.2 示例项目结构本文的实战案例会创建一个名称为app-test-skill的目录结构如下app-test-skill/ ├── skills/ │ ├── check_app_health.py # 健康检查 Skill 核心逻辑 │ ├── fetch_crash_log.py # 崩溃日志抓取 Skill │ └── run_smoke_test.py # 冒烟测试执行 Skill ├── server/ │ └── app.py # Skill HTTP 服务入口 ├── schema/ │ └── skill_openapi.json # 供 Agent 平台识别的接口描述 ├── config/ │ └── config.yaml # 设备、包名等配置 └── requirements.txt后面会逐步讲解每个文件的职责和关键实现。2.3 验证 adb 环境是否正常Skill 底层要调 adb所以第一步是确认 adb 可用。打开命令行执行adb version adb devices如果adb devices能看到设备列表说明环境正常。如果提示command not found需要先配置 Android SDK 环境变量或者在命令中写 adb 的完整路径。下面给一个通用的检查脚本放在config/同级目录下用于启动前的环境自检import subprocess def check_adb(): result subprocess.run([adb, version], capture_outputTrue, textTrue) if result.returncode ! 0: raise RuntimeError(adb 不可用请先配置 Android SDK Platform-Tools) device_list subprocess.run([adb, devices], capture_outputTrue, textTrue) print(device_list.stdout) if __name__ __main__: check_adb()这段代码的作用很直接用subprocess调用 adb并捕获返回结果。如果 adb 找不到会直接抛出异常方便你在接入 Agent 之前发现问题。3. 核心语法、配置或原理拆解3.1 Skill 的运行机制Agent 如何“看懂”你的技能要让 Agent 正确调用你的 Skill关键不在于代码写得多么复杂而在于接口描述是否清晰。Agent 平台通常通过 Schema 文件来理解技能name技能名称必须是英文Agent 靠它定位description一段详细的自然语言描述说明这个 Skill 的用途、适用场景、以及什么时候不应该调用parameters入参定义包括类型、是否必填、含义responses返回结构定义告诉 Agent 怎么解读结果。在 Coze 这类平台里你可以在“技能”或“插件”模块中上传 OpenAPI Schema也可以直接在页面里手动填写。在自建方案中则通过 Function Calling 把同样的描述传给大模型。3.2 入参和出参的设计原则宁愿啰嗦不要含糊很多人在定义 Skill 参数时容易犯一个错误把参数写得过于简单。比如只写一个package_name不说明格式。但 Agent 并不知道包名是什么格式它只会根据描述去猜。如果猜错整个调用链路就断了。建议参数描述写成这样{ name: package_name, type: string, description: 目标 App 的包名例如 com.example.demo可以通过 adb shell pm list packages 查询, required: true }返回值也一样。如果你返回的是一段纯文本Agent 虽然能看懂但无法做后续的二次加工。更推荐返回结构化 JSON把判断结果、数据明细、执行状态分开。{ status: success, app_name: demo, launch_time_ms: 1250, crash_count: 0, message: 启动正常无崩溃 }这样的返回结构Agent 可以直接引用其中的字段来生成报告。3.3 一个最小的 Skill 后端接口示例先用 Flask 写一个最小可用的 Skill 接口。这个接口接收包名返回 App 是否在运行、启动耗时等信息。先不追求完整只是把链路打通。# 文件路径server/app.py from flask import Flask, request, jsonify import subprocess app Flask(__name__) def get_app_status(package_name): 检查 App 当前是否在前台运行 cmd fadb shell dumpsys activity activities | grep -i {package_name} result subprocess.run(cmd, shellTrue, capture_outputTrue, textTrue) return run if package_name in result.stdout else stopped app.route(/skill/check_app_health, methods[POST]) def check_app_health(): data request.get_json() package_name data.get(package_name) if not package_name: return jsonify({status: failed, message: 缺少 package_name 参数}), 400 app_status get_app_status(package_name) return jsonify({ status: success, package_name: package_name, app_status: app_status, message: 查询完成 }) if __name__ __main__: app.run(host0.0.0.0, port8000)启动服务python server/app.py另开终端测试curl -X POST http://127.0.0.1:8000/skill/check_app_health \ -H Content-Type: application/json \ -d {package_name: com.example.demo}预期输出{ status: success, package_name: com.example.demo, app_status: stopped, message: 查询完成 }如果你在浏览器或 Postman 里能看到这段 JSON说明你的 Skill 后端接口已经跑通了。下面要做的是让 Agent 能识别并且愿意调用它。3.4 为什么大模型有时不调用你的 Skill这里有一个非常容易踩的坑你明明已经写了 Skill但 Agent 就是不调用。遇到这种情况先检查下面三件事第一你的description是否足够明确。如果描述里只写“查询应用状态”Agent 可能会忽略它。应改成“当用户询问某个 App 是否正在运行或启动是否正常时调用此技能。输入参数为 Android 应用包名。该技能通过 adb 获取设备状态并返回结果。”第二入参是否足够具体。如果 Agent 不知道从用户的哪句话里提取package_name它就只能放弃调用。建议在参数描述里给出示例。第三Schema 是否正确。很多 Agent 平台对入参类型有严格要求比如integer还是string写错了会直接导致无法解析。4. 完整实战案例调教一个 APP 冒烟测试 Agent 搭子下面进入正题我们完整实现一个“APP 冒烟测试 Agent 搭子”。它能接收用户的自然语言指令自动完成以下流程获取在线设备启动目标 App检查是否崩溃抓取启动日志返回一份简单的测试结论。4.1 第一步配置文件先把公共配置抽出来。包名、设备序列号、日志路径等不要硬编码在代码里。# 文件路径config/config.yaml device: serial: # 留空则使用 adb 自动获取第一台设备 app: package_name: com.example.demo main_activity: .MainActivity launch_retry_times: 2 log: logcat_output: logs/crash_{timestamp}.log keywords: - FATAL EXCEPTION - ANR in - Process: com.example.demo这里要注意main_activity的写法。在 adb 启动命令中通常使用包名/Activity全路径格式而 Activity 可以通过aapt dump badging或者从开发那询问获得。4.2 第二步实现设备管理 Skill设备管理是所有 APP 测试 Skill 的基础先实现它的核心方法。# 文件路径skills/device_skill.py import subprocess class DeviceManager: staticmethod def list_devices(): 返回当前连接的设备列表 result subprocess.run( [adb, devices], capture_outputTrue, textTrue ) lines result.stdout.strip().split(\n)[1:] devices [] for line in lines: if line.strip() and device in line: serial line.split(\t)[0] devices.append(serial) return devices staticmethod def get_first_device(): 获取第一台可用设备 devices DeviceManager.list_devices() if not devices: raise RuntimeError(没有检测到可用设备请检查 USB 调试连接) return devices[0]这个 Skill 本身不复杂但它是其他 Skill 的依赖基础。实际项目中设备可能时连时断所以建议增加重试机制。4.3 第三步实现 App 启动与崩溃检查 Skill这是核心部分。它要完成三件事启动 App、等待几秒、检查进程是否存在。# 文件路径skills/app_launch_skill.py import subprocess import time from skills.device_skill import DeviceManager class AppLaunchSkill: def __init__(self, package_name, main_activity, serialNone): self.package_name package_name self.main_activity main_activity self.serial serial or DeviceManager.get_first_device() self.base_cmd [adb, -s, self.serial] def launch_app(self, retry_times2): launch_target f{self.package_name}/{self.main_activity} cmd self.base_cmd [shell, am, start, -n, launch_target] for i in range(retry_times): result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode 0: return result.stdout raise RuntimeError(fApp 启动失败目标: {launch_target}) def wait_for_launch(self, wait_seconds3): time.sleep(wait_seconds) def is_process_alive(self): cmd self.base_cmd [shell, pidof, self.package_name] result subprocess.run(cmd, capture_outputTrue, textTrue) return result.stdout.strip() ! def check_crash(self): 启动 App 后检查是否发生崩溃 self.launch_app() self.wait_for_launch() if not self.is_process_alive(): return { status: failed, message: App 启动后进程不存在疑似崩溃 } return { status: success, message: App 启动正常进程存活 }pidof是一个很实用的命令用来直接获取进程 ID。如果进程不在说明 App 可能闪退了。当然这只是最基础的判断方式真实项目里还要看日志。4.4 第四步实现日志抓取 Skill日志抓取要解决两个问题什么时候开始抓、抓多久。# 文件路径skills/log_skill.py import subprocess import time from datetime import datetime from skills.device_skill import DeviceManager class LogSkill: def __init__(self, serialNone): self.serial serial or DeviceManager.get_first_device() self.base_cmd [adb, -s, self.serial] def clear_logcat(self): subprocess.run(self.base_cmd [logcat, -c], capture_outputTrue) def capture_log(self, duration10, keywordsNone): 抓取指定时长的日志并返回包含关键字的行 self.clear_logcat() cmd self.base_cmd [logcat, -v, time] proc subprocess.Popen(cmd, stdoutsubprocess.PIPE, textTrue) time.sleep(duration) proc.terminate() lines proc.stdout.read().splitlines() keywords keywords or [FATAL EXCEPTION, ANR in] matched_lines [] for line in lines: for keyword in keywords: if keyword in line: matched_lines.append(line) break return { capture_time: datetime.now().isoformat(), total_lines: len(lines), matched_lines: matched_lines, crash_count: len(matched_lines) }这段代码的原理是先清空旧日志再启动 logcat 抓取一段时间最后过滤出崩溃关键字。用Popen而不是run是因为 logcat 是一个持续输出的命令我们只抓固定时长。4.5 第五步把多个 Skill 组装成 Agent 能调用的 HTTP 服务单有 Skill 类还不够Agent 平台无法直接调用 Python 类。所以需要一层 HTTP 封装把 Skill 的能力暴露成 Agent 平台能识别的接口。下面写一个完整的 Flask 服务把启动检查、日志抓取、报告生成整合在一个接口里。# 文件路径server/app.py from flask import Flask, request, jsonify from skills.app_launch_skill import AppLaunchSkill from skills.log_skill import LogSkill from skills.device_skill import DeviceManager import yaml app Flask(__name__) with open(config/config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) APP_CONFIG config[app] LOG_CONFIG config[log] def generate_report(launch_result, log_result): 生成简单的冒烟测试报告 crash_count log_result.get(crash_count, 0) if launch_result.get(status) failed or crash_count 0: conclusion FAIL else: conclusion PASS return { conclusion: conclusion, launch_check: launch_result, log_summary: { capture_time: log_result.get(capture_time), total_lines: log_result.get(total_lines), crash_keyword_lines: log_result.get(matched_lines, [])[:5] } } app.route(/skill/smoke_test, methods[POST]) def smoke_test(): data request.get_json() package_name data.get(package_name) or APP_CONFIG[package_name] main_activity data.get(main_activity) or APP_CONFIG[main_activity] device_serial None if data.get(device_serial): device_serial data[device_serial] else: devices DeviceManager.list_devices() if devices: device_serial devices[0] if not device_serial: return jsonify({ status: failed, message: 未找到可用的 Android 设备 }), 400 try: launch_skill AppLaunchSkill(package_name, main_activity, serialdevice_serial) launch_result launch_skill.check_crash() log_skill LogSkill(serialdevice_serial) log_result log_skill.capture_log(duration8, keywordsLOG_CONFIG[keywords]) report generate_report(launch_result, log_result) return jsonify({status: success, data: report}) except Exception as e: return jsonify({status: failed, message: str(e)}), 500 app.route(/skill/check_app_health, methods[POST]) def check_app_health(): data request.get_json() package_name data.get(package_name) if not package_name: return jsonify({status: failed, message: 缺少 package_name 参数}), 400 launch_skill AppLaunchSkill(package_name, APP_CONFIG[main_activity]) is_alive launch_skill.is_process_alive() return jsonify({ status: success, package_name: package_name, is_running: is_alive }) if __name__ __main__: app.run(host0.0.0.0, port8000)启动服务python server/app.py用一个测试请求验证curl -X POST http://127.0.0.1:8000/skill/smoke_test \ -H Content-Type: application/json \ -d {package_name: com.example.demo, main_activity: .MainActivity}预期会返回类似下面的结果省去具体日志内容{ status: success, data: { conclusion: PASS, launch_check: { status: success, message: App 启动正常进程存活 }, log_summary: { capture_time: 2025-01-01T12:00:00, total_lines: 1500, crash_keyword_lines: [] } } }如果conclusion是FAIL说明进程死了或者日志中有崩溃关键字可以作为排查起点。4.6 第六步在 Agent 平台配置 Skill以 Coze 为例在“技能”或“插件”模块中新建一个技能选择“使用 API”方式然后填写接口地址和 Schema。如果以 OpenAI Function Calling 方案为例你需要构造一个 functions 描述并调用模型接口import openai functions [ { name: run_smoke_test, description: 对指定的 Android App 执行冒烟测试包括启动检查、崩溃日志抓取和结论判定。适用于用户询问应用是否正常启动、是否存在崩溃、冒烟测试是否通过等场景。, parameters: { type: object, properties: { package_name: { type: string, description: Android 应用包名例如 com.example.demo }, main_activity: { type: string, description: 主 Activity 路径例如 .MainActivity } }, required: [package_name] } } ] response openai.chat.completions.create( modelgpt-4o-mini, messages[ {role: user, content: 帮我对 com.example.demo 跑一下冒烟测试} ], functionsfunctions, function_callauto ) print(response.choices[0].message)需要注意现在很多平台已经调整了 API 参数名比如有的用tools替代functions有的用tool_choice替代function_call。请以你实际使用的 SDK 和模型版本为准这里的写法用于示意核心逻辑。4.7 第七步让 Agent 学会组合多个 Skill单接口方案虽然简单但不够“Agent”。更接近真实工作搭子的方案是把启动检查、日志抓取、报告生成拆成三个独立 Skill让 Agent 自己决定调用顺序。这里要特别重视返回值的结构化程度。如果每一个 Skill 都返回干净的 JSONAgent 就能做“工具之间传参”比如先拿到package_name再传给日志抓取 Skill。如果返回的是乱糟糟的一堆文本Agent 就失去了二次处理的能力。实际调教时你可以在测试对话里一步步引导。先问“设备在线吗”Agent 调用list_devices再问“启动 com.example.demo”Agent 调用launch_app最后问“看看有没有崩溃”Agent 调用fetch_crash_log。这个过程本质上是在帮 Agent 建立工具调用的思维链。5. 常见问题与排查思路问题现象常见原因解决思路Agent 不调用 Skill插件描述太模糊Agent 无法判断使用时机重写 description明确触发条件、输入格式和返回结果一直提示缺少参数入参描述不清楚Agent 无法从用户话术中提取在参数描述中补充示例格式如com.example.demoadb 报 device offlineUSB 调试不稳定或驱动问题重新插拔设备执行adb kill-server后重试Skill 接口请求超时logcat 抓取时间过长或后端没有设置超时缩短抓取时长在 Flask 侧增加回调超时提示返回值无法被 Agent 理解返回结果是非结构化文本改为 JSON 结构字段名用英文加message字段设备列表为空未开启开发者模式或未授权 USB 调试在手机上确认“允许 USB 调试”弹窗重新授权5.1 为什么 Agent 能调用 Skill但结果处理得很差这是 Skill 落地时最常见的高频问题。明明接口返回了 JSONAgent 却像没看到一样自顾自地编结果。出现这种情况大概率是返回结构里缺少“给 Agent 看的总结字段”。比如你返回了一堆崩溃日志原文Agent 没有精力逐行分析。建议在返回结果时除了原始数据外额外增加一个message或summary字段用一句完整的话描述关键结论。{ status: success, crash_count: 1, summary: 检测到 1 条 FATAL EXCEPTION 日志发生在 MainActivity建议优先排查空指针异常 }这个summary会直接成为 Agent 回答用户问题的素材。如果你能把这层做好Agent 的回答质量会有非常明显的提升。5.2 排查 Skill 调用问题的三步定位法如果 Skill 调用出了问题不要直接改代码先按下面的顺序排查第一步绕过 Agent 测接口。用 Postman 或 curl 直接调 HTTP 接口确认输入输出是否符合预期。如果接口本身有问题Agent 怎么调都是白搭。第二步看 Agent 的平台日志。大部分 Agent 平台都会记录工具调用日志包含模型选择调用了哪个 Skill、传了什么参数、收到什么结果。从这里能看到 Agent 的“思考过程”。第三步检查描述和 Schema。通过平台日志发现 Agent 传参不对时优先优化参数描述而不是改后端代码。6. 最佳实践与工程建议6.1 Skill 设计规范按动作拆分不按场景拆分在设计 Skill 时很多人喜欢把一个完整流程封装成一个 Skill比如“跑冒烟测试”。这种做法短期看很爽但复用性极差。下次你想让 Agent “只抓日志”、“只检查进程”就不得不面对一个过重的 Skill。建议按原子动作拆分启动 App、查询进程、抓日志、查设备、解析日志。每个 Skill 只做一件事然后让 Agent 或你自己在更高层级组合它们。这样做的好处很明显单个 Skill 逻辑简单测试容易Agent 的工具组合能力能得到发挥代码复用率更高。6.2 配置管理包名和设备序列号不要硬编码所有跟环境相关的信息包括包名、Activity、设备序列号、测试账号、超时时间都应该放到配置文件里。否则换个项目、换台设备你就得翻代码。推荐用 YAML 或环境变量来管理。连接多个环境时可以考虑按dev、test、prod拆分配置文件。# 按环境拆分示例 app: dev: package_name: com.example.demo.dev test: package_name: com.example.demo.test6.3 日志与可观测性Skill 执行过程要能回看Skill 一旦挂到 Agent 上执行过程就变成了“黑盒”。用户发一句话Agent 调了三次工具最终返回一个结果。如果结果不对你怎么知道是哪一步出了问题所以每一个 Skill 内部都要记录自己的执行日志至少包含调用时间、入参、出参、耗时、异常信息。日志输出格式建议统一为 JSON方便后续采集和分析。import json import logging from datetime import datetime logging.basicConfig(levellogging.INFO) logger logging.getLogger(skill) def log_skill_call(skill_name, params, result, cost_ms): logger.info(json.dumps({ skill: skill_name, params: params, result: result, cost_ms: cost_ms, timestamp: datetime.now().isoformat() }, ensure_asciiFalse))这个习惯在初期看起来有点浪费时间但一旦 Skill 数量超过十个没有日志几乎寸步难行。6.4 安全边界Skill 不能变成危险操作入口Skill 给了 Agent 调用命令的能力但也意味着用户可以通过自然语言诱导 Agent 执行危险操作。这里要特别强调几个安全规范涉及卸载 App、清空数据、修改系统设置的操作不要做成 Skill或必须增加人工确认环节后端接口要做请求鉴权不能裸奔在公网至少加一个简单的 Token 校验禁止把 shell 命令直接暴露给 Agent 自由拼接所有命令必须经过白名单封装涉及生产环境数据擦除、权限变更、账号操作的测试技能先在测试环境验证并做好备份。一个简单但有效的鉴权方式是在 Flask 里做装饰器校验。from functools import wraps from flask import request def require_token(func): wraps(func) def wrapper(*args, **kwargs): token request.headers.get(X-Skill-Token) if token ! your-secret-token: return {status: failed, message: 鉴权失败}, 401 return func(*args, **kwargs) return wrapper app.route(/skill/smoke_test, methods[POST]) require_token def smoke_test(): ...6.5 性能考量不要让 Skill 拖慢 Agent 响应Agent 与 Skill 之间是同步调用Skill 执行时间越长用户等得越久。如果你在做 logcat 抓取这类耗时操作建议设置合理的超时时间并把耗时时长控制在用户可接受范围内。如果某个 Skill 执行时间超过 30 秒建议改成异步模式Skill 提交任务后直接返回任务 IDAgent 告知用户“任务已提交”然后通过轮询或回调获取结果。这在执行完整回归测试时特别重要。7. 总结与学习路线讲到这里一条从零到一的 Skill 落地路径已经完整摆在你面前了。先理解 Skill 的本质再准备好 adb、Python、Agent 平台这些基础环境接着把设备管理、App 启动、日志抓取封装成原子技能最后通过 HTTP 接口接入 Agent 平台。这个流程跑通后你的 Agent 就不再是一个只会聊天的大模型而是一个能自己连设备、查状态、抓日志、出报告的测试搭子。下一步可以往这几个方向深入。第一个方向是扩展 Skill 类型。把性能数据采集、UI 自动化用例执行、接口测试断言也做成 Skill覆盖更多测试类型。第二个方向是完善组合编排。尝试让 Agent 在一次对话中完成多轮工具调用比如先执行冒烟测试再根据失败用例自动抓取对应模块的日志。第三个方向是沉淀评估体系。建立一套 Skill 质量评估标准比如调用成功率、参数正确率、返回结果可读性并定期用测试集去检验 Agent 的行为。如果你现在只记住一件事那就先把“单个 Skill 能跑通、返回结构化 JSON”这个最小闭环做出来。这个闭环一旦成立后面的工具组合、流程编排、多 Skill 协同都只是时间问题。先在模拟器上把check_app_health跑起来再逐步增加复杂度你的 AI Agent 工作搭子就会越来越像样了。