AI工程师的代码审查清单:AI项目的质量门禁标准
2026/10/7大约 14 分钟代码审查质量标准AI项目Java最佳实践
AI工程师的代码审查清单:AI项目的质量门禁标准
开篇故事:周建的"代码审查噩梦"
2026年1月,某AI创业公司的CTO周建经历了一次刻骨铭心的教训。
公司产品是一个AI简历优化服务,月活用户8万,正准备融资。一位投资人做技术尽调时,他的技术顾问花了3天时间Review了核心代码,反馈的结论让周建当场冷汗:
- API Key硬编码在代码里,且已提交到公开GitHub仓库(历史记录中存在)
- 提示词注入漏洞:用户输入直接拼接进提示词,可以绕过内容审核
- 成本失控风险:没有任何Token使用限制,一个恶意用户可以用免费账号耗尽公司的API预算
- 没有AI输出校验:简历AI直接返回原始输出,曾经输出过不雅内容(有客服记录为证)
- 关键提示词裸露:核心差异化的提示词工程没有任何保护,任何人都可以复制
融资黄了。损失的不只是那一笔投资,还有公司重新构建信任的时间。
周建事后整理出了一份AI项目专属代码审查清单,包含30个检查项。本文将这份清单完整分享给你。
TL;DR
- AI项目的代码审查需要覆盖普通代码看不到的维度
- 五大核心维度:提示词质量/错误处理/成本意识/安全性/可观测性
- 自动化工具:将检查清单集成到CI/CD流水线,自动拦截问题代码
- 金融/医疗等高风险场景:需要额外的AI输出验证层
一、为什么AI项目需要专属代码审查标准?
1.1 AI代码的新风险维度
传统代码审查关注:逻辑正确性、性能、安全性(SQL注入/XSS等)
AI项目额外需要关注:
AI项目专属风险:
├── 提示词工程风险
│ ├── 提示词注入攻击
│ ├── 提示词泄漏(商业机密)
│ └── 提示词可测试性差
├── 成本风险
│ ├── 无限制的Token消耗
│ ├── 调试/测试代码遗留到生产
│ └── 昂贵模型的不必要使用
├── AI输出风险
│ ├── 有害内容输出
│ ├── 错误信息(幻觉)输出
│ └── 输出格式不稳定导致解析崩溃
├── 可测试性风险
│ ├── AI调用没有Mock,测试不可靠
│ └── 非确定性输出导致断言失败
└── 合规风险
├── 用户数据发送给第三方
└── 模型版本变更影响合规二、完整代码审查清单(30项)
2.1 维度一:提示词质量(8项)
// PromptQualityChecker.java - 提示词审查自动化检测
/**
* 检查项 P-001: 提示词不应硬编码在业务逻辑中
*
* 错误示例:
*/
@Service
public class BadResumeService {
public String improveResume(String resumeContent) {
// ❌ 提示词硬编码,无法版本管理,无法A/B测试
String prompt = "你是简历优化专家,请优化以下简历:" + resumeContent;
return chatClient.prompt().user(prompt).call().content();
}
}
/**
* 正确示例:提示词外部化
*/
@Service
public class GoodResumeService {
@Value("${ai.prompts.resume-improve}") // 从配置文件读取
private String resumeImproveTemplate;
public String improveResume(String resumeContent) {
// ✅ 提示词从配置外部化,可独立版本管理
String prompt = resumeImproveTemplate
.replace("{{RESUME_CONTENT}}", sanitizeInput(resumeContent));
return chatClient.prompt().user(prompt).call().content();
}
}提示词审查清单(8项):
| 检查项 | 说明 | 严重性 |
|---|---|---|
| P-001 | 提示词是否从配置中心/数据库加载,而非硬编码 | HIGH |
| P-002 | 用户输入是否经过净化再拼入提示词 | CRITICAL |
| P-003 | 提示词长度是否有上限检查(防止过长截断) | MEDIUM |
| P-004 | 系统提示词是否有访问控制(防泄漏) | HIGH |
| P-005 | 提示词是否有版本标识(追踪变更影响) | MEDIUM |
| P-006 | 关键提示词是否有A/B测试机制 | LOW |
| P-007 | 提示词变更是否触发自动评测 | HIGH |
| P-008 | 是否有提示词注入防护(检测并拒绝恶意输入) | CRITICAL |
2.2 维度二:错误处理(7项)
/**
* 检查项 E-001: AI调用必须有完整的异常处理
*/
// ❌ 错误:异常直接向上传播,用户看到技术错误
public String answerQuestion(String question) {
return chatClient.prompt()
.user(question)
.call()
.content(); // 如果API超时,会向上抛出未处理异常
}
// ✅ 正确:完整的异常处理 + 降级响应
public String answerQuestion(String question) {
try {
return chatClient.prompt()
.user(question)
.call()
.content();
} catch (TimeoutException e) {
log.warn("AI响应超时 [question={}]: {}", question.substring(0, 50), e.getMessage());
return "AI服务响应较慢,请稍后重试。如需紧急帮助,请联系客服。";
} catch (RateLimitException e) {
log.error("AI API限流 [question={}]", question.substring(0, 50));
metricsService.recordRateLimit();
return "当前使用人数较多,请等待片刻后重试。";
} catch (Exception e) {
log.error("AI调用异常", e);
alertService.sendAiErrorAlert(e);
return "抱歉,AI服务暂时不可用,已通知技术团队处理。";
}
}错误处理审查清单(7项):
| 检查项 | 说明 | 严重性 |
|---|---|---|
| E-001 | 是否处理了超时异常(TimeoutException) | CRITICAL |
| E-002 | 是否处理了限流异常(429/RateLimitException) | HIGH |
| E-003 | 是否处理了API不可用(500/ServiceUnavailable) | HIGH |
| E-004 | JSON解析失败是否有兜底处理(AI输出格式不稳定) | HIGH |
| E-005 | 异常日志是否包含足够信息(不含敏感数据) | MEDIUM |
| E-006 | 是否有用户友好的降级响应(不暴露技术细节) | MEDIUM |
| E-007 | 重试逻辑是否合理(次数/间隔/幂等性检查) | HIGH |
2.3 维度三:成本意识(6项)
/**
* 检查项 C-001: 所有AI调用必须有Token预算限制
*/
// ❌ 错误:无限制输入,可能导致超额消费
public String summarize(String content) {
return chatClient.prompt()
.user("请总结以下内容:" + content) // content可能有100万字符
.call()
.content();
}
// ✅ 正确:输入截断 + 输出限制
@Value("${ai.cost.max-input-chars:2000}")
private int maxInputChars;
@Value("${ai.cost.max-output-tokens:500}")
private int maxOutputTokens;
public String summarize(String content) {
// 输入截断
String truncatedContent = content.length() > maxInputChars
? content.substring(0, maxInputChars) + "...[截断]"
: content;
return ChatClient.builder(chatModel)
.build()
.prompt()
.user("请总结以下内容:" + truncatedContent)
.options(OpenAiChatOptions.builder()
.withMaxTokens(maxOutputTokens) // 限制输出Token数
.build())
.call()
.content();
}
/**
* 检查项 C-002: 高频接口必须有缓存
*/
// ❌ 错误:每次请求都调用AI,重复的问题也付费
public String getFaqAnswer(String question) {
return chatClient.prompt().user(question).call().content();
}
// ✅ 正确:语义缓存减少重复调用
@Service
public class CachedFaqService {
private final SemanticCache semanticCache;
private final ChatClient chatClient;
public String getFaqAnswer(String question) {
// 先查语义缓存(相似问题返回已有答案)
return semanticCache.getOrCompute(question, () ->
chatClient.prompt().user(question).call().content(),
0.90 // 相似度阈值90%
);
}
}成本意识审查清单(6项):
| 检查项 | 说明 | 严重性 |
|---|---|---|
| C-001 | 用户输入长度是否有上限 | HIGH |
| C-002 | max_tokens是否设置合理上限 | HIGH |
| C-003 | 高频相同/相似请求是否有缓存 | HIGH |
| C-004 | 是否有用户级别的调用频率限制 | HIGH |
| C-005 | 是否使用了性价比合适的模型(不是所有场景都需要最贵的模型) | MEDIUM |
| C-006 | 是否有月度/日度Token消费预警 | MEDIUM |
2.4 维度四:安全性(6项)
/**
* 检查项 S-001: API Key不能出现在代码中(包括注释和测试代码)
*/
// ❌ 严重错误:API Key硬编码
@Service
public class BadAiService {
private static final String API_KEY = "sk-proj-AbCdEf..."; // 千万不要这样!
public String chat(String message) {
OpenAiApi api = new OpenAiApi("https://api.openai.com", API_KEY);
// ...
}
}
// ✅ 正确:通过环境变量/Secret Manager读取
@Service
public class SecureAiService {
@Value("${spring.ai.openai.api-key}") // 从配置读取,配置来自K8s Secret
private String apiKey; // 不要在类中打印这个值
}
/**
* 检查项 S-002: 提示词注入防护
*/
@Component
public class PromptInjectionGuard {
// 常见的提示词注入特征
private static final List<String> INJECTION_PATTERNS = List.of(
"ignore previous instructions",
"forget your system prompt",
"你现在是",
"忽略之前的所有指令",
"system:",
"</s>", // 一些模型的特殊token
"{{", // 模板注入
"}}"
);
public ValidationResult validate(String userInput) {
if (userInput == null || userInput.trim().isEmpty()) {
return ValidationResult.invalid("输入不能为空");
}
String lowerInput = userInput.toLowerCase();
for (String pattern : INJECTION_PATTERNS) {
if (lowerInput.contains(pattern.toLowerCase())) {
log.warn("检测到潜在的提示词注入: [{}]", userInput.substring(0, 50));
return ValidationResult.invalid("输入包含不允许的内容");
}
}
return ValidationResult.valid();
}
}
/**
* 检查项 S-003: AI输出必须经过内容安全过滤
*/
@Component
public class ContentSafetyFilter {
private final OpenAIModerationService moderationService;
public FilterResult filter(String aiOutput) {
// 检查AI输出是否包含有害内容
ModerationResult moderation = moderationService.moderate(aiOutput);
if (moderation.isFlagged()) {
log.error("AI输出触发内容安全审核: categories={}",
moderation.getFlaggedCategories());
return FilterResult.blocked("内容不符合安全标准");
}
return FilterResult.passed(aiOutput);
}
}安全性审查清单(6项):
| 检查项 | 说明 | 严重性 |
|---|---|---|
| S-001 | API Key是否通过环境变量/Secret Manager管理 | CRITICAL |
| S-002 | 是否有提示词注入防护 | CRITICAL |
| S-003 | AI输出是否经过内容安全过滤 | CRITICAL |
| S-004 | 用户数据是否在发送给AI之前做脱敏处理 | HIGH |
| S-005 | 日志中是否包含了API Key或用户隐私数据 | HIGH |
| S-006 | 系统提示词是否有访问控制,防止泄漏 | HIGH |
2.5 维度五:可观测性(3项)
/**
* 检查项 O-001: 所有AI调用必须有追踪和指标记录
*/
// ❌ 错误:AI调用没有任何可观测性
public String chat(String message) {
return chatClient.prompt().user(message).call().content();
}
// ✅ 正确:完整的可观测性
@Service
@Slf4j
public class ObservableAiService {
private final ChatClient chatClient;
private final MeterRegistry meterRegistry;
public String chat(String message, String userId) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
// 带追踪信息的调用
ChatResponse response = chatClient.prompt()
.advisors(advisorSpec -> advisorSpec
.param("user_id", userId) // 注入用户ID到追踪
.param("request_id", UUID.randomUUID().toString()))
.user(message)
.call()
.chatResponse();
String content = response.getResult().getOutput().getContent();
// 记录Token消耗
Usage usage = response.getMetadata().getUsage();
meterRegistry.counter("ai.tokens.input",
"user_id", userId, "model", "gpt-4o-mini")
.increment(usage.getPromptTokens());
meterRegistry.counter("ai.tokens.output",
"user_id", userId, "model", "gpt-4o-mini")
.increment(usage.getGenerationTokens());
// 记录延迟
sample.stop(meterRegistry.timer("ai.chat.latency",
"success", "true", "model", "gpt-4o-mini"));
return content;
} catch (Exception e) {
sample.stop(meterRegistry.timer("ai.chat.latency",
"success", "false", "model", "gpt-4o-mini"));
throw e;
}
}
}三、自动化代码审查工具
3.1 自定义SpotBugs/PMD规则
<!-- pmd-ai-rules.xml - PMD自定义规则 -->
<?xml version="1.0"?>
<ruleset name="AI-Specific Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>AI项目专属代码质量规则</description>
<!-- 规则1:检测硬编码的API Key -->
<rule name="HardcodedApiKey"
language="java"
message="检测到可能硬编码的API Key"
class="net.sourceforge.pmd.lang.rule.XPathRule">
<description>API Key不应该硬编码在代码中</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value>
//StringLiteral[matches(@Image, 'sk-[A-Za-z0-9]{20,}')]
</value>
</property>
</properties>
</rule>
<!-- 规则2:AI调用必须有try-catch -->
<rule name="AiCallWithoutExceptionHandling"
language="java"
message="AI服务调用应该有异常处理"
class="net.sourceforge.pmd.lang.rule.XPathRule">
<description>对ChatClient的调用应包含异常处理逻辑</description>
<priority>2</priority>
<properties>
<property name="xpath">
<value>
//MethodCall[./PrimaryExpression/Name/@Image = 'chatClient']
[not(ancestor::TryStatement)]
</value>
</property>
</properties>
</rule>
</ruleset>3.2 GitHub Actions集成AI代码审查
# .github/workflows/ai-code-review.yml
name: AI Code Quality Gate
on:
pull_request:
branches: [main, develop]
jobs:
ai-code-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 获取完整历史(检测API Key历史记录)
- name: Check for API Keys in Code
run: |
# 检测代码中的API Key模式
if git log --all --full-history -- '*.java' | grep -E 'sk-[A-Za-z0-9]{20,}'; then
echo "❌ 检测到API Key在代码历史中!"
exit 1
fi
# 检测当前代码
if grep -rn --include="*.java" -E '(sk-|api[_-]?key[_-]?=\s*["'"'"'][A-Za-z0-9]{20})' src/; then
echo "❌ 检测到硬编码的API Key!"
exit 1
fi
echo "✅ API Key检测通过"
- name: Check AI Error Handling
run: |
# 检测没有try-catch的AI调用(简单检测)
UNCAUGHT=$(grep -rn --include="*.java" "chatClient.prompt()" src/main/ | \
while read line; do
file=$(echo $line | cut -d: -f1)
linenum=$(echo $line | cut -d: -f2)
# 检查前后10行是否有try
if ! sed -n "$((linenum-10)),$((linenum+10))p" $file | grep -q "try {"; then
echo "$line"
fi
done)
if [ -n "$UNCAUGHT" ]; then
echo "⚠️ 以下AI调用可能缺少异常处理:"
echo "$UNCAUGHT"
# 这里设为警告,不阻断(可能false positive)
fi
- name: Run PMD with AI Rules
run: |
mvn pmd:check -Dpmd.rulesets=pmd-ai-rules.xml
- name: AI-Powered Code Review
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// 调用AI服务对PR进行智能代码审查
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
// 过滤Java文件
const javaFiles = files.filter(f => f.filename.endsWith('.java'));
for (const file of javaFiles.slice(0, 5)) { // 最多审查5个文件
const patch = file.patch;
// 使用OpenAI审查代码变更
const review = await fetch('${{ vars.OPENAI_API_URL }}/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${{ secrets.OPENAI_API_KEY }}'
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{
role: 'user',
content: `Review this Java code change for AI project quality issues:\n${patch}\n\nCheck for: 1) Missing error handling 2) Potential prompt injection 3) Missing rate limiting 4) Cost control issues. Reply in Chinese. Be concise.`
}],
max_tokens: 500
})
}).then(r => r.json());
const reviewText = review.choices[0].message.content;
// 发布PR评论
await github.rest.pulls.createReviewComment({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: `🤖 **AI代码审查**:\n\n${reviewText}`,
path: file.filename,
position: 1
});
}四、高风险场景的额外审查维度
4.1 金融/医疗场景的额外检查
// HighRiskAiOutputValidator.java
@Component
public class HighRiskAiOutputValidator {
// 金融场景:不允许给出具体投资建议
private static final List<String> FINANCE_FORBIDDEN_PATTERNS = List.of(
"建议买入", "建议卖出", "必涨", "稳赚不赔",
"guaranteed return", "certain profit"
);
// 医疗场景:不允许给出诊断结论
private static final List<String> MEDICAL_FORBIDDEN_PATTERNS = List.of(
"你得了", "确诊为", "可以确定是",
"不需要看医生", "自行服用"
);
public ValidationResult validateOutput(String output, AppScenario scenario) {
List<String> forbiddenPatterns = switch (scenario) {
case FINANCE -> FINANCE_FORBIDDEN_PATTERNS;
case MEDICAL -> MEDICAL_FORBIDDEN_PATTERNS;
default -> Collections.emptyList();
};
for (String pattern : forbiddenPatterns) {
if (output.contains(pattern)) {
log.error("AI输出违反{}场景安全规则: 包含'{}'", scenario, pattern);
return ValidationResult.blocked("输出内容违反行业规定");
}
}
// 还需要添加法律免责声明
if (scenario == AppScenario.FINANCE &&
!output.contains("不构成投资建议")) {
output = output + "\n\n*以上内容仅供参考,不构成任何投资建议。投资有风险,请谨慎决策。*";
}
return ValidationResult.passed(output);
}
}五、审查清单完整汇总表
AI项目代码审查清单(30项)
提示词质量 (P-001 ~ P-008) ——————————————————
P-001 提示词外部化(不硬编码) HIGH
P-002 用户输入净化(防注入) CRITICAL
P-003 提示词长度上限检查 MEDIUM
P-004 系统提示词访问控制 HIGH
P-005 提示词版本标识 MEDIUM
P-006 A/B测试支持 LOW
P-007 提示词变更触发评测 HIGH
P-008 提示词注入防护 CRITICAL
错误处理 (E-001 ~ E-007) ——————————————————
E-001 超时异常处理 CRITICAL
E-002 限流异常处理 HIGH
E-003 服务不可用处理 HIGH
E-004 JSON解析失败兜底 HIGH
E-005 异常日志合规性 MEDIUM
E-006 用户友好降级响应 MEDIUM
E-007 重试逻辑合理性 HIGH
成本意识 (C-001 ~ C-006) ——————————————————
C-001 输入长度上限 HIGH
C-002 max_tokens设置 HIGH
C-003 高频请求缓存 HIGH
C-004 用户调用频率限制 HIGH
C-005 模型选型合理性 MEDIUM
C-006 消费预警配置 MEDIUM
安全性 (S-001 ~ S-006) ——————————————————
S-001 API Key安全管理 CRITICAL
S-002 提示词注入防护 CRITICAL
S-003 AI输出内容安全 CRITICAL
S-004 用户数据脱敏 HIGH
S-005 日志敏感信息 HIGH
S-006 系统提示词保护 HIGH
可观测性 (O-001 ~ O-003) ——————————————————
O-001 调用追踪(TraceID) HIGH
O-002 Token消耗指标 MEDIUM
O-003 延迟分布指标 MEDIUM六、常见问题 FAQ
Q1:所有30个检查项每次PR都必须检查吗?
A:建议分级:
- CRITICAL(5项):每次PR必查,CI自动阻断
- HIGH(17项):PR Review时人工检查,违反需要Owner确认豁免
- MEDIUM/LOW(8项):定期(Sprint结束)批量审查
Q2:提示词应该如何做版本管理?
A:
- 将提示词存储在数据库(带version字段)
- 使用Git管理提示词配置文件(YAML/JSON)
- 提示词变更需要PR + Review + 评测通过才能合并
- 生产环境的提示词ID记录在AI请求的日志中,方便回溯
Q3:如何判断是否需要内容安全审核层?
A:判断标准:
- C端产品:必须(用户的输入和AI的输出都不可控)
- 内部工具:建议(防止误操作)
- 金融/医疗/法律场景:必须,且要额外的专项过滤
Q4:代码审查员自己不懂AI,如何培训?
A:
- 建立AI代码Review培训材料(本文就是很好的参考)
- 前几次Review有AI工程师陪同(师带徒)
- 将检查项转化为CI工具,减少对人工经验的依赖
- 定期组织AI安全事故案例分享(让血泪教训教育团队)
Q5:这份清单如何持续更新?
A:
- 每次AI相关线上事故后,复盘是否有对应的检查项
- 关注AI安全最新动态(OWASP LLM Top 10)
- 每季度团队Review一次清单,删掉过时的,加入新的
- 与其他团队分享互相参考
七、总结
周建的教训价值千金。AI项目代码审查不只是找Bug,更是在系统上线前筑起最后一道防线:
| 维度 | 发现的典型问题 | 带来的风险 |
|---|---|---|
| 提示词质量 | 注入漏洞/裸露 | 被攻击/商业机密泄漏 |
| 错误处理 | 无降级 | 服务雪崩,用户流失 |
| 成本意识 | 无限制调用 | 账单失控,公司损失 |
| 安全性 | API Key泄漏 | 账号被盗,数据泄漏 |
| 可观测性 | 无监控 | 出问题无法定位 |
把这30项清单加入你的PR模板,让每一行AI相关代码都经过这道门。
