Skip to content
Wen's Blog

如何解决长期 AI Coding 产生的代码屎山?

最近整理自己的 Coding Agent Skills 时,我研究了一个项目:simplify-codebase (opens in a new window)。它从 DeepSeek Harness 的 simplification 实践里提炼出了一套更通用的方法。

一开始我以为它做的就是普通的“代码重构”:找重复代码、删掉没用的 abstraction、简化复杂函数。看下去才发现,它想解决的问题和我长期使用 AI Coding 的感受很接近:AI 写出来的代码通常没错,甚至非常“规范”,却会一点一点把项目写成屎山。

这种屎山和传统印象不太一样:没有几千行代码塞在一个文件里,没有叫 a1、a2 的变量,也没有满屏复制粘贴,看起来甚至非常专业。Interface、Dependency Injection、Repository、Adapter、Factory、Strategy、Mock 一应俱全,RetryPolicy 也抽出来了,连 Clock 都可以注入。

单独 review 每一个 PR,好像都挑不出什么大问题,但半年之后再看,很容易疑惑:这玩意到底为什么需要这么复杂?

AI 特别擅长写“正确但没必要”的代码

假设原来有这么一个函数:

Future<User> loadUser() async {
final response = await api.getUser();
return parseUser(response);
}

让 Coding Agent 给它补测试,很可能最后变成:

Future<User> loadUser({
ApiClient? apiClient,
UserParser? parser,
RetryPolicy? retryPolicy,
Clock? clock,
Logger? logger,
Metrics? metrics,
}) async {
final client = apiClient ?? ApiClient.instance;
final userParser = parser ?? DefaultUserParser();
final retry = retryPolicy ?? DefaultRetryPolicy();
// ...
}

这样当然很好测试,可以注入 FakeApiClient、FakeClock、FakeRetryPolicy、FakeParser,甚至可以精确验证第一次失败、等待多久、第二次重试、parser 被调用几次、logger 收到什么、metrics 上报了什么。Coverage 也会很好看。

但真实生产代码需要这么多 variability 吗?如果整个应用永远只有一个 ApiClient、一个 parser,retry policy 也从来不会动态替换,那么这些注入点主要是给测试用的。为了让测试更容易控制内部行为,我们反过来改变了生产代码的设计。我现在越来越警惕这种 Test-induced Architecture。

测试方便,不等于生产抽象合理

测试当然重要,但仅凭“方便测试”,还不足以证明一个 production abstraction 应该存在。

合理的 Seam 往往来自真实系统边界,例如 HTTP、Database、File System、Clock、Process、External Service、Platform API。这些地方确实存在不同的 ownership、failure mode 或运行环境,把它们隔离出来通常是合理的。另一类 Seam 则只是因为“测试的时候想把这里替换掉”才出现,这种情况就要谨慎得多。

我现在判断这类设计时,经常会问四个问题:

  1. 如果没有测试,这个 Interface / injection point 还会存在吗?
  2. 是否有真实生产调用方需要替换这个 dependency?
  3. 它是否对应真实的 ownership、failure、process 或 I/O boundary?
  4. 删除这个 seam 后,是否仍然可以通过公开生产行为验证 correctness?

如果答案是 No / No / No / Yes,通常就很值得考虑简化。但不能仅凭“只有测试在调用”就直接删除;动手之前,还要确认调用方、动态注册、兼容性、持久化格式和行为是否真的不受影响。

最麻烦的不是 Dead Code

Dead Code 其实没有那么可怕,IDE、编译器和静态分析工具很容易告诉你哪里有 unused function、unused import、unreachable code。难清理的是那些有调用者、有测试、有类型、有文档,但没有多少真实业务价值的代码。

比如这样一套结构:

HttpClient
↓
HttpClientAdapter
↓
NetworkService
↓
ApiService
↓
UserRemoteDataSource
↓
UserRepository
↓
UserRepositoryImpl

每一层单独看都说得通,甚至每一层都有自己的职责说明和测试。但如果其中四层做的事情基本只是:

return nextLayer.call(...);

