很多人第一次让 AI 写 Web 自动化测试时,都会经历同一种挫败感:提示词写得挺清楚,AI 也确实生成了像模像样的代码,但一跑就挂。定位不到元素、浏览器没启动、断言写错位置,甚至 AI 还会一本正经地编造一个不存在的 API 名称。问题不一定出在模型能力上,而是给的执行链路太松了。

我的判断是:现阶段做 AI 驱动的 Web 自动化,重点不是“让 AI 多聪明”,而是“让 AI 的每一步都被结构约束住”。Pytest + Skills + MCP 正是这样一个轻量组合。Pytest 负责确定性的执行与断言,MCP 负责把测试能力开放给 AI,Skills 负责把测试规范写进 AI 的工作流。90 分钟跑通一个最小闭环,完全可行。

这篇文章只讲可落地的步骤。读完你可以做三件事:

  1. 用 Pytest 管理一套可重复执行的 Web 自动化用例;
  2. 用 MCP Server 暴露测试执行、技能读取等工具;
  3. 让 AI Agent 按照 Skills 规则调用这些工具完成一次真实回归。

我不打算堆概念,也不讲“AI 会取代测试工程师”这类空话。下面直接进入正题。

1. 90分钟能跑通的组合,解决的是什么问题

先回答一个更直接的问题:为什么我们需要 Pytest、Skills、MCP 三样东西凑在一起,而不是单纯“让 AI 写用例”?

因为“让 AI 写用例”这件事本身太不可控。模型能力确实在进步,但以下几点仍然是 Web 自动化的老问题:

  • 元素定位不稳定 。AI 生成的选择器可能上一步有效,下一步就把页面上另一个重名元素选到了。
  • 执行结果缺少校验 。AI 可能写出了很长的脚本,但最后没有断言,等于“执行了但没验证”。
  • 能力边界模糊 。如果直接把浏览器控制权交给 AI,它可能访问到生产环境页面,或者执行了高风险操作。
  • 重复建设严重 。团队里已经沉淀了很多 pytest 用例,AI 与其重新造轮子,不如直接调度这些既有资产。

Pytest + Skills + MCP 的组合,本质上是把三层责任分开:

层次 角色 承担什么
Pytest 执行与校验层 用例管理、断言、结果报告、浏览器操作稳定性
MCP 能力连接层 让 AI 能发现工具、调用工具、拿到结构化结果
Skills 行为约束层 提前写清楚“AI 应该按什么规范和步骤来做”
Agent 决策编排层 理解任务、拆解步骤、选择工具、观察结果并继续下一步

在这个结构里,AI 不再是那个“直接握着方向盘的人”,而是“坐在副驾驶,按照操作手册调用方向盘、刹车和油门的助手”。它负责调度,Pytest 负责兜底。

所以 90 分钟内我们要完成的不是做一个商业级平台,而是跑通一条最小链路:本地页面 → pytest 用例 → MCP Server 工具 → AI Agent 调度 → 自动回归 → 输出结果。链路通了,后面所有工程化改造都有基础。

2. 四个角色的分工:Pytest、Skills、MCP、Agent

很多人一看标题会问:Pytest 是测试框架,MCP 是协议,Skills 是技能模块,Agent 是智能体,这四个东西怎么组合在一起?

这里先把概念边界理清楚。

2.1 MCP 是什么,解决什么问题

MCP 的全称是 Model Context Protocol,中文一般叫“模型上下文协议”。它解决的是一个大模型开发中的经典问题: 大模型怎么安全、统一地调用外部工具和数据源 。

在没有 MCP 之前,每个模型接入一个工具,都要写一套对接逻辑。今天接 A 公司的搜索接口,明天接 B 公司的数据库,后天接入内部运维平台,每个接口的鉴权方式、参数格式、返回结构都不一样。AI Agent 的代码里会堆满各种定制化适配。

MCP 的做法是定义一套标准:服务器把工具能力暴露出来,客户端负责发现和调用。模型层只需要遵循同一套协议,就能动态拿到“有哪些工具、每个工具接受什么参数、会返回什么格式”,然后像调用普通函数一样去调用。

在 Web 自动化场景里,MCP 的价值很具体:你把 run_pytest 、 read_skill 这些能力注册成 MCP 工具后,AI 询问“当前有哪些工具”就能看到它们,不需要在提示词里手动粘贴一堆说明,也不需要为每个测试框架写私有对接。

