作者:前端转 AI 深度实践者
【💡 省流助手 / 核心观点】 AI 项目最危险的验收方式,不是没有测试,而是“我手动问了几句,看起来能用”。在真实的 AI 工程中,Agent 存在接口契约易变、工具调用分支爆炸、模型质量波动等多重风险,靠肉眼点页面只能覆盖 1% 的“开心路径”。 建议前端开发者将验收拆为 4 层:契约测试 (Zod) -> Mock 交互 (Playwright) -> 关键链路 E2E -> 评测集回归。 先守住工程底线,再谈模型效果。
很多团队第一次做 AI 项目,上线前的验收流程大概是这样:
打开页面。
问一句:帮我查订单 A1001。
页面有回答。
再问一句:帮我总结这份文档。
看起来也能答。
点两下按钮,没报错。
发版!🚀
这套方式在 Demo 阶段没问题。但只要项目进入生产环境,它就会迅速失效。
因为 AI 项目的失败方式,比普通 CRUD 多得多:
- 接口字段少了一个,前端确认弹窗不出现。
- 模型没拿到资料,却“自信”地编了一个看似合理的答案(幻觉)。
- 高风险工具没有二次确认,直接执行了删库操作。
- 权限判断只写在 Prompt 里,被模型轻松绕过。
traceId丢了,线上反馈“答非所问”,排查时完全查不到链路。- Loading 状态能跑,但发生 Error 时页面直接白屏,没人测过。
- 改了一句系统提示词,旧问题的答案质量整体掉了一截。
在 AI 工程化的浪潮下,前端开发者不再只是 UI 的搬运工。这篇文章不讲“测试很重要”这种空话,我们只解决一个具体痛点:
前端开发者做 AI 项目,如何把“手动点页面验收”升级成一套能进 CI 的上线门禁?
1. 先说清楚:AI 项目到底要验收什么?
很多人一提 AI 验收,就只盯着模型回答:“答得准不准?语气自然不自然?有没有幻觉?”
这些当然重要,但远远不够。AI Agent 的稳定性,很大程度上取决于前端对异常分支的容错处理。 在真实项目中,前端至少要关心 4 类验收对象:
- 接口契约:字段是否稳定,错误结构是否一致?
- 前端交互:Loading、确认、取消、失败、重试流转是否正确?
- Agent 规则:权限拦截、工具调用、风险确认、降级策略是否生效?
- 模型质量:检索命中率、答案相关性、引用依据 (Citations) 是否可靠?
如果只靠手动点页面,你最多能覆盖“今天我刚好想到的几条路径”。但 AI 项目的风险,经常藏在你没点到的分支里。
2. 真实惨案:用肉眼验收一个会变化的系统
先看一个很多新手踩坑的错误做法。前端页面只关心 answer 和 loading:
❌ 错误做法:防御性为 0 的“随缘型”渲染
// 很多前端默认 AI 只会成功返回一段文本
const response = await fetch("/api/agent", { /* ... */ });
const data = await response.json();
// 致命伤:没考虑 waiting_confirmation、error 等状态
// 一旦后端加了字段或模型报错,页面直接白屏或逻辑错乱
setAnswer(data.answer);
setLoading(false);
这段代码的问题不在于它不能跑,而是它默认 AI 接口永远是个乖孩子。 但真实的 Agent 可能会返回高风险确认状态:
{
"status": "waiting_confirmation",
"message": "取消订单属于高风险操作,请确认。",
"traceId": "tr_20260509_001",
"confirmation": { "toolName": "cancel_order", "riskLevel": "high" }
}
或者返回权限错误:
{
"status": "error",
"errorType": "permission_denied",
"message": "当前用户无权执行该操作。",
"traceId": "tr_20260509_002"
}
如果前端只写 data.answer,这些状态在页面上不是消失,就是变成空白。手动验收时,你刚好只问了成功路径,就会误以为项目完美,然后开开心心上线,直到客诉打爆你的钉钉。
3. 第一层门禁:契约测试,把不确定性挡在门外
AI 项目的第一层验收,不是看模型聪不聪明,而是看接口契约稳不稳定。
✅ 正确做法:用 Zod 强行锁死契约
import { z } from "zod";
// 1. 定义严丝合缝的契约,明确所有可能的分支
export const AgentResponseSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("success"),
answer: z.string(),
traceId: z.string().min(1),
citations: z.array(z.object({ title: z.string(), url: z.string().optional() })),
}),
z.object({
status: z.literal("waiting_confirmation"),
message: z.string(),
traceId: z.string().min(1),
confirmation: z.object({ toolName: z.string(), riskLevel: z.enum(["medium", "high"]) }),
}),
z.object({
status: z.literal("error"),
errorType: z.enum(["permission_denied", "tool_failed", "retrieval_empty"]),
message: z.string(),
traceId: z.string().min(1),
}),
]);
export type AgentResponse = z.infer<typeof AgentResponseSchema>;
// 2. 在请求层进行强校验
export async function sendAgentMessage(question: string): Promise<AgentResponse> {
const response = await fetch("/api/agent", { /* ... */ });
const json = await response.json();
// 只有通过契约的请求,才能进入业务逻辑
return AgentResponseSchema.parse(json);
}
契约测试不用等完整页面,直接测请求解析函数:
import { describe, expect, it } from "vitest";
describe("AgentResponseSchema", () => {
it("rejects response without traceId", () => {
expect(() =>
AgentResponseSchema.parse({
status: "success",
answer: "订单已发货",
// 缺少 traceId,立刻抛错拦截!
}),
).toThrow();
});
});
这一步非常值钱:后端字段一变,前端立刻报错;traceId 丢了,测试立刻发现。这是防止“接口轻微变更,页面静默坏掉”的最有效手段。
4. 第二层门禁:Mock Agent 交互,点亮所有黑暗分支
手动点页面最大的问题是点不到所有 Agent 分支。正确做法是让前端在测试里接管 /api/agent,把成功、确认、权限失败、空检索等分支都 Mock 出来。
以 Playwright 为例,测试高风险确认拦截:
import { expect, test } from "@playwright/test";
test("拦截高风险操作,必须展示确认卡片", async ({ page }) => {
// Mock Agent 返回等待确认状态
await page.route("/api/agent", async (route) => {
await route.fulfill({
contentType: "application/json",
body: JSON.stringify({
status: "waiting_confirmation",
message: "取消订单属于高风险操作,请确认。",
traceId: "tr_confirm_001",
confirmation: { toolName: "cancel_order", riskLevel: "high" },
}),
});
});
await page.goto("/agent");
await page.getByLabel("问题").fill("帮我取消订单 A1001");
await page.getByRole("button", { name: "发送" }).click();
// 核心断言:必须出现警告和确认按钮
await expect(page.getByText("取消订单属于高风险操作")).toBeVisible();
await expect(page.getByRole("button", { name: "确认执行" })).toBeVisible();
});
把 Agent 的每个重要分支,变成可重复执行的页面自动化测试,你晚上才能睡得着觉。
5. 第三层门禁:关键链路 E2E,只守最值钱的路径
Mock Agent 能保证前端状态机不坏。但上线前还需要少量真实 E2E,确认前后端、Agent、工具和日志能真实串起来。
⚠️ 注意:不要把所有用例都做成真实 E2E。 真实模型慢且贵,极易由于网络抖动导致测试 flaky(脆弱)。
我一般建议只守这 5 条生死线:
- 普通问答成功:页面展示答案、引用 (Citations) 和 traceId。
- 检索为空降级:页面不展示编造答案,明确提示资料不足。
- 权限不足拦截:页面展示明确错误,不出现操作按钮。
- 高风险阻断:先出现确认卡片,绝不直接执行工具。
- 确认后执行:二次确认请求发出后,页面进入最终状态。
连这 5 条自动化都没有,上线等于“裸奔”。
6. 第四层门禁:评测集回归,别让模型质量悄悄掉
前面三层解决的是工程底线。但 AI 项目还有一个幽灵:模型效果退化。 改了 Prompt、切了模型版本、调了温度参数……页面不报错,但答案变蠢了怎么办?
答案是:建立最小评测集 (Evaluation Set)。
// 一个朴素但管用的评测 Case 定义
export const evalCases = [
{
id: "order-status-a1001",
question: "订单 A1001 现在是什么状态?",
mustContain: ["A1001", "待发货"], // 必须包含关键信息
mustNotContain: ["已退款", "已取消"], // 绝对不能乱编的状态
expectedCitationCount: 1, // 必须有引用来源
}
];
每次发版前,用脚本跑一遍这些核心 Case。不用追求高大上的多维评测框架,先从“20 条高频问题、10 条失败路径、10 条线上坏 case”开始,这就足以挡掉 80% 的“越改越烂”。
7. 生产环境避坑指南 (💡 重点收藏)
- 别在 CI 流程里跑全量大模型测试。
- 真相:不仅慢,还会烧光你的 Token 预算。PR 阶段用 Mock (第二层) 跑前端逻辑,发版前再跑真实 E2E (第三层) 和评测集 (第四层)。
- 别相信模型能自己搞定权限。
- 真相:Prompt 里的“你没有权限执行 X”是防君子不防小人。权限必须由后端工具网关 (Tool Gateway) 兜底,并返回对应的
errorType给前端展示。
- 真相:Prompt 里的“你没有权限执行 X”是防君子不防小人。权限必须由后端工具网关 (Tool Gateway) 兜底,并返回对应的
- 漏掉
traceId等于在线上“盲跑”。- 真相:用户反馈“AI 刚才回答不对”时,如果没有
traceId,你根本不知道是检索没搜到、模型抽风了,还是上下文被截断了。traceId必须作为前端契约的必填项。
- 真相:用户反馈“AI 刚才回答不对”时,如果没有
- 别让评测集只活在某个人电脑里。
- 真相:评测集应该随代码入库。每新增一个线上 Bad Case,就加一条回归用例。把线上事故变成系统的免疫力。
结语
AI 项目不能靠手动点页面验收。不是因为手动验收没价值(它适合探索体验),而是它撑不起回归保障。
构建这套 4 层验收体系,是前端迈向高级 AI 工程师的必经之路:
- 先用 契约测试 守字段。
- 再用 Mock 交互 守页面状态流转。
- 再用 关键链路 E2E 守真实业务闭环。
- 最后用 评测集 守模型质量基线。
做到这一步,你的 AI 项目才不再是“今天看起来能用”,而是“下次改完我也敢大胆发版”!
你的团队是怎么验收 AI Agent 的?欢迎在评论区交流踩过的坑!如果觉得有启发,点个赞和收藏防走丢哦~ 👇
Tags: #前端 #人工智能 #AI Agent #自动化测试 #大模型