Slack 工程团队提出了一种 Agentic Testing 思路:让 AI Agent 根据测试意图执行端到端工作流,而不是机械地重放一串固定选择器。它关注的不是“点击第三个按钮”,而是“完成登录并确认工作区首页可用”。当按钮位置、DOM 层级或部分文案变化时,Agent 可以在运行时重新判断下一步动作,从而降低 UI 自动化测试的脆弱性。
这并不意味着确定性测试已经过时。更合理的定位是:Agent 驱动测试负责覆盖变化频繁、跨系统且难以用固定脚本表达的用户旅程,单元测试、集成测试和传统 E2E 测试继续承担精确、快速、可重复的验证任务。
固定脚本为什么容易失效
传统 UI 测试通常把页面结构写进测试代码:
page.locator("div.sidebar > div:nth-child(3) button").click()
这条语句验证的其实不只是产品行为,还隐含绑定了 DOM 层级、元素顺序和标签类型。侧边栏增加一个入口,测试就可能失败,即使用户仍然可以正常完成任务。
Agent 驱动测试把断言提升到意图层。例如:
目标:使用测试账号登录,打开 engineering 频道,发送消息“release ready”,并确认消息出现在时间线中。
Agent 在每一步读取当前页面状态,选择可用控件,执行动作,再观察结果。这里的关键变化不是简单地用大模型生成一次 Playwright 脚本,而是形成“观察、决策、执行、校验”的运行时循环。
这种方式尤其适合分布式系统。一次用户操作可能经过身份服务、前端路由、消息服务和异步任务。固定脚本只能等待预先设定的页面状态,而 Agent 可以根据错误提示、加载状态或替代入口调整动作。不过,适应性也会带来非确定性,因此必须限制 Agent 的权限和动作范围。
一套可控的执行模型
可以把 Agentic E2E 拆成四层:
- 意图层:描述用户目标和成功条件,不记录鼠标坐标或 DOM 路径。
- 观察层:向模型提供可访问名称、角色、可见文本、当前 URL 和必要的截图。
- 动作层:只开放
click、fill、select、wait和受约束的导航等白名单操作。 - 验证层:由确定性断言确认最终结果,保留 trace、截图和 Agent 决策记录。
模型不应直接执行任意 JavaScript、Shell 命令或数据库查询。测试运行器应当把模型输出解析成结构化动作,验证参数后再交给浏览器执行。这样既能利用模型的页面理解能力,也能把误操作限制在测试环境内。
可以这样实践:最小 Playwright Agent 循环
下面是一个可改造的实验示例,并非 Slack 内部实现。它假设你有一个 OpenAI 兼容的 Chat Completions 接口,以及运行在 http://localhost:3000 的测试应用。模型只能点击按钮或链接、填写输入框,以及检查文本。
安装依赖:
python -m venv .venv
source .venv/bin/activate
pip install playwright requests
playwright install chromium
export LLM_BASE_URL="https://your-llm-endpoint.example/v1"
export LLM_API_KEY="replace-me"
export LLM_MODEL="replace-with-model-name"
python agent_e2e.py
创建 agent_e2e.py:
import json
import os
import requests
from playwright.sync_api import sync_playwright
BASE_URL = os.environ["LLM_BASE_URL"].rstrip("/")
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ["LLM_MODEL"]
GOAL = "Log in with alice@example.com and password test-only, then verify Dashboard is visible."
def decide(page, history):
state = {
"url": page.url,
"title": page.title(),
"visible_text": page.locator("body").inner_text()[:8000],
"history": history[-8:],
}
prompt = f"""You control a browser in an isolated test environment.
Goal: {GOAL}
Current state: {json.dumps(state)}
Return exactly one JSON object, without Markdown.
Allowed actions:
{{"action":"click","role":"button|link","name":"accessible name"}}
{{"action":"fill","label":"field label","value":"text"}}
{{"action":"assert_text","text":"expected text"}}
{{"action":"finish","reason":"goal completed"}}
Choose one action. Never request shell, JavaScript, file, or network access."""
response = requests.post(
f"{BASE_URL}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": MODEL,
"temperature": 0,
"messages": [{"role": "user", "content": prompt}],
},
timeout=60,
)
response.raise_for_status()
return json.loads(response.json()["choices"][0]["message"]["content"])
def execute(page, step):
action = step.get("action")
if action == "click":
role = step.get("role")
if role not in {"button", "link"}:
raise ValueError(f"Disallowed role: {role}")
page.get_by_role(role, name=step["name"], exact=True).click()
elif action == "fill":
page.get_by_label(step["label"], exact=True).fill(step["value"])
elif action == "assert_text":
page.get_by_text(step["text"], exact=False).wait_for(state="visible")
elif action != "finish":
raise ValueError(f"Disallowed action: {action}")
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()
page.goto("http://localhost:3000/login")
history = []
for _ in range(12):
step = decide(page, history)
print(json.dumps(step, ensure_ascii=False))
execute(page, step)
history.append(step)
if step["action"] == "finish":
break
else:
raise RuntimeError("Agent exceeded the 12-step budget")
page.get_by_text("Dashboard", exact=False).wait_for(state="visible")
page.screenshot(path="agent-e2e-result.png", full_page=True)
browser.close()
运行前需要把登录地址、目标描述、测试账号和最终断言替换成你的应用信息。示例故意保留了最终的确定性断言:模型可以寻找路径,但“Dashboard 必须可见”仍由 Playwright 判断。
真实项目还应增加 JSON Schema 校验、敏感字段脱敏、域名白名单、最大 token 数、每步截图、Playwright trace,以及失败后的人工可读报告。不要把生产凭据或真实用户数据放进模型上下文。
与现有测试金字塔配合
Agent 测试不适合替代所有 E2E 用例。金额计算、权限边界、API 状态码和数据一致性需要稳定且可重复的断言;如果这些检查依赖模型判断,失败原因会变得模糊,持续集成结果也更容易波动。
更实际的分工是:
- 单元测试验证纯逻辑和边界条件。
- 集成测试验证服务、队列、数据库与外部契约。
- 确定性 E2E 覆盖支付、权限、数据写入等关键路径。
- Agentic E2E 探索跨页面工作流、兼容 UI 调整,并发现脚本未预料到的状态。
接入 CI 时,可以先让 Agent 测试以非阻断模式运行,记录成功率、平均步骤数、模型成本和重复执行的一致性。只有当某条旅程在多次运行中足够稳定,且失败证据可以复现时,才适合升级为合并门禁。
落地时要守住的边界
采用 Agent 驱动测试前,建议检查以下事项:
- 测试环境与生产环境隔离,账号权限遵循最小化原则。
- Agent 只能调用结构化、可审计的浏览器动作。
- 成功条件尽量由确定性代码验证,而不是只相信模型的
finish。 - 保存 prompt、模型版本、动作序列、网络日志、trace 和截图。
- 为步骤数、运行时间、token、重试次数和可访问域名设置硬限制。
- 保留传统测试,避免把模型波动误判为产品回归。
Agentic Testing 的价值不在于消灭选择器,而在于把测试脚本从页面结构中适度解耦。真正可靠的方案通常是混合架构:Agent 负责理解变化和寻找路径,测试框架负责执行受限动作、验证结果并留下可复现证据。