这里最容易误解的点是:MCP 不是一个执行引擎,它不负责跑测试,也不负责操作浏览器。它是一个传输和协议层。真正的测试执行仍然是 Pytest 来完成的。

2.2 Skills 和 MCP 有什么区别

这是最近提问率很高的问题,也是新手最容易混的一点。

MCP 解决的是“AI 能调用什么”,Skills 解决的是“AI 应该按什么规则调用”。两者不在同一个层次。

MCP 更像是一个“电话线”,把 AI 和工具接通;Skills 更像是一本“操作手册”,告诉 AI 什么时候该用工具、用之前先做什么、哪些操作不能做、完成后要保留什么证据。

举个例子。一个 Web 自动化测试 Skill 文件里通常包括:

  • 允许访问的域名范围;
  • 元素定位优先使用 data-testid ;
  • 每一步操作后需要截图;
  • 遇到页面弹窗要先记录,不直接关闭;
  • 只允许在测试环境执行 pytest。

这些规则不属于 MCP 的职责范围。没有 Skills 时,AI 可能会写“我猜应该可以访问这个网址”,或者为了完成任务直接执行了高风险操作。有了 Skills,AI 会在执行前先读操作手册,把不确定性降下来。

在纯技术视角下,Skills 可以是一系列 Markdown、YAML 或 JSON 文件,也可以是一套带脚本模板的目录。关键不是格式,而是“把领域知识外置给模型读取”,而不是每次都在提示词里手工灌输。

2.3 Pytest 在整条链路里的定位

Pytest 是 Python 生态里使用率很高的测试框架,它的核心能力包括:用例发现、fixture 管理、断言、失败重试、插件扩展、报告输出。

在 AI + Web 自动化这条链路里,Pytest 真正承担的是“确定性执行层”。AI 可以决定“跑哪个测试文件”,但它不应该临时发挥一套自己的断言逻辑。正确的流程是:AI 选择用例集 → 调用 run_pytest 工具 → Pytest 执行并返回结果 → AI 根据结果继续分析。

这样做的最大好处是:所有执行行为都是可重复、可审计、可回放的。

2.4 Agent 在链路里的角色

Agent 是决策层。它接收自然语言任务,分解为步骤,选择 MCP 工具,观察工具返回结果,然后决定下一步动作。

它也是一个循环:任务解析 → 读取 skill → 选择工具 → 执行 → 观察结果 → 调整策略 → 再次执行。这个循环并不是什么神秘算法,本质上就是代码里的 while 循环加 LLM 调用。理解这一点后,你才能不被“AI 自动化”这个概念迷惑。

3. 环境准备与工程结构

下面进入实操。整个示例用 Python 实现,推荐 Python 3.10 及以上版本,因为 FastMCP 和 MCP SDK 对 Python 版本有要求,新版本兼容性更好。

3.1 创建虚拟环境

建议在项目根目录创建独立虚拟环境,避免污染全局 Python 环境:

mkdir web-auto-ai && cd web-auto-ai
python -m venv .venv
source .venv/bin/activate

Windows 下激活命令是:

.venv\Scripts\activate

3.2 安装依赖

在项目根目录创建 requirements.txt :

pytest
pytest-playwright
playwright
fastmcp
mcp
requests

安装命令:

pip install -r requirements.txt

如果下载速度慢,可以切换国内镜像源,例如:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

之后安装 Playwright 浏览器内核:

playwright install chromium

这一步会下载 Chromium 到本地缓存。如果下载慢,可以配置 PLAYWRIGHT_DOWNLOAD_HOST 指向国内镜像,或者使用公司内部的统一源。不配置也不影响后面的核心代码逻辑,只是首次安装时间会长一些。

3.3 项目目录结构

为了后续好维护,建议按下面的结构组织工程:

web-auto-ai/
├── demo.html
├── requirements.txt
├── tools/
│   ├── mcp_server.py
│   └── mcp_client.py
├── skills/
│   └── web_auto.md
├── tests/
│   ├── conftest.py
│   └── test_web.py
└── reports/
    └── screenshots/

其中:

  • demo.html 是本地被测页面;
  • tests/ 放普通 Pytest 测试用例;
  • tools/mcp_server.py 是 MCP 服务端;
  • tools/mcp_client.py 是 MCP 客户端验证脚本;
  • skills/web_auto.md 是给 AI 读的规则手册。

