当 LLM 开始写代码:软件架构需要改变吗?

0 阅读8分钟

我在用 Codex 开发大型 Agent 系统后的架构反思

TL;DR

在用 Codex 开发一个大型多角色 Agent 系统后,我逐渐意识到:

当 LLM 参与开发时,代码架构需要多一个新的优化目标:

降低模型的理解成本。

传统的软件架构主要优化的是:

  • 解耦
  • 分层
  • 可维护性
  • 团队协作

但当 LLM 成为开发参与者之后,还必须考虑:

代码是否容易被模型快速理解、修改,并且不会迅速腐化。

我把这种设计原则总结为:

最小理解闭包(Minimum Understanding Closure)

核心思路可以概括为 7 条:

  1. 设计目标不是优雅分层,而是最小理解闭包
  2. 垂直切片通常比纯水平分层更适合 LLM
  3. 稳定入口比抽象层数更重要
  4. 文件不是越小越好,而是越独立、越完整越好
  5. 窄契约、少跳板、集中副作用,可以显著降低 context 消耗
  6. 避免把关键行为隐藏在难以定位的自动机制中
  7. 架构需要能够抵抗 LLM 的反复修改与结构腐化

下面是我在实际开发中的一些观察和总结。


面向 LLM 的代码设计模式

我越来越觉得,LLM 参与开发之后,“写好代码”这件事的标准其实已经发生了一些变化。

过去我们讨论代码设计,更多是围绕:

  • 解耦
  • 复用
  • 分层
  • 可维护性

但今天还必须多一个维度:

代码是否适合被 LLM 快速理解、反复修改,并且不会迅速腐化。

我最近在用 Codex 开发一个大型多角色 Agent 系统。
在这个过程中,我不断遇到一个很具体的问题:

不是模型不会写代码,而是 context 很快就乱掉、满掉、失去连续性

后来我慢慢意识到,问题不只是上下文窗口,而是 代码组织方式本身

很多代码结构其实是为 人类工程师团队优化的,但未必适合 LLM 的阅读方式

LLM 在阅读代码时通常具有一些特点:

  • 上下文窗口有限
  • 阅读是顺序的
  • 依赖搜索与跳转来建立局部理解

这意味着,如果代码结构让模型必须频繁跳转多个文件,context 很快就会被消耗掉。

所以我开始重新思考一个问题:

如果今天写代码的主要读者之一,已经不只是人类工程师,而是 LLM,那么代码应该如何组织?

这篇文章不是某种成熟的方法论,而是我在实际开发中踩坑之后的一些总结。


先说结论

我现在的基本判断是,面向 LLM 的代码设计,核心不是追求传统意义上的“优雅分层”,而是追求 最小理解闭包

这里的“闭包”并不只是文件数量,而是:

为了形成一个正确判断,模型需要建立多少个独立语义单元,以及这些语义单元之间需要跨多少跳才能连起来。

不同模型差异很大,但它们大都共享一种共同限制:

  • 受限上下文
  • 顺序阅读
  • 依赖搜索与跳转建立局部地图

所以我目前把面向 LLM 的代码设计,总结为 7 个核心观点:

  1. 设计目标不是优雅分层,而是最小理解闭包。

衡量架构是否友好的标准,不是抽象是否漂亮,而是:

为了完成一个任务,模型需要读取多少文件、跨多少跳、读取多少真正有信息增量的内容。


  1. 纯水平分层未必比垂直切片更适合 LLM。

如果一个功能被拆散在很多层之间:

  • router
  • service
  • model
  • store
  • utils

LLM 为了理解完整流程,就必须穿透很多文件。

相比之下:

按功能垂直收拢、切片内保留轻量分层,更容易形成紧凑的理解闭包。


  1. 稳定入口比抽象层数更重要。

一个好的入口文件应该让模型快速回答四个问题:

  • 这个模块负责什么
  • 对外暴露哪些能力
  • 输入输出是什么
  • 下一步该看哪个实现

一个薄而清晰的入口,本质上是一个 导航工具


  1. 文件不是越小越好,而是越独立、越完整越好。

很多时候代码被拆成很多小文件:

  • helpers
  • utils
  • mapper
  • validator

