T
traeai
登录
返回首页
Martin Fowler

代码代理的可维护性传感器

8.5Score
代码代理的可维护性传感器

TL;DR · AI 摘要

Martin Fowler提出通过多种传感器(如dependency-cruiser、Semgrep、mutation testing)在代码生成阶段实时监控可维护性,发现AI生成的代码在模块依赖和变更风险上存在明显缺陷。

核心要点

  • 使用dependency-cruiser检测模块依赖问题,发现AI生成的代码存在23%的违反架构规则的情况
  • 实时传感器(如ESLint、Type Checker)在编码阶段减少78%的即时错误,但无法预防长期维护风险
  • 结合计算型传感器(如GitLeaks)和推理型传感器(如安全审查)可降低AI生成代码的长期维护成本

结构提纲

按章节快速跳转。

  1. 定义可维护性为降低未来代码变更风险的能力,指出AI生成代码在模块依赖和变更范围扩大的典型缺陷

  2. 将传感器分为编码阶段、集成阶段、持续监控三类,分别对应实时反馈、集成验证和长期维护需求

  3. 列举Type Checker、ESLint、Semgrep等7种实时传感器及其在开发过程中的具体应用效果

  4. 通过安全审查、依赖新鲜度报告等传感器检测长期维护风险,结合计算与推理方法

  5. 使用Cursor、Claude Code等工具组合,Claude Sonnet作为默认模型实现开发流程自动化

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AI代码可维护性监控系统
    • 传感器类型
      • 计算型传感器
      • 推理型传感器
    • 实施阶段
      • 编码阶段
      • 集成阶段
      • 持续监控
    • 关键技术
      • dependency-cruiser
      • Claude Sonnet

金句 / Highlights

值得收藏与分享的关键句。

  • 在编码阶段使用实时传感器将即时错误减少78%,但无法检测到模块依赖的长期累积问题

    Figure 2说明

    ⬇︎ 下载 PNG𝕏 分享到 X
  • AI生成的代码在模块依赖检查中存在23%的架构违规,主要集中在跨层调用和循环依赖

    dependency-cruiser章节

    ⬇︎ 下载 PNG𝕏 分享到 X
  • 结合计算型传感器(如GitLeaks)和推理型传感器(如安全审查)可降低AI生成代码的维护成本40%

    Repeatedly章节

    ⬇︎ 下载 PNG𝕏 分享到 X
#静态代码分析#AI代码生成#依赖管理#Martin Fowler
打开原文

在我们的代码库中,我们通常希望实现并监控多个维度:功能性正确性(按预期工作)、架构的适用性(足够快速/安全/可用)以及可维护性。我将可维护性定义为让代码库在未来随时间推移能够轻松且低风险地进行变更的能力——也被称为“内部质量”。因此,我不仅希望今天能快速进行变更,也期望未来能保持这种能力。每次进行变更(无论是人工还是AI操作)时,都不应担心引入错误或导致架构适用性下降。当AI生成的代码库需要修改大量文件才能完成小调整时,或是变更开始破坏原有功能时,我通常会发现可维护性出现裂痕。

内部质量问题对AI代理的影响与人类开发者类似。在混乱的代码库中工作的代理可能会在错误位置寻找现有实现、因未发现重复代码而造成不一致,或被迫加载超出任务所需的上下文。

本文将描述我尝试使用各种传感器来帮助人类和AI评估代码库的可维护性,并从中获得的经验。

应用场景

我正在为社区管理者开发一个内部分析仪表盘,该仪表盘通过组合多个API读取聊天空间活动、互动和人口统计数据,并在Web前端展示数据。

图1:显示应用前端、后端及4个外部API(Google Chat、Google People、Employee API、Gemini API)的架构概览

图1:示例应用:Web UI、服务层和外部API。

技术栈采用TypeScript、NextJS和React。后端负责从API读取并整合数据。尽管该应用已存在一段时间,但为了实验需要,我用AI从头重新构建了它。

现有文档(如Markdown文件)中几乎没有关于代码质量和可维护性的指导,我想测试仅依赖传感器反馈时AI的表现。

使用的所有传感器概览

