AI agent 之所以能够让工作变得更轻松,本质上是因为它们在人与任务之间增加了一层又一层的委托机制;然而这些委托层最终会沉淀为依赖,而依赖又会逐渐演变成风险。
Mitchell Hashimoto 最近提出了一个听上去几乎有些离经叛道的建议:停止更新依赖。从软件行业过去几十年的经验来看,这种说法几乎称得上疯狂。尤其是在 Mythos 出现、AI 已经开始大幅降低零日漏洞发现成本的当下,这种观点似乎更加难以接受。不过,如果回头看看今年春天 npm 生态发生的一系列安全事故,你会发现 Hashimoto 的主张或许并不像表面看起来那么极端,反而透露出一种重新掌控风险的思路。
他的原则很简单:Fork 你所依赖的项目,把代码裁剪到只保留自己真正需要的部分,然后除非用户确实受到影响,否则不要轻易更新。按照他的逻辑,更新依赖不应该成为一种默认动作——无论是因为 GitHub Dependabot 自动提交了升级 PR,还是因为上游发布了一个号称更安全的新版本。如果你决定升级,那么理解整个传递依赖树中相关变更的责任在于你自己,而不在维护者身上。
对于一个长期把“最新版”等同于“最安全版”的行业而言,这种观点听起来难免有些危险。但今年春天发生的几起事件恰恰说明,现实情况并没有那么简单。
在今年最严重的两起 npm 供应链攻击中,受害最深的往往不是那些长期停留在旧版本上的项目,而是那些积极跟进最新发布版本的用户。当 HTTP 客户端库 axios 遭到入侵时,攻击者成功发布了两个被植入恶意代码的版本。在大约三个小时的窗口期内,任何执行全新安装的机器都会自动获得一个远程访问木马;相反,如果你的项目已经锁定在一个干净版本上,并且没有重新安装,那么整场攻击几乎与你无关。几周之后,在 node-ipc 投毒事件余波未平之际,Mini Shai-Hulud 蠕虫又借助 TanStack 生态开始传播,并进一步波及 Mistral、UiPath 以及大量每周下载量达到数百万次的软件包。
面对这样的攻击,应该如何防御?
答案或许出人意料:有时候,什么都不做反而更安全。
因为抵御 Mini Shai-Hulud 最有效的措施既不是漏洞扫描器,也不是恶意代码特征库,而是一个简单得近乎无聊的机制——冷却期。StepSecurity 在向客户提供新版本之前,会先将其冻结一个可配置的时间窗口,通常约为十天。在这段时间里,用户获得的始终是最后一个已知安全版本,因此完全避开了恶意发布;而其他没有采用这一机制的人,则不得不亲身经历整个事故。
换句话说,真正发挥作用的防御策略恰恰是那个在软件工程历史上常常被视为保守甚至愚蠢的原则:不要仅仅因为某个版本更新了,就立即采用它。
颇具讽刺意味的是,当整个行业开始拥抱 AI 开发时,许多团队给出的答案却是继续引入更多依赖。而问题在于,我们真的清楚这样做会带来什么后果吗?
依赖树已经逃离了包管理器
过去几十年里,软件行业一直在通过开源库来共享那些缺乏差异化价值的基础能力,而这总体上是一件好事。毕竟没有多少团队愿意重新实现 TLS、日期解析、日志框架或者其他基础设施——当然,那些把编译 Gentoo 当作爱好的人可能是个例外。
开源协作之所以能够成功,正是因为不同的人专注于不同的领域,然后将成果共享给整个生态。然而共享的从来不只是代码本身,我们同时也在共享维护者账号、包管理器、CI/CD 流水线、发布脚本以及各种围绕软件构建起来的基础设施。
现代软件工程最令人惊叹的地方在于,一个规模不大的团队往往能够利用数千个自己从未编写过的组件,构建出世界级产品;但风险也恰恰来源于此。随着 AI 的出现,这种风险进一步扩大,因为依赖树已经不再局限于代码层面。
今天的编程 agent 不只是简单地导入一个软件包,它还会阅读仓库中的说明文件,遵循系统提示词,选择工具,与 MCP(Model Context Protocol)服务器交互,并执行 Shell 命令。每增加一种能力,就意味着增加一种新的依赖,而这些依赖所对应的行为往往存在于模型之外。它们确实提高了能力边界,却也不可避免地扩大了攻击面。
这一点已经开始体现在实际研究中。Purdue 大学研究人员分析了七个软件生态中的 117,062 次依赖变更后发现,AI agent 选择已知存在漏洞的软件包版本的概率高于人类开发者,分别达到 2.46% 和 1.64%。更糟糕的是,这些错误决策往往更加难以修复:由 agent 引入的问题中,有 36.8% 需要进行主版本升级才能消除漏洞,而人类开发者引入的问题中,这一比例只有 12.9%。
从整体结果来看,人类开发者完成的依赖调整净减少了 1,316 个漏洞,而 agent 主导的变更却净增加了 98 个漏洞。除此之外,agent 还会凭空捏造根本不存在的软件包;一旦有人注册这些 hallucination(幻觉)出来的包名,它们就会立刻变成新的攻击入口。InfoWorld 的 Lucian Constantin 之前已经详细讨论过这一问题。
当然,这些数据并不能证明人类永远优于 AI。事实上,在聊天机器人出现之前,开发者就已经把无数项目的依赖图搞得一团糟。但如果让 agent 在没有任何约束的情况下自动添加和更新依赖,那么我们只是把一个老问题用更高的速度重新演绎了一遍。
MCP 生态其实也面临着类似的挑战。微软已经公开记录过一种被称为“工具投毒(tool poisoning)”的攻击方式:恶意指令被隐藏在工具元数据之中,而模型恰恰会读取这些元数据来决定调用什么工具。在微软自己的红队测试中,单纯依赖模型遵守安全指令时,超过四分之一的场景最终会产生策略违规,因此微软明确指出,提示词遵循能力不应被视为安全边界。
OWASP 的描述更加直接:工具返回的内容会直接进入模型上下文,而不像工具描述那样在接入时经过审查;至于“不要读取 /tmp 之外的文件”之类的限制,本质上依赖的是模型的自觉,而不是访问控制机制。
如果说工具层已经存在这种问题,那么提示词层面的情况可能更加复杂。
Sean Goedecke 最近提出,提示词同样会形成技术债,而且这种债务往往是悄无声息累积起来的。一个在当前模型上效果良好的提示词,到了下一个模型版本未必还能保持同样行为;而当团队不断叠加 AGENTS.md、CLAUDE.md、skills、规则以及工具描述之后,实际上已经构建出了一个决定软件如何被编写的隐形控制平面。问题在于,大多数团队既不会系统测试它,也不会定期审查或清理它,于是 agent 的决策质量会逐渐下降,却依然能够用看似合理的语言为自己辩护。
“最新”并不等于“安全”
Hashimoto 最激进的那套规则显然无法解决所有问题,而且对于大多数企业而言也缺乏现实可操作性。绝大多数组织既没有足够的人力,也没有足够的时间去逐个审查依赖树中的每一次变更。不过,如果抛开那些极端部分不谈,他真正强调的纪律性却非常值得重视:每一个依赖都应该有明确存在的理由,而每一次升级也都应该有明确发生的理由。
这种思维方式与现代开发文化多少有些格格不入,因为如今添加一个软件包往往比认真思考更便宜;而在 agent 驱动开发中,这种倾向又被进一步放大。Agent 非常擅长搜索一个库、导入它、通过测试,然后继续前进,但它优化的是最短路径,而不是长期维护成本。它关心的是代码是否能跑起来,而不是依赖图是否正在变得越来越复杂。
这也是我在讨论评测(evals)时不断强调的一点:AI 并没有消除工程纪律的重要性,相反,它只是提高了忽视工程纪律所需要付出的代价。模型能够比人类更快地完成修改,却无法可靠地判断这些修改是否真正属于你的系统,因此最终的架构判断、风险判断和取舍判断,依然只能由开发者自己完成。
潜伏的漏洞不会永远潜伏
然而,即使采用 Hashimoto 相对温和的策略,也依然存在一个根本性问题:这种思路实际上假设那些隐藏在冻结依赖中的漏洞会一直保持未被发现状态。
在软件行业过去的大部分历史里,这种假设基本成立,因为寻找深层逻辑漏洞是一项缓慢、昂贵且高度依赖专家经验的工作。但 AI 正在迅速改变这一前提。
今年 4 月,Anthropic 的 Claude Mythos Preview 在大约一千次运行、总成本不到两万美元的情况下,自动发现并构建出了针对 FFmpeg 一个存在 16 年的漏洞以及 FreeBSD NFS 服务器一个存在 17 年的 root 权限漏洞的有效利用代码。仅仅一个月后,Google 威胁研究团队又报告了首个在真实攻击活动中观察到的 AI 开发零日漏洞,而且这种漏洞恰恰属于传统扫描器最难发现的语义逻辑缺陷。
过去那些陈旧而稳定的依赖之所以显得安全,很大程度上只是因为发现漏洞的成本太高;而当 AI 显著降低这种成本之后,攻击者完全可以按需租用漏洞发现能力,持续寻找系统中隐藏已久的薄弱环节。
这并不意味着 Hashimoto 错了,只是意味着他的原则需要进一步演化。将依赖裁剪到仅覆盖实际使用场景,仍然能够有效缩小攻击面,因为那些从未被导入的代码自然无法成为针对你的攻击入口;Fork 依赖也依然有价值,因为它让你能够按照自己的节奏完成修补,而不必完全依赖上游维护者。
与此同时,那些能够帮助攻击者审查代码的模型,也必须首先被用来审查自己的系统。
因此,真正的原则从来不是“不要更新”,而应该是:了解自己的攻击面,把它控制在尽可能小的范围内,并持续对其进行评估,因为如今所有人都拥有了同样的分析能力。
更少的东西,更深入的理解
这确实改变了游戏规则。
未来最优秀的 AI 工程团队,未必是那些把 agent 接入一切系统的团队;真正优秀的团队更可能清楚地知道自己接入了什么、为什么接入,以及当这些东西发生变化时会产生什么影响。
这并不意味着应该禁止 MCP 服务器、agent 工具或者第三方软件包,因为那样同样不现实。更合理的做法,是把它们全部视为生产环境依赖来管理。如果一个 MCP 服务器拥有读取邮件、访问客户数据或者执行代码的能力,那么它就理应接受与任何高权限系统集成相同级别的审查;如果一个提示词文件决定着 agent 如何修改代码库,那么它也应该进入版本控制流程,接受代码审查,并在失去价值时及时删除。
从这个角度来看,Goedecke 的观点与治理派的思路其实并不冲突。前者倾向于尽可能减少配置层的规模,因为最便宜的技术债往往是那个从未被写出来的提示词;后者则认为,所有最终保留下来的配置都应该被版本化、审查并设置适当的准入机制。
事实上,这种故事在软件行业已经上演过很多次。每一代新技术出现时,人们都会把它视为一种解放力量,而随着时间推移,它又逐渐演变成新的运维负担。微服务曾经被认为能够帮助团队摆脱单体架构,后来许多人发现自己只是把一个庞大的问题拆成了两百个通过网络连接的小问题;Kubernetes 曾经被视为基础设施标准化的答案,后来企业意识到自己实际上把一个完整的分布式系统项目引入了每个团队。
Agent 正在沿着同样的轨迹前进,只不过速度更快。它们通过不断增加委托层来降低工作的复杂度,而这些委托层最终会沉淀为依赖,依赖又会进一步转化为风险。因此,这篇文章真正想表达的并不是“停止使用 AI”,而是提醒我们:在享受 AI 带来的效率提升时,同样需要对新增的依赖关系保持足够清醒的认知,因为决定系统长期质量的,从来都不是自动化本身,而是我们是否真正理解自己正在依赖什么。