公司动态
智能审查别跳过前端的确定性检查
智能审查别跳过前端的确定性检查1. 告警群里的反打卡AI 审查助手把正常组件全踢回去了接入 LLM 代码审查后常见问题是正常的useMemo被误报为风险模型给出的修复无法通过编译或在同步逻辑中建议加入无意义的异步处理。把 LLM 直接接到 Git Hook 并不能替代编译器、类型检查和既有规则。没有明确输入范围与输出约束时它很容易制造告警噪音开发者也会逐渐忽略结果。更合适的定位是用确定性工具提取和验证代码事实再让模型补充解释或提出待人工确认的建议。2. 深入 AST 链路为什么把整段代码丢给大模型审查必然失效将整个前端文件作为纯文本直接交给模型通常会降低审查结果的相关性。前端代码不是小说。一个现代 React 或 Vue 组件中包含了大量的 import 依赖、类型声明、样式处理以及核心业务逻辑。在上下文窗口有限且注意力机制分布衰减的情况下将一个 500 行的.tsx文件全量发给 LLM大模型关注的焦点会被大量的 boilerplate 代码样板代码分散。全量输入通常会带来两类问题第一类是上下文干扰。模型同时看到 CSS-in-JS 和复杂 Hook 时可能关注风格问题而遗漏useEffect依赖等需要工具验证的风险。第二类是语义信息缺失。模型无法替代 TypeScript 的符号解析复杂泛型和条件类型的修改必须经类型检查和测试验证。下面的表格说明两种输入方式应比较的指标。具体数值需要由团队在自身仓库中采集审查维度全量文本直接投喂AST 节点过滤后定向投喂虚假告警率 (False Positive)容易受无关上下文影响应在标注样本上评估致命逻辑漏报率不应仅依赖模型判断与静态规则和人工复核对照平均 Token 消耗随文件长度增长可按高风险节点控制单文件审查耗时受上下文和模型调用影响受节点数量与并发控制影响开发者采纳率需通过反馈数据统计需通过反馈数据统计先用样本库测量误报、漏报和采纳情况再调整输入与规则。在代码进入 LLM 之前必须在本地用确定性的编译器工具如 Babel Parser 或 TypeScript Compiler API将代码切碎剔除那些无关紧要的静态声明只抽取核心的函数体、副作用 Hook、状态变更点以及异步调用链路。3. 从幻觉到死循环AI 自动补全在复杂泛型与状态机里的崩塌现场自动补全适合重复性较高的代码处理复杂状态机或 TypeScript 高级类型时生成结果需要逐行审阅。看一个我们在真实业务中抽取的真实反例。这段代码原本是一个管控多步表单提交的状态机 Hook开发者试图让 AI 补全状态转移逻辑// ❌ 错误做法盲目信任 AI 补全的无限递归与类型断言 export type MachineState idle | loading | success | error; export function useFormStateMachine() { const [state, setState] useStateMachineState(idle); // AI 自动补全生成的所谓智能重试机制 const dispatch async (action: string) { if (state loading) { // 幻觉AI 自作聪明地加入了无限自旋等待直接卡死渲染主线程 while (state loading) { await new Promise((resolve) setTimeout(resolve, 100)); } } try { setState(loading); // 幻觉为了避开 TS 类型检查AI 直接使用了 any 强制断言 const res await fakeApiCall(action as any); if ((res as any).ok) { setState(success); } else { // 幻觉死循环重试未设置最大重试次数与退避窗口 dispatch(action); } } catch (e) { setState(error); } }; return { state, dispatch }; }上面的代码至少有三处需要修正在 React 组件内用while和setTimeout等待状态变化读取到的可能是旧闭包中的state逻辑无法按预期退出。用as any压过类型报错会丢失类型约束应补齐动作和响应类型。异常分支递归调用dispatch(action)没有最大次数和退避策略可能形成重复请求。审查工具应结合循环、递归、any和副作用等确定性规则而不是仅根据模型评价决定是否放行。4. 搭建确定性防护网用 AST 预清洗与结构化 Schema 约束 AI 审查为了消除上述反模式我们重构了团队的 AI 代码审查引擎。新架构可用 AST 静态分析缩小输入范围用 JSON Schema 校验 LLM 输出。格式不合法或缺少证据的结果应标记为“需人工复核”而不是阻断提交。下面是完整的生产级 Node.js 审查管道实现代码。你可以直接放到自动化 CI 工具链中运行import * as parser from babel/parser; import traverse from babel/traverse; import * as t from babel/types; import { z } from zod; // 1. 严格定义 LLM 输出的 JSON Schema防止模型输出伪文本 const ReviewResultSchema z.object({ hasIssue: z.boolean(), severity: z.enum([low, medium, high, critical]), line: z.number().optional(), targetCode: z.string().optional(), reason: z.string().describe(具体的代码隐患说明禁止使用泛化词汇), suggestion: z.string().describe(符合 TS 规范的修复代码片段), }); export type ReviewResult z.infertypeof ReviewResultSchema; export interface CodeSnippet { location: string; code: string; contextType: useEffect | customHook | stateMutation; } // 2. 确定性 AST 节点抽取器只提取高风险代码段 export function extractRiskSnippets(sourceCode: string): CodeSnippet[] { const snippets: CodeSnippet[] []; try { const ast parser.parse(sourceCode, { sourceType: module, plugins: [typescript, jsx], }); traverse(ast, { // 专门提取 React.useEffect CallExpression(path) { const callee path.node.callee; if ( t.isIdentifier(callee, { name: useEffect }) || (t.isMemberExpression(callee) t.isIdentifier(callee.property, { name: useEffect })) ) { const loc path.node.loc; snippets.push({ location: loc ? L${loc.start.line}-L${loc.end.line} : Unknown, code: sourceCode.slice(path.node.start!, path.node.end!), contextType: useEffect, }); } }, }); } catch (err) { console.error(AST 解析失败回退至确定性安全兜底:, err); } return snippets; } // 3. 带防护网的 AI 审查调用器 export async function reviewCodeWithGuardrails( snippet: CodeSnippet, llmClient: { invoke: (prompt: string) Promisestring } ): PromiseReviewResult | null { const systemPrompt 你是一个严谨的 React/TypeScript 代码审查引擎。 必须且只能输出合法的 JSON 格式。 绝对不能使用 any 类型绝对不能在无网络请求的代码中增加 async/await。 如果不具备明确的逻辑漏洞必须返回 {hasIssue: false, severity: low, reason: 无, suggestion: }; const userPrompt 审查目标片段类型: ${snippet.contextType} 代码位置: ${snippet.location} 代码内容: \\\typescript ${snippet.code} \\\ 请评估是否存在内存泄露、闭包陷阱或死循环风险。; const MAX_RETRIES 2; for (let attempt 1; attempt MAX_RETRIES; attempt) { try { const rawResponse await llmClient.invoke(${systemPrompt}\n${userPrompt}); // 提取 JSON 内容抵御 LLM Markdown 嵌套包覆 const jsonMatch rawResponse.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error(LLM 输出未包含标准 JSON 结构); } const parsed JSON.parse(jsonMatch[0]); // 使用 Zod 校验返回结构 const validatedResult ReviewResultSchema.parse(parsed); return validatedResult; } catch (error) { console.warn(第 ${attempt} 次 LLM 审查结果未通过防线校验: ${(error as Error).message}); if (attempt MAX_RETRIES) { // 返回空结果交由人工审查处理 return null; } } } return null; }核心在于边界清晰不直接把全量代码发送给 API。使用babel/parser提取需要审查的节点。使用Zod校验返回结构失败后返回空结果或人工审查提示。5. 落地验证评估审查结果是否可用上线前后可在同一批 PR 上比较全量输入与 AST 定向输入记录 Token 使用量、响应时间、误报率、漏报率和开发者采纳率。误报和漏报要由代码所有者或独立审查者标注避免只统计模型自评。编译、类型检查、测试和安全规则仍应是提交门禁LLM 适合作为补充信号。把输入、输出和降级路径写清楚审查结果才可复核。