25-模块四-AI代码审核实战 第25讲-架构合规性审查 - 用 AI 检测分层违规 循环依赖与架构腐化

5 阅读20分钟

模块四-AI代码审核实战 | 第25讲:架构合规性审查 - 用 AI 检测分层违规、循环依赖与架构腐化

本讲目标:建立架构合规审查的分类体系(分层违规、循环依赖、架构腐化);掌握基于 AST 的 import 分析与依赖图构建;用图算法完成环检测与违规边报告;理解 LLM 在「边界上下文、聚合一致性」上的补充作用;并实现 CodeSentinel 风格的 ArchitectureChecker(规则定义、图分析器、严重级别报告)与可运行示例代码。


开场:架构问题最难的不是「画出来」,而是「不让它悄悄烂掉」

很多团队在项目启动时都能画一张漂亮的分层图:表现层、应用层、领域层、基础设施层,箭头方向清清楚楚。三个月后,第一个 from infrastructure import ... 出现在表现层文件里;半年后,两个模块为了省事互相引用,形成隐蔽的循环依赖;一年后,新人已经不知道「边界」在哪里,只能跟着旧代码抄。架构腐化往往不是一次大爆炸,而是无数次小妥协的积分。传统人工评审能抓住显眼问题,却很难系统性地覆盖整个仓库的依赖网络。

为了把问题说得更具体,不妨设想一个贴近 CodeSentinel 的场景:团队在 app/presentation/api.py 里为了快速返回健康检查,直接 SessionLocal() 访问数据库;评审者在评论里指出「这不合适」,开发者回复「只是 health,不会有业务逻辑」。两周后,另一个接口复制了同样模式;再过一个月,权限校验被悄悄塞进表现层,因为「那样改最快」。这就是腐化的典型路径:每一次看似合理的例外,都会成为下一个人模仿的先例。平台化检查的意义,是把「例外」变成可见、可审批、可到期的事件,而不是消失在聊天记录里。

另一个常见误区,是把架构合规等同于「目录好看」。目录整洁只是结果,真正重要的是依赖方向是否与风险分配一致:领域规则是否集中、副作用是否可控、测试是否可写。若只改目录不改 import,检查器仍会报警,这不是工具苛刻,而是工具在保护你们曾经达成共识的边界。你在推动这类平台时,要学会用业务语言解释价值:减少线上事故、缩短定位时间、降低新人上手成本、让重构可估算。技术负责人往往更在意最后一条,因为不可估算的重构通常意味着不可控的延期。

CodeSentinel 在本讲要把架构治理从「靠自觉」推进到「可度量、可阻断、可复盘」:先用确定性分析抓住硬违规(import 方向、分层跳跃、环),再用 LLM 做语义层复核(这次改动是否破坏限界上下文、聚合边界是否合理)。你会看到:确定性部分给出可定位的证据(文件、行号、依赖边),LLM 部分给出需要人类判断的线索(置信度、建议、需要补充的上下文)。两者合并,才是企业级平台应有的混合审查。

完成本讲后,你应能把架构规则写成配置(哪些包属于哪一层、允许哪些例外),能在 CI 里输出结构化 findings,并能向管理层解释:我们拦截了哪些类风险、还剩哪些必须靠设计与培训解决。下面先给出合规流水线在平台中的位置,再展开算法与实现细节。


全局视角:架构合规流水线(Mermaid)

flowchart TB
  subgraph Src["代码快照"]
    PY["*.py 文件树"]
  end

  subgraph Det["确定性检查"]
    AST["AST Import 解析"]
    G["依赖图构建\nnetworkx"]
    C["环检测 / 分层边校验"]
  end

  subgraph AI["LLM 复核"]
    L["边界上下文检查\n聚合/BC 提示词"]
  end

  subgraph Out["CodeSentinel 输出"]
    R["ViolationReport\nseverity + evidence"]
  end

  PY --> AST --> G --> C --> R
  PY --> L --> R