4. 第一步:用 Pytest 把 Web 自动化跑通

我们先用一个本地 HTML 页面作为被测目标,这样不需要依赖外网环境,稳定且可重现。

4.1 准备被测页面

在项目根目录创建 demo.html :

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>AI 测试演示页</title>
</head>
<body>
  <h1>AI Web 自动化演示</h1>
  <input id="task-input" placeholder="输入任务" />
  <button id="submit-btn" data-testid="submit">提交</button>
  <div id="result">等待输入</div>
  <script>
    document.getElementById("submit-btn").onclick = function () {
      var value = document.getElementById("task-input").value;
      document.getElementById("result").innerText = "处理完成:" + value;
    };
  </script>
</body>
</html>

这个页面包含输入框、按钮、结果区。测试时只要在输入框键入内容,点击提交按钮,就能看到结果区文本变化。

在实际项目中,你执行的往往是登录、查询、下单这类真实业务链路。这里用本地页面是为了把注意力集中在框架链路上,不牵扯测试数据准备和环境配置。

4.2 编写 fixture

创建 tests/conftest.py :

from pathlib import Path

import pytest
from playwright.sync_api import sync_playwright


@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        yield browser
        browser.close()


@pytest.fixture
def page(browser):
    context = browser.new_context(viewport={"width": 1280, "height": 720})
    page = context.new_page()
    yield page
    context.close()


@pytest.fixture(autouse=True)
def ensure_reports_dir():
    Path("reports/screenshots").mkdir(parents=True, exist_ok=True)

这段代码里有两个关键设计。

浏览器实例使用 session 作用域,整个测试会话只启动一次浏览器进程,避免每条用例都反复启动。每个用例则使用独立的 context 和 page ,这样用例之间的登录状态、Cookie 和页面数据不会互相污染。

ensure_reports_dir 是自动 fixture,保证截图目录一定存在,避免后续截图时因为目录不存在而报错。

4.3 编写测试用例

创建 tests/test_web.py :

from pathlib import Path

DEMO_URI = Path(__file__).resolve().parent.parent.joinpath("demo.html").as_uri()


def test_demo_page_interaction(page):
    page.goto(DEMO_URI)
    page.fill("#task-input", "Pytest Skills MCP")
    page.click("[data-testid=submit]")

    expected_text = "处理完成:Pytest Skills MCP"
    assert page.inner_text("#result") == expected_text

    page.screenshot(path="reports/screenshots/demo_page.png")

这里使用 Path.as_uri() 把本地文件路径转成标准的 file:// URL,在不同操作系统下都比较稳定,不用手写拼路径字符串。

断言方面,核心是:

assert page.inner_text("#result") == expected_text

这个断言虽然简单,但它代表了自动化测试的本质:不是让代码“跑一遍”,而是让代码“验证一个预期结果”。

4.4 运行 Pytest

在项目根目录执行:

python -m pytest tests/test_web.py -v

预期输出大致为:

collected 1 item

tests/test_web.py::test_demo_page_interaction PASSED

同时会生成 reports/screenshots/demo_page.png 截图。

如果这一步失败,常见原因包括:

  • 未安装 Playwright 浏览器,执行 playwright install chromium ;
  • 浏览器启动后无法打开 file:// 地址,检查 DEMO_URI 是否生成正确;
  • fixture 名称写错,Pytest 报 fixture 'page' not found ,检查 conftest.py 是否在 tests 目录下。

到这里,我们已经完成了 Pytest + Playwright 的最小 Web 自动化闭环。它不依赖 AI,但为后续步骤提供了可复用资产。

5. 第二步:用 MCP Server 把测试能力开放给 AI

现在进入核心部分:把上一步的测试能力暴露给 AI。这样模型才知道“你的工程里有哪些工具可用”,而不是靠猜。

5.1 实现 MCP Server

创建 tools/mcp_server.py :

import subprocess
from pathlib import Path

from fastmcp import FastMCP

PROJECT_ROOT = Path(__file__).resolve().parent.parent

mcp = FastMCP("web-test-mcp")


@mcp.tool()
def list_test_files() -> str:
    """列出 tests 目录下所有测试文件清单。"""
    tests_dir = PROJECT_ROOT / "tests"
    files = sorted(tests_dir.glob("test_*.py"))
    names = [f.name for f in files]
    if not names:
        return "没有找到 test_*.py 文件"
    return "\n".join(names)


