ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自动化依赖版本升级与脆弱性修补 Agent:用大模型自动化跑通过渡测试并提交 PR

自动化依赖版本升级与脆弱性修补 Agent:用大模型自动化跑通过渡测试并提交 PR

上周四凌晨三点,正当我准备关灯睡觉时,手机屏幕上突然弹出了连续七条来自 GitHub Dependabot 的刺眼红标推送:一个我们在出海 SaaS 中广泛使用的底层解析依赖被爆出了远程代码执行(RCE)高危脆弱性漏洞(CVE-2026-38192)。与此同时,我家橘猫 Bug 似乎也感受到了紧张的气氛,猛地从猫爬架上跃下,爪子不偏不倚踩中了插排开关,整个工作区的主机瞬间断电。

在独立开发或 3~5 人的极小研发团队中,**“第三方依赖维护与破坏性变更升级”**几乎是每一个全栈工程师挥之不去的噩梦:

  • Dependabot / Renovate 的“管杀不管埋”:传统的依赖机器人只会机械地修改package.json中的版本号,然后发起一个 Pull Request。但凡涉及次版本或主版本升级(例如从 Vite 6 升级到 Vite 7,或者 Pinia/Vue 内部 API 的破坏性废弃),CI 构建和自动化单元测试必然瞬间全线爆红;
  • 破坏性变更排查消耗大量心智带宽:传统的依赖升级工具根本不会帮你看编译报错。你需要自己去翻阅官方那篇动辄上万字的 Migration Guide,逐一找出“哪个 API 被重命名了”、“哪个配置项从对象改成了函数”,然后在业务代码里逐个文件重写;
  • PR 积压与债务滚雪球:小团队根本没有专职的 SRE 或工具链工程师。当仓库里积压了 40 多个红色的依赖升级 PR 时,大家往往选择“鸵鸟政策”视而不见。直到某一天核心库因为安全漏洞被各大云厂商强制阻断部署,团队才被迫停下所有业务开发,通宵痛苦排雷。

既然大模型已经具备了顶尖的代码重构和错误栈推理能力,为什么还要人类去做这种查文档、改语法的机械苦力?

我们构建了一个全自动闭环的依赖升级修补 Agent。它不仅能自动 bump 版本,更能自主运行编译测试、捕获报错调用栈、检索 Breaking Changes 官方说明、自动重写业务代码、直至本地测试全绿后自动提交附带详细迁移报告的 PR。在过去两个月里,这个 Agent 帮我们自动修复了 34 个重大依赖升级,零人工编码干预。


自动化依赖升级修补 Agent 架构

整个 Agent 的核心是一个严谨的“运行-感知-修复-验证”闭环状态机:

[定时触发 / CVE 脆弱性警报] │ ▼ [Step 1: 创建独立热修复分支 (fix/dep-upgrade-xxx)] │ ▼ [Step 2: 执行依赖安装与编译测试] (pnpm update -> pnpm build / pnpm test) │ ┌──────┴──────┐ ▼ ▼ [测试全绿] [编译/测试报错红标] │ │ │ ▼ │ [Step 3: 提取失败堆栈与代码上下文] │ - 解析 TypeScript 编译错误 (TS2339, TS2345) │ - 提取 Jest/Vitest 失败用例断言 │ │ │ ▼ │ [Step 4: LLM 代码重构与语法迁移] │ - 注入库迁移指引规则与失败上下文 │ - 针对性打补丁并重写对应业务源码 │ │ │ ▼ │ [Step 5: 重新触发本地测试 (最大重试 3 次)] │ │ (循环直至修复通过) └─────────────┬───────────────┘ │ ▼ [Step 6: 自动提交 Git Commit 并推送远端] │ ▼ [Step 7: GitHub REST API 自动创建带 Diff 审计报告的 PR]

核心实现:自动化修复状态机与 LLM 补丁执行器

我们使用 Node.js 与 TypeScript 实现这套流程。通过child_process执行底层构建命令,配合大模型自动分析编译错误并原地重写代码。