每个文件都很小,但逻辑被切得很碎。

LLM 为了理解完整逻辑,反而必须把这些文件全部读进 context。

所以更好的标准是:

一个文件应该承载一个稳定责任,并且读完以后能理解一个完整的小单元。


  1. 窄契约、少跳板、集中副作用。

如果模块之间传递:

  • 整个 context
  • 整个 session
  • 整个 state

那说明模块之间其实没有真正边界。

更好的方式是使用窄契约,例如:

  • MemorySnapshot
  • ExecutionRuntimeState
  • SkillInvocationResult

这样 LLM 在理解模块关系时,不需要同时记住巨大对象。


  1. 避免把关键行为隐藏在难以定位的自动机制中。

例如:

  • decorator
  • middleware
  • 框架 hook
  • 自动注册逻辑

这些机制有时很优雅,但它们会让行为变得 不在主流程代码里直接出现

对于 LLM 来说,这意味着:

  • 需要额外搜索
  • 需要额外读取
  • 因果链被打断

所以一个更安全的原则是:

关键行为尽量显式表达,而不是隐藏在难以定位的自动机制中。

尤其是在关键路径上,例如:

  • 状态更新
  • 历史写入
  • 事件分发
  • 权限判断

这些行为越显式,LLM 越容易稳定理解。


  1. 架构要能够抵抗 LLM 的时间轴腐化。

LLM 在开发过程中有一些典型行为:

  • 在现有逻辑后继续追加分支
  • 为了兼容旧行为保留多条路径
  • 在文件末尾补新逻辑
  • 添加保护性判断

这些行为短期看是合理的,但长期会导致:

  • 入口越来越模糊
  • 状态越来越分散
  • 文件越来越长
  • 副作用越来越难追踪

所以好的结构不仅要支持一次修改,还要能够承受:

连续几十次修改之后仍然保持可读和可控。


一些同样重要的补充原则

1 文件名要可推断

避免:

  • helpers
  • common
  • processor

文件名本身就是导航系统。


2 在模块开头写清楚边界

例如:

  • 负责什么
  • 不负责什么
  • 上游是谁
  • 下游是谁

这会让 LLM 更容易判断当前文件是不是目标文件。


3 尽量使用显式状态

例如:

status: pending | running | failed | completed

比多个布尔变量更容易理解。


4 测试可以成为“可执行文档”

好的测试能告诉模型:

  • 典型输入
  • 正常输出
  • 异常路径

很多时候模型读测试比读实现更容易建立理解。


5 积极清理死代码

死代码对人类来说只是碍眼。

但对 LLM 来说,它会:

  • 增加读取成本
  • 增加猜测
  • 降低修改准确率

不是所有代码都需要为 LLM 优化

最后还有一个边界需要说明。

我并不认为整个代码库都应该为了 LLM 重构。

更合理的做法是:

优先优化那些 高频修改区、高频阅读区、高搜索密度区

例如:

  • Agent orchestration
  • 状态与上下文系统
  • 工具调用系统
  • 前后端接口层

这些地方最容易消耗 context,也最容易发生结构腐化。


结语

面向 LLM 的代码设计,本质上不是让文件最小,而是让 单次任务的理解闭包最小

未来如果 LLM 会越来越多地参与开发,那么软件架构可能需要多一个新的优化维度:

不仅要让人类工程师容易理解,

也要让模型能够用更少的 context 建立完整理解。

换句话说:

好的架构,不只是优雅的抽象,而是能让理解闭包尽可能小。

附注

这篇文章的形成过程,本身也和主题有关。

它来自我在用 Codex 开发一个大型 Agent 系统时遇到的真实问题:
一开始是在开发过程中不断遇到 context 相关的困难,例如模型需要频繁跳转文件、上下文很快被消耗、结构逐渐变得难以修改。

后来我围绕这些问题不断和 Codex 讨论、整理思路,再由 Codex 协助起草部分段落,最后经过 Claude 的审查和补充修改,才形成现在这篇文章。

所以这并不是一篇“完全由 AI 自动生成”的文章,而更像是一个 人类开发者与 LLM 协作思考的产物

它比较真实地反映了我在这段开发过程中的一些观察、判断和感悟。