@mcp.tool()
def run_pytest(target: str = "tests") -> str:
    """
    运行指定测试文件、目录或用例节点的 pytest 测试。
    参数 target 示例:tests/test_web.py、tests、tests/test_web.py::test_demo_page_interaction
    返回测试结果摘要。
    """
    result = subprocess.run(
        ["python", "-m", "pytest", target, "-q", "--tb=short"],
        cwd=PROJECT_ROOT,
        capture_output=True,
        text=True,
        timeout=180,
    )
    std_out = result.stdout[-2000:]
    std_err = result.stderr[-2000:]
    if result.returncode == 0:
        return f"PASS\n{std_out}"
    return f"FAIL\n{std_out}\n{std_err}"


@mcp.tool()
def read_skill(skill_name: str = "web_auto") -> str:
    """读取 skills 目录下的技能说明文件,用于约束 AI 执行行为。"""
    skill_path = PROJECT_ROOT / "skills" / f"{skill_name}.md"
    if skill_path.exists():
        return skill_path.read_text(encoding="utf-8")
    return f"SKILL_NOT_FOUND: {skill_name}"


if __name__ == "__main__":
    mcp.run()

这里用 FastMCP 封装了三个工具。

list_test_files 让 AI 在执行任务前先了解项目里有哪些测试资产,不需要读取整个目录树,也不需要在提示词里手动维护清单。

run_pytest 是核心工具,接受 target 参数,支持文件、目录、用例节点三种粒度。AI 接到“跑一遍回归”这类任务时,会传入 tests ;接到“只跑某条用例”时,会传入具体的节点路径。工具内部用 subprocess 调用 pytest,并把最后 2000 字符的输出返回给 AI 作为判断依据。

read_skill 让 AI 可以主动读取规则手册。这比在系统提示词里塞大段规则更灵活,因为规则可以独立更新,不依赖模型版本。

需要注意, subprocess.run 使用了 timeout=180 ,这是为了限制单个用例集执行时间,避免 AI 调度失控时任务一直挂在那里。

FastMCP 的 mcp.run() 默认走 stdio 标准输入输出传输。这是目前 MCP 客户端与本地服务最常见的连接方式,适合本机调试。

5.2 用 MCP 客户端验证服务可用

创建 tools/mcp_client.py :

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

SERVER_PARAMS = StdioServerParameters(
    command="python",
    args=["tools/mcp_server.py"],
    cwd=".",
)


async def main():
    async with stdio_client(SERVER_PARAMS) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools_result = await session.list_tools()
            print("可用工具:")
            for tool in tools_result.tools:
                print("-", tool.name, ":", tool.description)

            result = await session.call_tool(
                "run_pytest",
                arguments={"target": "tests/test_web.py"},
            )
            print("\n执行结果:")
            print(result.content[0].text)


if __name__ == "__main__":
    asyncio.run(main())

运行:

python tools/mcp_client.py

预期效果是:先打印出工具列表,然后输出 run_pytest 的调用结果。如果能看到类似以下内容,说明 MCP 链路已经通了:

可用工具:
- list_test_files : 列出 tests 目录下所有测试文件清单。
- run_pytest : 运行指定测试文件、目录或用例节点的 pytest 测试。
- read_skill : 读取 skills 目录下的技能说明文件,用于约束 AI 执行行为。

执行结果:
PASS
1 passed in 0.42s

这里真正容易踩坑的地方有两个。

第一, cwd 建议写成绝对路径或从当前目录推导,不要直接用相对路径假设命令行所在的目录。如果用相对路径,MCP 客户端在不同位置启动时,服务端可能找不到 mcp_server.py ,或者 pytest 跑在错误的项目目录下。

第二, stdio_client 的传输协议是标准输入输出,所以服务端里不要随便 print 调试信息,否则会污染协议通道。上面 mcp_server.py 中所有输出都通过工具返回值传递,这是 MCP 服务端开发的基本纪律。

6. 第三步:用 Skills 约束 AI 的执行行为

MCP 跑通后,AI 已经有了“手”和“眼”,但还没有“规矩”。这个“规矩”就是 Skills。

6.1 创建 Skill 文件

创建 skills/web_auto.md :

# Web 自动化测试技能

