AI 生成 UI 代码的可靠性问题:幻觉、格式错误与最佳纠错策略
一、引子:那个不存在的 CSS 属性
"帮我生成一个带有玻璃拟态效果的卡片组件。"AI 愉快地吐出了一段 CSS,其中包含 backdrop-filter: blur(10px) 和 -webkit-backdrop-filter: blur(10px)——都没问题。但它还写了 glass-opacity: 0.8 和 border-glass: 1px solid rgba(255,255,255,0.3)。这两个属性在 CSS 规范中不存在。浏览器静默忽略它们,效果看起来"还行",直到 QA 在一个老版本 Safari 上报 Bug:"卡片背景变成了纯白色。"
AI 生成 UI 代码的可靠性问题可以分为三类:幻觉(生成了不存在的属性/API)、格式错误(JSX 未闭合、CSS 语法错误)、逻辑缺陷(功能上看起来对但边界条件错误)。三者的危害程度递增。
幻觉最容易发现但最影响信任——用户可能会想"这个属性为什么不起作用?"格式错误最容易被构建工具拦截——TypeScript 编译器或 ESLint 会在构建阶段直接报错。逻辑缺陷最危险——它们能通过编译和 Lint,静默地潜入生产环境。
二、三类错误的全景与应对策略
三、生产级代码:AI 代码可靠性检查工具链
/**
* AI 生成 UI 代码的可靠性检查器
*
* 三条防线:
* 1. 编译时:类型检查 + Lint + CSS 属性验证
* 2. 快照时:截图对比 + 布局验证
* 3. 运行时:行为测试 + 可访问性检查
*/
// 虚构 CSS 属性黑名单
const FAKE_CSS_PROPERTIES = new Set([
'glass-opacity', 'border-glass', 'frost-blur',
'neo-shadow', 'morph-radius', 'ai-generated-style',
'dynamic-theme-color', 'auto-layout-mode',
]);
// 虚构 HTML 属性黑名单
const FAKE_HTML_ATTRIBUTES = new Set([
'glassmorphism', 'neumorphic', 'aos-animation',
'parallax-speed', 'custom-cursor-image',
]);
/**
* CSS 属性合法性检查器
*
* 检测 AI 生成的 CSS 中是否存在虚构属性
* 策略:白名单 + CSS.supports() 验证
*/
class CSSPropertyValidator {
// 已知合法的 CSS 属性白名单(从规范中提取)
private readonly VALID_PROPERTIES: Set<string>;
constructor() {
// 简化版本:使用 CSS.supports 实时验证
// 生产环境用完整的属性数据库
this.VALID_PROPERTIES = new Set();
}
/**
* 验证一段 CSS 代码中的所有属性是否合法
*
* 返回两个列表:合法属性和可疑属性
*/
validate(cssCode: string): {
valid: string[];
suspicious: {
property: string;
reason: 'not_in_spec' | 'vendor_specific' | 'deprecated';
suggestion?: string;
}[];
} {
const suspicious: any[] = [];
const valid: string[] = [];
// 提取所有 CSS 属性声明
const declarationRegex = /([a-zA-Z-]+)\\s*:/g;
let match: RegExpExecArray | null;
while ((match = declarationRegex.exec(cssCode)) !== null) {
const property = match[1].toLowerCase();
// 跳过已知合法属性
if (this.isValidProperty(property)) {
valid.push(property);
continue;
}
// 跳过 CSS 变量
if (property.startsWith('–')) {
valid.push(property);
continue;
}
// 检查是否是 vendor prefix
if (property.startsWith('-webkit-') || property.startsWith('-moz-')) {
const unprefixed = property.replace(/^-(webkit|moz|ms|o)-/, '');
if (this.isValidProperty(unprefixed)) {
suspicious.push({
property,
reason: 'vendor_specific',
suggestion: `考虑同时添加标准属性 ${unprefixed}`,
});
continue;
}
}
// 剩余的都是可疑属性
suspicious.push({
property,
reason: 'not_in_spec',
suggestion: this.suggestAlternative(property),
});
}
return { valid, suspicious };
}
/**
* 使用 CSS.supports() 检测属性合法性
*/
private isValidProperty(property: string): boolean {
// 在已知白名单中
if (this.VALID_PROPERTIES.has(property)) return true;
// 在已知黑名单中
if (FAKE_CSS_PROPERTIES.has(property)) return false;
// 使用浏览器 API 检测
try {
const supported = CSS.supports(property, 'initial');
if (supported) {
this.VALID_PROPERTIES.add(property);
}
return supported;
} catch {
return false;
}
}
/**
* 为虚构属性提供替代建议
*/
private suggestAlternative(fakeProp: string): string | undefined {
const alternatives: Record<string, string> = {
'glass-opacity': '使用 backdrop-filter: blur() + background: rgba() 实现',
'border-glass': '使用 border: 1px solid rgba(255,255,255,0.3)',
'frost-blur': '使用 backdrop-filter: blur()',
'neo-shadow': '使用 box-shadow 组合模拟新拟态',
};
return alternatives[fakeProp];
}
}
/**
* UI 快照对比器
*
* 确保 AI 重新生成后的界面与上次生成的差异在可接受范围内
*/
class SnapshotComparator {
/**
* 对比两个元素的布局结构
*
* 检查项:
* – 元素数量变化 < 20%
* – 关键元素存在(header、main、footer 必须有)
* – 无意外的大面积空白区域
*/
compareLayout(
oldHtml: string,
newHtml: string
): {
similarity: number;
missingElements: string[];
extraElements: string[];
isAcceptable: boolean;
} {
const oldElements = this.extractSemanticElements(oldHtml);
const newElements = this.extractSemanticElements(newHtml);
const missingElements = oldElements.filter(
(el) => !newElements.includes(el)
);
const extraElements = newElements.filter(
(el) => !oldElements.includes(el)
);
const intersection = oldElements.filter((el) =>
newElements.includes(el)
);
const similarity = intersection.length / oldElements.length;
return {
similarity,
missingElements,
extraElements,
isAcceptable: similarity > 0.7 && missingElements.length < 3,
};
}
/**
* 提取语义元素标签名
*/
private extractSemanticElements(html: string): string[] {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const elements: string[] = [];
doc.querySelectorAll(
'header, nav, main, section, article, aside, footer, button, input, select, table, form'
).forEach((el) => {
elements.push(el.tagName.toLowerCase());
});
return elements;
}
}
/**
* AI 代码修复流水线
*
* 集成所有检查器,提供完整的验证→反馈→重试流程
*/
class AICodeRepairPipeline {
private cssValidator = new CSSPropertyValidator();
private snapshotComparator = new SnapshotComparator();
/**
* 验证一段 AI 生成的代码
* 返回通过/失败及详细的错误信息
*/
validate(code: {
html?: string;
css?: string;
tsx?: string;
componentName: string;
}): {
passed: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
fixedCode?: typeof code;
} {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
const fixedCode = { …code };
// 1. CSS 属性检查
if (code.css) {
const { suspicious } = this.cssValidator.validate(code.css);
for (const s of suspicious) {
if (s.reason === 'not_in_spec') {
errors.push({
type: 'fake_css_property',
location: 'css',
message: `虚构 CSS 属性: ${s.property}`,
suggestion: s.suggestion,
});
} else {
warnings.push({
type: 'vendor_prefix',
message: `仅 vendor 前缀: ${s.property}, ${s.suggestion}`,
});
}
}
}
// 2. HTML/JSX 结构检查
if (code.tsx || code.html) {
const html = code.tsx || '';
// 检查 JSX 闭合
const unclosed = this.findUnclosedTags(html);
if (unclosed.length > 0) {
errors.push({
type: 'unclosed_tag',
location: 'tsx',
message: `未闭合标签: ${unclosed.join(', ')}`,
});
}
// 检查虚构属性
const fakeAttrs = this.findFakeAttributes(html);
if (fakeAttrs.length > 0) {
errors.push({
type: 'fake_html_attribute',
location: 'tsx',
message: `虚构 HTML 属性: ${fakeAttrs.join(', ')}`,
});
}
// 检查事件处理函数引用
const undefinedHandlers = this.findUndefinedHandlers(html);
if (undefinedHandlers.length > 0) {
errors.push({
type: 'undefined_handler',
location: 'tsx',
message: `未定义的事件处理函数: ${undefinedHandlers.join(', ')}`,
});
}
}
return {
passed: errors.length === 0,
errors,
warnings,
fixedCode: errors.length > 0 ? undefined : fixedCode,
};
}
/**
* 查找未闭合的 JSX 标签
*/
private findUnclosedTags(code: string): string[] {
const unclosed: string[] = [];
const selfClosing = new Set([
'br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col',
'embed', 'source', 'track', 'wbr',
]);
// 简化版检查:统计每种标签的开始和结束数量
const tagCounts = new Map<string, number>();
const tagRegex = /<\\/?([a-zA-Z][a-zA-Z0-9]*)/g;
let match;
while ((match = tagRegex.exec(code)) !== null) {
const tag = match[1].toLowerCase();
if (selfClosing.has(tag)) continue;
const isClosing = match[0].startsWith('</');
tagCounts.set(tag, (tagCounts.get(tag) || 0) + (isClosing ? -1 : 1));
}
for (const [tag, count] of tagCounts) {
if (count !== 0) unclosed.push(tag);
}
return unclosed;
}
/**
* 查找虚构 HTML 属性
*/
private findFakeAttributes(html: string): string[] {
const found: string[] = [];
for (const attr of FAKE_HTML_ATTRIBUTES) {
if (html.includes(attr)) found.push(attr);
}
return found;
}
/**
* 查找引用了但未定义的事件处理函数
*/
private findUndefinedHandlers(code: string): string[] {
const handlers: string[] = [];
const handlerRegex = /on(?:Click|Change|Submit|Blur|Focus|KeyDown|KeyUp)\\s*=\\s*\\{(\\w+)\\}/g;
let match;
while ((match = handlerRegex.exec(code)) !== null) {
const handlerName = match[1];
// 检查函数是否在代码中定义
if (!code.includes(`const ${handlerName}`) && !code.includes(`function ${handlerName}`)) {
handlers.push(handlerName);
}
}
return handlers;
}
}
// 类型定义
interface ValidationError {
type: string;
location: string;
message: string;
suggestion?: string;
}
interface ValidationWarning {
type: string;
message: string;
}
四、边界分析
检查器本身的假阳性:CSS 属性白名单可能会漏掉一些少用的合法属性(如 scroll-timeline、animation-timeline 等较新的规范属性),将它们标记为"虚构"。需要定期更新属性数据库。
CSS.supports() 的局限性:浏览器对 CSS.supports() 的支持不完整,某些属性即使合法也可能返回 false。需要维护一份手动维护的属性白名单作为补充。
自动修复的边界:类型错误和语法错误可以自动修复(告诉 AI "修复这些错误"并附带错误信息),但逻辑错误(如状态管理中的 Bug)几乎无法自动修复——它们需要理解业务逻辑。

