技能即模块:当你的智能体堆栈需要导入系统时
我上个月交流过的一个团队遇到了一个任何资深包管理器用户都能一眼识破的 Bug。他们的智能体中有两个技能(skills)都提供了相同的 search_orders 能力 —— 一个来自计费工具包(billing toolpack),另一个来自 CRM 工具包。最后添加到清单(manifest)中的那个成了生效者。智能体在三周内都在静默地调用错误的能力。退款发到了错误的客户 ID。他们告诉我,修复方法是召集 CRM 和计费工程师开会以“协商命名”。开会。只为了解决两个可安装模块之间的名称冲突。
就在那一刻,我意识到了当前的智能体运行时(agent runtimes)正在发生什么。运行时加载能力模式 —— 技能、工具包、提示词片段、检索提供程序、MCP 服务器 —— 正汇聚到几十年前编程语言通过导入系统(import systems)解决的相同问题上:名称解析、版本锁定、依赖图、冲突检测、延迟加载。而大多数智能体运行时要么在拙劣地重新发明每一项,要么干脆无视,并以“开会”的形式将账单寄给用户。
这种模式本身是合理的。Anthropic 的 Skills、Microsoft 的 Agent Framework skills、GitHub 的 gh skill 系统,以及众多的 MCP 服务器 —— 它们都趋向于同一个理念:一个带有元数据、指令和可选执行资源的组件单元,在相关时由智能体在运行时加载。到 2026 年中期,Composio 在各注册表中的已发布技能已超过一千个。Awesome-agent-skills 仓库的条目超过了 1,000 条。由微软主导的 Agent Package Manager (APM) 对待智能体配置的方式,就像 package.json 对待 npm 依赖项一样。
这只是表象。在底层,其架构比语言设计落后了一到两个世代。而这种差距已经开始产生负面影响。
包管理器已经解决的四个问题
通过审视导入系统实际在做什么,智能体运行时中缺失的部分就能清晰地显现出来。
名称解析(Name resolution)。 Python 根据 sys.path 顺序、虚拟环境以及包声明的名称来决定你获取哪个 requests。Node 根据 node_modules 的遍历和 package 字段来决定。这有一套 系统 —— 虽然有时会出错或令人困惑,但它是经过规范化的。在当今的大多数智能体运行时中,当两个安装的技能定义了同名的工具时,行为通常是“最后写入者胜出”或未定义的。MCP 工作组关于跨服务器重复工具名称的讨论帖,读起来就像 2008 年关于 Python 2 与 3 的争论:前缀?命名空间?别名?作用域?目前还没有标准答案,每个 CLI 都在实现自己的临时规则。Gemini CLI 自动添加服务器名称作为前缀。一些 Claude 技能则完全不加前缀。智能体根本不知道它正在调用哪个 delete_record。
版本锁定(Version pinning)。 package-lock.json、带有哈希值的 requirements.txt、Cargo.lock —— 这些之所以存在,是因为“这段代码昨天还能用”应该是有意义的。直到最近,智能体技能通常还是从 GitHub 的原始 URL 或注册表的 “latest” 标签拉取的。APM 提供了 apm.lock.yaml。SkillFortify 和 skills-lock 的出现是因为这种缺失已经足够痛苦,以至于多个团体编写了相互竞争的工具来填补空白。GitHub 的 gh skill --pin 于 2026 年 4 月问世。目前这些都还没有普及。一个团队在周一安装了一个技能,上游维护者在周三更改了系统提示词以“修复”某些内容,结果在周四出现了一个回归错误,需要花费三天时间对评估集进行二分查找(bisecting evals)才能找到原因。这就是依赖锁定方面的差距,它使得“这个智能体昨天还能用”在最字面的意义上变得不可复现。
依赖图(Dependency graphs)。 技能越来越依赖于其他技能。一个 code-review 技能可能由 read-pr、lint 和 summarize-diff 组成。TDCommons 的出版物《技能指令文件依赖解析》(Skill Instruction File Dependency Resolution)提议在每个 SKILL.md 中声明语义化版本(semver)风格的范围约束,并使用侧车锁文件(sidecar lockfile)记录解析后的版本。这大致就是 npm 在 2010 年发布的功能。我们晚了 16 年,而且还在争论是否要发布它。如果没有真实的依赖图,“安装此技能”就无法可靠地同时安装该技能所需的内容。因此,技能只能通过内联依赖、重复提示词片段以及发布臃肿的单文件包来补偿,而这恰恰是库作者为了逃避而发明模块系统时的情况。
冲突 检测(Conflict detection)。 即使两个技能不共享工具名称,它们也经常共享 行为 —— 两者都想格式化智能体的响应,两者都附加了“before-tool-call”钩子,两者都注入了系统提示词片段。Microsoft 的 Agent Skills Standard 明确指出了“编码决策顺序和冲突解决规则”的要求,并警告不要重复具有略微不同表述的重叠技能。但作为运行时关注点,该警告是作为文档发送给用户的,而不是作为约束强制执行的。将其与链接器(linker)处理重复符号定义的方式进行对比 —— 在链接时显式报错。智能体运行时将行为重复冲突推向生产环境,并将其表现为不稳定的输出。
没人谈论的延迟加载问题
延迟加载值得专门拿出来讨论,因为在这里,与传统导入系统的类比以一种有趣的方式破裂了。
Python 的导入是即时(eager)的:当 import foo 返回时,foo 的模块主体已经执行完毕。Agent 技能不能是即时的——技能太多了,如果把每个可用技能的指令都塞进系统提示词(system prompt),在用户输入任何内容之前,整个上下文窗口就会被耗尽。因此,主流模式是延迟加载(lazy):保留一个轻量级目录(名称、描述、使用时机),只有当模型决定该技能相关时,才加载完整的技能主体。
这更接近于动态插件加载而非静态导入。这意味着运行时正在代表模型做出*按内容寻址(content-addressable)*的加载决策。Hermes Agent 的 GitHub 问题追踪器中有多个关于延迟加载技能无法注册的 Bug,原因是目录扫描器只匹配字面上名为 SKILL.md 且具有特定 YAML frontmatter 格式的文件。这类 Bug 与 2003 年 Java 的类路径(classpath)扫描器所犯的错误如出一辙:一个带有隐式格式要求的索引器,静默地丢弃任何不匹配的内容,且没有任何诊断信息。
更深层的问题是:当加载决策是按内容寻址并由模型做出时,模型可能会选错要加载的技能。这里没有编译时检查。没有“你是想说 read_pr_v2 吗?”这种提示。模型选择了一个几乎匹配的技能并执行它,失败表现为错误的答案,而不是缺少导入的错误。模块系统之所以有效,部分原因在于当某些内容缺失或模棱两可时,它们会显式报错(fail loudly)。而根据定义,延迟加载技能的程序会隐式报错(fail quietly)。
一个值得使用的加载器会将此视为核心关注点:它会跟踪模型选择了哪个技能、原因(匹配的描述 Token)以及它用该技能做了什么。如果没有这些追踪信息,调试起来就像是在读羊肠占卜。
供应链问题在好转之前会变得更糟
每个包管理器最终都会遇到供应链事件。PyPI 的拼写劫持(typosquats)、npm 的 event-stream 事件、Crates.io 的维护者弃坑。Agent 技能生态正处于 npm 在 2014 年左右所处的阶段:成千上万的技能,数百名贡献者,没有真正的签名方案,没有来源审计,锁定文件(lockfiles)记录了 SHA 但没有签名。
“工具影子(tool shadowing)”攻击——即恶意 MCP 服务器注册与合法服务器同名的工具,利 用命名空间强制执行的缺失进行攻击——这在 RunLayer 对 OpenAI Agent Builder MCP 的分析中已有记载。这是 Agent 版本的 DLL 劫持。防御手段不是更聪明的提示词,而是由运行时强制执行的命名空间隔离,就像 Linux 动态加载器强制执行版本化的符号作用域一样。
版本固定(Pinning)有帮助。锁定文件中类似 SkillFortify 的内容哈希有帮助。对于任何带有提权工具凭证运行的内容,技能包的来源证明(Sigstore 式)最终将是强制性的。这些在技术上都称不上新颖——这只是每个其他生态系统都经历过的老剧本。Agent 只是以 2026 年的速度在重演,相应地压缩了“首次事故”发生的时间线。
Agent 真正的模块层是什么样的
如果我要提出架构目标,而不是描述目前的混乱现状,它应该具备以下属性——注意,其中有多少在任何严肃的语言生态中都是不可逾越的底线:
- 稳定标识。 每个技能都有一个全限定名(
org/pack/skill@version),这是运行时解析的对象。显示名称只是显示名称。解析使用全限定名。 - 声明式依赖。 每个技能声明其所需的资源(其他技能、MCP 服务器、模型、工具能力)。解析要么成功并获得一套满足要求的配置,要么在 Agent 运行之前就宣告失败。
- 默认锁定文件。 解析后的集合会记录内容哈希。
agent install是可复现的。agent update是一个独立的、审慎的动词。 - 安装时而非运行时的冲突策略。 如果两个技能注册了同一个工具,那就是冲突。由你选择:为一个设置别名、排除一个,或为两者都加前缀。运行时永远不会静默地为你做选择。这是对发布我在开篇轶事中提到的那类 Bug 的严词拒绝。
- 使用真正目录格式的延迟加载。 一个独立的、经过机器验证的索引文件——而不是对 SKILL.md 文件的启发式扫描。添加技能会更新索引;加载器永远不会猜测。
- 能力作用域凭证。 技能清单(manifest)声明调用哪些工具需要哪些凭证。运行时仅授予这些作用域。借用 OAuth 的作用域词汇;问题本质是一样的。
- 来源证明与签名。 锁定文件条目带有签名。未签名的技能在受限沙箱中运行。在生产环境 Agent 中添加未签名技能需要显式的覆盖操作。
这一切都不稀奇。列表中的每一项在 npm、cargo、pip、apt 或 homebrew 中都有成熟的先例。这项工作不是科研,而是将这些被广泛理解的模式记录下来,作为主要的 Agent 运行时可以遵循的标准。
架构层面的博弈
这部分内容可能会引起争议。编排框架之争——LangGraph vs. CrewAI vs. AutoGen vs. Microsoft Agent Framework,以及上周刚刚发布的任何框架——大多是在错误的层面上争论。
一个新的编排拓扑结构(topology)为你提供了不同的 Agent 调用排序方式。这很有用。但我开篇故事中那个吞噬了三周退款的 Bug,并不是因为编排器采用了错误的拓扑结构。它的发生是因为编排器下方的 模块层(module layer) 没有命名解析策略(name resolution policy)。在破碎的模块层之上构建更好的拓扑结构,无法为你带来可重现性、供应链安全或可组合性。
在需要另一个编排框架之前,Agent 运行时(runtime)更需要一个包管理器级别(package-manager-grade)的模块层。APM 是目前该层面上最可靠的尝试,正是因为它研究了 npm 和 yarn,并借鉴了其中行之有效的部分。Lockfile、清单文件(manifests)、作用域命名空间(scoped namespaces)、确定性安装——这些并不令人兴奋。但它们是承重基础设施,所有更有趣的东西最终都将构建其上。
如果你在 2026 年构建 Agent 平台,在发布另一个编排原语(orchestration primitive)之前,值得思考的问题是:你公司中两个不同的团队能否在不通过会议解决命名冲突的情况下,安装重叠的技能集?如果答案是否定的,那么你还没有一个运行时。你只是拥有一个附带注册表的 Demo。修复方法不是增加新功能,而是一个导入系统(import system)。
- https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview
- https://microsoft.github.io/apm/
- https://www.tdcommons.org/dpubs_series/9912/
- https://github.blog/changelog/2026-04-16-manage-agent-skills-with-github-cli/
- https://github.com/pcomans/skills-lock
- https://github.com/orgs/modelcontextprotocol/discussions/291
- https://www.runlayer.com/blog/openai-agent-builders-mcp-problem
- https://benjamin-abt.com/blog/2026/02/12/agent-skills-standard-github-copilot/
- https://learn.microsoft.com/en-us/agent-framework/agents/skills
- https://lirantal.com/blog/managing-ai-agents-and-skills-with-apm
- https://github.com/qualixar/skillfortify/blob/main/docs/skill-lock-json.md