## 使用范围
- 只能对本地 demo.html 与测试环境域名执行浏览器操作。
- 禁止访问生产环境地址,禁止操作支付、个人信息修改等高风险页面。
- 本技能只用于测试团队内部自动化回归。

## 执行规范
1. 执行任何测试任务前,先调用 read_skill 读取本文件。
2. 元素定位优先使用 data-testid,其次使用稳定的 CSS 选择器,最后才考虑 XPath。
3. 每个测试步骤必须附带断言,不能只执行不校验。
4. 每次执行完成必须截图保存到 reports/screenshots 目录。
5. 如果页面出现异常弹窗,先记录弹窗文案,不要直接关闭。

## 失败处理
- 当 find_element 找不到元素时,先等待 2 秒再重试一次。
- 连续重试 3 次仍然失败,记录当前页面截图并终止该步骤。
- 不要伪造测试结果;失败就是失败,要在返回信息里说明原因。

这份 Skill 文件的核心价值在于“把规范外置”。当 AI 执行任务时,系统提示词只需要说“先读取 skill,再开始执行任务”,后续的领域规则全部由文件承载。

这样调整规则也不需要改代码。比如你想增加一条“周五不执行发版回归”的规则,只需要编辑 Markdown 文件,AI 下次执行前读取时自然就看到了。

6.2 为什么 Skills 比“多写几句提示词”更可靠

如果只在提示词里写规则,会遇到三个问题。

第一,提示词长度有限,规则一长就压缩走样。测试团队的规范往往很多,不可能全部塞进系统提示词。

第二,规则改动需要改 Agent 的配置或代码,流程太重。而 Skill 文件做成独立文档后,可以单独评审、单独版本化。

第三,提示词里的规则是“一次性注入”,模型可能在后面的长对话里慢慢遗忘。而 Skill 是“任务开始前主动读取”,相当于每个任务前都重新看一遍操作手册,记忆负担小很多。

需要强调的是,Skills 不是一个特定厂商的专有格式。只要你愿意,可以把它设计成自己的目录规范和文档模板。重要的是思路:把领域知识从提示词中剥离出来,变成 Agent 可以按需读取的资产。

7. 第四步:串起 AI + MCP + Pytest 的闭环

现在到了整篇文章最核心的部分:让 AI Agent 自动完成任务。下面用一个最小 Agent Loop 示例说明。

7.1 最小 Agent Loop

创建 tools/agent_runner.py :

import asyncio
import json

import requests
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

LLM_API_URL = "https://api.your-llm-provider.com/v1/chat/completions"
LLM_API_KEY = "sk-your-key"
LLM_MODEL = "your-model"

SYSTEM_PROMPT = """你是 Web 自动化测试助手,必须遵循以下规则:
1. 执行任务前先调用 read_skill 读取 skills/web_auto.md。
2. 只允许执行 pytest 相关工具,禁止尝试执行任意系统命令。
3. 如果任务不明确,先列出执行计划,再调用工具。
"""


def call_llm(messages, tools):
    response = requests.post(
        LLM_API_URL,
        headers={"Authorization": f"Bearer {LLM_API_KEY}"},
        json={
            "model": LLM_MODEL,
            "messages": messages,
            "tools": tools,
        },
        timeout=60,
    )
    response.raise_for_status()
    return response.json()


async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["tools/mcp_server.py"],
        cwd=".",
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools_result = await session.list_tools()
            tools = [
                {
                    "type": "function",
                    "function": {
                        "name": t.name,
                        "description": t.description,
                        "parameters": t.inputSchema,
                    },
                }
                for t in tools_result.tools
            ]

            messages = [
                {"role": "system", "content": SYSTEM_PROMPT},
                {
                    "role": "user",
                    "content": "先读取技能说明,然后运行 tests 目录下的测试,最后告诉我结果",
                },
            ]

            response_data = call_llm(messages, tools)
            message = response_data["choices"][0]["message"]

            if not message.get("tool_calls"):
                print("模型没有返回工具调用,结果:", message)
                return

            for call in message["tool_calls"]:
                fn_name = call["function"]["name"]
                fn_args = json.loads(call["function"]["arguments"])

                if fn_name == "read_skill":
                    result = await session.call_tool(
                        "read_skill",
                        arguments=fn_args,
                    )
                    print("Skill 内容:")
                    print(result.content[0].text)

                elif fn_name == "run_pytest":
                    result = await session.call_tool(
                        "run_pytest",
                        arguments=fn_args,
                    )
                    print("测试结果:")
                    print(result.content[0].text)


