一个 Agent 会写代码,但它能不能像用户一样,亲自把页面点一遍,确认按钮位置正确、文案显示完整、跳转流程符合预期?在很长一段时间里,这个问题的答案都是否定的。AI 编程助手能把需求变成代码,却很难把代码变成“已经验证过可用的功能”。这里的缺口,不是代码生成能力不足,而是验证能力缺失。
pstack 新增的创建与维护验证技能,解决的就是这个缺口。简单说,它把“验证应用”这件事,从人工执行的测试流程,变成了 Agent 可以随时调用的技能:定义好验证场景,配置好验证脚本,Agent 在完成开发或修改后,能像真实用户一样打开应用、输入内容、点击操作、读取页面反馈,并给出明确的通过或失败结论。
这篇文章不打算停留在功能简介上,而会从 Agent 验证能力的定位说起,分析验证技能与普通测试脚本、MCP 工具的区别,然后带你走一遍创建与维护验证技能的完整流程。即使你暂时不使用 pstack,这套思路也能帮你想清楚一个关键问题:Agent 在生产应用代码之后,到底应该如何证明自己的产出可用。
1. 为什么 Agent 需要“像真实用户一样验证”
过去我们评价一个 Agent 的能力,默认标准是“能不能把需求变成代码”。代码生成、单元测试补全、代码审查建议,这些都是 Agent 的典型能力。但真实项目里的问题往往发生在代码之外:登录按钮在移动端被遮挡、某个接口返回成功但页面没有刷新、权限控制在前端被跳过、不同浏览器下布局错位。这些问题,单元测试覆盖不到,静态分析发现不了,只有真正在浏览器里实际操作一遍才能暴露。
这就是“真实用户验证”的价值:不是验证函数的返回值,而是验证用户能感知到的页面行为。一个会写代码的 Agent,如果能继续完成“打开页面 → 输入数据 → 点击操作 → 观察页面反馈 → 判断是否符合预期”这一整套动作,它才算是真正对产出负责。
pstack 新增的验证技能,正是把这一整套动作封装成 Agent 可以理解、调用、组合的能力模块。与传统的自动化测试脚本相比,它有三个明显变化:
- 从“测试人员编写用例”变成“技能创建者定义场景,Agent 自动执行”。
- 从“固定脚本跑固定用例”变成“Agent 根据当前任务上下文选择合适的验证技能”。
- 从“执行完给出日志”变成“执行完给出结构化结论:哪里通过、哪里失败、为什么失败”。
对于正在做 AI 应用开发、Agent 平台建设的团队来说,这个方向值得关注。它意味着 Agent 的闭环能力正在补齐:过去 Agent 只负责“造”,现在开始负责“验”。
2. 验证技能的核心概念与定位
要理解验证技能,先要分清几个容易混淆的概念:Agent、Skill(技能)、MCP 工具、自动化测试脚本。
2.1 Agent 与 Skill 的关系
Agent 是一个具备理解任务、规划步骤、调用工具、反思结果的智能体。Skill 则是 Agent 可以复用的能力单元,相当于把完成某一类目标的流程固化下来。Agent 在需要时加载并执行 Skill,而不是每次从零开始规划。
验证技能就是 Skill 的一个具体类型。它的目标是:验证某个应用功能是否符合预期。它与生成类型 Skill 的区别在于,生成 Skill 的产出是代码、文档或配置,而验证 Skill 的产出是“验证结论”,包括通过状态、失败信息、截图、关键页面数据等。
2.2 验证技能与 MCP 工具的区别
MCP(Model Context Protocol)是 Agent 与外部工具通信的协议。一个 MCP 工具通常对应一次原子操作,比如“读取网页内容”“调用某个 API”“执行一段代码”。它解决的是 Agent 能不能调用外部能力的问题,粒度较细,由 Agent 在规划中临时组合。
验证技能则是一个面向完整目标的流程编排,至少包含:
- 技能元数据:名称、描述、输入参数、输出格式。
- 执行脚本:具体验证步骤。
- 判断逻辑:什么情况算通过,什么情况算失败。
- 异常处理:超时、元素不存在、网络波动时如何处理。
可以这样理解:MCP 工具是 Agent 的“手和眼睛”,验证技能是 Agent 的“操作手册和检查表”。没有 Skill,Agent 也可以临时调用工具去验证,但每次都要重新规划,产出的结果也可能不稳定。有了 Skill,验证过程变得可复用、可维护、可评估。
| 对比维度 | MCP 工具 | 验证技能(Skill) |
|---|---|---|
| 定位 | 原子能力调用 | 面向目标的能力流程 |
| 粒度 | 单次操作 | 多步骤编排 |
| 执行方式 | Agent 临时调用 | 按预定义流程执行 |
| 可复用性 | 工具级复用 | 场景级复用 |
| 典型例子 | 读取 URL 内容 | 登录流程验证技能 |
2.3 验证技能不是“又一套自动化测试框架”
熟悉 Playwright、Selenium、Cypress 的开发者可能会问:验证技能和这些自动化测试框架有什么区别?区别不在底层技术上,而在使用方式上。
自动化测试框架服务的对象是“测试用例”,由测试人员在本地或 CI 中主动触发。验证技能服务的对象是“Agent”,是 Agent 执行任务链路中的一个专业环节。Agent 发现应用有改动,自主决定调用验证技能,根据执行结果决定下一步是继续修改还是完成任务。技能内部可以用 Playwright、Selenium,也可以调用 HTTP 接口做轻量冒烟,关键是它要能被 Agent 理解、加载和结果消费。
因此,验证技能的设计重点,不只是脚本本身,还要考虑元数据与输出结构。Agent 拿到技能执行结果后,要能明白“验证未通过,因为首页未出现预期横幅”,而不是面对一堆原始日志。
3. 验证技能的设计思路与典型执行流程
验证技能看起来是“写一段自动化脚本”,但真正决定它好不好用的,是设计思路。
3.1 从验证场景出发,而不是从脚本出发
创建验证技能的第一步,不是写代码,而是定义验证场景。一个场景要回答几个问题:
- 验证的对象是什么?是某个页面、某个流程,还是某个权限逻辑?
- 用户在真实使用时,会经过哪些步骤?
- 每一步的预期结果是什么?
- 如何判断结果是否成立?
比如“登录验证”场景,真实用户的操作路径是:打开登录页 → 输入用户名和密码 → 点击登录 → 进入首页 → 页面显示当前用户信息。预期的判断条件包括:登录后 URL 是否跳转、首页是否渲染出用户信息、是否出现错误提示。
3.2 验证技能的典型执行流程
一个验证技能在 Agent 中的执行流程通常包括:
- Agent 根据任务上下文,加载匹配的验证技能。
- 解析技能声明的输入参数,并向执行体传递。
- 执行体启动验证环境,执行验证脚本。
- 脚本按照真实用户的操作路径,与应用页面交互。
- 采集关键页面状态、截图、接口数据。
- 根据判断条件生成结构化结果。
- Agent 读取结果,并决定下一步行动。
这个流程里有两个容易被忽略的环节:第 5 步和第 6 步。只有采集到足够的证据,验证结论才可信;只有结构化输出,Agent 才能消费结果。否则验证技能就退化成“跑了一遍脚本但没有结论”的半成品。
4. 创建验证技能:从场景设计到技能注册
下面以一个“登录与首页模块验证”技能为例,走一遍创建流程。整体流程分为四步:定义技能声明、编写验证脚本、注册技能、试运行。
4.1 第一步:定义技能声明
技能声明文件用于描述技能的名称、用途、输入参数和运行方式。以 YAML 为例:
# wallet/skills/login_page_verification/skill.yaml name: login_page_verification description: 验证登录流程和登录后首页的展示是否符合预期 version: 1.0.0 inputs: base_url: type: string required: true description: 被测应用地址 username: type: string required: true description: 测试账号 password: type: string required: true description: 测试密码(由安全配置注入,不写死在技能中) outputs: status: type: string description: pass 或 fail messages: type: array description: 每步验证结果明细 screenshot: type: string description: 验证失败或关键步骤的截图路径 run: command: python run_verification.py --base-url {{base_url}} --username {{username}} --password {{password}} workdir: ./login_verification技能声明文件是 Agent 理解技能的关键。名称和描述要写得足够清晰,便于 Agent 在匹配技能时做出正确选择。输入参数要明确哪些必需、哪些可选。运行命令采用占位符方式,技能调用时由 Agent 或平台注入实际值。
4.2 第二步:编写验证脚本
脚本是技能的执行核心。这里使用 Playwright 的 Python 版作为底层实现,因为它能真实驱动浏览器,符合“像真实用户一样验证”的需求。注意,技术栈不限于 Playwright,关键是对真实页面状态的验证。
# wallet/skills/login_page_verification/run_verification.py import argparse import json from playwright.sync_api import sync_playwright def parse_args(): parser = argparse.ArgumentParser(description="登录与首页模块验证") parser.add_argument("--base-url", required=True, help="被测应用地址") parser.add_argument("--username", required=True, help="测试账号") parser.add_argument("--password", required=True, help="测试密码") return parser.parse_args() def verify_login(base_url, username, password): result = { "status": "fail", "messages": [], "screenshot": "", } with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1440, "height": 900}) try: page.goto(base_url, wait_until="networkidle", timeout=15000) result["messages"].append({"step": "打开应用", "status": "pass"}) page.get_by_role("link", name="登录").click() page.get_by_label("用户名").fill(username) page.get_by_label("密码").fill(password) page.get_by_role("button", name="登录").click() page.wait_for_url("**/dashboard", timeout=10000) result["messages"].append({"step": "登录并跳转首页", "status": "pass"}) welcome_locator = page.locator(".user-welcome") welcome_text = welcome_locator.inner_text(timeout=5000) if username in welcome_text: result["messages"].append({"step": "首页展示用户信息", "status": "pass"}) else: result["messages"].append({ "step": "首页展示用户信息", "status": "fail", "actual": welcome_text, }) page.screenshot(path="verification_result.png", full_page=True) result["screenshot"] = "verification_result.png" result["status"] = "pass" except Exception as e: page.screenshot(path="verification_failed.png", full_page=True) result["screenshot"] = "verification_failed.png" result["messages"].append({"step": "执行异常", "status": "fail", "error": str(e)}) finally: browser.close() return result if __name__ == "__main__": args = parse_args() output = verify_login(args.base_url, args.username, args.password) print(json.dumps(output, ensure_ascii=False, indent=2))这段脚本的关键逻辑在于:每一步都记录独立的验证结果;失败时不直接中断,而是先截图保存现场证据,再统一输出结论;最终以 JSON 格式打印,方便 Agent 解析。
4.3 第三步:注册技能
技能目录和脚本准备好之后,需要在 pstack 平台中进行注册。注册动作的目的是让 Agent 能够发现并加载该技能。注册时需要确认三件事:
- 技能名称是否唯一。
- 输入参数描述是否准确。
- 运行命令是否可以在目标环境中正常执行。
注册完成后,通常还需要做一次试运行。试运行建议使用测试环境,避免直接操作生产环境。如果技能运行过程中出现了环境依赖问题,此时是排查的最佳时机。
4.4 第四步:试运行
试运行可以用脚本本身的命令行来验证,这样可以先排除 Agent 平台侧的干扰:
cd wallet/skills/login_page_verification python run_verification.py \ --base-url http://localhost:8080 \ --username demo \ --password demo123如果脚本能在命令行正常跑通并输出 JSON 结果,再回到 pstack 平台,通过 Agent 触发一次完整调用。
5. 完整示例:技能内部如何加载配置与处理超时
一个真正可维护的验证技能,不应该把所有的选择器、超时时间、重试次数都硬编码在代码里。这里给出一个更工程化的示例:把页面元素选择器抽离到 JSON 配置,并加入超时重试逻辑。
5.1 选择器配置抽离
页面元素会随前端版本迭代而变化。如果把选择器写在脚本里,前端元素一调整,脚本就要改源码。抽离到 JSON 配置文件后,维护技能时可以只改配置,不用改代码。
# wallet/skills/login_page_verification/selectors.json { "login_link": "role=link[name='登录']", "username_input": "aria-label=用户名", "password_input": "aria-label=密码", "login_button": "role=button[name='登录']", "dashboard_url": "**/dashboard", "user_welcome": ".user-welcome" }脚本中加载配置,并使用配置中的选择器来完成操作。
# wallet/skills/login_page_verification/run_verification_with_config.py import argparse import json import time from pathlib import Path from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError BASE_DIR = Path(__file__).resolve().parent def load_config(filename): with open(BASE_DIR / filename, "r", encoding="utf-8") as f: return json.load(f) def parse_args(): parser = argparse.ArgumentParser(description="登录与首页模块验证(配置化版本)") parser.add_argument("--base-url", required=True) parser.add_argument("--username", required=True) parser.add_argument("--password", required=True) parser.add_argument("--config", default="selectors.json") return parser.parse_args() def retry_on_timeout(func, retries=2, interval=2): last_exc = None for i in range(1 + retries): try: return func() except PlaywrightTimeoutError as e: last_exc = e if i < retries: time.sleep(interval) raise last_exc def verify_login(base_url, username, password, selectors): result = {"status": "fail", "messages": [], "screenshot": ""} with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() try: page.goto(base_url, wait_until="networkidle", timeout=15000) result["messages"].append({"step": "打开应用", "status": "pass"}) retry_on_timeout(lambda: page.click(selectors["login_link"])) page.fill(selectors["username_input"], username) page.fill(selectors["password_input"], password) page.click(selectors["login_button"]) page.wait_for_url(selectors["dashboard_url"], timeout=10000) result["messages"].append({"step": "登录并跳转首页", "status": "pass"}) welcome_text = page.locator(selectors["user_welcome"]).inner_text(timeout=5000) if username in welcome_text: result["messages"].append({"step": "首页展示用户信息", "status": "pass"}) else: result["messages"].append({ "step": "首页展示用户信息", "status": "fail", "actual": welcome_text, }) page.screenshot(path="verification_result.png", full_page=True) result["screenshot"] = "verification_result.png" result["status"] = "pass" except Exception as e: page.screenshot(path="verification_failed.png", full_page=True) result["screenshot"] = "verification_failed.png" result["messages"].append({"step": "执行异常", "status": "fail", "error": str(e)}) finally: browser.close() return result if __name__ == "__main__": args = parse_args() selectors = load_config(args.config) output = verify_login(args.base_url, args.username, args.password, selectors) print(json.dumps(output, ensure_ascii=False, indent=2))这个示例展示了两个工程化手段:
- 选择器配置化:页面变更时优先改 selectors.json。
- 超时重试:使用
retry_on_timeout处理页面加载不稳定导致的偶发超时,提升验证技能在真实网络环境下的稳定性。
5.2 运行验证
执行命令与前一版类似:
python run_verification_with_config.py \ --base-url http://localhost:8080 \ --username demo \ --password demo123成功情况下,返回结果大致如下:
{ "status": "pass", "messages": [ { "step": "打开应用", "status": "pass" }, { "step": "登录并跳转首页", "status": "pass" }, { "step": "首页展示用户信息", "status": "pass" } ], "screenshot": "verification_result.png" }可以看到,Agent 拿到这份结果后,不需要再做额外的日志分析,就能直接判断当前应用是否符合预期。
6. 维护验证技能:页面变了,技能不能“废”
验证技能最容易遇到的坑,不是第一次创建,而是后续维护。应用页面只要改版一次,技能里的选择器就可能失效。如果没有一套维护机制,验证技能很快会变成“总是失败”的摆设。
6.1 选择器变更优先于脚本变更
维护的第一原则是:尽量让选择器可配置,把页面变更的影响范围控制在配置层。当页面调整了按钮的文案、输入框的 name 属性、页面的 URL 路径时,通常只需要更新 selectors.json 或相关参数声明,不需要重新编写脚本。
6.2 建立“技能版本”概念
验证技能应当有明确的版本。页面选择器更新、判断逻辑调整、运行环境变化,都应该升级技能版本。Agent 在选择技能时,如果能够感知版本信息,就可以避免线上 Agent 还在使用旧版脚本。
版本管理可以复用 git 等方式:
cd wallet/skills/login_page_verification git add skill.yaml run_verification_with_config.py selectors.json git commit -m "fix(verify): 更新登录按钮选择器,适配新版登录页" git tag v1.1.0技能脚本和平台代码一样需要版本管理。记录变更原因,尤其要记录“页面发生了什么变化,技能因此做了什么适配”。
6.3 记录失败现场
当验证技能失败时,最重要的事情不是立刻修复,而是把失败现场保存下来。常见的失败现场包括:
- 失败截图。
- 页面 HTML 快照。
- 当前页面 URL。
- 执行的最后一步操作。
- 控制台日志中的关键报错。
没有这些信息,维护者很难判断是技能本身的问题,还是应用真的没有通过验证。更危险的情况是:技能脚本因为选择器失效而误报失败,实际上应用功能完全正常。这种“假失败”会让 Agent 不断尝试修复不存在的问题,反而干扰开发流程。
6.4 定期演练与回归
验证技能不能创建完就不管了。更稳妥的方式是,在 CI/CD 流程中定期跑一遍技能,或者在测试环境每次部署后触发一次验证。这样,技能本身是否仍然有效会得到持续反馈。否则,等 Agent 真正需要调用技能时才发现技能已损坏,就失去了验证的意义。
7. 运行结果与效果评估
验证技能运行成功,并不代表验证结论一定可靠。要评估一个验证技能是否真正有效,可以从以下四个维度来判断。
7.1 可信度
验证脚本是否真正模拟了用户操作,还是只检查了静态标签?比如验证登录,只检查“登录按钮存在”是不够的,要检查点击后是否真的跳转、页面是否出现当前用户信息。只有验证到用户可感知的结果,结论才可信。
7.2 稳定性
同一个验证技能在相同环境下连续运行多次,结果是否一致?如果脚本因为网络波动、元素渲染延迟而频繁出现不稳定的失败,Agent 就无法信任技能的结论。超时重试、等待策略、环境隔离都是提升稳定性的手段。
7.3 可解释性
失败时是否清楚说明了“哪一步失败、预期是什么、实际观察到什么”?可解释的结果不仅方便排查,也能帮助 Agent 更好地理解应用当前的状态,从而决定下一步操作。
7.4 可维护成本
页面每次变更后,技能需要多久才能恢复可用?如果每次都要重写脚本,技能的可维护成本就太高了。选择器抽离、参数化输入、版本化维护,都能降低这个成本。
7.5 验证失败时的第一步排查
当验证技能运行失败时,不要急着改代码,按下面的顺序排查:
- 查看失败截图,判断页面当前实际状态。
- 对比预期和实际结果,确认是应用问题还是验证脚本问题。
- 检查选择器配置是否仍需更新。
- 检查测试环境账号是否有效。
- 检查运行环境依赖是否正常。
8. 常见问题与排查思路
根据日常使用经验,验证技能最常见的几类问题如下。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 登录页元素找不到 | 页面改版,选择器失效 | 查看失败截图,检查页面 DOM | 更新 selectors.json 中的选择器 |
| 验证超时 | 页面加载慢或接口响应慢 | 查看网络请求和接口耗时 | 增大超时时间,增加重试机制 |
| 验证结果不稳定 | 存在异步渲染,未等待元素出现 | 增加显式等待,检查页面状态 | 使用 wait_for 替代固定 sleep |
| 测试账号不可用 | 账号被锁定、密码过期 | 查看认证日志 | 使用专门的测试账号并周期维护 |
| 脚本在本地通过,在 Agent 环境失败 | 环境依赖缺失,浏览器版本不一致 | 对比本地与 Agent 运行环境 | 统一运行时依赖,使用容器化环境 |
| Agent 无法选择技能 | 技能描述不清晰 | 检查技能元数据 description | 优化技能描述,补全关键场景词 |
这里需要特别强调:验证技能如果要在生产环境执行,应遵循最小权限原则。建议使用独立的测试账号,不要在生产环境使用真实用户账号;如果验证任务涉及数据变更,优先使用测试数据;任何涉及敏感信息的注入,都应通过安全配置管理,不能直接明文写在技能描述或脚本中。
9. 最佳实践与生产环境建议
验证技能从能跑到能稳定用于生产,还需要补上几件配置和工程化的事情。
9.1 技能粒度:一个场景一个技能
不要试图一个技能验证所有功能。技能粒度越细,复用性越高,也越容易维护。登录验证、列表页验证、表单提交验证、权限控制验证,应该分别创建独立技能。如果发现多个技能存在大量重复步骤,可以考虑抽取公共执行库,但技能本身的入口要保持独立。
9.2 命名与描述要面向“Agent 检索”
技能描述不只是给人看的,也是给 Agent 做匹配判断的重要依据。命名建议使用“行为 + 对象”的格式,比如login_page_verification、order_list_verification。描述要包含触发场景,例如“当 Agent 需要验证登录流程或登录后首页展示时使用”。描述越清晰,Agent 选择技能就越准确,误配概率越低。
9.3 安全与权限边界
验证技能可能携带账号、密码、认证信息等敏感参数。以下几点需要特别注意:
- 密码等敏感参数优先通过安全配置或密钥管理注入,不要写死在技能目录中。
- 验证环境与生产环境隔离,生产环境验证必须经过审批。
- 技能执行日志可能包含敏感信息,要对日志做脱敏处理。
- 涉及数据库写入、删除操作的验证,使用事务回滚或测试数据方案。
9.4 观察与可观测性
技能执行过程中,应该有完整的可观测手段。除了页面的截图,还可以采集接口请求记录、控制台错误、执行耗时等数据。这些数据既用于 Agent 决策,也用于技能维护者持续评估技能质量。
9.5 与 CI/CD 协同
验证技能不应该只在 Agent 被触发时运行,还应该纳入持续集成的回归体系。比如每日凌晨在测试环境运行一次核心验证技能,变更页面后触发相关技能验证,这些机制能提前发现技能的失效问题,降低对 Agent 在线任务的影响。
9.6 关注 Agent 对验证结果的利用
最后也是容易被忽略的一点:验证技能产出结果后,Agent 会不会正确消费结果?如果 Agent 拿到失败结果后只会重试一轮,不会根据失败原因调整方案,那么验证技能的价值就大打折扣。在设计 Agent 工作流时,应明确失败分支的处理逻辑,比如“当验证失败时,根据失败信息回到对应步骤修复,然后再跑同一技能做回归”。
10. 结论:验证技能的本质,是让 Agent 为结果负责
pstack 新增的创建与维护验证技能,表面上是增加了一个自动化验证能力,本质上是改变了 Agent 的工作方式。过去,Agent 的产出以代码提交为终点;现在,Agent 可以继续执行验证,用真实用户视角检查应用,得到一个明确的通过或失败结论。这种变化让 Agent 从“生成者”变成了“生成并负责验证者”。
对于正在搭建 AI 应用开发流程、Agent 平台或自动化验证体系的团队,建议从一个小场景开始:选取一个高频、稳定、用户操作路径清晰的模块,先创建第一个验证技能,再逐步扩展。重点不在于脚本写得多么复杂,而在于技能声明清晰、验证结果结构化和维护成本可控。
验证技能真正沉淀下来的价值,是可复用的验证经验。它把团队对某个应用的理解、对典型用户路径的梳理、对关键判断条件的定义,固化成一个 Agent 可以直接调用的资产。当这类资产积累得足够多时,Agent 应用开发的验证短板才会真正被补齐。