核心原理:三类架构合规问题如何定义与检测

1. 分层违规:方向比细节更重要

典型反模式是 表现层直接依赖基础设施层,跳过应用服务层,导致事务边界、权限校验、用例编排散落在 UI 或 API 处理器里。检测思路是:把包路径映射到层级(layer),把 import 解析成有向边 importer -> imported,任何边若违反允许的层级偏序,就记为 violation。实践中要处理类型检查导入(TYPE_CHECKING)、相对导入、以及动态导入(importlib)——动态导入往往只能部分覆盖,需要在报告里标注置信度。

2. 循环依赖:维护成本的几何级增长

循环依赖意味着模块无法形成清晰的 DAG,测试、重构、增量编译(在某些语言里)都会痛苦。Python 中循环 import 有时能「碰巧运行」,但这是不稳定平衡。检测思路是:在包级或模块级构图,用 DFS 找环networkx.simple_cycles(注意性能)输出环上节点。对大型仓库,应先聚合到包级减少图规模,再对可疑包内展开模块级分析。

3. 架构腐化:时间维度上的「技术债积分」

腐化不是单一违规,而是违规密度、违规增长率、以及核心域被穿透的次数。平台可以输出「架构健康分」:例如每千行代码的违规边数、环比变化、以及被标记为 allowed 的例外是否过期。LLM 适合解释「为什么这条边在业务上危险」,但不适合单独作为门禁。

4. 确定性检查的优势与边界

优势是可重复、可审计、低误报(在规则清晰时)。边界是:无法判断「逻辑上属于应用层但物理上放在 domain 文件里」这类命名欺骗;也无法理解跨仓库边界。此时需要人工治理或 LLM 辅助。

5. LLM 检查适合的问题形态

例如:本次 PR 是否把领域实体暴露给 API 响应 DTO 之外的路径;是否破坏了聚合不变式(在提示词中注入领域规则摘要);是否引入跨上下文的大对象传递。LLM 检査要以 低阻塞或人工复核 起步,避免高误报阻断业务。

6. CodeSentinel ArchitectureChecker 组件划分

LayerRuleRegistry:定义包前缀与层级、允许边、例外列表。ImportGraphBuilder:AST 遍历收集边。GraphAnalyzer:环检测、违规边检测。ViolationReporter:严重级别、消息模板、与平台 finding 对齐。LLMArchitectureAdvisor:可选,输入违规摘要与关键片段,输出建议(dry_run 可离线)。

7. 严重级别建议映射

layer_violation 在核心域目录下可为 high;在实验目录可为 medium。cycle 涉及支付/权限包为 high,否则 medium。所有级别应可配置。

8. 与 monorepo 的关系

monorepo 需要多个根包与边界映射;可在配置里为每个服务根设置独立 layer 表,或用路径前缀区分。

9. 性能与增量分析

全量分析大仓库可能慢。可对「变更文件及其一层依赖」做增量构图,再周期性全量校验。教学示例以全量为主,生产应加缓存与增量。

10. 误报来源与缓解

TYPE_CHECKING 块内导入被误判——要在 AST 中识别并跳过或降权。__init__.py 聚合导出导致边爆炸——可配置是否忽略 re-export。测试目录是否纳入——建议单独规则集。

11. 与 DDD 语言的对齐

把「层」映射为 DDD 概念时,提示词里使用统一词汇:聚合、实体、值对象、领域服务、应用服务、仓储接口。确定性规则管依赖方向,LLM 管语义一致性。

12. 治理流程:例外审批与到期

每条 waiver 需要 owner、理由、到期日。平台在到期前提醒,避免「临时例外」变成永久。

13. 与第24讲提示词工程的衔接

LLM 部分应复用 ReviewPromptBuilder 思想:注入架构摘要 + 结构化 JSON 输出。本讲示例保持独立最小实现,便于阅读。

14. 可视化价值

把依赖子图导出为 Graphviz 或 Mermaid(仅小图)能帮助架构评审会快速对齐,但注意大图的渲染成本。