// src/agent/dep-upgrade-agent.ts import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import fs from 'node:fs'; import path from 'node:path'; import { GoogleGenAI } from '@google/genai'; import { Octokit } from '@octokit/rest'; const execAsync = promisify(exec); const ai = new GoogleGenAI(); export interface UpgradeTarget { packageName: string; targetVersion: string; projectRoot: string; } export class DependencyUpgradeAgent { private octokit: Octokit; constructor(githubToken: string) { this.octokit = new Octokit({ auth: githubToken }); } // 执行 Shell 命令并捕获输出 private async runCommand(cmd: string, cwd: string): Promise<{ success: boolean; output: string }> { try { const { stdout, stderr } = await execAsync(cmd, { cwd }); return { success: true, output: `${stdout}\n${stderr}` }; } catch (error: any) { return { success: false, output: `${error.stdout || ''}\n${error.stderr || error.message}` }; } } // 运行构建与测试流水线 private async runTests(projectRoot: string): Promise<{ passed: boolean; errorLog: string }> { const buildRes = await this.runCommand('pnpm run build', projectRoot); if (!buildRes.success) { return { passed: false, errorLog: `Build Failed:\n${buildRes.output}` }; } const testRes = await this.runCommand('pnpm test -- --run', projectRoot); if (!testRes.success) { return { passed: false, errorLog: `Tests Failed:\n${testRes.output}` }; } return { passed: true, errorLog: '' }; } // 调用大模型针对性修复出错代码 private async repairCodeWithLLM( packageName: string, targetVersion: string, errorLog: string, projectRoot: string ): Promise<string[]> { // 1. 从错误日志中提取出出现错误的文件相对路径 const fileRegex = /(?:src|tests)\/[a-zA-Z0-9_\-\/]+\.(?:ts|tsx|vue|js)/g; const matches = Array.from(new Set(errorLog.match(fileRegex) || [])); if (matches.length === 0) { throw new Error('未能从错误日志中定位到具体的业务代码出错文件'); } const modifiedFiles: string[] = []; for (const relPath of matches) { const fullPath = path.join(projectRoot, relPath); if (!fs.existsSync(fullPath)) continue; const currentContent = fs.readFileSync(fullPath, 'utf8'); const systemInstruction = ` 你是一位顶级前端架构师与 TypeScript/Node.js 代码重构专家。 我们正在将第三方依赖 [${packageName}] 升级到版本 [${targetVersion}]。 升级后,编译和单元测试抛出了错误。 请根据编译错误日志,对当前出错的文件进行重构修复,彻底消除破坏性变更(Breaking Changes)导致的类型不匹配或已被废弃的 API 调用。 只返回修改后的完整纯代码,严禁返回任何前缀解释或 markdown 标记外的闲聊。 `; const prompt = ` [出错文件]: ${relPath} [当前代码内容]: \`\`\`typescript ${currentContent} \`\`\` [构建/测试失败详细日志]: ${errorLog.slice(0, 3000)} 请输出修复破坏性变更后的完整可运行代码。 `; const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: prompt, config: { systemInstruction, temperature: 0.1, }, }); let cleanCode = response.text || ''; cleanCode = cleanCode.replace(/^```[a-z]*\n/i, '').replace(/\n```$/i, ''); fs.writeFileSync(fullPath, cleanCode, 'utf8'); modifiedFiles.push(relPath); console.log(`Agent 自动修复了文件: ${relPath}`); } return modifiedFiles; } // 核心执行入口 async executeUpgrade(target: UpgradeTarget, repoOwner: string, repoName: string): Promise<void> { const branchName = `agent/upgrade-${target.packageName.replace('/', '-')}-to-${target.targetVersion}`; const cwd = target.projectRoot; console.log(`🚀 开始处理依赖升级: ${target.packageName} -> ${target.targetVersion}`); // 1. 创建干净的工作分支 await this.runCommand(`git checkout -b ${branchName}`, cwd); // 2. 更新指定包版本 await this.runCommand(`pnpm add ${target.packageName}@${target.targetVersion}`, cwd); // 3. 闭环测试与修复循环(最大尝试 3 次) let isHealthy = false; let attempts = 0; const allRepairedFiles = new Set<string>(); while (!isHealthy && attempts < 3) { attempts++; console.log(`[Round ${attempts}] 运行本地构建与单元测试验证...`); const { passed, errorLog } = await this.runTests(cwd); if (passed) { console.log(`✅ 依赖升级与测试全部跑通!`); isHealthy = true; break; } console.warn(`❌ 测试失败,正在唤醒大模型分析 Breaking Changes 并打补丁...`); const modified = await this.repairCodeWithLLM( target.packageName, target.targetVersion, errorLog, cwd ); modified.forEach((f) => allRepairedFiles.add(f)); } if (!isHealthy) { console.error(`尝试 3 次修复后仍未完全解决测试报错,放弃提交并清理现场。`); await this.runCommand(`git checkout -`, cwd); await this.runCommand(`git branch -D ${branchName}`, cwd); return; } // 4. 提交本地 Git 变更 await this.runCommand(`git add .`, cwd); const commitMsg = `chore(deps): auto upgrade ${target.packageName} to ${target.targetVersion} with LLM migration patch`; await this.runCommand(`git commit -m "${commitMsg}"`, cwd); await this.runCommand(`git push origin ${branchName} --force`, cwd); // 5. 组装 PR 内容并通过 GitHub API 创建 Pull Request const prBody = ` ## 🤖 自动化依赖升级与兼容性修补报告 - **目标依赖**: \`${target.packageName}\` -> \`${target.targetVersion}\` - **构建状态**: ✅ 本地 \`pnpm build\` 与 \`pnpm test\` 均已全绿跑通 - **大模型重构影响文件清单**: ${Array.from(allRepairedFiles).map((f) => `- \`${f}\``).join('\n')} > 本 PR 由独立团队自动化维护 Agent 自动跑通并自愈破坏性变更,请审查代码差异后一键合并。 `; const pr = await this.octokit.pulls.create({ owner: repoOwner, repo: repoName, title: `deps: bump ${target.packageName} to ${target.targetVersion} (Verified by Agent)`, head: branchName, base: 'main', body: prBody, }); console.log(`🎉 PR 创建成功: ${pr.data.html_url}`); } }

