很多工程实践看起来像“经验之谈”,但往前追一层,往往能找到更早、更稳定的方法论来源。
我计划写一个系列文章,围绕 12-Factor App 的各项原则做一次重新解读。目的不是复述概念,而是把这些原则放回今天的软件工程、DevOps、容器化、供应链安全和 AI 辅助开发语境中,看看它们为什么仍然有效。
第一篇,先从 Dependencies,依赖关系 开始。
1. 什么是 12-Factor App
12-Factor App 可以理解为一组面向现代应用程序的设计原则。它最初主要服务于 SaaS 应用,但其思想早已超出某一种部署形态。
它关注的是一个应用如何被构建、配置、发布、运行和扩展。换句话说,它不只是代码风格问题,而是应用从开发机到生产环境全过程的工程约束。
在今天看,12-Factor 仍然有价值,原因很直接:我们面对的问题没有消失,只是换了外壳。
过去的问题可能是“为什么在我的机器上可以运行”;今天的问题可能变成了:
为什么同一份代码在 CI 里构建失败? 为什么容器镜像换了基础镜像后行为变了? 为什么本地、测试、生产环境的依赖版本不一致? 为什么安全扫描发现漏洞后,很难定位到底是谁引入的? 为什么 AI 生成的修复建议看似正确,但落到项目里无法复现?
这些问题背后,都有一个共同关键词:上下文是否明确。
Dependencies 原则讨论的,就是应用对外部依赖的上下文应该如何表达。
2. Dependencies 原则到底说什么
Dependencies 原则的核心可以概括为一句话:
应用不应该隐式依赖系统环境中“碰巧存在”的包或工具,而应该显式声明并隔离自己的依赖。
这里有两个关键词:显式声明 和 隔离。
所谓显式声明,是指应用需要清楚写出自己依赖了什么。例如:
Go 项目使用 go.mod和go.sum声明模块依赖与校验信息。Python 项目使用 pyproject.toml、requirements.txt或锁文件声明运行依赖。TypeScript 项目使用 package.json和package-lock.json、pnpm-lock.yaml、yarn.lock等文件声明包和锁定版本。
所谓隔离,是指应用不应该默认使用系统全局环境里的依赖。例如:
Python 不应该依赖某台机器全局安装过的包,而应该使用虚拟环境、构建环境或容器隔离。 TypeScript 不应该依赖全局安装的某个 CLI,而应该把工具放进项目依赖,通过 npm scripts、pnpm或npx调用。Go 虽然编译后通常得到独立二进制,但构建阶段仍然需要明确模块版本、工具链版本和外部系统依赖。
Dependencies 原则反对的不是“使用依赖”,而是反对依赖关系只存在于某个人的机器、某个基础镜像、某个隐含路径、某段口口相传的部署说明里。
依赖可以复杂,但不能漂浮。
3. 为什么一定要显式声明依赖
很多团队一开始并不是不懂依赖管理,而是觉得“现在能跑就行”。问题是,隐式依赖的成本不会立刻出现,它通常在环境迁移、人员交接、CI/CD 重建、基础镜像升级、安全审计时集中爆发。

