AI 代码审查别吞整份 diff:AST 增量筛选与 Token 预算

发布时间:2026/8/16 9:55:43
AI 代码审查别吞整份 diff:AST 增量筛选与 Token 预算 AI 代码审查别吞整份 diffAST 增量筛选与 Token 预算AI 代码审查最忌讳把整份 diff 原样扔给模型。先用 AST 和确定性规则筛掉格式、导入与无关文件再把函数签名、变更节点和必要类型控制在可解释的 Token 预算里。为什么全量 Code Review 会把 CI 拖垮大部分团队搭 AI Code Review 脚本时写法非常随意获取git diff或者直接读源文件然后拼一段 Please review this code 的 Prompt发请求给 OpenAI 或本地部署的 Ollama。这里有三个常见问题。第一无效上下文挤爆 Token。代码库里大量无意义的import语句、格式化缩进、接口声明不需要大模型重新看一遍。把这些全扔进去会增加无关的调用成本。第二长上下文通常会增加排队、预填充和生成时间。具体幅度取决于模型和服务端实现不能只用固定 Token 阈值判断。第三模型输出不应直接决定 CI 成败。结构校验、超时、重试与人工复查通道都应独立于模型输出。要搞定这事思路很明确在前端打包与编译阶段先用 AST抽象语法树做增量差分计算只把发生实际逻辑变更的函数节点与上下文类型抽出来配上 Token 预算计算器后再交给大模型。SWC 增量 AST 解析与 Token 预算流转控制在 Node.js 环境中可用 SWC 或现有构建工具解析 TSX。解析耗时受文件大小、机器和缓存影响应在仓库中基准测试后再决定是否替换工具。下面是这个增量提取与流式审查控制器的完整实现示例。import { parseSync, Visitor } from swc/core; import type { Module, FunctionDeclaration, ClassMethod } from swc/core; import crypto from node:crypto; import EventEmitter from node:events; export interface CodeChunk { id: string; name: string; kind: function | method | type; code: string; startLine: number; endLine: number; } export interface ReviewOptions { maxTokensPerChunk: number; timeoutMs: number; apiKey: string; baseUrl: string; } export class ASTDiffExtractor { /** * 从源码中精准提取函数与关键节点 */ public extractChangedChunks(code: string): CodeChunk[] { let ast: Module; try { ast parseSync(code, { syntax: typescript, tsx: true, target: es2022, }); } catch (err) { console.error([AST Parser Error] 无法解析源码降级为文本行处理, err); return this.fallbackLineSplit(code); } const chunks: CodeChunk[] []; const lines code.split(\n); // 遍历 AST 提取目标函数与方法节点 class ScopeVisitor extends Visitor { visitFunctionDeclaration(n: FunctionDeclaration): FunctionDeclaration { if (n.identifier n.span) { // SWC 的 span 是源码偏移量不是行号示例用偏移量截取源码。 const startOffset Math.max(0, n.span.start - 1); const endOffset Math.min(code.length, n.span.end - 1); const funcCode code.slice(startOffset, endOffset); chunks.push({ id: crypto.createHash(md5).update(funcCode).digest(hex), name: n.identifier.value, kind: function, code: funcCode, startLine: code.slice(0, startOffset).split(\n).length, endLine: code.slice(0, endOffset).split(\n).length, }); } return n; } } const visitor new ScopeVisitor(); visitor.visitModule(ast); return chunks.length 0 ? chunks : this.fallbackLineSplit(code); } private fallbackLineSplit(code: string): CodeChunk[] { return [{ id: crypto.createHash(md5).update(code).digest(hex), name: FullModuleFallback, kind: function, code: code.slice(0, 4000), // 硬限制截断 startLine: 1, endLine: code.split(\n).length, }]; } } export class AICodeReviewEngine extends EventEmitter { private options: ReviewOptions; constructor(options: ReviewOptions) { super(); this.options options; } /** * 执行流式审查带有超速控制与 JSON 格式纠错防线 */ public async processStreamReview(chunk: CodeChunk): Promisestring { const prompt 你是一名严格的前端工程专家。请对以下 TypeScript 代码节点进行审查。 仅输出格式化的 JSON禁止包含 Markdown 标记 { score: number (0-100), issues: [{line: number, severity: error|warn, message: string}], suggestion: string } 代码节点名称: ${chunk.name} 源码: \\\typescript ${chunk.code} \\\; const controller new AbortController(); const timer setTimeout(() controller.abort(), this.options.timeoutMs); try { const response await fetch(${this.options.baseUrl}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${this.options.apiKey}, }, body: JSON.stringify({ model: deepseek-coder, messages: [{ role: user, content: prompt }], stream: true, temperature: 0.1, }), signal: controller.signal, }); if (!response.ok || !response.body) { throw new Error(HTTP 异常状态码: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let rawResult ; while (true) { const { done, value } await reader.read(); if (done) break; const chunkText decoder.decode(value, { stream: true }); const lines chunkText.split(\n).filter(l l.trim().startsWith(data: )); for (const line of lines) { const jsonStr line.replace(/^data: /, ).trim(); if (jsonStr [DONE]) continue; try { const parsed JSON.parse(jsonStr); const delta parsed.choices?.[0]?.delta?.content || ; rawResult delta; this.emit(chunk_token, delta); } catch { // 忽略流中的中间解析抖动 } } } clearTimeout(timer); return this.validateAndSanitizeJson(rawResult); } catch (err: unknown) { clearTimeout(timer); const message err instanceof Error ? err.message : String(err); console.warn([Review Pipeline Warnings] 节点 ${chunk.name} 触发熔断兜底: ${message}); return JSON.stringify({ score: 80, issues: [], suggestion: 流式响应超时或网络异常已自动跳过深度审查放行构建。, }); } } /** * 确定性校验器清理大模型可能混入的 Markdown 标记并校验 JSON 结构 */ private validateAndSanitizeJson(rawText: string): string { const sanitized rawText .replace(/json/gi, ) .replace(//g, ) .trim(); try { const obj JSON.parse(sanitized); if (typeof obj.score ! number || !Array.isArray(obj.issues)) { throw new Error(Schema 校验失败缺少关键字段); } return JSON.stringify(obj); } catch { // 解析失败时输出安全格式绝不卡死 CI 进程 return JSON.stringify({ score: 70, issues: [{ line: 1, severity: warn, message: 大模型返回格式非标准 JSON已强行修复 }], suggestion: sanitized.slice(0, 200), }); } } }上线前如何验证不要把其他项目的延迟和 Token 数据当作自己的结论。可以在 CI 中记录每次审查的输入 Token、TTFT、总耗时、超时比例和缓存命中率并按 PR 大小分组比较“全量 diff”和“函数级片段”两种策略。确认审查覆盖率没有明显下降后再据此设置预算。这里有几个生产踩坑经验应警惕。第一AST 解析不能盲目信任。遇到模板语法诡异或者写了大量宏扩展的代码语法树可能直接报错崩溃。这时候代码里应带 fallback 机制退回到常规行数切割防止脚本抛异常把 CI 搞死。第二Token 限制不能光看字数。应使用目标模型对应的 tokenizer 估算输入并为提示词、输出和重试预留预算。超限时优先拆分并保留函数签名、调用点和相关类型而不是静默截断关键逻辑。先用规则缩小输入范围再让模型处理需要语义判断的部分通常比单纯扩大上下文更可控。