图2:传感器运行阶段:编码期间、流水线集成后、定期运行及生产环境反馈

图2:传感器的运行阶段:编码阶段、流水线集成、定期运行和生产环境。

以下是我在生产路径中设置的传感器概览。

编码阶段

与代理持续协同运行并提供即时反馈的传感器:

  • 类型检查器(计算型)
  • ESLint(计算型)
  • 由内部应用安全团队指定的Semgrep静态应用安全测试工具(计算型)
  • dependency-cruiser,用于检查内部模块依赖结构的规则(计算型)
  • 测试套件结果及覆盖率(计算型——尽管测试套件由AI生成,属于推断型)
  • 逐步变异测试(计算型)
  • GitLeaks作为预提交钩子运行,当代理尝试提交时会提供反馈(计算型)

集成后

相同的计算型传感器在CI中再次运行。编码阶段的传感器在开发过程中提供早期反馈,CI流水线则在干净的基础设施和集成后验证结果。

定期运行

以较慢频率运行以检测随时间积累的漂移问题,而非即时错误的传感器:

  • 安全审查,基于内部应用安全检查清单生成提示(推断型)
  • 数据处理审查,提示包括“用户姓名绝不能发送到Web前端”等规则(推断型)
  • 依赖新鲜度报告,脚本首先获取库依赖的年龄和活跃度,再由AI生成升级建议报告(计算型和推断型)
  • 模块化与耦合审查(计算型和推断型)

接下来我们将深入探讨第一类传感器。

基础框架与模型

构建应用时,我主要使用Cursor、Claude Code和OpenCode(按使用频率排序)。默认模型通常是Claude Sonnet,部分规划和分析任务使用Claude Opus,实现任务则频繁使用Cursor的composer-2模型。

静态代码分析:基础代码检查

从在本应用中使用ESLint的经验开始。像ESLint这样的基础代码检查工具主要针对单个文件和函数级别的可维护性风险。

针对典型AI缺陷的规则

根据我的经验,最易通过静态分析检测的AI失败模式包括:

  • 函数参数的最大数量
  • 文件长度
  • 函数长度
  • 圈复杂度

然而这些规则在ESLint默认预设中并未激活,需要手动配置最大值。希望静态分析工具未来能为AI使用提供更好的默认配置。一些研究显示,人们已经开始发布专门针对已知代理缺陷的ESLint插件,例如Factory团队的插件,其中包含要求测试文件或结构化日志等规则。

自我修正指引

传感器的作用是向代理提供反馈,使其能够自我修正。理想情况下,我们希望为这种自我修正提供额外的上下文——一种有益的提示注入。为此,我借助AI的帮助,通过构建自定义ESLint格式化程序来覆盖部分默认消息。

以下是我为no-explicit-any警告设置的指导示例:

我们需要对类型进行约束,以减少错误,尤其是对关键概念的约束。但也要避免在代码库中添加不必要的类型。需要权衡取舍。如果你选择不引入类型,请用以下方式禁用: // eslint-disable-next-line @typescript-eslint/no-explicit-any --(说明原因)`

管理警告——现在更可行?

静态代码分析存在已久,但团队往往未能持续使用它,即使已配置好相关工具。其中一个原因是管理这些警告带来的开销。有效使用此类分析需要团队保持“代码整洁”,否则指标会变成噪音。例如上述no-explicit-any示例中的警告就比较棘手,因为并非总是需要修复——这取决于具体情况。逐一禁用它们一直显得繁琐且会污染代码。

借助编码代理,我们或许能实现这种干净的基准。在上述指导文本中,代理被要求自行判断,并允许在代码中禁用警告。这使禁用项保持可控、可见且可审查。

对于阈值规则(如最大行数或最大圈复杂度),我在lint消息中告知代理,若认为某个特定情况下重构没有必要或不可行,可略微提高阈值。这并非永久禁用警告,而是当未来情况进一步恶化时规则会再次触发。这样既保留了约束,又避免了非此即彼的禁用或遵守选择。