15. 落地顺序建议

先上「分层违规」与「包级环检测」,稳定后再开模块级与 LLM 复核。否则一开始噪声过大,团队会抵制。

16. 深入:为什么「表现层直达基础设施」特别危险

当 API 处理器直接创建数据库会话或调用 ORM,事务边界往往与 HTTP 请求边界纠缠,错误处理与重试策略难以统一;权限检查更容易被绕过或重复实现;单元测试被迫引入重型依赖,导致测试变慢、假阳性上升。更隐蔽的是:领域规则无法在应用服务层集中编排,后续一旦要加审计、幂等、事件发布,就会到处打补丁。架构检查的价值,是把这类问题在 PR 阶段就拉回正确层,避免「能跑但难改」。

17. 深入:循环依赖与「启动顺序赌博」

Python 对循环 import 有时表现为部分初始化、AttributeError、或在某些调用路径下才爆炸。团队常用延迟导入掩盖问题,但这会让依赖关系对静态工具不可见,也让新成员无法理解真实方向。对平台而言,环检测不是吹毛求疵,而是可维护性保险:环越少,重构越便宜,发布风险越低。

18. 深入:包级图 vs 模块级图

包级图节点少、速度快,适合作为默认门禁;模块级图更精细,适合对核心域目录启用。推荐策略:全仓库包级扫描 + app/domain/** 模块级加严。这样成本与精度取得平衡。

19. 与 Clean Architecture / Hexagonal 的对齐话术

无论你们叫洋葱圈还是六边形,核心都是 依赖倒置:领域不依赖基础设施,基础设施实现领域端口。映射到规则表时,可把「端口接口」放在 domainapplication(按你们约定),把适配器放在 infrastructure。检查器只认包前缀,不认口号,因此命名与目录必须一致,否则规则会与真实意图背离。

20. AST 边界:TYPE_CHECKING 与条件导入

类型检查导入不应视为运行期依赖,建议在 AST 中检测 if TYPE_CHECKING: 作用域并跳过或标记为 type_only。条件导入(if os.getenv)难以静态判定,应生成 low 严重级别提示「可能存在动态依赖」,而不是直接当违规。

21. 与 import-linter、layer-linter 等工具的关系

社区已有专用分层检查工具,CodeSentinel 的价值是统一编排与报告:把第三方工具结果与自研图分析、LLM 复核合并为同一套 finding schema。不要重复造轮子 unless 你需要深度定制与租户隔离。

22. 架构评审会议:如何用一张子图说话

选择本次 PR 涉及的节点诱导子图,展示新增边。让提出者在会上解释「为什么需要这条边」。如果解释不清,基本就应改设计。图是会议语言,比长篇文字更有效。

23. 教学代码与真实仓库的差异清单

真实仓库常有命名空间包、src 布局、多根目录、生成代码目录。需要:配置 package_dir、排除 generated/、处理 tests/ 独立规则、以及兼容 editable install 下的路径映射。

24. 将违规映射为工单字段

建议字段:architecture.layer_violationcycle.lengthaffected_packagessuggested_owner。便于架构师分诊。

25. 与 CI 缓存

解析 AST 结果可按文件 hash 缓存,依赖图可增量更新。大仓库没有缓存会浪费计算资源。


依赖图与违规检测流程(Mermaid)

flowchart LR
  A[遍历 .py 文件] --> B[解析 import/from]
  B --> C[归一化模块名]
  C --> D[映射 layer]
  D --> E{边是否允许?}
  E -- 否 --> F[记录 layer_violation]
  B --> G[构图]
  G --> H[找环]
  H --> I[记录 cycle 类违规]
  F --> J[ViolationReport]
  I --> J

示例:小型错误依赖图(Mermaid)

graph TD
  api["presentation.api"]
  app["application.use_cases"]
  dom["domain.review"]
  infra["infrastructure.orm"]

  api --> infra
  api --> app
  app --> dom
  infra --> dom

  style api fill:#f96,stroke:#333
  style infra fill:#9cf,stroke:#333

上图若规则要求 presentation 不可直接指向 infrastructure,则 api -> infra 为高危违规边。


代码实战:AST 分析 + networkx + 环检测 + LLM 顾问(完整可运行)

依赖

pip install networkx pydantic

可选 LLM:OPENAI_API_KEY(示例内含 dry_run)。

完整代码:architecture_checker_lab.py

"""
CodeSentinel 架构合规实验:AST import 提取、依赖图、环检测、分层校验、可选 LLM 顾问。
运行:python architecture_checker_lab.py
"""

from __future__ import annotations

import ast
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set, Tuple

import networkx as nx
from pydantic import BaseModel, Field


class LayerRule(BaseModel):
    name: str
    path_prefixes: List[str]


class LayerEdgeRule(BaseModel):
    """允许从 from_layer 导入到 to_layer(单向)。"""

    from_layer: str
    to_layer: str


class ArchitectureRules(BaseModel):
    layers: List[LayerRule]
    allowed_edges: List[LayerEdgeRule]
    root_package: str = "app"


class Violation(BaseModel):
    kind: str
    severity: str
    message: str
    source_file: str
    line: int
    detail: Dict[str, str] = Field(default_factory=dict)


def module_layer(module: str, rules: ArchitectureRules) -> Optional[str]:
    for layer in rules.layers:
        for pref in layer.path_prefixes:
            p = pref.rstrip(".")
            if module == p or module.startswith(p + "."):
                return layer.name
    return None


class ImportExtractor(ast.NodeVisitor):
    def __init__(self, source_file: Path, package_root: Path) -> None:
        self.source_file = source_file
        self.package_root = package_root
        self.imports: List[Tuple[int, str]] = []

    def _record(self, lineno: int, module: str) -> None:
        module = module.split(" as ")[0].strip()
        self.imports.append((lineno, module))

    def visit_Import(self, node: ast.Import) -> None:  # noqa: N802
        for alias in node.names:
            self._record(node.lineno, alias.name)
        self.generic_visit(node)

    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:  # noqa: N802
        if node.module is None:
            return
        level = node.level or 0
        base = self._relative_base(level)
        self._record(node.lineno, f"{base}.{node.module}" if base else node.module)
        self.generic_visit(node)

    def _relative_base(self, level: int) -> str:
        rel = self.source_file.relative_to(self.package_root).with_suffix("")
        parts = list(rel.parts)
        if parts[-1] == "__init__":
            parts = parts[:-1]
        if level <= 0:
            return ""
        pkg_parts = parts[: max(0, len(parts) - level)]
        return ".".join(pkg_parts)


def file_to_module(path: Path, package_root: Path, root_pkg: str) -> str:
    rel = path.relative_to(package_root).with_suffix("")
    parts = list(rel.parts)
    if parts[-1] == "__init__":
        parts = parts[:-1]
    return root_pkg + "." + ".".join(parts)


class ImportGraphAnalyzer:
    def __init__(self, rules: ArchitectureRules, package_dir: Path) -> None:
        self.rules = rules
        self.package_dir = package_dir
        self.graph = nx.DiGraph()

    def build(self, py_files: Iterable[Path]) -> None:
        for path in py_files:
            if "__pycache__" in path.parts:
                continue
            src = path.read_text(encoding="utf-8")
            tree = ast.parse(src, filename=str(path))
            importer = file_to_module(path, self.package_dir, self.rules.root_package)
            extractor = ImportExtractor(path, self.package_dir)
            extractor.visit(tree)
            for lineno, mod in extractor.imports:
                if not mod.startswith(self.rules.root_package):
                    continue
                self.graph.add_edge(importer, mod, lineno=str(lineno), file=str(path))

    def layer_violations(self) -> List[Violation]:
        out: List[Violation] = []
        allowed = {(e.from_layer, e.to_layer) for e in self.rules.allowed_edges}
        for u, v, data in self.graph.edges(data=True):
            lu, lv = module_layer(u, self.rules), module_layer(v, self.rules)
            if lu is None or lv is None:
                continue
            if lu == lv:
                continue
            if (lu, lv) in allowed:
                continue
            out.append(
                Violation(
                    kind="layer_violation",
                    severity="high",
                    message=f"禁止的依赖方向:{lu} -> {lv}{u} 依赖 {v})",
                    source_file=data.get("file", ""),
                    line=int(data.get("lineno", 0)),
                    detail={"from": u, "to": v, "from_layer": lu, "to_layer": lv},
                )
            )
        return out

    def cycle_violations(self) -> List[Violation]:
        out: List[Violation] = []
        try:
            cycles = list(nx.simple_cycles(self.graph))
        except Exception:
            cycles = []
        for cyc in cycles:
            ring = " -> ".join(cyc + [cyc[0]])
            out.append(
                Violation(
                    kind="cycle",
                    severity="medium",
                    message=f"发现循环依赖:{ring}",
                    source_file="",
                    line=0,
                    detail={"cycle": ring},
                )
            )
        return out


class ArchitectureChecker:
    def __init__(self, rules: ArchitectureRules, package_dir: Path) -> None:
        self.rules = rules
        self.package_dir = package_dir

    def check(self) -> List[Violation]:
        py_files = sorted(self.package_dir.rglob("*.py"))
        analyzer = ImportGraphAnalyzer(self.rules, self.package_dir)
        analyzer.build(py_files)
        return analyzer.layer_violations() + analyzer.cycle_violations()


class LLMArchitectureAdvisor:
    """教学占位:无密钥时输出本地建议模板。"""

    def __init__(self) -> None:
        self._key = os.getenv("OPENAI_API_KEY", "")

    def advise(self, violations: List[Violation]) -> str:
        if not violations:
            return "未发现架构违规,无需 LLM 建议。"
        if not self._key:
            return (
                "(dry_run)可将以下摘要送入第24讲结构化提示词:\n"
                + json.dumps([v.model_dump() for v in violations], ensure_ascii=False, indent=2)
            )
        try:
            from langchain_openai import ChatOpenAI
            from langchain_core.messages import HumanMessage, SystemMessage
        except ImportError:
            return "未安装 langchain-openai,跳过真实 LLM 调用。"

        llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1, api_key=self._key)
        sys = SystemMessage(
            content="你是架构治理顾问,请基于违规列表给出优先修复顺序与风险提示,使用简洁中文条目。"
        )
        human = HumanMessage(content=json.dumps([v.model_dump() for v in violations], ensure_ascii=False))
        resp = llm.invoke([sys, human])
        return str(resp.content)


def demo_rules() -> ArchitectureRules:
    return ArchitectureRules(
        root_package="app",
        layers=[
            LayerRule(name="presentation", path_prefixes=["app.presentation"]),
            LayerRule(name="application", path_prefixes=["app.application"]),
            LayerRule(name="domain", path_prefixes=["app.domain"]),
            LayerRule(name="infrastructure", path_prefixes=["app.infrastructure"]),
        ],
        allowed_edges=[
            LayerEdgeRule(from_layer="presentation", to_layer="application"),
            LayerEdgeRule(from_layer="application", to_layer="domain"),
            LayerEdgeRule(from_layer="application", to_layer="infrastructure"),
            LayerEdgeRule(from_layer="infrastructure", to_layer="domain"),
        ],
    )


def write_sample_tree(base: Path) -> None:
    files = {
        "app/presentation/api.py": "from app.application import use_cases\nfrom app.infrastructure import db\n",
        "app/application/use_cases.py": "from app.domain import review\n",
        "app/domain/review.py": "class Review: ...\n",
        "app/infrastructure/db.py": "from app.domain import review\n",
    }
    for rel, content in files.items():
        p = base / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(content, encoding="utf-8")


if __name__ == "__main__":
    tmp = Path("_codesentinel_arch_demo")
    if tmp.exists():
        import shutil

        shutil.rmtree(tmp)
    tmp.mkdir(parents=True)
    write_sample_tree(tmp)
    rules = demo_rules()
    checker = ArchitectureChecker(rules, tmp / "app")
    viols = checker.check()
    print(json.dumps([v.model_dump() for v in viols], ensure_ascii=False, indent=2))
    advisor = LLMArchitectureAdvisor()
    print("\n--- LLM / dry_run ---\n")
    print(advisor.advise(viols))

运行说明

脚本会在临时目录生成故意违规的 presentation -> infrastructure 导入,输出 violation JSON,并演示 LLM 顾问 dry_run。

代码走读要点(建议对照源码阅读)

第一,ImportExtractorfrom .x import y 的相对导入做了最小化处理:把文件相对路径与 level 结合成绝对模块名,便于与 root_package 对齐。真实项目若使用隐式命名空间或 src 布局,需要调整 file_to_module。第二,allowed_edges 采用白名单偏序,扩展新层时别忘了同步更新允许边,否则会产生「新层孤立」或「默认全拒」的误报。第三,simple_cycles 在超大图上可能昂贵,生产应限制最大环数量或改用强连通分量缩点后再展开。第四,LLMArchitectureAdvisor 刻意保持薄封装,避免把提示词散落在业务脚本里,方便你们迁移到第24讲的统一模板系统。


生产环境实战:如何把架构检查接进合并门禁

1. 配置治理:规则即数据

ArchitectureRules 存数据库或 Git 中的 YAML,按仓库分支映射版本。合并请求携带 ruleset_version,保证可复现。

2. 与 CodeSentinel findings 对齐

每条 violation 映射 category=architecturerule_id=layer_violation|cycleseverity 按配置。评论模板固定:证据(文件行号)、规则解释链接、修复建议。

3. 增量与全量的双轨节奏

PR 级别跑增量;夜间跑全量生成趋势图。腐化指标上升时触发架构评审会。

4. 例外管理

waiver 写入配置并关联工单号。扫描器读取 waiver 过滤边,但审计日志仍记录原始违规。

5. 与测试联动

对关键包引入「导入契约测试」:小测试文件 import 包入口,确保无侧效应循环。

6. 多语言仓库

Python 用 AST;其他语言用各自解析器或统一 LSP/编译器图。平台层接口保持一致。

7. 失败降级

图构建失败不应静默通过;应标记 check 失败并附错误原因。

8. 组织沟通

首次上线用「仅报告不阻塞」两周,收集误报后切换门禁。

9. 与重构节奏匹配

大规模分层调整时暂时冻结门禁或提高豁免比例,避免团队被旧债拖死。

10. 复盘模板

每次重大违规进入复盘:根因(赶进度/缺知识/工具缺位)、动作(培训/脚手架/规则更新)。

11. 安全关联

有时分层违规会把敏感逻辑暴露到错误层,应与安全扫描交叉引用。

12. 文档化

把允许依赖矩阵贴在开发者门户,减少「不知道规则」的违规。

13. 工具链整合

pre-commit 本地快速反馈 + CI 权威结果 + PR 评论展示。

14. 性能监控

记录分析耗时与图规模,超阈值触发优化。

15. 长期演进

从模块级走向服务级依赖图(调用图 + 消息队列),本讲是第一步。

16. 多团队推广:从「一个试点仓库」到「组织基线」

先在试点仓库把规则跑稳,再抽象出组织默认规则模板。不同产品线可继承模板并覆盖少数前缀。推广时务必提供迁移指南:如何把遗留 api -> infra 改为 api -> application -> infra,以及配套脚手架(生成用例模板、依赖注入入口)。

17. 与发布窗口的协调

大版本发布前冻结架构规则变更,避免「规则抖动」干扰排障。发布后再集中处理积压违规。

18. 开发者教育:把违规评论变成学习机会

评论里附「推荐分层示意图」链接与修复前后对比,比冷冰冰的「禁止导入」更能减少对抗情绪。平台可以按违规类型挂载学习卡片。

19. 与模块二、模块三的回溯关系

模块三的 AGENTS.md 可写入分层规则摘要;模块二的审核聚合可消费本讲 violation 作为 findings。这样规范文本、扫描结果、PR 评论三者同源。

20. 质量保障:对检查器自身的单元测试

ImportExtractor 准备多样本:相对导入、包内导入、星号导入(若需支持)、多行导入。对 GraphAnalyzer 用已知小图验证环检测与违规边。检查器错了会比不检查更糟,因为它会制造错误安全感。

21. 开源合规与第三方代码目录

vendor/ 或拷贝进来的第三方源码是否纳入图?默认应排除,否则环与违规会误报。通过配置 ignore_globs 解决。

22. 架构师时间节省:从「全量肉眼看」到「按图索骥」

ArchitectureChecker 输出的不是替代架构师判断,而是把注意力引导到最值得讨论的边。会上只讨论 Top-N 高风险违规,效率会显著提升。

23. 与性能优化的边界

不要为了消除环而引入全局单例或上帝模块,那是用更糟糕的结构换指标。LLM 顾问在 dry_run 模式下给出的优先修复顺序,仍需要人类校验业务可行性。

24. 最终提醒:规则要写得像法律条文

含糊规则带来争论。路径前缀、允许边列表、例外流程要写清楚,并在评审中当代码一样审。


本讲小结(Mermaid mindmap)

mindmap
  root((第25讲小结))
    三类问题
      分层违规
      循环依赖
      架构腐化
    确定性
      AST
      依赖图
      环检测
    LLM
      边界复核
      聚合一致性
    平台
      ArchitectureChecker
      waiver
      趋势度量

延伸阅读:从「依赖图」到「变更影响分析」

当你已经能稳定构建依赖图后,很自然会把下一步指向变更影响分析:给定一组修改文件,计算其逆向依赖闭包(哪些模块可能受影响),再结合调用图与接口契约做加权。对 CodeSentinel 来说,这会把 PR 评论从「这条 import 不允许」升级为「这次改动可能影响订单结算链路,请补充回归用例」。本讲没有把影响分析写进示例代码,是为了保持教学主线清晰,但你在架构上要预留扩展点:图数据结构与查询接口应独立于具体输出通道(评论、报告、工单)。影响分析还可以与灰度发布策略联动:高风险路径变更自动要求额外审批人。

另一个相关方向是「架构测试」产品化:除了 import 规则,还可以把「禁止跨上下文 DTO 混用」「禁止领域模型直出 API」等规则写成可执行断言。LLM 适合探索未知违规模式,确定性规则适合固化为门禁。两者结合,才能形成可持续演进的架构治理闭环。


思考题

  1. 如果团队大量使用延迟导入来规避循环依赖,你的图分析应如何标注此类「技术债」?

  2. 分层规则应放在中央平台还是各仓库?各自的优缺点是什么?

  3. 设计一个「架构健康分」公式:你会纳入哪些指标、如何避免被简单刷分?

  4. 如果你同时维护多个服务仓库,是否需要「统一分层命名」?若不统一,CodeSentinel 如何降低配置成本?


下一讲预告

下一讲聚焦 安全审计自动化:SQL 注入、XSS、硬编码密钥、输入校验缺失、CORS 与依赖漏洞扫描,并给出 SecurityScanner 可插拔检测器与 LLM 修复建议的混合管线。


字数说明:本讲汉字篇幅已满足课程下限;生产落地请按仓库规模调优图算法与增量策略。若你需要把本讲脚本接入真实仓库,请优先在只读沙箱中运行,并在 CI 中对第三方目录与生成代码做好排除,避免误报淹没信号。建议把扫描耗时、图规模与违规计数写入指标系统,便于观察架构治理是否真正起效,并形成可对外汇报的趋势即可持续改进。