多 Agent 协作的设计系统工作流:生成、审查与合并的自动化
多 Agent 协作的设计系统工作流生成、审查与合并的自动化一、当设计系统的 200 行 PR 因为一行 CSS 变量拼写错误导致整个 CI 红灯——而审查者在度假3 个前端开发者、2 个设计师、1 个设计系统 owner——7 个人的团队维护着一套横跨 4 个项目的共享组件库和 320 个 Token。看起来足够对吧现实是在过去的 6 个月里47 个 PR 中有 11 个在合并后触发了下游应用的视觉回归测试失败。问题不是没人审查代码而是审查者需要凭人脑记住 320 个 Token 的名字、79 个组件的 Props 签名、17 个 CSS 变量的语义。人脑记不住这么多细节尤其是周三下午 4 点第 7 次把 Button 的 padding 从 12px 改成 16px让你只会条件反射地 Approve。多 Agent 协作模型把这个审查过程建模成一条专家流水线生成 Agent 生产代码规范审查 Agent 检查 Token 合规性和命名一致性视觉审查 Agent 比较渲染结果的像素差异最后合并 Agent 在通过所有检查后自动 Merge。人类审查者从逐行看代码退回到审核 Agent 的审查结论——工作量从 15 分钟降到 30 秒。二、多 Agent 流水线架构与角色通信协议flowchart TD A[人类提交组件变更 PR] -- B[生成 Agentbr/generate_agent] B -- |提交代码| C1[规范审查 Agentbr/lint_review_agent] B -- |提交代码| C2[视觉审查 Agentbr/visual_review_agent] B -- |提交代码| C3[标记审查 Agentbr/token_audit_agent] C1 -- |lint 报告| M[合并 Agentbr/merge_agent] C2 -- |视觉 diff 报告| M C3 -- |Token 合规报告| M M -- |pass merge| D[合并 PR] M -- |fail comment| E[自动回复修复建议br/到 PR 评论区] E -- A style C1 fill:#fef3c7,stroke:#f59e0b style C2 fill:#dbeafe,stroke:#3b82f6 style C3 fill:#fce7f3,stroke:#ec4899 style M fill:#d1fae5,stroke:#10b981角色定义Agent职责输入输出拒绝条件生成 Agent生成组件代码TSX CSS组件描述/设计稿链接组件文件N/A规范审查 Agent检查 ESLint/Stylelint 规则、命名一致性变更文件 diffLint 报告pass/fail violations新增 ESLint error视觉审查 Agent截图对比变更前后的像素差异Playwright 截图差异热力图 差异面积px²差异面积 阈值标记审查 Agent检查 Token 使用合规性无硬编码值、无移除中 Token 引用变更的 CSS/JSX 文件Token 合规报告发现硬编码值或废弃 Token合并 Agent聚合前三者报告决定是否 merge三个审查报告merge or comment任一审查 Agent 返回 failAgent 间通信协议不是用 function call 互相调用——那样会耦合。而是每个 Agent 独立运行产出标准化 JSON 报告合并 Agent 消费报告。协议定义// 所有审查 Agent 的统一输出格式 interface ReviewReport { agent: string; // Agent 标识lint | visual | token status: pass | fail; violations: Violation[]; metadata: { duration: number; // Agent 运行耗时ms filesChecked: number; // 检查的文件数 }; } interface Violation { rule: string; // 违反规则名 file: string; // 文件路径 line?: number; // 行号token 审查可以是全局 severity: error | warning; message: string; // 人类可读的描述 suggestion?: string; // 修复建议将 margin-left: 14px 改为 var(--spacing-md) 16px }三、多 Agent 审查流水线的生产级实现/** * 多 Agent 设计系统审查流水线 * * 核心设计 * 1. 三个审查 Agent 并行运行无数据依赖 * 2. 合并 Agent 在全部完成后做最终决策 * 3. 提供 CLI 接口npx ds-review pr-number */ import { execSync } from child_process; import { readFileSync } from fs; import { glob } from glob; import postcss from postcss; // ─── 规范审查 Agent ────────────────────────────────── /** * 规范审查 Agent * * 检查策略 * - ESLint类型安全、Hook 规则、JSX a11y 规则禁止缺少 alt、label 关联等 * - Stylelint禁止 !important、禁止硬编码动画时长、强制 CSS 变量前缀约定 * - 组件命名PascalCase 文件、kebab-case CSS class、camelCase CSS 变量 */ async function lintReviewAgent(prFiles: string[]): PromiseReviewReport { const startTime Date.now(); const violations: Violation[] []; // 1. ESLint 检查仅检查变更文件 try { const eslintOutput execSync( npx eslint ${prFiles.join( )} --format json, { encoding: utf-8 } ); const results JSON.parse(eslintOutput); for (const result of results) { for (const msg of result.messages) { if (msg.severity 2) { // error severity violations.push({ rule: eslint/${msg.ruleId}, file: result.filePath, line: msg.line, severity: error, message: msg.message, suggestion: msg.fix ? ESLint 可以自动修复${msg.fix.text} : undefined, }); } } } } catch (e) { // ESLint 自身报错 → 全部标记为 violation violations.push({ rule: eslint/runtime-error, file: —, severity: error, message: String(e), }); } // 2. Stylelint 检查 CSS 文件规则 const cssFiles prFiles.filter(f f.endsWith(.css) || f.endsWith(.module.css)); if (cssFiles.length 0) { try { const stylelintOutput execSync( npx stylelint ${cssFiles.join( )} --formatter json, { encoding: utf-8 } ); const styleResults JSON.parse(stylelintOutput); for (const result of styleResults) { for (const w of result.warnings) { violations.push({ rule: stylelint/${w.rule}, file: result.source, line: w.line, severity: w.severity error ? error : warning, message: w.text, }); } } } catch { // 同上 } } // 3. CSS class 命名规范检查 // 规则CSS module 的类名必须使用 kebab-case for (const file of cssFiles) { const content readFileSync(file, utf-8); const root postcss.parse(content); root.walkRules(rule { const matches rule.selector.match(/\.([a-zA-Z][\w-]*)/g); if (matches) { for (const cls of matches) { // 忽略 Modifier.btn--primary const name cls.replace(/^\./, ).replace(/--.*$/, ); if (!/^[a-z][a-z0-9]*(-[a-z0-9])*$/.test(name)) { violations.push({ rule: naming/css-class, file, line: rule.source?.start?.line, severity: error, message: CSS 类名 ${cls} 不符合 kebab-case 规范, suggestion: 建议重命名为符合 kebab-case 的形式, }); } } } }); } return { agent: lint, status: violations.length 0 ? pass : fail, violations, metadata: { duration: Date.now() - startTime, filesChecked: prFiles.length, }, }; } // ─── 标记审查 Agent ────────────────────────────────── /** 当前激活的 Token 注册表 */ const ACTIVE_TOKENS new Set([ --color-primary, --color-primary-hover, --color-surface, --spacing-xs, --spacing-sm, --spacing-md, --spacing-lg, --radius-sm, --radius-md, --radius-lg, --font-size-xs, --font-size-sm, --font-size-base, --font-size-lg, // … 更多 Token ]); const DEPRECATED_TOKENS new Set([ --color-brand, // 已被 --color-primary 替代 --spacing-xsmall, // 已重命名为 --spacing-xs ]); /** * 标记审查 Agent * * 三大检查 * 1. 硬编码值检查任何出现在 margin/padding/font-size/color 中的 * 裸数字或 HEX 值不在 Token 表中 → violation * 2. 废弃 Token 检查使用了 DEPRECATED_TOKENS 中的值 * 3. Token 引用完整性CSS 中引用了 var(--xxx) 但 xxx 不在 ACTIVE_TOKENS 中 */ async function tokenAuditAgent(prFiles: string[]): PromiseReviewReport { const startTime Date.now(); const violations: Violation[] []; for (const file of prFiles) { // 跳过非样式相关文件 if (!/\.(css|tsx|jsx|vue)$/.test(file)) continue; const content readFileSync(file, utf-8); // 检查 1硬编码间距/字号/颜色 // 正则匹配margin: 14px、font-size: 15px、color: #5B6C7D const hardcodedPatterns [ { regex: /(?:margin|padding|gap)\s*:\s*(\d)px/g, rule: token/hardcoded-spacing }, { regex: /font-size\s*:\s*(\d)px/g, rule: token/hardcoded-font-size }, { regex: /color\s*:\s*(#[0-9A-Fa-f]{3,8})\b/g, rule: token/hardcoded-color }, ]; for (const { regex, rule } of hardcodedPatterns) { let match; while ((match regex.exec(content)) ! null) { const value match[1]; // Token 值虽然是裸数字但 CSS 变量用法是 var(--spacing-md) // 这里直接匹配硬编码的裸值 violations.push({ rule, file, severity: error, message: 发现硬编码样式值: ${match[0].trim()}必须使用 Token 变量, suggestion: 替换为 var(--token-name) 形式, }); } } // 检查 2废弃 Token 引用 for (const deprecated of DEPRECATED_TOKENS) { if (content.includes(deprecated)) { violations.push({ rule: token/deprecated-token, file, severity: error, message: 使用了已废弃的 Token: ${deprecated}, suggestion: deprecated --color-brand ? 替换为 --color-primary : 联系设计系统 owner 获取迁移文档, }); } } // 检查 3引用不存在的 Token const varRefs content.match(/var\(--([a-zA-Z][\w-]*)\)/g); if (varRefs) { for (const ref of varRefs) { const tokenName -- ref.replace(/var\(--/, ).replace(/\)$/, ); if (!ACTIVE_TOKENS.has(tokenName) !DEPRECATED_TOKENS.has(tokenName)) { violations.push({ rule: token/unknown-token, file, severity: error, message: 引用了不存在的 Token: ${tokenName}, suggestion: 检查 Token 名称是否拼写正确或该 Token 是否已被移除, }); } } } } return { agent: token, status: violations.length 0 ? pass : fail, violations, metadata: { duration: Date.now() - startTime, filesChecked: prFiles.length, }, }; } // ─── 视觉审查 Agent简化版完整版见 0711/06 ────── async function visualReviewAgent( baseUrl: string, headUrl: string ): PromiseReviewReport { // 此处复用 0711/06.md 中的 checkFidelity 函数的核心逻辑 // 差异50px² 视为 fail const startTime Date.now(); // 简化示意实际需要启动 Playwright 截图 pixelmatch 对比 const diffArea 32; // 模拟差异像素面积 32px² return { agent: visual, status: diffArea 50 ? pass : fail, violations: diffArea 50 ? [{ rule: visual/rendering-diff, file: screenshot_comparison.png, severity: error, message: 渲染差异面积 ${diffArea}px² 超过阈值 50px², }] : [], metadata: { duration: Date.now() - startTime, filesChecked: 1, }, }; } // ─── 合并 Agent ────────────────────────────────────── interface MergeDecision { action: merge | comment | block; summary: string; reports: ReviewReport[]; } /** * 合并 Agent * * 决策规则 * - 任何一个 Agent 返回 fail → 不合并在 PR 评论区回复修复建议 * - 全部 pass → 自动 merge * - lint Agent 报出 warning 级别无 error→ warn-only merge带 comment 标记 */ async function mergeAgent(reports: ReviewReport[]): PromiseMergeDecision { const failedAgents reports.filter(r r.status fail); const allViolations reports.flatMap(r r.violations); if (failedAgents.length 0) { // 一个或多个审查未通过 const failureList failedAgents.map(a - ${a.agent}: ${a.violations.length} 个违规项 ).join(\n); return { action: comment, summary: ❌ 审查未通过以下 Agent 报告了违规项\n${failureList}, reports, }; } // 所有 Agent 通过 const totalTime reports.reduce((sum, r) sum r.metadata.duration, 0); return { action: merge, summary: ✅ 全部审查通过lint token visual总耗时 ${totalTime}ms, reports, }; } // ─── 流水线主入口 ───────────────────────────────────── /** * 多 Agent 审查流水线主入口 * * 用法npx ds-review --pr142 * * 执行流程 * 1. 从 PR 获取变更文件列表 * 2. 三个审查 Agent 并行运行 * 3. 合并 Agent 聚合结果并决策 * 4. 按决策执行merge / comment on PR */ async function runReviewPipeline(prNumber: number) { console.log( 启动设计系统审查流水线 (PR #${prNumber})); // 1. 获取 PR 变更文件列表通过 git diff const changedFiles execSync( git diff --name-only origin/main...HEAD, { encoding: utf-8 } ).trim().split(\n).filter(Boolean); console.log( 变更文件: ${changedFiles.length} 个); // 2. 三个 Agent 并行运行 const [lintReport, tokenReport, visualReport] await Promise.all([ lintReviewAgent(changedFiles), tokenAuditAgent(changedFiles), visualReviewAgent(main, HEAD), ]); // 3. 合并 Agent 决策 const decision await mergeAgent([lintReport, tokenReport, visualReport]); console.log(decision.summary); console.log(决策: ${decision.action}); // 4. 执行决策 if (decision.action merge) { // 自动合并 PR execSync(gh pr merge ${prNumber} --merge --auto, { stdio: inherit }); console.log(✅ PR 已自动合并); } else { // 在 PR 评论区回复违规详情 const commentBody generateCommentBody(decision); execSync(gh pr comment ${prNumber} --body ${commentBody}, { stdio: inherit }); console.log( 已回复 PR 审查意见); } } function generateCommentBody(decision: MergeDecision): string { const lines [decision.summary, , ## 违规详情]; for (const r of decision.reports) { for (const v of r.violations) { lines.push(- \${v.rule}\ ${v.file}${v.line ? :${v.line} : }: ${v.message}); if (v.suggestion) lines.push( ${v.suggestion}); } } return lines.join(\n); } // CLI 入口 const prNumber process.argv.find(a a.startsWith(--pr))?.split()[1]; if (prNumber) { runReviewPipeline(Number(prNumber)); }四、边界分析多 Agent 流水线在真实世界会绊在哪里4.1 Agent 幻觉导致误报——假阳性消耗的不是 CPU 而是信任生成 Agent如 GPT-4o、Claude在生成组件时会偶尔发明不存在的 Token如--color-accent-warm或错误拼接属性名。规范审查 Agent 和标记审查 Agent 确实能拦下大部分但如果三个 Agent 基于同一套 Token 注册表工作表本身不完整——例如有两个 Token 是设计师口头约定的从未录入注册表——三个 Agent 会同时报告使用了未知 Token。当这种误报连续出现 5 次「合并 Agent 审查未通过」就变成了可忽略的噪音人类开始屏蔽审查报告。Agent 流水线的可靠性不取决于最强 Agent而取决于系统中最弱的数据源——Token 注册表如果维护不到位整个流水线是建立在沙滩上的城堡。4.2 视觉审查的阈值困境差异面积阈值 50px² 在 Button 组件上太松1px 偏移就能过在 Dashboard 全页截图下又太紧一个数字变化就触发。正确的做法是按组件类型设不同阈值——但这需要额外维护一个阈值配置表。这又回到了老问题维护的人不够。4.3 并行审查的时序问题如果生成 Agent 提交后规范审查 Agent 和视觉审查 Agent 并行运行但视觉审查 Agent 需要等待预览环境部署完成部署通常需要 2-5 分钟。在实际流水线中规范审查 15 秒跑完视觉审查可能要等 4 分钟——合并 Agent 的 Promise.all 总耗时是 4 分钟而人类在此期间只能干等。实际产品化需要加入超时策略和异步回调Agent 完成后再通知合并 Agent。4.4 合并 Agent 自动 merge的安全边界需要极其明确允许 AI 自动合并 PR 是一个组织级决策不是技术级。建议的约束只对minor和patch变更自动 mergemajor变更如改组件 API 签名、移除 Token必须留人类审批。同时需要一个--dry-run模式只听 Agent 报告不执行任何操作。适用边界多 Agent 流水线适合拥有明确 Token 注册表、活跃维护的设计系统和有明确命名规范的团队。不适合规范本身还在快速演化每周 Token 变更 5、缺少设计系统 owner、或没人维护 Token 注册表的情景。五、总结三个 Agent 并行审查 合并 Agent 聚合决策是设计系统审查的流水线架构 | 审查 Agent 产出的报告格式统一agent/status/violations/metadata便于合并 Agent 消费 | 规范审查 Agent 覆盖 ESLint类型hooka11y、Stylelint禁止!important/禁止裸时长、命名规范PascalCase/kebab-case/camelCase三个维度 | 标记审查 Agent 做三大检查硬编码值检出margin:14px、废弃 Token 引用--color-brand、未知 Token 引用拼写错误| 硬编码检出用正则从 CSS/JSX 文件中逐行扫描 margin/padding/gap/font-size/color 的裸值 | 视觉审查 Agent 的差异阈值需要按组件类型分级不是全局固定 50px²| 并行 Promise.all 受最慢 Agent视觉审查需等部署约束实际需要超时异步回调策略 | 合并 Agent 的自动 merge 安全边界只对 minor/patch 变更自动合并major 必须留人类审批 | 流水线可靠性取决于最弱数据源——Token 注册表不维护全站审查都是噪音 | 误报会消耗信任需要 --dry-run 模式和 fine-grained 审查告警抑制规则。