Pytest+Skills+MCP:让AI在约束下完成Web自动化测试
很多人第一次让 AI 写 Web 自动化测试时,都会经历同一种挫败感:提示词写得挺清楚,AI 也确实生成了像模像样的代码,但一跑就挂。定位不到元素、浏览器没启动、断言写错位置,甚至 AI 还会一本正经地编造一个不存在的 API 名称。问题不一定出在模型能力上,而是给的执行链路太松了。
我的判断是:现阶段做 AI 驱动的 Web 自动化,重点不是“让 AI 多聪明”,而是“让 AI 的每一步都被结构约束住”。Pytest + Skills + MCP 正是这样一个轻量组合。Pytest 负责确定性的执行与断言,MCP 负责把测试能力开放给 AI,Skills 负责把测试规范写进 AI 的工作流。90 分钟跑通一个最小闭环,完全可行。
这篇文章只讲可落地的步骤。读完你可以做三件事:
- 用 Pytest 管理一套可重复执行的 Web 自动化用例;
- 用 MCP Server 暴露测试执行、技能读取等工具;
- 让 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
流程大致是:
- Agent 启动,初始化 MCP 连接;
-
Agent 获取到
read_skill、run_pytest等工具; -
LLM 根据用户任务,先返回
read_skill的调用请求; -
客户端执行
read_skill,把 Skill 内容打印出来; - LLM 继续生成第二个工具调用请求;
-
客户端执行
run_pytest,运行测试用例并打印结果; - 最终 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 直接生成一段代码并自动提交到代码仓库仍然有风险。更稳妥的做法是:
- AI 生成或修改测试代码;
- 开发人员 review;
- 人工确认后提交;
- 在 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 能帮上忙的场景就越多,而你需要担心的不确定性反而越少。
更多推荐
所有评论(0)