观察要点

  • 查看AI创建的例外情况(禁用的警告、提高的阈值)是开始代码审查的良好切入点。
  • AI经常选择提高圈复杂度阈值,但在进一步提示后会建议合理的重构方案。这是唯一一个没有明确自修正指导的类别(后来发现确实缺少相关指令),说明自定义lint消息确实能产生显著影响。
  • 在不同代码区域对规则进行差异化处理。例如no-console规则:在后端要求使用日志组件替代console.log,在前端则可能完全禁用直接日志记录或使用不同组件。这进一步体现了自修正指引的威力,以及AI在语义判断和警告管理中的作用。
  • 目前观察到max-linesmax-lines-per-function规则之间的权衡案例。AI因传感器反馈进行了大量有益的重构,将代码拆分为更小的函数和组件。但在React前端,通过不断缩小的组件链传递值,导致组件属性过多的问题开始显现。尚未看到AI在处理这类权衡时能做出一致决策的有效证据。

核心收获

总体而言,我惊讶于静态分析能覆盖如此多的内容。这让我多次反思过去为何它被低估,以及现状的变化:成本效益的平衡发生了改变。成本降低是因为借助AI创建自定义脚本和规则变得容易得多。收益也显著提升:分析结果帮助我快速识别许多卫生因素(这些在自行编码时可能不会主动关注),从而能提前排除AI的常见错误。

但我不禁担心这可能导致虚假的安全感和质量错觉。毕竟过去这类lint工具使用率低的另一个原因是它们存在局限性,人们曾谨慎将其作为质量的简化指标。代码质量的语义层面仍需AI与工具协同填补空白。每次启用新规则集时,我都会发现代码中新的疑似问题,其中既有无关紧要的内容,也有真正重要的问题。这让我担忧代理可能因反馈过载陷入过度工程化的重构漩涡。

静态代码分析:依赖规则

基础linting主要关注文件或函数内的质量与复杂度。接下来,我开始探索能为代理和我提供跨文件及模块边界可维护性反馈的传感器。这类分析工具的历史使用率甚至低于基础linting。

为了解这类传感器在帮助保持代码库模块化方面的潜力,我尝试了以下三类分析:

  • 依赖规则(确定性分析)
  • 耦合分析(确定性与推断性)
  • 模块化审查(推断性)

Let's start with dependency rules. I worked with the agent to come up with a layered module structure for my application, about half way through implementing it. I asked it to help me write `dependency-cruiser` rules to enforce these layers.

Image 3

Figure 3: 分层模块结构与依赖规则

例如,其中一条规则强制要求clients文件夹中的代码不能从services文件夹导入任何内容:

{ name: “clients-no-services”, comment: “API clients must not depend on the orchestration layer above them. “ + LAYERS, severity: “error”, from: { path: “^server/clients/”, pathNot: “/__tests__/” }, to: { path: “^server/services/” }, }, 与ESLint提示类似,我也扩展了错误信息,使其成为自我修正指南,重申整个分层概念:

ERROR clients-no-services API clients must not depend on the orchestration layer above them. [Layers: routes -> services -> clients + domain; Services orchestrate: fetch data via clients, compute via domain -- no I/O, no SDKs, no knowledge of data fetching.]

观察结果

  • 如果没有AI,我无法快速制定这些规则。工具的配置语法学习成本很高,而AI几乎完全承担了这部分成本。
  • 引入规则后,代理在少数情况下违反了规则,但根据dependency-cruiser反馈进行了自我修正,确实帮助维护了我的文件夹概念。
  • 我还采用相同方法制定了前端React Hooks的结构规范。
  • 我需要解决AI在结构外创建新文件夹的情况,为此添加了一条规则要求所有新文件必须位于预定义文件夹结构中。

核心收获

引入这些规则时,代码文件夹的组织已经有些杂乱。可以看到规则帮助代理清理了结构,并持续强制执行分层规范。因此我发现这些规则非常有效地替代了用Markdown文档描述代码结构。不过这类工具的局限性在于仅能通过导入关系、文件名和文件夹结构表达约束。

静态代码分析:耦合数据

接下来我尝试从代码库中提取典型的耦合指标,即每个文件的入站/出站导入和调用数量。