这六个 abstraction 没有给系统带来相应的价值,却成了以后必须长期保持一致的六个概念。改一个参数,六层跟着改;改一个 error type,六层重新 mapping;加一个字段,六层 DTO 跟着传。大家忙着同步这些改动,业务却没有得到与复杂度相称的收益。

这类代码不能简单叫 Dead Code,更像是一种 Maintenance Obligation:只要它存在,你以后就必须继续理解它、维护它、测试它、迁移它、兼容它。

DeepSeek Harness 如何寻找简化机会

DeepSeek Harness 把这类调查做成了具体流程。它在仓库里维护了一个 dsh-find-simplifications (opens in a new window) Skill,专门寻找当前设计成本已经超过收益的代码面。

它列出的强候选包括:

它还明确要求先区分 production consumer、non-production consumer 和 ambiguous consumer,不能因为静态搜索没找到调用就直接删。

仓库里还有一个很典型的真实案例:Remove the agent/steering mirror emit (opens in a new window)。这个 transient event 和前一行已经写入 durable log 的 steering/message 表达的是同一个事实,调查后确认它没有任何生产 listener,唯一 subscriber 是一个 regression test。

调查之后,他们把这个 event 的 declaration、emit、README、架构文档、catalog 以及对应测试依赖一起删除,测试改为验证保留下来的 durable event。这段代码有测试依赖,却没有独立的生产价值,继续保留只会增加维护面;清理这类代码和普通的 unused code cleanup 不是一回事。

AI 特别容易制造哪些 Maintenance Obligation

长期使用 Coding Agent 后,我现在会特别留意下面几类代码。

比如过度 Dependency Injection,一个函数有十几个参数:clock、logger、parser、client、retryPolicy、metrics、scheduler、validator、callback。看起来高度解耦,但生产环境只有一种组合。很多时候,业务并不需要这么多“解耦”,这些参数只是为了让测试能“控制一切”。

Relay Layer 则是一层调用下一层,下一层再调用下一层。每层都没有真正隐藏复杂度,也没有形成独立 ownership,只是把调用链拉得更长。

还有为测试暴露内部状态的代码,例如 retryCount、pendingCount、debugState、onStarted、onCompleted、onRetry、isInitialized。如果生产调用方完全不需要知道这些状态,而它们唯一的消费者是测试,那么 production Interface 很可能已经被测试污染了。

至于 Defensive Programming Theater,AI 很喜欢写 validate、copy、fallback、try/catch、retry、rollback、null check、duplicate guard,因为每一条单独看都显得更“安全”。但在同一个 trusted boundary 内部,如果函数 A 调函数 B,B 又把 A 已经验证过的所有东西重新验证一遍,通常没有增加多少安全性,只是增加了更多分支。

Ownerless Flexibility 往往长这样:Strategy、StrategyFactory、StrategyRegistry、DefaultStrategy、FallbackStrategy 都有了,最后生产代码永远只有 DefaultStrategy。问为什么要保留 Factory,答案往往是“以后可能需要扩展”。未来当然可能需要,也可能永远不需要,但我们从今天开始就得为这个假设付出维护代码的成本。

AI 为什么特别容易把这些东西写出来?

AI Coding 的工作方式和人不太一样。一个有经验的工程师做设计时,会在脑子里尝试很多方案:要不要抽 Interface,好像没必要;要不要做 Factory,目前只有一种实现,先算了;这里加个 callback 测试更方便,但不值得污染生产接口。这些被否定的方案通常不会进入 Git。

AI Agent 更容易把探索时尝试的方案留成代码:Interface、Adapter、Helper、Fixture、Fallback、Compatibility Layer、Callback、Temporary Abstraction。尤其当我们同时要求它“实现功能、补齐测试、覆盖 edge case、遵循最佳实践、保持可扩展”时,很容易得到一份逐项满足要求的实现。单独看,每一项都能解释得通,组合起来却往往没人再问:我们真的需要长期维护这么多东西吗?

这些设计逐渐积累,探索成本也就留在了 production codebase 里。 人类会把很多失败方案丢在脑子里,AI 则更容易把其中一部分以接口、抽象、兼容层或测试支撑代码的形式留在仓库里。

把 Simplification 加入开发流程