if __name__ == "__main__":
    asyncio.run(main())

这是简化版 Agent 循环,真实场景里还需要处理多轮调用、工具结果回传模型、失败重试等逻辑,但核心链路已经完整:模型发现工具 → 返回 tool_call → 客户端调用 MCP 工具 → 工具执行 pytest → 返回结果给调用方。

这里有几个重点需要解释。

模型向客户端返回 tool_calls ,表示它想要调用哪些工具。这不是模型直接执行代码,而是“请求调用”。真正的执行权在客户端这边。这个设计很重要,因为它让每一步工具调用都在你的控制范围内,你可以校验参数、记录日志、拒绝高风险操作。

7.2 运行预期

当你在项目根目录运行:

python tools/agent_runner.py

流程大致是:

  1. Agent 启动,初始化 MCP 连接;
  2. Agent 获取到 read_skill 、 run_pytest 等工具;
  3. LLM 根据用户任务,先返回 read_skill 的调用请求;
  4. 客户端执行 read_skill ,把 Skill 内容打印出来;
  5. LLM 继续生成第二个工具调用请求;
  6. 客户端执行 run_pytest ,运行测试用例并打印结果;
  7. 最终 AI 返回一个结论性文本。

由于不同大模型的响应节奏不同,实际运行时打印的顺序可能会略有差异。但判断标准一致:只要看到 Skill 内容 和 测试结果 都正常输出,且测试结果里有 PASS 或 1 passed ,就说明 AI 已经成功把任务拆解成工具调用,并在 Skills 的约束下执行了真实测试。

你可以尝试把用户任务改成:

先读取技能说明,然后只运行 test_demo_page_interaction 这条用例

看看 Agent 是否会把 target 参数设置为 tests/test_web.py::test_demo_page_interaction 。如果可以,说明工具提示词和模型意图理解都已经工作。

8. 常见问题与排查方法

在跑通这套链路的过程中,你大概率会遇到下面这些问题。我整理成一张排查表,方便直接对照。

问题现象 可能原因 排查方式 解决方案
pytest 报 fixture 'page' not found conftest.py 不在当前 pytest 扫描目录 查看项目目录结构,检查 conftest 是否和 test 文件同级或在上层目录 将 conftest.py 放到 tests/ 目录,或按需要移动到 test 文件根目录
浏览器启动失败 Playwright 浏览器内核未安装 运行 playwright install chromium ,查看安装日志 安装 chromium;如果下载速度慢,配置下载镜像地址
file:// 地址打不开 本地路径拼接错误 打印 DEMO_URI ,检查是否为 file:///... 绝对路径 使用 Path.as_uri() 生成标准 URI
MCP 客户端启动后没有工具列表 mcp_server.py 报错导致 stdio 中断 先在前台运行 python tools/mcp_server.py ,观察是否有语法错误 修正服务端代码;注意不要在服务端代码里随意 print
MCP 服务端报 FastMCP 未安装 依赖没有安装到当前虚拟环境 执行 pip list 查看是否包含 fastmcp 执行 pip install fastmcp
run_pytest 返回空结果 subprocess.run 的 cwd 指向错误目录 在工具里打印 PROJECT_ROOT ,确认 pytest 是在项目根目录运行 将 cwd 改为 Path(__file__).resolve().parent.parent
LLM 一直不返回 tool_calls 模型不支持 function calling,或调用方式不对 检查模型 API 文档,确认 tools 参数格式 换支持 function calling 的模型,或检查请求体格式
openai 兼容接口报 401 API Key 无效或鉴权头格式错误 用 curl 直接调用接口测试 检查 Authorization 头和 API Key 配置
AI 执行了不在白名单内的操作 Skills 没有被读取 在 Agent 日志中确认是否先调用了 read_skill 在系统提示词中强调必须先读取 skill,或在前端代码中强制注入规则
截图文件生成但内容空白 浏览器在截图前未等待页面稳定 在截图前增加 page.wait_for_load_state("networkidle") 增加等待时间或在截图中加入等待逻辑

这里最值得提前预防的是“AI 不按规则执行”这个问题。解决方案不是不断修改提示词,而是在客户端代码里加硬校验。例如在调用 run_pytest 之前,先检查 target 参数是否以 tests/ 或 demo 开头,如果不符合就拒绝执行。这样即使模型出了问题,工具层也有最后一道防线。