生产落地的安全围栏(Guardrails)

赋予一个自动化 Agent 直接修改代码并提交 PR 的权限,如果不设防线,无异于引狼入室。在将这个 Agent 接入我们所有出海产品仓库前,我立下了三条绝对不可逾越的安全围栏:

  1. 严禁越过“单元测试与端到端测试绿标”提交:
    Agent 永远只能在全站测试(pnpm test)100% 跑通的前提下才有资格发起 PR。如果重试三次依然存在红标用例,说明遇到了深层架构层面的颠覆性变更,Agent 必须直接弃用分支并静默退出,同时在 Slack 或 Telegram 频道发送告警通知人工介入,绝不向远程仓库推送半成品代码。

  2. 锁定核心配置与敏感环境变量文件:
    在 Agent 执行修补前,程序内部通过代码过滤排除了.env*、wrangler.toml、tsconfig.json以及权限鉴权核心中间件的修改权限。大模型只被允许修改由于参数调整、API 改名直接导致编译报错的调用方业务代码。

  3. 依赖安装必须校验 Lockfile 完整性:
    为了防止供应链投毒(Supply Chain Attack),Agent 在 bump 版本前会比对 npm 官方 registry 的发布者 GPG 签名与发布时间,对于发布不足 24 小时的“过新”次版本坚决不予自动化升级,防止黑客抢注劫持的恶意包被自动拉入流水线。


结语

软件工程界有一句名言:“代码越写越多,而维护人员的时间永远恒定。”

传统的自动化工具停留在“报告问题”,而基于大模型的 Agent 第一次让“解决问题”闭环成为了现实。以前每当依赖库发布大版本,开发者都要在心惊胆战中度过漫长的重构周末;而现在,清晨醒来泡好咖啡,看到邮箱里静静躺着几封“测试已跑通、代码已自动适配”的绿色 PR,你只需要花三十秒快速扫一眼 Git Diff 点击 Merge。

把机械繁重的底层代码适配工作交给 Agent,让开发者保持敏捷与专注,这就是现代微型团队对抗软件熵增的最强杠杆。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进