以前我的开发流程大概是:

Requirement
↓
Design
↓
Implement
↓
Test
↓
Code Review

现在我越来越倾向于加入一个阶段:

Requirement
↓
Design
↓
Implement
↓
Test
↓
Simplify
↓
Code Review

除了用 Simplify 把当前 PR 写得简单一点,我认为长期运行 Coding Agent 的项目还应该定期做一次代码库级别的 simplification survey:寻找没有真实生产 ownership 的维护义务,证明哪些东西可以删,再在删除后重新验证行为。

Codebase
↓
Simplification Survey
↓
寻找没有真实生产 ownership 的维护义务
↓
证明可以删除
↓
删除
↓
重新验证行为

我更喜欢把这个过程叫 Entropy Reclamation,也就是“熵回收”。系统运行久了,本来就会留下历史实验、迁移残留、测试脚手架、过时兼容层、推测性 abstraction、没人使用的 extension point、重复状态和重复 wiring,AI Coding 只是把这个过程加速了。

Simplify 不是追求更少的代码

如果目标变成“谁删的代码多谁厉害”,很容易走向另一个极端。比如删掉一个 Repository,然后让所有调用方自己写数据库逻辑,LOC 确实下降了,系统反而更差。

我想减少的是需要维护的事实、需要同步的状态、需要兼容的 Contract、需要理解的 abstraction,以及需要长期支持的 variability。这也是我认同下面这句话的原因:

Delete obligations, not lines.

删除的是维护义务,不是代码行。有些 500 行代码只承担一个清晰、稳定的复杂行为,完全应该留下;有些 30 行的 Interface + Factory + Adapter,却让整个项目从此多维护三个概念,那反而更值得质疑。

Architecture Review、Codebase Design 和 Simplify 不是一回事

我最近整理自己的 Coding Agent Skills 时,也专门把这三个能力拆开了。

review-architecture 回答的是“当前架构合理吗?”,它会去发现状态 ownership 错误、依赖方向倒置、同一个事实被多处维护之类的问题。

codebase-design 回答的是“如果要解决,正确的 boundary 应该怎么设计?”,它讨论的是 Module、Interface、Seam、Adapter、Dependency Direction。

simplify 则先问:这个东西真的需要存在吗? 在改善一个 abstraction 之前,先确认它存在的理由。如果没有 production consumer,没有真实变化来源,也没有真实 ownership,只剩下一堆测试和历史原因在支撑它,那么删除可能比继续重构更合适。

测试也需要重新审视

过去我们经常说,不要为了代码方便而牺牲测试,这当然没问题;同样,也不要为了测试方便,长期污染生产设计。

好的测试应该尽可能验证真实 Interface。如果 production consumer 调的是 submitOrder(),测试也尽量从 submitOrder() 验证最终结果,而不是为了测试方便,把内部的 validate()、calculate()、retry()、persist()、notify() 全部暴露出来,再分别 assert 每一步。否则最后得到的可能是一套非常容易测试的 Implementation,以及一套越来越难修改的系统。

长期 AI Coding 需要“生成”和“回收”两个方向

现在大家讨论 AI Coding,关注的基本都是 Agent 能不能写更多代码、能不能跑更久、能不能自己修 bug、能不能自己开 PR,这些都属于 Generation。

但如果 Agent 真的开始长期参与一个项目,它也会越来越需要 Reclamation。它得判断什么时候不该再加一层,某个 Interface 是否已经没人需要,Factory 是否可以删掉,以及 test-only seam 是否还应该留在生产设计里。它也得识别已经结束的 migration、已经过期的 compatibility path,以及描述同一件事的两个 state。

一个长期运行的 Coding Agent,如果只会不断添加东西,却不会主动证明哪些东西可以安全删除,最终很可能只是一个更高效的技术债生成器。这也是我最近研究 simplify-codebase 最大的感受。

下次 AI 写出一个 Interface、Factory、Adapter、Strategy,或者往函数里塞进十几个 dependency injection 参数时,不妨问问:这是业务真的需要,还是 AI 为了证明自己写得对? 这个问题可能比“代码够不够规范”重要得多。