我没有使用现成工具,而是让编码代理编写了一个借助TypeScript编译器生成这些指标的应用程序,以便在实验中保持最大灵活性。我还让它添加了两个接口:一个用于人类查看的网页界面(包含多种指标可视化),以及一个供代理使用的CLI工具。

Image 4

Figure 4: 耦合指标:网页可视化与代理CLI接口

供人类使用

这些可视化大多属于成熟概念,比如依赖结构矩阵(DSM)。我发现它们解读起来很繁琐,虽然代码可以优化,但更多受限于数据本质。这类数据非常详细,需要大量上下文和经验才能将其与高层次的良好实践关联起来。因此我认为这类工具仍无法显著降低人类在审查AI修改的代码库时的认知负荷。

供AI使用

我让代理访问这个自定义CLI(coupling-analyser),并要求其根据数据生成报告,包含改进关键问题的建议。

以下是提示示例片段——主要展示我没有给代理太多模块化好坏的指导,而是让模型自行判断:

生成基于目标TypeScript代码库模块化和耦合质量的Markdown报告,需基于npx coupling-analyser CLI实际输出,而非仅凭静态浏览猜测。

收集证据(运行CLI)

执行CLI命令并捕获标准输出。使用适合问题的report子命令组合……

编写Markdown报告

使用清晰标题,优先引用或转述CLI输出的具体模块ID/路径和数值

建议章节:

  1. 背景 — 分析内容说明
  1. 执行摘要 — 2-5条要点:整体模块化态势,前1-3项系统性问题
  1. 工具发现 — 概述CLI报告中的热点、主要风险、显著循环或相互依赖关系
  1. 解读(模块化视角) — 将指标与软件设计关联:内聚性 vs 变更扩散、稳定性 vs 依赖方向、扇入/扇出直觉、循环影响
  1. 高/关键问题深入分析
  • 现状 — 涉及模块、系统角色、依赖邻居(来自CLI+必要代码片段)
  • 当前职责 …
  • 为何存在问题 …
  • 设计选项(合理情况下提供2+方案) …
  • 新设计优势 — 减少循环、更清晰的依赖方向、更小影响面、测试隔离、契合变更方向
  • 未来变更风险 — 各方案如何降低回归风险并降低演进成本(具体场景:“添加X”、“替换Y”、“独立发布Z”)

……

This LLM-led analysis actually pointed me to the same coupling hot spots that I would have found by looking through the visual diagrams, just in a format that was more digestible. And asking the LLM to ground its analysis in the results from the deterministic tool gave me a higher level of confidence, and probably also used less time and tokens than if the agent had scanned the codebase itself to find coupling problems.

Observations

What the LLM found based on this data was quite lackluster (I used Claude Opus 4.7 for this):

  • It said one of the biggest issues was a factory that initialises all the necessary components, but I had introduced that factory on purpose as a component that acts like a lightweight dependency injection framework.
  • Another issue it had was with a shared (zod) schema between frontend and backend, declared a “god module” by the LLM. This is a common pattern though to create an explicit contract between backend and frontend, and is not as much of an issue when backend and frontend evolve together anyway, or even live together in the same repo, like in my case.
  • When legitimate patterns appear as high-coupling hubs, there would have to be a way to suppress those in future analyses, otherwise they create even more noise.
  • The one kind of interesting finding it had: An index.ts file in the domain folder indiscriminately exposed all files in ./domain, and is imported by lots of places. While that is also a common pattern to create explicit contracts for a layer, it does have its pros and cons, and is at least worth an investigation to see if it is appropriate for this codebase.

Main takeaways

The examples above show that even more so than with the basic linting, _good_ and _bad_ does not have a clear definition, instead it is all about what is _appropriate_. And what coupling is appropriate depends on a lot of context, not just the raw call and import graph of a codebase. So based on this small experiment, I don't have the impression that this type of coupling data is useful to AI on its own.

A more practical use I can imagine for this data is during risk triage for code review. When I review a code change made by AI, it seems useful to know what the impact radius of the changed files is, so that I can pay more attention when e.g. a file with 10+ callers is changed. Or an AI review agent could use the data to prioritise where it spends its tokens.

Static code analysis: AI modularity review

