让 AI 当自己的 UI 审查员:Codex 生成组件 + 视觉模型截图验证闭环实战
让 AI 当自己的 UI 审查员:Codex 生成组件 + 视觉模型截图验证闭环实战
我让 AI 给我生成了一个登录表单。
代码看起来无懈可击——语义正确、类名规范、逻辑清晰。我满怀期待地跑起来,然后看到了这个:提交按钮跑到了输入框下面三屏的位置,像一个迷路的孩子站在空旷的白色页面里。
你有没有经历过这种事?让 AI 生成组件 → 复制粘贴 → 跑起来一看惨不忍睹 → 回去改 Prompt → 再试一次 → 还是不对 → 循环往复,直到你忘了自己最开始想做什么。
问题的根源不是 AI 写的代码质量差,而是"生成"和"验证"之间有一道肉眼检查的断层。AI 不知道自己生成的东西跑起来长什么样,你也没有时间每次都手动截图对比。
这篇文章要填上这道断层。
---
全局地图:三步闭环是什么
在动手之前,先建立完整的心智模型。整个流程只有三步:
┌─────────────────────────────────────────────────────────┐
│ │
│ ① Codex 生成组件代码 │
│ └─ 输入:组件需求 Prompt │
│ └─ 输出:React/Vue 组件文件 │
│ │ │
│ ▼ │
│ ② Playwright 无头渲染 + 自动截图 │
│ └─ 输入:组件文件路径 │
│ └─ 输出:PNG 截图存入 /output 目录 │
│ │ │
│ ▼ │
│ ③ 视觉模型读图 → 输出结构化评审意见 │
│ └─ 输入:截图 base64 + 审查 Prompt │
│ └─ 输出:PASS / FAIL + 具体问题描述 │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ FAIL? │ PASS? │
│ ▼ ▼ │
│ 将问题描述拼入新 Prompt 流程结束 ✓ │
│ 触发第二轮 Codex 生成 │
│ (最多重试 N 次) │
│ │
└─────────────────────────────────────────────────────────┘
用到的工具和端点:
- 代码生成:Codex(通过 Chat Completions API,model 参数指定)
- 无头渲染:Node.js + Playwright
- 视觉评审:支持图像输入的多模态模型(本文以视觉理解能力强的模型为例)
- 统一 API 入口:
api.884819.xyz(所有调用共用同一个baseURL,文中代码直接可用)
---
手把手实操:把三步跑通
在开始之前:文中所有 API 调用均通过统一端点完成。如果你还没有可用的 API Key,或者想用一个稳定支持 Codex 和视觉模型全系的转发服务,直接访问 [api.884819.xyz](https://api.884819.xyz),注册即送体验 token,把文中代码里的 baseURL 替换一下就能跑。
第一步:用 Codex 生成组件代码
我们以生成一个 Button 组件为例。Prompt 的关键是约束输出格式,让返回值直接是可运行的代码,不要多余的解释文字。
// codex-generate.js,import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: "https://api.884819.xyz/v1",
});
async function generateComponent(requirement) {
const response = await client.chat.completions.create({
model: "codex-mini-latest", // 或使用你有权限的 Codex 模型
messages: [
{
role: "system",
content:
你是一个 React 组件专家。只输出完整的 .jsx 文件内容,不要任何解释文字,不要 markdown 代码块标记。
组件必须:使用 Tailwind CSS 样式、可直接在浏览器渲染、export default 导出。
},
{
role: "user",
content: requirement,
},
],
});
return response.choices[0].message.content;
}
// 示例调用
const buttonCode = await generateComponent(
"生成一个主色调为蓝色的 Primary Button 组件," +
"包含 hover 状态、disabled 状态,尺寸 medium,圆角 8px。"
);
// 写入文件
import { writeFileSync } from "fs";
writeFileSync("./components/Button.jsx", buttonCode);
console.log("✅ 组件已生成:./components/Button.jsx");
Codex 通常会返回类似这样的组件:
// 生成的 Button.jsx(示例)}export default function Button({ children, disabled, onClick }) {
return (
>
{children}
);
}
代码看起来没问题。但"看起来"不算数——我们继续。
第二步:Playwright 无头渲染 + 自动截图
这一步的核心是时机。组件渲染需要时间,截图太早只会拍到空白页面(这是后面踩坑部分要讲的第一个坑)。
到这里,截图已经自动存到// screenshot.js;import { chromium } from "playwright";
import { readFileSync, mkdirSync } from "fs";
async function screenshotComponent(componentPath, outputPath) {
// 构造一个最小化的 HTML 测试页面
const html =
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 800, height: 400 });
await page.setContent(html, { waitUntil: "networkidle" }); // 关键:等网络空闲
// 额外等待 500ms,确保 Tailwind CDN 样式全部应用
await page.waitForTimeout(500);
mkdirSync("./output", { recursive: true });
await page.screenshot({ path: outputPath, fullPage: false });
await browser.close();
console.log(
📸 截图已保存:${outputPath});}
await screenshotComponent("./components/Button.jsx", "./output/button-v1.png");
/output 文件夹了。 打开看看,如果组件正常渲染,继续下一步;如果是空白图,先检查 waitUntil 参数和 CDN 加载时间。
第三步:视觉模型读图 + 输出结构化评审
这是整个流程最有意思的一步。我们把截图 base64 编码,连同一段精心设计的审查 Prompt,一起发给支持图像输入的多模态模型。
// review.js;import OpenAI from "openai";
import { readFileSync } from "fs";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: "https://api.884819.xyz/v1",
});
async function reviewScreenshot(imagePath, componentRequirement) {
const imageData = readFileSync(imagePath);
const base64Image = imageData.toString("base64");
const reviewPrompt =
你是一个专业的 UI 审查员。请仔细观察这张组件截图,对照以下需求进行评审:
【原始需求】
${componentRequirement}
【评审维度】
1. 视觉呈现:颜色、圆角、间距是否符合需求描述
2. 状态完整性:所有要求的状态(hover/disabled等)是否可见
3. 布局正确性:元素位置和对齐是否合理
4. 可用性:按钮文字是否清晰可读
【输出格式】请严格按以下 JSON 格式返回,不要任何额外文字:
{
"result": "PASS" | "FAIL",
"score": 0-100,
"issues": ["问题1", "问题2"],
"suggestions": ["建议1", "建议2"],
"summary": "一句话总结"
}
const response = await client.chat.completions.create({
model: "gpt-4o", // 使用支持视觉的模型
messages: [
{
role: "user",
content: [
{ type: "text", text: reviewPrompt },
{
type: "image_url",
image_url: {
url:
data:image/png;base64,${base64Image},},
},
],
},
],
response_format: { type: "json_object" },
});
return JSON.parse(response.choices[0].message.content);
}
const requirement = "主色调为蓝色的 Primary Button,包含 hover 状态、disabled 状态,尺寸 medium,圆角 8px";
const reviewResult = await reviewScreenshot("./output/button-v1.png", requirement);
console.log("📋 评审结果:", JSON.stringify(reviewResult, null, 2));
一次真实的评审返回长这样:
{
"result": "FAIL",
"score": 72,
"issues": [
"disabled 状态的按钮颜色过浅,与背景对比度不足,可读性差",
"两个按钮之间的间距偏大,视觉上显得松散"
],
"suggestions": [
"disabled 状态建议使用 bg-gray-400 而非 bg-gray-300,提升文字可见度",
"按钮间距从 ml-4 调整为 ml-3"
],
"summary": "基本结构正确,主按钮样式符合需求,但 disabled 状态可访问性需要改进"
}
issues 和 suggestions 这两个字段是金子——它们将直接作为下一轮 Codex 的修复指令。
---
进阶玩法:让评审结果自动触发修复
三步跑通之后,加一层主控脚本,把整个流程串成真正的闭环。
关于循环终止条件,这里有两个设计原则:// auto-loop.js — 完整闭环主控脚本.trim();import { generateComponent } from "./codex-generate.js";
import { screenshotComponent } from "./screenshot.js";
import { reviewScreenshot } from "./review.js";
import { writeFileSync } from "fs";
const MAX_RETRIES = 3; // 最大重试次数,防止 AI 自嗨式无限循环
async function runLoop(initialRequirement) {
let requirement = initialRequirement;
let iteration = 0;
while (iteration < MAX_RETRIES) {
iteration++;
console.log(
\n🔄 第 ${iteration} 轮生成中...);// Step 1: 生成代码
const code = await generateComponent(requirement);
writeFileSync(
./components/Button-v${iteration}.jsx, code);// Step 2: 截图
const screenshotPath =
./output/button-v${iteration}.png;await screenshotComponent(
./components/Button-v${iteration}.jsx, screenshotPath);// Step 3: 评审
const review = await reviewScreenshot(screenshotPath, initialRequirement);
console.log(
📊 评审得分:${review.score} | 结果:${review.result});if (review.result === "PASS") {
console.log(
✅ 第 ${iteration} 轮通过评审!流程结束。);console.log(
最终组件:./components/Button-v${iteration}.jsx);return { success: true, iterations: iteration, finalCode: code };
}
// FAIL:把问题描述拼进新 Prompt,触发下一轮
const issuesList = review.issues.join("\n- ");
const suggestionsList = review.suggestions.join("\n- ");
requirement =
【原始需求】${initialRequirement}
【上一版本的问题,必须修复】
- ${issuesList}
【修复建议】
- ${suggestionsList}
请在修复以上所有问题的基础上,重新生成完整组件代码。
console.log(
❌ 评审未通过,问题已记录,准备第 ${iteration + 1} 轮修复...);}
console.log(
⚠️ 已达到最大重试次数 ${MAX_RETRIES},请人工介入检查。);return { success: false, iterations: MAX_RETRIES };
}
// 启动闭环
await runLoop("主色调为蓝色的 Primary Button,包含 hover 状态、disabled 状态,尺寸 medium,圆角 8px");
1. 评审通过即终止:不要贪心,第一次 PASS 就停下来,不要追求满分
2. 硬上限保底:MAX_RETRIES = 3 是经验值,超过 3 次还没过,大概率是 Prompt 本身有问题,需要人工介入
实测中,第二轮生成的组件,视觉模型给出了 PASS,得分从 72 提升到 91。那一刻的感觉,确实有点爽。
---
踩坑记录 & 成本控制
三个真实的坑
坑一:截图时机不对最常见的问题。用 waitUntil: "load" 截图,Tailwind CDN 还没加载完,拍出来是一张没有任何样式的白板。
waitUntil: "networkidle",并在之后额外 waitForTimeout(500)。如果你的组件有动画,这个数字还要加大。
坑二:视觉模型对细微颜色差异误判
bg-blue-600(#2563EB)和 bg-blue-500(#3B82F6)在截图里肉眼几乎看不出差别,但模型有时会把这当成"颜色不符合需求"来报错,触发不必要的重试。
解决方案:在审查 Prompt 里加一句:"颜色判断请以视觉观感为准,不要纠结具体色值的细微差异。" 能显著减少误报率。
坑三:Codex 重复生成相同错误代码
如果问题描述不够具体,Codex 可能在第二轮生成几乎相同的代码,只改了一个无关紧要的地方。这是真正的"AI 自嗨"。
解决方案:把视觉模型的suggestions 字段直接转成代码级指令("将 bg-gray-300 改为 bg-gray-400"),而不是模糊的"提升对比度"。越具体,Codex 越听话。
费用估算
按"每个组件跑一次完整闭环(平均 1.5 轮)"估算:
| 调用步骤 | 模型 | 预估 Token | 约合美元 | | Codex 生成(每轮) | Codex | ~800 tokens | ~$0.004 | | 视觉模型评审(每轮) | 视觉模型 | ~1200 tokens + 图像 | ~$0.015 | | 单组件完整闭环(1.5轮) | — | — | ~$0.03 | | 100个组件批量跑 | — | — | ~$3.00 |如果你用的是 [api.884819.xyz](https://api.884819.xyz) 的聚合服务,可以在控制台实时看到每次调用的 token 消耗,比自己算省心很多。这套流程适合哪些场景:
- 组件库批量生成和质检
- Design Token 变更后的回归验证
- 外包代码的 UI 验收自动化
- 需要交互测试的复杂组件(截图只能验证静态视觉)
- 对像素级还原度有严苛要求的场景(误差容忍度有限)
- 实时性要求极高的 CI/CD 流水线(单次闭环约 15-30 秒)
---
现在就可以跑起来
整个流程的代码量不超过 200 行,没有复杂的依赖,没有需要自己搭建的服务。
你现在就可以用文中的脚本跑一遍,整个流程不超过 20 分钟。第一次看到"第 2 轮通过评审"那行日志打印出来,你会理解为什么我觉得这件事值得写一篇文章。
---
这套流程目前还是"组件级"的验证——它只能看一个组件对不对。
下一篇,我们会把镜头拉远:让 AI 截下整个页面,对照 Figma 设计稿做像素级 Diff,自动标出"哪里和设计稿不一样"。
如果你做过还原度验收,你知道那有多痛。敬请期待。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI编程 #前端开发 #Codex #自动化测试 #UI验证 #8848AI #AI工具 #Playwright