9. 工程化建议与安全边界

链路跑通只是第一步,要真正用到日常测试工作中,还需要做好工程化。下面是我提炼的几个关键建议。

9.1 用目录结构管理 Skills

当 Skills 数量多起来后,建议用统一目录结构管理,每个技能一个独立目录:

skills/
├── web_auto/
│   ├── SKILL.md
│   └── templates/
│       └── login_case.py
├── api_regression/
│   ├── SKILL.md
│   └── examples/
│       └── demo_request.json

SKILL.md 是主说明文件,配套的模板和示例放在子目录。这样 AI 读取一个技能时,除了主文件,还能看到可复用的代码模板,减少从零生成的概率。

9.2 MCP 工具要遵循最小权限原则

MCP 工具不是越多越好,而是越少越安全。这里的“少”指的是职责收敛:一个工具只做一件事,权限只覆盖必要范围。

在 MCP Server 里要避免暴露通用执行接口,例如:

  • 不要暴露一个接受任意命令的 execute_shell(command) 工具;
  • 不要暴露能读取任意文件路径的 read_file(path) 工具;
  • 不要暴露能访问生产数据库的工具。

如果某个操作无法确切判断是否安全,宁可先不加 MCP 工具,等需求明确后再补。

9.3 工具参数要加白名单校验

从模型传过来的参数本质上是不受信任的输入。必须在工具内部做校验。

比如 run_pytest 的 target 参数,可以限制只允许以 tests/ 开头,或者只允许出现在 list_test_files 返回的清单里:

ALLOWED_TARGET_PREFIXES = ("tests/", "tests/")


def validate_target(target: str) -> str:
    if not target.startswith(ALLOWED_TARGET_PREFIXES):
        raise ValueError(f"target 参数不在允许范围内: {target}")
    return target

这种方式本质上和 Web 表单后端校验是一个思路:模型在前端只是“提出请求”,后端必须自己判断这个请求是否合法。

9.4 执行结果要可追溯

AI Agent 执行测试时,一定要保留三类证据:

  • 测试执行日志;
  • 页面截图;
  • Agent 的工具调用记录。

其中工具调用记录可以通过改造 agent_runner.py 实现:在每次 session.call_tool 前后打印/保存参数和返回结果。后续如果出现误操作,你可以回看 Agent 当时的判断。

9.5 不要直接让 AI 修改测试代码后自动入库

当前阶段,让 AI 直接生成一段代码并自动提交到代码仓库仍然有风险。更稳妥的做法是:

  1. AI 生成或修改测试代码;
  2. 开发人员 review;
  3. 人工确认后提交;
  4. 在 CI 里执行。

这个流程虽然多了一步,但能防止模型生成错误选择器、错误断言甚至错误业务逻辑后直接通过自动化管道进入主干。

9.6 用 pytest-html 或 Allure 输出报告

可以在 run_pytest 工具的命令里追加报告参数:

[
    "python", "-m", "pytest", target,
    "-q", "--tb=short",
    "--html=reports/report.html",
]

每次 AI 执行回归后,团队都能打开一份 HTML 报告查看通过率、失败原因和截图。这比 AI 用自然语言说“测试通过了”可靠得多。

10. 总结

Pytest + Skills + MCP 这套组合,核心价值不是“让 AI 写脚本”,而是把 AI 放进一条可约束、可执行、可验证的链路里。

Pytest 提供确定性执行和断言校验,MCP 负责把团队已有的测试资产开放给模型,Skills 负责把领域规则外置,Agent 则承担任务拆解与调度。四者结合后,AI 的能力边界和测试团队的工程质量反而都有了保障。

下一步你可以从这几件事开始实践:

  • 把你现有的 Web 自动化用例整理进 tests/ 目录,先跑通 Pytest 层;
  • 用 MCP Server 暴露一个最小工具集,让 AI 能发现并调用;
  • 写一份属于自己团队的 Skill 文件,从“只允许访问测试环境”这类最关键的规则开始;
  • 再慢慢扩展到接口自动化、性能测试、多环境切换等场景。

回到标题里的“90分钟”:这个时间窗口只是为了让你建立最小闭环,不是终点。真正有价值的,是你在这套链路里积累下来的用例资产、规则资产和工具资产。这三样资产越多,AI 能帮上忙的场景就越多,而你需要担心的不确定性反而越少。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