3.1 避免环境不一致
最典型的问题是:本地能跑,测试环境不能跑;测试环境能跑,生产环境不能跑。
原因可能只是某台机器上曾经手动安装过一个系统库,某个容器基础镜像里刚好带了一个命令,或者某个开发者全局安装过一个 CLI。
这些依赖没有写在项目里,就不属于项目契约的一部分。它们存在,但不可见;可用,但不可控。
显式声明依赖,本质上是在告诉所有环境:运行这个应用需要哪些东西,版本边界是什么,安装入口在哪里。
3.2 让构建可复现
可复现构建不是一句口号,而是工程系统的底线能力。
如果今天构建出的产物和明天构建出的产物不一致,而代码没有变化,那么问题通常出在环境、依赖或构建过程本身。
Go 的 go.sum、Python 的锁文件、TypeScript 生态中的 lockfile,都是为了降低这种不确定性。它们不能解决所有问题,但至少把“依赖版本漂移”这个风险从黑盒里拿了出来。
一个成熟的构建系统应该尽量做到:换一台机器、换一个 CI runner、换一个构建节点,只要输入一致,输出就应该尽可能一致。
3.3 优化镜像体积
在容器时代,依赖声明还会直接影响镜像体积。
如果依赖关系不清晰,构建镜像时很容易把不需要的工具、缓存、开发依赖、测试依赖一起打进最终镜像。
例如:
Go 项目可以通过多阶段构建,把编译工具链留在 builder 阶段,最终镜像只保留运行所需的二进制和必要文件。 Python 项目可以区分运行依赖和开发依赖,避免把测试框架、格式化工具、构建缓存带进生产镜像。 TypeScript 项目可以在构建阶段安装完整依赖,在运行阶段只保留构建产物和生产依赖。
依赖越明确,裁剪越有依据。否则所谓“瘦身”只能靠猜。
3.4 支持安全审计
安全审计也依赖显式信息。
如果项目的依赖没有被清楚声明,漏洞扫描工具就很难准确判断当前系统到底使用了哪些组件、哪些版本、哪些传递依赖。
这不仅影响漏洞发现,也影响漏洞修复。
当一个依赖被曝出漏洞时,团队需要快速回答几个问题:
我们是否使用了它? 使用的是哪个版本? 是直接依赖还是传递依赖? 哪些服务受影响? 升级后会影响哪些构建与运行环境?
这些问题如果只能靠人工登录机器排查,就说明依赖关系没有成为工程资产。
4. 在 Go、Python、TypeScript 中如何实践
Dependencies 原则不是抽象口号,它最终一定要落到项目文件、构建命令和运行环境里。
4.1 Go:把模块和工具链边界写清楚
Go 项目的依赖管理通常从 go.mod 开始。
基本实践包括:
使用 go.mod声明模块路径、Go 版本和直接依赖。保留 go.sum,用于记录依赖校验信息。在 CI 中检查 go mod tidy后是否仍有差异,避免依赖声明和实际代码脱节。对构建工具、代码生成工具保持明确管理,不依赖开发者机器上的全局状态。 容器构建使用多阶段构建,区分编译环境和运行环境。
Go 的优势是编译后产物相对独立,但这不代表依赖问题消失。构建阶段的模块版本、工具链版本、CGO 开关、系统库依赖,都会影响最终产物。
如果一个 Go 服务依赖了系统动态库、证书文件、时区数据或外部命令,也应该在构建和镜像定义中明确表达。
4.2 Python:不要把全局环境当项目环境
Python 的依赖管理复杂度更高,因为解释器版本、包版本、系统库、虚拟环境之间关系密切。
基本实践包括:
使用 pyproject.toml作为现代项目元数据入口。对应用运行依赖、开发依赖、测试依赖做清晰分组。 使用锁文件或固定版本策略提高构建稳定性。 在 CI 和容器中从空环境安装依赖,避免复用本机状态。 对包含原生扩展的依赖,明确系统库和构建工具要求。
Python 项目尤其要警惕“我本地已经装过”的问题。全局 Python 环境里多出来的一个包,可能会掩盖项目声明缺失;到了 CI 或容器里,这个问题才会暴露。
真正可靠的做法,是让项目在一个干净环境里也能完成安装、测试和启动。
4.3 TypeScript:把工具也视为依赖
TypeScript 项目中,依赖不只是运行时包,还包括构建工具、类型工具、测试框架、代码生成器和打包器。
基本实践包括:
使用 package.json声明 dependencies、devDependencies 和 scripts。提交 lockfile,确保团队和 CI 使用一致的依赖解析结果。 避免依赖全局安装的 tsc、vite、eslint、prettier等工具。使用 npm ci、pnpm install --frozen-lockfile或类似命令保证安装过程不随意改写依赖图。生产镜像中只保留运行所需依赖和构建产物。
前端和 Node.js 服务都容易出现“工具链隐式依赖”。例如某个人全局安装了新版本 CLI,本地构建通过了;CI 使用项目内旧版本工具,构建结果却不同。
工具链也是依赖。只要它影响构建结果,就应该进入项目契约。
5. 显式是一种原则
Dependencies 原则真正值得延伸的地方,不只是“依赖要写进文件”,而是背后的工程哲学:显式是一种契约原则。
“默认”听起来很方便,但默认值从来不是抽象存在。它一定由某个系统、某个版本、某个配置、某个维护者在某个时刻定义。
而这些东西,都可能变化。
生活里也一样。
你去咖啡馆说“默认浓度”,不同店可能完全不同;去面馆说“正常硬度”,师傅的理解可能跟你不一样;餐厅默认给不给纸巾,外卖默认给不给餐具,也都不是天然确定的事情。
如果你真的在意结果,就不能只说“默认就行”。你需要把关键条件说清楚。

工程系统更是如此。
例如 Kubernetes manifest 里,如果没有明确写资源 requests 和 limits,调度依据、资源争抢和高负载下的运行表现都可能变得不可预期。某个环境下看起来没问题,不代表另一个集群、另一个版本、另一个负载条件下仍然稳定。
显式不是为了啰嗦,而是为了减少猜测。
契约的双方都需要明确知道契约内容,才能更好地履行契约。
对应用来说,依赖声明是应用和运行环境之间的契约。
对团队来说,依赖声明是开发者、CI/CD、运维系统和安全工具之间的契约。
对 AI 来说,显式上下文同样重要。
AI 辅助开发越来越普遍后,一个新的问题出现了:AI 并不会天然知道你的项目约束。它依赖你给出的上下文,也依赖仓库中已经存在的显式信息。
如果依赖关系、构建命令、运行方式、环境变量、资源限制都写得清楚,AI 更容易给出贴合项目的建议。反过来,如果一切都靠隐式知识和口头约定,AI 只能在不完整上下文里猜测,结果自然不稳定。
这不是 AI 的特殊问题,而是工程系统长期存在的问题。AI 只是把“上下文不显式”的成本放大了。
小结
12-Factor App 的 Dependencies 原则,看似只是在讲依赖管理,实际讲的是现代应用的可移植性、可复现性和可治理性。
显式声明依赖,至少带来五个直接收益:
减少不同环境之间的行为差异。 提高构建过程的可复现性。 为容器镜像瘦身提供依据。 让安全审计和漏洞修复有据可查。 为团队协作和 AI 辅助开发提供明确上下文。
软件工程里,很多问题不是因为系统太复杂,而是因为重要信息没有进入契约。
Dependencies 原则提醒我们:不要把应用能否运行,寄托在某个环境“刚好有”的东西上。
把依赖写下来,把边界说清楚,把默认值变成明确选择。
这不是繁琐流程,而是工程系统走向可靠的起点。
如果你也在维护服务、构建流水线或容器镜像,可以回头检查一下:你的项目依赖,是写在契约里,还是藏在某台机器里?
欢迎点赞、在看,也欢迎在评论区聊聊:你遇到过哪些因为隐式依赖导致的线上或构建问题?
夜雨聆风