用文件系统心智治理Agent技能树:SkillFS的设计与实践
我第一次意识到 Skill 管理出问题,是在一个周五的下午。当时我维护的个人 Agent 框架已经积累了 47 个 Skill,从网页抓取、笔记整理到定时提醒无所不包。起因很简单:我想临时禁用一个试验用的"自动打标签"技能,结果 Prompt 里注册的依赖链瞬间塌了一半,连带着几个核心 skill 也罢工了。那一刻我盯着注册表里密密麻麻的键值对,突然理解了为什么 Linux 的文件系统比一个巨大的哈希表更可靠——命名空间、挂载点、权限、快照,这些机制天然就是为"几十个东西混在一起还能有序治理"设计的。于是我花了两周时间,做了一个叫 SkillFS 的治理层,把 Agent 的 Skill 集合当成一个虚拟文件系统来管理。
这篇文章会讲清楚三件事:为什么 Skill 一旦超过一定数量就会失控,SkillFS 的目录树/挂载点/权限模型到底怎么设计,以及我把一个真实项目迁移到 SkillFS 的完整过程和踩坑记录。如果你正在做 Agent 开发,或者维护的 Agent 项目已经长出十几个 skill 但还没想好怎么管理,这篇应该能帮你省下不少折腾时间。
1. Skill 失控的本质:为什么几十个 Skill 会变成灾难
很多 Agent 项目早期只有三五个 skill 的时候,根本不需要管理。一个 Python 字典、一个数组,甚至直接在 System Prompt 里硬编码都行。但 skill 数量一旦跨过 20 这个量级,问题就开始以组合爆炸的方式出现。
1.1 从"函数清单"到"技能森林"的失控曲线
我先还原一下失控是怎么发生的。起初你的 skill 是这样的:
search_web(search_query):搜索save_note(content, path):保存笔记send_email(recipient, subject, body):发邮件
每个功能清晰,边界明确,调用关系一眼看穿。但当 skill 涨到三十几个之后,你会发现几个典型症状:
命名空间碰撞。你可能会同时存在fetch_url、fetch_page、fetch_web_content三个功能几乎一样的 skill,分别由不同时期的代码生成,名字不同但行为雷同。更糟的是,某一天你新加了一个get_knowledge,和旧的fetch_knowledge在语义上高度重叠,Agent 在规划任务时可能随机选一个,行为产生分叉。
依赖暗链。Skill 之间不再只是"调用关系",而是通过 shared memory、缓存、中间产物形成隐式依赖。比如summarize_article会先调用fetch_web_content然后把结果写到一块临时缓存区,后续generate_report又隐式依赖这块缓存。表面看两个 skill 互不相干,实际是 A 的中间态成了 B 的输入。一旦禁用fetch_web_content,generate_report会在运行到一半的时候神秘失败。
上下文污染。几十个 skill 如果每个都在 system prompt 里占几行描述,LLM 就超过上下文预算了。于是很多框架开始搞"动态加载"——只有需要时才注入 skill 描述。但动态加载又引入新问题:什么时候预加载、什么时候延迟加载、多个 skill 同时用到同一个上下文变量时怎么隔离?这些恰恰是传统注册表模型完全没有处理的事情。
1.2 传统 Skill 管理方案的三个盲区
我试过几乎所有常见的"轻量方案",它们各有致命盲区。
第一种是最原始的字典注册表。用一个全局dict[str, Skill]存所有 skill。优点是没有比这更简单的了,缺点是没有任何层次结构。服务演进就是"把越来越多的函数塞进同一个命名空间",冲突只是时间问题。
第二种是装饰器注册 + 插件目录扫描。比如@register_skill("fetch_url"),启动时自动扫描 Plugins 目录。这个方案能解决代码组织问题,但解决不了运行时治理问题。目录扫描只是把"代码文件"映射成 skill,并没有提供 enable/disable、版本切换、权限隔离这些生命周期操作。禁用某个 skill 要么改代码重新部署,要么加一堆if分支。
第三种是配置文件开关。一个 YAML 里列所有 skill 和enabled: true/false。直观,但配置文件和实际运行状态之间极易漂移。你改了配置忘了重启,或者某个 skill 启动时初始化失败但配置还标记成可用,这些全部靠人肉保证一致性。当你有 47 个 skill 时,这种"配置文件即真相"的假设完全不成立。
1.3 文件系统隐喻的直觉来源
我真正被点醒是在一次看 VFS 源码分析文章的时候。Linux 的文件系统为什么能管理几十万个文件不乱?因为它的结构里同时包含了几种机制:目录树提供分层命名空间,mount 点提供子系统隔离,inode 与目录项分离让多个路径能引用同一份数据,权限位控制谁能读写执行,日志与快照支撑恢复。
这些需求跟"管理几十个 Agent skill"几乎是逐条对应的。Skill 需要分层命名空间(避免重名冲突)、需要隔离与启用停用(避免连锁故障)、需要在多个语义分类下被引用而不用复制逻辑(依赖复用)、需要权限控制(高风险技能不能随便被触发)、需要版本与回滚(升级失败不拖垮整个 Agent)。
所以我说的 SkillFS 不是一个真的文件系统,而是把文件系统的管理语义搬到 Agent Skill 治理上,用路径、挂载点、权限、快照这些大家已经熟悉了四十年的心智模型,代替"全局注册表+配置文件开关"这种脆弱结构。
2. SkillFS 的核心设计:用路径、挂载点与权限模型重构 Skill 治理
设计 SkillFS 时,我给自己定了一条原则:所有管理操作都必须能通过"文件系统命令"完成。换句话说,治理 Skill 的最高频动作应该能被抽象成ls、ln、mount、chmod、snapshot这类操作,而不是靠改代码。
2.1 一颗目录树就是一张技能地图
第一个设计决定是把所有 Skill 挂到一颗统一的虚拟目录树下。根节点是/skills,下面按用途划分子树。我的项目里最终长这样:
/skills/ ├── web/ │ ├── fetch_url │ ├── extract_text │ └── search_engine ├── editor/ │ ├── create_note │ ├── edit_note │ └── attach_tag ├── memory/ │ ├── store_fact │ ├── recall_fact │ └── forget_fact ├── automation/ │ ├── run_shell │ ├── send_email │ └── scheduler └── trial/ ├── classify_doc └── sentiment_score这个目录树的直接收益是:路径即为 skill 的全局唯一标识。/skills/web/fetch_url就是完整 id,替代了原来平铺的fetch_url这种容易撞车的短名字。当一个 skill 需要在多个分类下出现时,不用复制代码,只在另一目录下创建软链接。
为了实现这个树形结构,我把 Skill 对象拆分成了两个层面。
- 目录项(dentry):只是路径字符串、名称、指向元数据的指针,以及挂载状态、权限掩码这些"视图层"信息。
- 元数据(inode):skill 真正的身份,包括描述、版本号、入口端点、依赖声明、超时时间、上下文缓存策略。
这种拆分的用意跟文件系统一模一样:多个目录项可以指向同一个 inode,也就是同一个 skill 可以被多个分类引用;重命名一个 skill 只是改 dentry,不影响 inode 里的行为定义。我踩过的实际坑是,早期直接用字符串做 skill id,一重命名所有引用它的地方全得跟着改。引入 dentry/inode 两层之后,重命名 cost 几乎降到零。
2.2 软链接与依赖解析:skill 之间的"引用"语义
Skill 之间有依赖关系,比如summarize_article依赖fetch_url,如果fetch_url被禁用,前者应该在执行前就被标记为不可用。传统方案里这种依赖靠 import 或者代码内调用绑定,改起来牵一发动全身。
SkillFS 把依赖声明当作软链接处理。每个 skill 的元数据里有depends_on字段,值是一组虚拟路径:
name: summarize_article version: 2.1.0 depends_on: - /skills/web/fetch_url - /skills/editor/create_note entry: summary.main context_cache: 10m运行时,SkillFS 在加载依赖前会做一次软链接解析:把这些路径展开成真实 skill id,并检测环。如果发现/skills/web/fetch_url不存在或者已被 unmount,调度器会返回明确的错误,而不是让 skill 在运行到一半的时候才炸。
软链接还解决了一个非常实际的需求:同一个 skill 服务于多个上层 skill 时,权限和审计可能需要区分。比如send_email这个 skill,用户在"客服 Agent"和"私人助理 Agent"两种场景下被调用,我不想复制两份代码,但想分别记录调用来源。我的做法是在两个分类目录下各放一个send_email链接项,权重和 action 都相同,只在audit_namespace字段区分。这跟文件系统里硬链接/软链接共享 inode 的思路是一致的。
2.3 挂载点与命名空间:隔离试验 Skill 和稳定 Skill
这是 SkillFS 里我个人认为最值得抄的设计。文件系统的挂载点,本质上是"把一个子树接入全局命名空间"。SkillFS 把这种语义映射成"一个 skill 集合只有在 mount 之后才可被 Agent 可见"。
我有几个试验 skill 放在/skills/trial下面,默认不 mount。只有当我显式执行:
skillfs mount /skills/trial /mnt/agent/skills之后,Agent 的调度器才能在规划阶段看到这几个 skill。不 mount 就等同于禁用,但和"删掉"不同,它还在原地,随时可以挂回来。
挂载点还有一个高级用法:按会话隔离。我会在起一个新的 Agent 会话时,为这个会话单独构造一个技能命名空间——比如这个会话只允许访问 web 类和 memory 类 skill,其余一概不可见。方法就是维护一个 per-session 的 mount table:
session_mounts = { "research": ["/skills/web", "/skills/memory"], "writer": ["/skills/editor", "/skills/tool"], }这样同一个框架跑多个不同职责的 agent 实例时,skill 集合不会互相污染。我实际跑下来,这个设计把"不同职责 agent 共享代码库"从灾难变成了顺理成章的事。
2.4 权限模型:谁允许调用哪个技能
SkillFS 的权限模型我直接借鉴了类 Unix 的 rwx 语义,但针对 Agent 场景做了简化。每个 skill 项有三个 chunk 位:read(查看描述与元数据)、execute(实际调用)、admin(修改版本与配置)。调用者分三类:用户本人、子 agent、webhook 外部请求。
权限检查发生两个层面。第一层在调度器做路径解析时,拦掉无权访问的路径;第二层在 skill 实际执行时,以调用链的 root skill 身份重新校验一次。为什么要两道?因为一个危险 skill(比如run_shell)可能被一个普通 skill 间接调用,如果只在第一层检查,就会被"套娃调用"绕过。两个层级的检查位置不同,防止权限逃逸。
举个例子,我的run_shellskill 权限默认是rwx------,只有 admin 能直接执行。某个业务 skill 需要用 shell 做数据处理时,我不会放开run_shell本身,而是让这个业务 skill 以受限子进程方式执行 shell,并屏蔽掉多数命。这种事在传统全局注册表里很难控制,因为你无法精确表达"谁在什么上下文里可以用哪个技能"。
3. 从 VFS 到 SkillFS:我在实现里借鉴的四个内核思想
上边讲的是设计蓝图,这一节说实现层面的关键机制。我实际写代码的时候,发现文件系统内核里的很多细节并不是多余的,它们每一个都对应一个具体的 SetSkill 治理事故。
3.1 目录项与 inode 分离:元数据不随路径漂泊
第一版 SkillFS 我做错了,当时直接拿路径字符串当 skill 的唯一 id,结果每次重命名都要全仓库搜索替换。后来刻意模仿 Linux 的 dcache 与 inode 分离:目录项只是缓存/视图,真正的 skill 实体数据存储在 inode 对象里,包含 skill_id、版本、行为入口、权限元数据等。
这种分离带来的第一个好处是路径可以随便改而 id 不变。/skills/web/fetch_url改名为/skills/fetch/webpage时,所有depends_on引用只要指向同一个 skill_id,就能自动跟随新路径。第二个好处是同一 skill 可以出现在多个路径下而不用复制行为。这非常适用于一个通用 skill 被多个业务线复用的场景。
我在实现里用了一个 sqlite 表存 inode,一个单独的表存 dentry 关系:
CREATE TABLE inodes ( skill_id TEXT PRIMARY KEY, version TEXT NOT NULL, entry_point TEXT NOT NULL, metadata TEXT NOT NULL, cr_time INTEGER NOT NULL ); CREATE TABLE dentries ( path TEXT PRIMARY KEY, inode_id TEXT NOT NULL REFERENCES inodes(skill_id), is_link INTEGER DEFAULT 0, mode TEXT DEFAULT 'rwxr-x---' );任何路径查询,先走 dentry 拿 inode id,再进 inodes 取真实定义。这个表结构我已经跑了几个月,稳定性很好,而且非常容易导出、审计、备份。
3.2 事务日志与原子提交:activate Skill 不再是"一把梭"
文件系统为什么敢让用户随时 kill 进程还保证数据不坏?因为日志文件系统有 journal。SkillFS 借鉴这一点设计了 skill 启停的原子协议。
以前我启用一个新版 skill,流程是:替换函数实现,修改注册表的描述,把配置开关置为 true。如果哪个环节做到一半进程 crash,Agent 就处于一个"配置说可用但实现是旧版"的中间状态。SkillFS 把这个过程改成了 prepare/commit 两段:
- prepare:把新版 skill 写入数据库(标记为 pending),不影响当前运行版本。
- validate:对新版本做烟测,确认入口可调用、依赖路径完整、权限掩码没把危险动作漏出去。
- commit:把 pending 标记切换为 active,同时更新 dentry 指向新的 inode 版本。
如果第 2 步失败,整个事务回滚,执行中的 Agent 仍然用旧版本。这个动作等价于文件系统里的原子 rename,但不是把"替换操作"当成猴子补丁,而是作为一条事务日志记录在案。我遇到过一个真实案例:新版本fetch_url引用了html2text包但没装,在旧框架里这会导致运行时报错,在 SkillFS 里 validate 阶段就拦下了。
3.3 快照与回滚:Skill 升级失败时的秒级恢复
快照是文件系统治理的最后一道防线。SkillFS 支持对整个 skill 树打快照,相当于对 inode 表 + dentry 表 + 权限配置做一次事务性导出。我用 sqlite 的在线备份接口实现,命令就一行:
skillfs snapshot /backup/skilltree-20250101.db回滚更简单:
skillfs restore /backup/skilltree-20250101.db这个动作的执行时间极快,因为它只恢复元数据,不恢复实际运行中的内存状态。实际应用场景是我在调试一个会连写多个文件的 skill 时,反复改版本、打快照、触发 bug、回滚。整个过程成本低到让我愿意频繁尝试新方案,而不是把 skill 树当成一张越改越不敢动的大网。
3.4 同步屏障(sync):强制 flush 技能上下文状态
LLM skill 跟普通函数不一样:它可能有内部记忆、缓存、会话上下文。普通程序可以随便 kill 进程,因为状态在磁盘或数据库里。但对 Agent skill 来说,状态可能散落在 LLM 的会话历史、向量数据库的临时写入、skill 自己的变量缓存里。
文件系统里有个著名的sync命令,强制把内存中的脏数据刷到持久层。SkillFS 里我也实现了一个skillfs sync,作用是:
- 强制所有带
context_cache的 skill 把缓存写入持久化存储; - 丢弃会话内存里已过期或已失效的上下文块;
- 更新每个 skill 的 last_synced 时间戳,供审计。
没有这个 sync,最典型的灾难是:一个长期运行的 Agent 在清理上下文时,把另一个 skill 还在用的共享内存块删了,导致后续调用全部乱套。有了 sync,你可以在每个 Agent 会话结束前调用一次,保证技能状态边界清晰。
4. 落地实操:一个 47 个 Skill 的 Agent 迁移记录
理论讲再多,不如把真实迁移过程写出来。这是我的个人 Agent 框架 AgentOS 从全局字典迁移到 SkillFS 的完整记录,时间跨度大约两周,中间踩了不少坑。
4.1 阶段一:盘点并把 Skill 重新挂到目录树下
第一步不是写代码,而是盘点。我把所有 skill 列成一张 CSV,字段包括:当前名字、实际功能、被哪些 skill 调用、调用哪些 skill、是否还在用、代码入口路径。跑了一个脚本扫代码里的 import 和调用点,自动生成依赖矩阵。
盘点结果触目惊心:47 个 skill 里,有 4 个完全重复(三个功能都是"提取网页正文"),6 个已经没人调用,还有 3 个是无主"孤儿"——代码在注册表里但没有任何文档。这其实就是没有命名空间管理的必然结果。
然后我按前面说的目录结构把全部 skill 归类并改名为路径。这个阶段里的关键动作是创建旧名字到新路径的软链接,避免外部调用立刻中断。例如:
skillfs ln -s /skills/web/fetch_url /skills/fetch_url_old_alias里面保留了一周的兼容期,方便我逐步更新所有调用点,而不是一次性大爆炸式重构。
4.2 阶段二:用权限和命名空间隔离高风险技能
盘点结束后,我按照风险等级给每个 skill 标注了权限掩码。
高风险技能包括:执行 shell 命令、发送邮件、修改或删除文件、访问密钥存储。这些我全部设成rwx------,即只有 admin 可执行。中风险技能如调用外部 API 抓数据,权限设为rwxr-x---,子 agent 可调用但只读。低风险技能如文本处理、格式转换,设为rwxr-xr-x,任何已认证调用方都可以用。
同时我把所有试验 skill 挪到/skills/trial目录下,默认不 mount。迁移后,生产环境的默认 mount 表只包含稳定子目录。这直接改变了我的开发习惯:以前是"新 skill 写完直接上线,出了事再回滚",现在是"新 skill 写完先在 trial 挂载点里用真实 Agent 对话测试,稳定后再 mount 到生产路径"。
4.3 阶段三:实现一个 skillctl 命令行工具
迁移不只是一次性动作,日常维护需要顺手。我写了一个 CLI 工具skillctl,只实现了八个子命令,但覆盖了所有高频治理操作:
| 命令 | 对应文件系统语义 | 实际行为 |
|---|---|---|
skillctl ls [path] | ls | 列出路径下的 skill,显示版本、权限、挂载状态 |
skillctl tree | tree | 展示完整 skill 目录树 |
skillctl cat <path> | cat | 查看 skill 元数据与当前运行版本 |
skillctl mount <path>/umount <path> | mount | 启用/停用指定 skill 集合 |
skillctl ln -s <target> <link> | ln | 创建分类目录下的引用 |
skillctl chmod <path> <mode> | chmod | 调整调用者权限 |
skillctl snapshot <file> | snapshot | 备份整个 skill 树元数据 |
skillctl restore <file> | restore | 从快照恢复 |
这个工具大概六百行 Python,核心逻辑就是上面 sqlite 表结构的增删改查,加上对 Agent 调度器的通知回调。写完后我把原来散落在各个脚本里的 skill 注册逻辑全部删掉,只留skillctl作为唯一的写入入口。这样做的好处是:所有变更都走同一条路径,审计日志天然完整——谁在什么时候改了哪个 skill 的版本、权限或挂载状态,全部有记录。
4.4 实际效果:从"不敢动"到"随便动"
迁移完成后,运行状态变化非常明显。举三个具体例子。
第一个是废弃 skill 处理变果断。以前要下线一个 skill,我会担心牵连其他调用,拖几周不敢删。现在一条skillctl umount /skills/web/legacy_fetch就把该 skill 从 Agent 可见范围移除了。如果后续发现还有调用链在引用,错误信息会直接提示因缺失路径导致的依赖失败,我可以快速定位修复。
第二个是并行会话互不干扰。我的 Agent 框架同一时间会跑多个会话,比如一个在写技术文章,一个在做资料调研。在 SkillFS 的 per-session mount 机制下,写作会话只 mount/skills/editor和/skills/tool,调研会话只 mount/skills/web和/skills/memory。两边用到的 skill 在路径层面就是隔离的,再也不出现 A 会话的缓存污染 B 会话的上下文这种诡异 bug。
第三个是升级 skill 不再如临大敌。现在改一个新版 skill 的流程是:复制当前版本到 pending 状态 → 修改代码 → validate 烟测 → 提交切换到 active。如果效果不佳,一条skillfs restore回到昨天。实际操作中我做了一次"全 skill 回滚"也只需要几秒钟,这给了我极大的尝试自由的底气。
5. 踩坑清单与经验边界
SkillFS 帮了忙,但它不是银弹。这一节把我在实现和使用中踩过的坑,以及基于这些坑总结出的适用边界,一次说清楚。
5.1 五个高频故障模式
下面五个问题,是我实际运行中真实遇到过的,也是任何想仿照这套设计的人应该提前防备的。
循环依赖。软链接把 A 引到 B,B 又引回 A,解析依赖时直接死循环。我在调度器里加了环检测,每次 resolve 后标记 visited 节点,重复访问就报错。配置项的报错信息建议写成"依赖环已检测到:A -> B -> A",否则在几十个 skill 里找环会想撞墙。
重名遮蔽。在目录树模型下,如果两个路径指向同一个 skill_id,且其中一个被更新到新版本,另一个路径可能因为还停留在旧版本而出现行为不一致。这个问题的本质是软链接只解决"多个分类引用同一逻辑",但在多版本场景下必须明确"哪个路径对应哪个版本"。我的解决方案是在 dentry 表里显式存inode_id和version,升级时同步更新所有关联 dentry,而不是只改一处。
孤儿文件。一个 skill 被 unmount 后,其他 skill 仍引用它。这对应文件系统的"悬空引用"。我在 resolve 阶段会统一检查依赖路径是否存在于当前 mount 表中,如果不存在直接抛异常。这样至少保证错误信息是明确的,不会让 Agent 在运行中才发现自己缺失能力。
权限逃逸。一个低权限 skill 可以通过调用一个高权限 skill 来完成原本被禁止的操作。如果权限检查只在注册时做一次,这种逃逸拦不住。我现在的做法是:调度器在路径解析层做初步检查,skill 执行入口做二次检查,并且高权限 skill 在执行时必须校验调用链的原始请求者,而不是信任传进来的参数。这里要小心,直接透传调用者身份容易被伪造,我最后是给每次调用生成不可控的 invocation_id,连同权限上下文一起传给下游。
幻影挂载。配置文件里写了某个 mount 项,但实际运行的 Agent 实例因为缓存或者加载顺序问题没有吃到这个配置。这种问题极其隐蔽,你ls能看到 skill,但 Agent 调度时它就是不可见。我的解决办法是让skillctl mount不仅改配置,还主动向所有运行中的 Agent 进程广播一个 reload 事件,要求它们重新同步 mount 表。同时,我在每个 skill 上记录了last_mounted_at和last_seen_at,两相对比可以快速发现哪些 Agent 实例没跟上配置变更。
5.2 文件系统隐喻的适用边界
也不是所有场景都适合用文件系统模型来治理 skill。根据我这段时间的经验,以下三种情况你可能不需要 SkillFS:
- 如果你的 Agent 核心流程只有固定的三五个 skill,而且这些 skill 之间几乎不互相调用,那用全局注册表完全够用。引入目录树和挂载点反而增加心智负担。
- 如果你的 skill 之间存在极强耦合,比如每个 skill 的输入输出都是同一个上下文对象,拆开治理会让事务性和状态同步变复杂。这种情况你应该把几个 skill 合并成一个更大的 skill,而不是研究怎么管理边界。
- 如果 skill 没有"启用/停用/回滚"的需求,整个生命周期只有"创建"和"永久存在",那权限和快照都是多余的。
还有一点要认清:SkillFS 治理的是"元数据与调度视图",它不负责 skill 内部代码的重构。两个 skill 功能重复,目录树和权限模型不会帮你自动合并,它只是让重复变得可见、可删。真正的合并还是得靠人。
5.3 SkillFS 与 Kubernetes、普通插件系统的区别
有人可能会说,Kubernetes 的 CRD 或插件体系也能管理这类东西。我明确说下区别。
Kubernetes 的设计重心是"声明式状态 + 控制器协调",它假设你管理的资源会不断漂移,需要不断调谐。SkillFS 的定位更轻:它管理的是一个 Agent 运行时里"技能集合的当前状态",不需要持久化的 reconciliation,因为 skill 本身是代码,不是被外部持续修改的活资源。你不可能像控制 K8s Pod 数量一样去自动扩缩 skill 的版本。
插件系统通常只解决"发现 + 加载"问题,一个插件目录,扫描完插件加载完,治理动作就结束了。SkillFS 比插件系统多了三件东西:挂载隔离、权限提升校验、快照恢复。这三样正是几十个 skill 相互纠缠时最需要的,而普通插件系统默认没有。
所以用一句话总结适用边界:SkillFS 适合你已经有一个"技能森林",并且开始害怕触碰它的时刻。
我自己的体会是,用文件系统治理 skill,本质上不是炫技,而是给 Agent 的技能集合补齐了三样基础设施:命名空间、生命周期和审计。命名空间消灭命名冲突和分类混乱,生命周期让启用/停用/升级/回滚变成低成本操作,审计让每一次变更都有迹可循。这三点一旦齐了,一个拥有几十个 skill 的 Agent 项目,维护成本能降一个数量级。
最后再分享一个小技巧:我把 skill 的描述文本、调用示例和注意事项都存在了 skill 元数据的xattr字段里,也就是 SkillFS 的"扩展属性"。这样在命令行可以直接用skillfs cat /skills/web/fetch_url查看一段可以直接注入 prompt 的技能描述,不用翻代码文件。建议所有做 Agent 开发的朋友,尽早给 skill 建立标准化的元数据格式,哪怕你暂时不做完整治理,先把"每个技能有名字、有描述、有依赖声明"养成习惯,后面迁移成本会低很多。