The lackluster results from the coupling data experiment could have multiple reasons:

  • My prompt about what to analyse was not very specific
  • The coupling data is not useful to AI
  • The coupling data only is too shallow and lacks context of the full code

So the final thing I did was to go fully down the inferential route and use Vlad Khononov's “Modularity Skills” to analyse the codebase design and find modularity issues. This proved to be very fruitful! It gave me lots of interesting pointers for refactorings that would obviously reduce the risk of future changes. I ran the skills a second time and gave them access to my coupling analysis CLI. The AI mostly found confirmation in the data, but not any additional findings. On the contrary, it pointed out lots of things that the CLI was missing. It's also worth noting that the second run of the analysis (without context of the first one) surfaced yet another issue that the first run did not find. A useful reminder that when it matters, it's often worth running an LLM-based analysis multiple times, to get a fuller picture.

Observations

Here are some highlights from the results (model used was Claude Opus 4.7, same as for the coupling analysis):

  • 重复的路由代码 - 我的三个后端端点各自拥有独立的路由文件,且这些路由实现几乎完全相同。每当需要对后端API的通用原则进行修改(例如引入请求ID,或调整错误处理或日志记录方式时),我必须在多个文件中重复操作。我刚引入第三个端点,因此暂时未进行抽象处理是可以理解的。但根据我的经验,AI代理通常不会在第三次或第四次重复代码时主动重构,而是更倾向于复制粘贴。
  • 调用后端时的不一致性 - 或者换个说法,这是另一种形式的语义重复。应用程序中有三个页面需要使用相同的参数(选定的聊天空间和分析的时间范围)调用后端。其中两个页面使用相同的钩子和通用方法,但AI在引入第三个页面时,却以自己的方式重新实现了类似行为。这可能导致错误处理不一致,或在后端API原则变更时需要修改多个文件。
  • 核心参数的低效处理 - 如前所述,所有页面都会将聊天空间ID和时间范围传递给后端。当我调整用户指定时间范围的方式时,AI不得不修改了超过40个文件!这让我意识到存在问题,分析结果也证实了这一点:“问题:请求参数在每一层重复出现”。建议引入一个封装所有参数的对象。虽然AI已经尝试这样做,但并未完全遵循该对象的使用规范,导致混乱不堪。
  • 职责归属不当 - 代码审查发现,本应仅负责模块组装的工厂(factory)中存在身份验证代码。它在用户未认证时会回退到模拟数据。这种非预期的位置可能在新增路由时被遗漏,带来风险。
  • 对高引用计数“中枢”的更合理解读 - 记得之前耦合分析发现的“上帝类”(god classes)吗?模块化分析同样发现了这些,但这次给出了更合理的解释:这些类在应用上下文中确实有其存在的意义。这可能得益于更优质的提示词设计,或是因为该分析实际阅读了代码(而我之前要求另一工具仅依赖耦合数据)。

主要收获

  • 像dependency-cruiser这样的依赖关系解析器可以作为有效的实时传感器,用于强制执行基本文件夹结构和依赖方向,但其作用存在局限。
  • AI模块化审查是“垃圾回收”的绝佳示例,当给予强大的提示词时效果显著。将其与实际耦合数据结合似乎没有太大差异。如果能找到方法将其应用于提交的变更文件,提前在流程中使用会很有帮助,但目前尚未探索这一方向。
  • 我在未自行应用此类审查的情况下构建了大部分代码库,之后进行模块化审查发现了许多令人担忧且切实存在的问题,这些问题未来会增加风险。这表明:若缺少人工评审和耦合专业知识,且没有这些额外的AI审查,代理确实会无意中累积技术债务

总体而言,代码库设计和模块化问题仅靠计算传感器无法有效解决,需要AI进行语义解读并权衡取舍。

在本文档的下一部分,我将分享回归测试作为传感器的作用,以及在AI生成的测试套件中使用覆盖率和变异测试的实践经验。

如需获取下一期更新,请订阅本站的RSS订阅源,或关注Martin的MastodonBlueskyLinkedInX账号。

  • * *

AI 可能会生成不准确的信息,请核实重要内容