用大模型打造严苛的 PR 审查机器人:自动化拦截潜在 SQL 注入与 N+1 慢查询

独立开发者或者三五人的微型团队,最大的研发隐患是什么?既不是前端 UI 没对齐,也不是某个 CSS 动画不够丝滑,而是缺乏专职的代码审计员与 DBA。当你一个人或者拉着合伙人通宵赶双 11 促销版本时,大脑处于极度疲劳状态,极容易在 ORM 查询或者后端 API 接口里写下两大致命隐患:
- 为了图省事,用字符串模板直接拼了一句原生 SQL,埋下了毁灭性的 SQL 注入漏洞;
- 在
for...of循环里随手await db.select().from(userProfiles)...,在几十上百条列表循环中引入了隐蔽的 N+1 慢查询,本地测试只有 2 条数据丝滑顺畅,一旦线上大促几千个用户并发访问,数据库连接池瞬间被打爆,CPU 直接飙升到 100%。
很多商业级的 SAST 代码扫描工具(比如 SonarQube、Snyk)要么笨重臃肿难配置,要么规则死板产生海量误报,独立开发根本没精力维护。今天蔓蔓就来分享我们团队内部搭建的大模型代码审计机器人(PR Reviewer Agent):通过 GitHub Actions 自动化挂载,利用大模型对 Git Diff 进行深度语义与调用链路分析,精准定位潜在安全风险并在 PR 对应代码行直接发表审查建议与红线阻断。
审查架构:为什么传统正则做不到精准检测?
为什么传统的 ESLint 或者静态正则匹配很难揪出 N+1 查询?因为在现代全栈开发(比如 Next.js / Nuxt 结合 Drizzle ORM、Prisma 或 TypeORM)中,数据库调用往往被包装在 Repository 层或者 Service 函数内部:
// 典型隐蔽的 N+1 慢查询陷阱
const orders = await getPaidOrders();
const enrichedOrders = await Promise.all(
orders.map(async (order) => {
// 看起来只是调用了一个业务函数,但内部其实发起了单条 SQL 查询!
const user = await getUserById(order.userId);
const shipping = await getShippingDetail(order.id);
return { ...order, user, shipping };
})
);
静态正则很难跨函数上下文判断 getUserById 究竟是纯内存缓存计算还是发起了网络 IO 数据库查询。而结合了大模型上下文推断能力的审查机器人,能够理解函数语义、结合上下文上下文引用,甚至推测出在 Promise.all 映射列表时对数据库造成的巨大并发风暴。
整个审查流程由 GitHub Actions 自动化触发:
[开发者提交 Pull Request]
│
▼
[GitHub Actions 触发 workflow]
│
▼
[过滤 Diff: 提取 .ts, .js, .sql 服务端文件]
│
▼
[大模型审查引擎(带特定安全与性能 Prompt)]
│
┌────┴────────────────────────┐
▼ ▼
[发现严重安全/N+1隐患] [审查通过 / 仅普通优化]
│ │
├─ 在 PR 行级添加 Review 评论 └─ 给出通过 Badge 标签
└─ 打上 `block-merge` 标签阻断合并
核心实现:GitHub Actions 审查机器人核心脚本
我们使用 TypeScript 编写一个轻量级的 GitHub PR 审查 Agent,借助 @octokit/rest 与 Google GenAI SDK 进行驱动:
// scripts/pr-reviewer-agent.ts
import { Octokit } from '@octokit/rest';
import { GoogleGenAI } from '@google/genai';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const ai = new GoogleGenAI();
interface VulnerabilityReport {
file: string;
line: number;
type: 'SQL_INJECTION' | 'N_PLUS_ONE_QUERY' | 'TRANSACTION_RISK';
severity: 'CRITICAL' | 'WARNING';
reason: string;
suggestedFix: string;
}
export async function reviewPullRequest(owner: string, repo: string, pullNumber: number) {
// 1. 获取 PR 的文件变更列表
const { data: files } = await octokit.pulls.listFiles({
owner,
repo,
pull_number: pullNumber,
});
// 2. 仅过滤与数据库、数据层相关的后端代码文件
const targetFiles = files.filter(f =>
/\.(ts|js|mjs|sql)$/.test(f.filename) &&
!f.filename.includes('test') &&
f.patch // 确保有变更 patch
);
if (targetFiles.length === 0) {
console.log('未发现服务端数据库相关变更,跳过审查');
return;
}
// 3. 逐个文件或批量组装 Diff 供大模型审查
for (const file of targetFiles) {
const findings = await analyzeDiffWithLLM(file.filename, file.patch!);
if (findings.length > 0) {
for (const finding of findings) {
// 在 GitHub PR 代码变更的具体行数上打出审查建议
await octokit.pulls.createReviewComment({
owner,
repo,
pull_number: pullNumber,
commit_id: (await octokit.pulls.get({ owner, repo, pull_number: pullNumber })).data.head.sha,
path: finding.file,
line: finding.line,
body: `🚨 **[AI 安全与性能防线 - ${finding.type}]**\n\n**严重等级**: \`${finding.severity}\`\n\n**问题分析**: ${finding.reason}\n\n**推荐修复方案**:\n\`\`\`typescript\n${finding.suggestedFix}\n\`\`\``,
});
}
// 如果有 CRITICAL 级别隐患,给 PR 增加警告标签
if (findings.some(f => f.severity === 'CRITICAL')) {
await octokit.issues.addLabels({
owner,
repo,
issue_number: pullNumber,
labels: ['block-merge: safety-check-failed'],
});
}
}
}
}
精准 Prompt 策略:如何把大模型调教成“魔鬼审查官”?
大模型代码审查最容易出现“废话连篇”的问题,比如挑剔命名格式、建议拆分小函数等琐碎小事,导致开发者直接忽略 PR 评论。我们的核心策略是:极度聚焦致命缺陷,严禁给与安全性能无关的鸡毛蒜皮建议:
// scripts/diff-analyzer.ts
import { GoogleGenAI } from '@google/genai';
import { VulnerabilityReport } from './pr-reviewer-agent';
const ai = new GoogleGenAI();
export async function analyzeDiffWithLLM(
filename: string,
patch: string
): Promise<VulnerabilityReport[]> {
const prompt = `
你是一位极为严谨的生产级架构安全专家与高级 DBA。请审查以下 Git Patch 代码差异:
文件名: ${filename}
代码 Patch 内容:
${patch}
审查核心红线规则(只关注以下三项,其他格式、拼写等琐事一律忽略):
1. 【SQL 注入风险 (SQL_INJECTION)】:
- 严禁使用字符串拼接(如 \`SELECT * FROM users WHERE id = \${id}\`)直接执行 SQL。
- 严禁在 ORM 的 raw 查询中未经参数化直接拼接外部入参。
2. 【N+1 慢查询风险 (N_PLUS_ONE_QUERY)】:
- 严禁在 for、while、forEach、map、Promise.all 内部遍历执行数据库查询语句或 ORM 调用。
- 必须推荐使用 JOIN 批量查询或 WHERE id IN (...) 批量拉取后在内存中根据 Map 聚合并发填充。
3. 【长事务持有风险 (TRANSACTION_RISK)】:
- 严禁在数据库事务(tx.transaction)内发起第三方 HTTP 请求(如 Stripe API、外部 Webhook)。这会导致数据库连接被长期占用直至连接池枯竭。
如果发现问题,请提供精确的 Patch 行号(注意必须是新增行或修改行对应的行数),并给出最小可执行的重构代码。
输出格式要求为严格的 JSON 数组,若没有发现上述三类问题,则返回空数组 []。
`;
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: prompt,
config: {
responseMimeType: 'application/json',
},
});
try {
return JSON.parse(response.text || '[]') as VulnerabilityReport[];
} catch (error) {
console.error('解析大模型审查结果失败:', error);
return [];
}
}
生产拦截实测案例:拯救了一次线上故障
在某次双 11 前置营销活动迭代中,我们团队的新功能需要给批量报名的用户发放折扣券。开发者提交的代码如下:
// 提交的代码片段
export async function claimDiscountsForUsers(userIds: string[], couponId: string) {
const results = [];
for (const uid of userIds) {
// ⚠️ 极其致命的隐蔽循环单条写入与查询!
const user = await db.query.users.findFirst({ where: eq(users.id, uid) });
if (user && !user.hasClaimed) {
await db.insert(userCoupons).values({ userId: uid, couponId });
results.push(uid);
}
}
return results;
}
PR 刚刚推送到 GitHub 仅 18 秒,审查机器人立刻在 PR 行上阻断并评论:
🚨 [AI 安全与性能防线 - N_PLUS_ONE_QUERY]
严重等级:CRITICAL
问题分析: 在for...of循环中逐条执行了findFirst和insert操作。当userIds达到 500 个以上时,将产生 1000 次数据库网络往返,高并发下将耗尽连接池导致网关 504 崩溃。
推荐修复方案:// 改用批量 IN 查询与批量 INSERT const eligibleUsers = await db.query.users.findMany({ where: and(inArray(users.id, userIds), eq(users.hasClaimed, false)), columns: { id: true }, }); if (eligibleUsers.length > 0) { const insertData = eligibleUsers.map(u => ({ userId: u.id, couponId })); await db.insert(userCoupons).values(insertData); }
同时,PR 自动被打上了 block-merge: safety-check-failed,GitHub 分支保护规则直接禁用了合并按钮。这次拦截避免了一次潜在的大促高并发宕机惨剧。
总结
对独立开发者而言,AI Agent 最有价值的落脚点不是替代你写无聊的 HTML,而是成为你身后的全天候安全架构师与守护哨兵。
小团队没有资源请人轮流值守,但花 20 分钟配置一个针对 SQL 注入与 N+1 慢查询的 GitHub Actions 审查机器人,就能让你的每一个生产提交多一道坚不可摧的安全护城河。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)