架构漂移检测:保持代码与设计的一致性
在你的组织中,某处存在着一张错误的架构图。也许它展示了一个六个月前已被合并到其他服务中的微服务。也许它标注着 Redis 作为缓存层,而团队早在一次生产事故中就切换到了 Memcached。也许它描绘了一个整洁的六边形架构,而这个服务实际上已经积累了足够多的捷径和变通方案,看起来更像一团意大利面。
这就是架构漂移:系统文档化描述与实际运行方式之间逐渐的、无声的偏离。与 Bug 不同,漂移不会触发告警。与性能退化不同,它不会出现在监控中。它静静地潜伏着,直到有人基于过时的文档做出了决策——而那个决策最终被证明是错误的。
架构漂移是普遍存在的。每个团队都会经历它。问题不在于你的文档是否会漂移,而在于你多快能发现它,以及你将如何应对。
关于后半个问题,建议从来不缺。把文档放在代码旁边。在同一个 Pull Request 里评审它们。把它纳入 Definition of Done。这些都是好建议,其中大部分在本页下文也会出现,而它们共有一个盲区:它们告诉你该做什么,却不告诉你这些做法是否奏效。通行建议里最接近一次验证的东西,是最后编辑时间戳——它告诉你有人在什么时候动过这个文件,而不是这个文件是否属实。
检测漂移,是被跳过的那一半。本指南涵盖这个问题本身、检测方法的五个流派,以及每一种能看见什么、看不见什么。另有两篇姊妹文章各自深入一件事:漂移分数是如何计算的,以及这个数字意味着什么,以及有了模型之后,让它保持为真的那些实践。
什么是架构漂移?
架构漂移是指软件系统的实际实现偏离其文档化或预期架构的现象。Perry 和 Wolf 在 Foundations for the Study of Software Architecture(ACM SIGSOFT Software Engineering Notes,1992)中为这个问题命了名,他们把源自违反架构的 erosion(侵蚀)与源自对架构不敏感的 drift(漂移)区分开来。日常用法此后已经变化:今天大多数工程师用"漂移"指代文档与代码之间的任何落差,本指南也在这个意义上使用它。这个区分仍然值得保留,下文有专门的一节谈它。
漂移在架构文档的各个层面都会出现:
结构漂移
文档化的结构不再与代码库匹配:
- 一个记录为独立容器的服务被吸收进了单体应用
- 一个组件被重命名了,但图表仍然显示旧名称
- 一个新服务被创建了,但从未被添加到架构模型中
- 数据库从 MySQL 迁移到了 PostgreSQL,但容器图仍然标注为 MySQL
行为漂移
文档化的行为不再与现实匹配:
- 一个同步 API 调用被替换为异步消息,但关系描述仍然标注为 "REST/HTTP"
- 数据流被改为通过 API Gateway,但图表仍显示服务间的直接通信
- 添加了一个认证步骤,但未反映在系统上下文图中
依赖漂移
文档化的依赖关系不再匹配实际的集成情况:
- 一个第三方 API 被替换为内部自研方案
- 添加了一个新的外部依赖(支付服务商、监控服务),但未被记录
- 一个集成已被停用,但仍出现在系统上下文图中
决策漂移
文档化的架构决策不再被遵循:
- 一条 ADR 规定"所有持久化存储使用 PostgreSQL",但某个团队开始使用 MongoDB
- 合规性规则规定"前端不得直接访问数据库",但有人添加了客户端的 Supabase 集成
- 部署架构标明"单区域部署",但服务实际上已部署到了多个区域
架构漂移发生的原因
理解漂移的成因,是防止它的前提。漂移通常既非恶意,也谈不上疏忽——它是软件开发方式的自然产物。
速度优先于文档
当一个功能必须在周五交付时,更新架构图是第一个被砍掉的事项。代码变更是交付物,文档更新是额外开销。这在短期内是理性行为,在长期则是毁灭性的。
大量微小的变更
漂移很少发生在某个戏剧性的时刻。它是通过数百个细小变更累积起来的,每一个都小到不足以触发一次文档更新:
- 重命名一个文件
- 添加一个工具包
- 更换一个库依赖
- 把一个函数抽取到独立模块
没有哪一次变更单独看足够重要到需要更新文档。但它们合在一起,改变了整个架构。
团队人员流动
工程师离开时,会把隐性知识一并带走。新团队继承了代码库,却没有继承"它为什么是这样结构"的理解。他们依据在代码里看到的东西做修改,而不是依据文档所写的,于是漂移被进一步拉大。
缺少反馈回路
如果没有人检查文档是否与现实一致,漂移就是不可见的。没有检测机制,发现漂移的唯一途径就是在一次事故中、一次审计中,或者当一位新工程师指出图表与代码对不上的时候。到那时,漂移可能已经相当严重。
紧急变更
生产事故常常要求架构上的捷径:直连数据库而不走 API 层、硬编码配置而不用 config 服务、一个变成永久的临时缓存。这些变更绕过了正常的评审流程,也很少被记录下来。
架构漂移的代价
漂移不只是个美观问题。它有具体且可衡量的代价。
糟糕的决策
当架构师基于过时的文档做决策时,那些决策可能是错的。"这个服务流量很小,所以我们可以承担一个同步依赖"——只不过文档已经过时,而该服务实际处理着文档中所记载负载的 10 倍。
缓慢的上手过程
新工程师依赖架构文档来建立心智模型。如果文档是错的,他们建立的就是错的心智模型。他们写出与实际架构不匹配的代码。他们提出暴露自身困惑的问题,消耗资深工程师的时间。
故障响应
在一次生产事故中,架构图本应帮助团队理解影响范围和依赖关系。如果这些图是错的,团队就会浪费宝贵的几分钟去追查错误的依赖链条,或者遗漏关键的上游系统。
合规与审计失败
在受监管的行业,架构文档常常是合规(SOC 2、ISO 27001、HIPAA)所要求的。如果审计人员发现文档与现实不符,那就是一项审计发现——而且可能是严重的一项。
AI 智能体的混乱
随着 AI 编码智能体日益普及,它们越来越依赖架构文档来获取上下文。一个读取了过时 C4 模型的智能体,会生成符合文档化架构而非实际架构的代码。这放大了漂移,而不是修复它。
如何检测架构漂移
常见的方法有五种,它们回答的是不同的问题。人工评审问的是:在场的人看来,这张图是否仍然正确。适应度函数和静态分析问的是:是否有具体规则正在被打破。LLM 评估问的是:这份代码读起来是否像它声称在实现的那个设计。漂移评分问的是:文档化的模型还有多少仍然存在。看哪个问题正在让你付出代价,就选哪一个。
人工评审(传统方法)
最简单的方法是定期人工评审:把团队召集起来,过一遍架构图,检查它们是否仍与现实一致。
何时有效:小团队、简单架构、季度节奏。
何时失效:大型系统、快速推进的团队,或者最了解代码的人没有时间参加评审会议时。人工评审还会受到确认偏误的影响——人们倾向于看到自己预期看到的东西。
架构适应度函数
由 Neal Ford 和《Building Evolutionary Architectures》一书推广的适应度函数,是用来验证架构属性的自动化测试:
// Example: Ensure no direct database imports in handler packages
func TestNoDatabaseImportsInHandlers(t *testing.T) {
packages := analyzeImports("./internal/handler/...")
for _, pkg := range packages {
for _, imp := range pkg.Imports {
assert.NotContains(t, imp, "database/sql",
"Handler %s imports database/sql directly", pkg.Name)
assert.NotContains(t, imp, "gorm.io",
"Handler %s imports GORM directly", pkg.Name)
}
}
}
适应度函数在强制执行特定规则方面很强大,但编写和维护都需要前期投入。它们检查的是约束,而不是完整的模型。
静态分析工具
ArchUnit(Java)、Deptrac(PHP)和 go-arch-lint(Go)之类的工具会分析代码结构并强制执行依赖规则:
// go-arch-lint configuration
components:
handler:
in: ./internal/handler/
service:
in: ./internal/service/
repository:
in: ./internal/repository/
rules:
handler:
can_depend_on: [service]
service:
can_depend_on: [repository]
repository:
can_depend_on: []
这些工具非常适合在单个代码库内强制执行分层架构。它们不处理跨服务的漂移,也不验证架构模型是否与代码一致。
LLM 辅助评估
Thoughtworks 把用 LLM 减少架构漂移放进了技术雷达第 34 期(2026 年 4 月)的 Assess 环。他们对这个问题的表述值得引用,因为它来自厂商以外的地方:
Increased use of AI coding agents can accelerate drift from the intended codebase and architecture designs. Left unchecked, this drift compounds as agents and humans replicate existing patterns, including degraded ones, creating a feedback loop where poor code begets poorer code.
中文大意:AI 编码智能体使用的增加,会加速代码库与架构设计相对于原本意图的漂移。若放任不管,随着智能体和人类不断复制既有模式(包括已经劣化的模式),这种漂移会不断累积,形成劣质代码催生更劣质代码的反馈回路。
他们描述的技术,是把确定性分析工具(他们点名了 Spectral、ArchUnit 和 Spring Modulith)与 LLM 评估搭配起来,捕捉规则引擎无法表达的语义违规,然后再用 LLM 协助修复所发现的问题。他们的团队已把它应用在 API 质量规范上,以及用于定义引导智能体生成变更的架构区域。
他们的两条经验,无论你用什么工具都值得带走。第一次扫描会翻出多到没人分诊得完的违规,所以确定优先级才是真正的工作。以及,智能体给出的修复需要它自己的验证回路,因为"它改动了代码"和"它改善了系统"是两个不同的主张。
Assess 是 Thoughtworks 用来表示"值得一看,但我们还不推荐"的那个环。请就照这个意思理解它。它所确定的是:这个问题真实到足以让一家大型咨询公司把它写下来——而这已经比大多数关于漂移的论述能拿出的依据要多了。
自动化漂移评分
这是 Archyl 采用的方法。它不是检查具体规则,而是将整个架构模型与代码库进行校验:
- 每个文档化的系统是否对应一个仓库?
- 每个文档化的容器是否对应代码库中的一个目录?
- 每个文档化的代码元素是否指向一个仍然存在的文件?
- 每条文档化关系的两端是否仍然有效?
结果是一个 0 到 100 的分数,以及一份按元素展开的明细:哪些匹配上了,哪些文档里有但实际已消失,哪些代码里存在却从未被写下来过。适应度函数检查的是你想到要写下来的那些约束,而这里检查的是你已经拥有的整个模型。
Archyl 漂移检测的几项关键设计决策:
轻量。 不调用 AI,也不拉取文件内容。向你的 Git 提供商发起一次递归树请求,然后用路径和名称与模型做匹配。计算耗时以秒计。
确定性。 相同的代码库、相同的模型,得到相同的分数。不存在 LLM 温度或提示工程带来的波动。
低成本。 每次 push 都可以跑,不必担心费用。一天算一百次也没问题。
可执行。 明细会点名哪些元素发生了漂移,你因此知道该修什么。
代价就在第一条里。检查路径和名称而不是读代码,让分数变得快速、免费且可复现,同时也意味着这项检查是结构性的。它能看见目录已消失的容器,能看见文件已被删除的代码元素。它看不见那种两个服务名字都没变、而 REST 调用已经变成队列消息的情况。那是行为漂移,也是本指南开头那份分类里唯一一种廉价检查抓不到的类型。面对它,你手上有的是人工评审和 LLM 评估。
漂移分数的详细计算方式一文涵盖了计算公式、分母中排除了什么以及为什么排除,还有其余的限制。
闭合回路
只有检测,什么也改变不了。一个由某人算过一次、看过一眼的分数,是审计,不是反馈回路。有三种机制能把它变成回路,另外还有一个区分,最好在接线之前就搞清楚。与它们并行的那些工作流实践——architecture as code、把文档纳入 Definition of Done、采用合规性规则——是活的架构文档那篇文章的主题。
在 CI 中自动化漂移检测
最有牙齿的机制,是一个在漂移超过阈值时失败的 CI 门禁,因为只有它能拦住一次合并:
on:
push:
branches: [main]
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: archyl-com/actions/drift-score@v1
with:
api-key: ${{ secrets.ARCHYL_API_KEY }}
organization-id: ${{ secrets.ARCHYL_ORG_ID }}
project-id: 'your-project-uuid'
threshold: '70'
当构建因为漂移分数下降而失败时,必须有人在合并前把它修好。文档的准确性就变得和测试通过一样不可妥协。
把阈值设在你当前分数之下,而不是设成你希望达到的那个数字。一个第一次运行就失败的门禁,会在第一次运行时就被关掉。随着团队养成习惯,再逐步提高它。
配置漂移告警
Archyl 支持针对漂移事件的 Webhook 告警:
drift.score_computed:每次漂移计算完成时触发。可以推送到 Slack 频道以提高可见性。drift.score_degraded:当分数下降 10 分以上时触发。这是你的早期预警系统。
把这些告警配置到团队真正会看的频道。意识到问题,是采取行动的第一步。
进行架构评审
每月或每季度的架构评审有多重作用:
- 验证文档化的架构是否仍与现实一致
- 识别自动化工具遗漏的漂移(例如行为漂移)
- 讨论已漂移的组件应该在代码中修正,还是在文档中修正
- 复查并更新那些可能需要重新考虑的决策所对应的 ADR
别把漂移和合规性混为一谈
这两者被放在一起跑的频率高到值得专门区分,因为它们的计算方式不同,失败的原因也不同。
漂移检测问的是:你的模型是否与现实一致。它把文档化的架构与仓库做比对,产出一个分数。
合规性规则问的是:现实是否遵循你的规则,比如前端容器不得依赖数据库容器、所有公开 API 都必须经过网关、每个服务拥有自己的数据库。一个已经严重漂移的模型,合规性检查照样可能通过;而一个完全准确的模型,也可能违反你定下的每一条规则。
两者你都需要,而且不该把其中一个数字当成另一个来读。
架构漂移 vs. 架构侵蚀
这两个术语相关但不同:
架构漂移是文档与实现之间的偏离。代码可能完全没问题——只是文档错了。
架构侵蚀是架构本身的退化。代码违反架构原则,累积技术债,变得越来越难以维护。侵蚀是代码质量问题。漂移是文档准确性问题。
1992 年,Perry 和 Wolf 把这条线划在了别的地方:在他们那里,两者都是系统的属性而非文档的属性,侵蚀源自违反架构,漂移源自对架构不敏感。现代用法更宽松,也对一线团队更有用,但如果你去读关于架构侵蚀的学术文献,要预料到这些术语的位置与这里并不相同。
它们常常同时出现。当文档发生漂移时,团队会失去对预期架构的认知。缺少这种认知,他们所做的修改就会侵蚀架构。漂移使侵蚀成为可能。
这就是为什么漂移检测的意义超出了单纯的文档准确性。准确的文档充当一个防止侵蚀的参照。当所有人都能看到预期的架构时,他们更有可能去维护它。
随时间衡量与追踪漂移
单次的漂移分数是有用的。趋势才是强大的。
建立基线
在改变团队任何工作方式之前,先跑第一次计算。它返回什么,那就是你的基线;而第一个数字偏低是信息,不是判决。一份从来没有人被要求维护的文档,并不是失败了,它只是从未被衡量过。
忍住在第一次运行前先修一修的冲动。你要的是描述你真实处境的那个数字,而不是大扫除一个周末之后得到的那个。
追踪趋势
单个分数是关于今天的一个事实。真正告诉你"你改的东西有没有起作用"的,是趋势:
- 漂移随时间在改善还是在恶化?
- 是否某个特定的 sprint 或发布造成了下滑?
- CI 阈值是在守住底线,还是大家都在往下调?
Archyl 会连同完整明细一起保存每一次计算,因此历史报告可以重新打开,逐个元素地对比。无论你用什么工具,请保留历史。一个每个季度从零重算一遍、然后就丢掉的漂移分数,又变回审计了。
设一个你真能守住的目标
选下一个数字,而不是理想的那个。如果今天是 58,有用的目标是 65,有用的讨论是哪五个元素能把你带到那里。一个约定"季度末达到 90%"的团队,通常什么也没约定。
漂移检测在 AI 辅助开发中的作用
这是最近变化最大的部分,也是 Thoughtworks 写下前面那条雷达条目的原因:智能体会复制它们找到的模式,包括已经劣化的模式,所以过去以人类 commit 的速度累积的漂移,现在以生成 commit 的速度累积。
AI 智能体越来越依赖架构文档来获取上下文。通过 MCP 之类的协议,智能体可以在生成代码之前读取你的 C4 模型、ADR 和合规性规则。这让它们更有效——它们生成符合你架构的代码,而不是靠猜。
但这只有在文档准确时才成立。一个读取了过时 C4 模型并据此生成代码的智能体,会产出符合错误架构的代码。智能体放大了漂移,而不是防止它。
漂移检测创建了让 AI 智能体保持诚实的反馈回路:
- 智能体读取架构(通过 MCP)
- 智能体生成代码,符合文档化的架构
- 代码被合并,可能改变了实际架构
- 漂移检测运行,捕捉任何偏离
- CI 门禁失败,如果漂移超过阈值
- 团队更新文档,以反映现实
- 智能体读取更新后的架构 —— 回路闭合
没有第 4 步,回路就是开放的。文档会越来越像虚构。智能体会越来越多地生成符合幻想架构的代码。差距随着每一次 commit 而扩大。
漂移检测正是闭合这个回路的机制。
开始进行漂移检测
如果你已经在某处有一个模型
先测量它,再改其他任何东西。这是现成可用的最便宜的第一步,而且不会让你对任何事情做出承诺。
如果你的架构已经存在于 Structurizr DSL、LikeC4、IcePanel 或 Backstage 目录中,把那个模型迁移过来,按它原本的样子算一个分数。你测量的是你早已写好的文档,就以你当初留下它的状态。不需要改工作流,不需要团队养成新习惯,也还不需要就工具做出决定。这个数字是那个决定的输入,而不是它的结果。
有两点需要老实说明。导入器不是无损的:视图、样式和布局不会保留,而 Structurizr 解析器会跳过部署环境和部署节点,但会在警告列表里带行号点名,所以在你信任这个分母之前,请先读一遍那份列表和导入后的模型。另外,分数描述的是抵达的那个模型,而不是你导出的那个文件。
返回的结果是一份按元素展开的清单。84 分是一个你可以排进日程的维护问题。41 分意味着一直有人在依据一份描述着另一个系统的文档做决策,而这件事现在知道,总好过在下一次事故中知道。
如果你没有架构文档
从 AI 发现开始。连接一个仓库,让发现功能提议 C4 模型,然后审批或驳回它的建议,而不是自己去画。一旦有了模型,让它保持诚实的就是漂移检测。
如果你已经在追踪漂移
把它放进 CI。把阈值设在当前分数之下。配置好降级告警。让漂移成为团队每周都会看到的指标,而不是某个人在评审前才算一次的数字。
无论你从哪里开始
漂移像技术债一样累积:你放着不管的时间越久,需要对账的东西就越多,而在这期间信任这份文档的人就越少。区别在于,你可以先不修任何东西,就搞清楚自己今天的处境。
你的架构文档要么反映现实,要么不反映。漂移分数的意义就在于:你不必再去猜是哪一种了。
深入了解:机制看漂移分数是如何计算的,让模型保持为真的实践看活的架构文档,从零开始的话看什么是 C4 模型。术语定义:架构漂移、活文档,以及产品中的漂移检测。Developer 套餐免费且无需绑卡,如果你想给已有的文档一个数字:archyl.com。