王者荣耀壁纸
16.25M · 2026-04-06
上一篇文章我们聊了用 AI Agent 生成 React 组件,从产品需求到可运行代码,效率提升了 3-5 倍。但是,代码能跑和代码质量高是两回事。
生产环境的代码需要经过严格的审查和测试。传统上,这两个环节都是人力密集型工作:
这篇文章我们要解决的问题是:AI Agent 能不能承担这些质量保障工作?
答案是:能,而且效果超预期。
让我们诚实一点:
结果就是:要么 Review 成本高昂(每个 PR 30 分钟+),要么流于形式(点个 LGTM 了事)。
AI 不会累,不会情绪化,而且可以同时检查几十个维度。更重要的是:它 24/7 在线,成本可控。
但关键是怎么集成到工作流里。我的方案是:Git Hook + AI API。
我们用 Husky 在 pre-commit 阶段触发 AI 代码审查。完整实现如下:
npm install -D husky lint-staged
npx husky install
在 scripts/ai-code-review.js:
#!/usr/bin/env node
import { execSync } from 'child_process';
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs';
import path from 'path';
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
// 获取暂存区的文件变更
function getStagedDiff() {
try {
return execSync('git diff --cached', { encoding: 'utf-8' });
} catch (error) {
console.error('无法获取 git diff:', error.message);
return '';
}
}
// AI 审查 Prompt(这是关键)
const REVIEW_PROMPT = `你是一个资深前端工程师,负责代码审查。请审查以下 git diff,关注:
**性能问题:**
- 不必要的重渲染(缺少 useMemo、useCallback)
- 大数组操作没有做虚拟化
- 图片/资源未优化
- 阻塞主线程的同步操作
**安全隐患:**
- XSS 风险(dangerouslySetInnerHTML 未做转义)
- CSRF 防护缺失
- 敏感信息泄露(API key、token 硬编码)
- eval() 或 Function() 构造器的使用
**最佳实践:**
- 组件职责是否单一
- props 类型检查(TypeScript / PropTypes)
- 错误边界处理
- 可访问性(aria 属性、语义化标签)
- 命名规范和代码风格
**边界情况:**
- 空数组、null、undefined 的处理
- 异步操作的错误处理
- 网络请求失败的降级方案
请按以下格式输出:
## Critical Issues(阻断性问题)
- [文件名:行号] 问题描述 + 修复建议
## ️ Warnings(需要关注)
- [文件名:行号] 问题描述 + 优化建议
## Good Practices(做得好的地方)
- 简要列出亮点
## Summary
- 总体评分(1-10)
- 是否建议合并
如果没有发现任何问题,输出 " LGTM - 代码质量良好,建议合并"`;
async function reviewCode(diff) {
if (!diff || diff.trim().length === 0) {
console.log(' 没有代码变更需要审查');
return { shouldBlock: false, report: '' };
}
console.log(' AI 正在审查代码...\n');
try {
const message = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 4096,
messages: [{
role: 'user',
content: `${REVIEW_PROMPT}\n\n## Git Diff:\n\`\`\`diff\n${diff}\n\`\`\``
}]
});
const report = message.content[0].text;
// 保存审查报告
const reportPath = path.join(process.cwd(), '.ai-review-report.md');
fs.writeFileSync(reportPath, report);
console.log(report);
console.log(`\n 完整报告已保存到: ${reportPath}\n`);
// 判断是否有阻断性问题
const hasCriticalIssues = report.includes('## Critical Issues')
&& !report.match(/## Critical Issues\s*\n\s*无/);
return {
shouldBlock: hasCriticalIssues,
report
};
} catch (error) {
console.error(' AI 审查失败:', error.message);
// 降级策略:AI 失败不阻断提交
console.log('️ AI 服务不可用,跳过审查(降级模式)');
return { shouldBlock: false, report: '' };
}
}
async function main() {
const diff = getStagedDiff();
const { shouldBlock, report } = await reviewCode(diff);
if (shouldBlock) {
console.error('\n 发现阻断性问题,请修复后再提交\n');
process.exit(1);
} else {
console.log('\n 代码审查通过\n');
process.exit(0);
}
}
main();
在 .husky/pre-commit:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 运行 AI 代码审查
node scripts/ai-code-review.js
# 如果审查通过,继续执行 lint-staged
npx lint-staged
# .env
ANTHROPIC_API_KEY=your_api_key_here
上周我提交了一个表单组件,AI 审查发现了一个我完全没注意到的问题:
// 我的原始代码
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button type="submit">搜索</button>
</form>
);
}
AI 的审查报告:
## ️ Warnings
- [SearchInput.jsx:7] **可访问性问题**:input 缺少 label 或 aria-label,
屏幕阅读器用户无法理解这个输入框的用途。
建议修复:
<label htmlFor="search-input" className="sr-only">搜索</label>
<input
id="search-input"
aria-label="搜索内容"
...
/>
- [SearchInput.jsx:12] **用户体验**:提交空字符串会触发无意义的搜索。
建议在 handleSubmit 中添加校验:
if (query.trim().length === 0) return;
## Summary
- 总体评分:7/10
- 功能正常,但可访问性和边界情况处理需要改进
- 建议修复后合并
这是一个典型的"功能能跑,但不够专业"的例子。人工审查很可能会漏掉可访问性问题,但 AI 每次都会检查。
老实说:写测试太枯燥了。
你写完一个复杂组件,兴奋地想看效果,结果还要花同样的时间写一堆重复的测试用例。最后就变成了:
结果就是测试覆盖率 30%,然后生产环境各种边界 case 爆炸。
AI 看代码比我们快,而且它知道所有的测试模式。我的做法是:
创建脚本 scripts/generate-tests.js:
#!/usr/bin/env node
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs';
import path from 'path';
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
const TEST_GENERATION_PROMPT = `你是一个测试工程师,擅长编写高质量的前端测试。
请为以下代码生成完整的测试用例,使用 Jest + React Testing Library。
**要求:**
1. 覆盖所有主要功能路径
2. 包含边界情况:空数组、null、undefined、错误数据
3. 测试用户交互:点击、输入、提交
4. 测试异步逻辑:API 调用成功和失败的情况
5. 测试可访问性:aria 属性、键盘导航
6. 使用语义化查询(getByRole > getByTestId)
**输出格式:**
- 完整可运行的测试文件
- 包含必要的 import
- 每个测试用例有清晰的描述
- 使用 describe 分组组织测试
现在请为以下代码生成测试:`;
async function generateTestForFile(filePath) {
const sourceCode = fs.readFileSync(filePath, 'utf-8');
const fileName = path.basename(filePath, path.extname(filePath));
console.log(` 正在为 ${fileName} 生成测试...\n`);
try {
const message = await client.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 8192,
messages: [{
role: 'user',
content: `${TEST_GENERATION_PROMPT}\n\n\`\`\`jsx\n${sourceCode}\n\`\`\``
}]
});
const testCode = message.content[0].text
.replace(/^\`\`\`[a-z]*\n/, '')
.replace(/\n\`\`\`$/, '');
// 确定输出路径
const testFilePath = filePath.replace(/\.(jsx?|tsx?)$/, '.test.$1');
fs.writeFileSync(testFilePath, testCode);
console.log(` 测试已生成: ${testFilePath}\n`);
return testFilePath;
} catch (error) {
console.error(` 生成失败: ${error.message}`);
throw error;
}
}
// 示例:为指定文件生成测试
const targetFile = process.argv[2];
if (!targetFile) {
console.error('用法: node generate-tests.js <文件路径>');
process.exit(1);
}
generateTestForFile(targetFile);
我有一个 UserProfile.jsx 组件:
import { useState, useEffect } from 'react';
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('加载失败');
const data = await res.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div role="status">加载中...</div>;
if (error) return <div role="alert">{error}</div>;
if (!user) return null;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
运行 node scripts/generate-tests.js src/components/UserProfile.jsx,AI 生成的测试:
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
// Mock fetch
global.fetch = jest.fn();
describe('UserProfile', () => {
beforeEach(() => {
fetch.mockClear();
});
describe('加载状态', () => {
test('初始渲染时显示加载提示', () => {
fetch.mockImplementation(() => new Promise(() => {})); // 永不 resolve
render(<UserProfile userId="123" />);
expect(screen.getByRole('status')).toHaveTextContent('加载中...');
});
});
describe('成功场景', () => {
test('成功获取用户数据后显示用户信息', async () => {
const mockUser = {
id: '123',
name: '张三',
email: 'zhang@example.com'
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByText('张三')).toBeInTheDocument();
});
expect(screen.getByText('zhang@example.com')).toBeInTheDocument();
expect(fetch).toHaveBeenCalledWith('/api/users/123');
});
});
describe('错误处理', () => {
test('API 返回错误时显示错误信息', async () => {
fetch.mockResolvedValueOnce({
ok: false
});
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('加载失败');
});
});
test('网络请求失败时显示错误信息', async () => {
fetch.mockRejectedValueOnce(new Error('网络错误'));
render(<UserProfile userId="456" />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('网络错误');
});
});
});
describe('边界情况', () => {
test('用户数据为 null 时不渲染任何内容', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => null
});
const { container } = render(<UserProfile userId="789" />);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
});
test('userId 变化时重新获取数据', async () => {
fetch.mockResolvedValue({
ok: true,
json: async () => ({ id: '1', name: 'User 1', email: 'user1@example.com' })
});
const { rerender } = render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('User 1')).toBeInTheDocument();
});
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: '2', name: 'User 2', email: 'user2@example.com' })
});
rerender(<UserProfile userId="2" />);
await waitFor(() => {
expect(screen.getByText('User 2')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledTimes(2);
});
});
describe('可访问性', () => {
test('加载状态使用正确的 ARIA role', () => {
fetch.mockImplementation(() => new Promise(() => {}));
render(<UserProfile userId="123" />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
test('错误信息使用正确的 ARIA role', async () => {
fetch.mockRejectedValueOnce(new Error('测试错误'));
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
});
});
注意 AI 做了什么:
userId 变化时的重新获取逻辑(这个我自己写可能会漏)这个测试我自己写至少要 30 分钟,AI 生成只要 10 秒,Token 成本 ¥0.15。
对于端到端测试,我用类似的思路生成 Playwright 脚本。生成的测试示例:
import { test, expect } from '@playwright/test';
test.describe('用户登录流程', () => {
test('成功登录并跳转到首页', async ({ page }) => {
await page.goto('/login');
// 填写表单
await page.getByLabel('用户名').fill('testuser');
await page.getByLabel('密码').fill('password123');
// 提交
await page.getByRole('button', { name: '登录' }).click();
// 等待跳转
await expect(page).toHaveURL('/dashboard');
// 验证用户信息
await expect(page.getByText('欢迎, testuser')).toBeVisible();
});
test('用户名为空时显示错误', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('请输入用户名')).toBeVisible();
await expect(page).toHaveURL('/login'); // 未跳转
});
test('密码错误时显示提示', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('用户名').fill('testuser');
await page.getByLabel('密码').fill('wrongpassword');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByRole('alert')).toHaveText('用户名或密码错误');
});
test('网络错误时显示友好提示', async ({ page }) => {
// 模拟网络错误
await page.route('/api/auth/login', route => route.abort());
await page.goto('/login');
await page.getByLabel('用户名').fill('testuser');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('网络错误,请稍后重试')).toBeVisible();
});
});
我在一个中型项目(15k 行代码,30+ 组件)上用了一个月 AI 代码审查 + 测试生成,数据如下:
| 指标 | 使用前 | 使用后 | 提升 |
|---|---|---|---|
| 单元测试覆盖率 | 32% | 78% | +144% |
| E2E 测试场景数 | 5 | 23 | +360% |
| 发现的边界 case bug | - | 17 个 | - |
人工成本:
AI 成本:
一个月数据(50 个 PR,30 个组件):
时间节省:
AI 发现的问题类型分布:
最有价值的发现:一个组件在处理大数组(10k+ 项)时会卡死,AI 建议加虚拟滚动,修复后性能提升 95%。
我的 Prompt 迭代了 5 个版本才稳定。关键点:
最佳实践:
AI 代码审查和测试生成已经证明了价值,但还有两个问题需要解决:
下一篇文章我们会深入讨论:
敬请期待。