Agent技能治理新思路:像管理文件一样管理Skill
最近一直在跟一个特别具体的问题较劲当一个 Agent 手里有几十个甚至上百个 Skill 的时候这些技能的治理到底该怎么做。不是简单列一张总清单而是要真正让 Agent 自己也能快速定位、按需加载、不互相冲突、权限可控。我把这套方案命名为 SkillFS标题里这个说法就是这么来的——把技能的集合想象成一个文件系统用文件系统的逻辑去治理。带团队做 Agent 应用到现在最大的体感是技能仓库的膨胀速度远超预期。第一版通常只有十几个函数式工具等业务陆续接入两三个月后技能数量就能到四五十再跑一两个季度破百也常有。问题在于人肉维护的总清单很快就失效了——Agent 每轮决策都要在几百个技能描述里翻来覆去上下文被撑爆命中率不断下降。SkillFS 想解决的就是这件事让每个技能像磁盘上的一个文件有路径、有索引、有权限、有生命周期。1. 从技能堆积到技能文件系统问题本质与方案选型1.1 技能膨胀带来的三个典型症状先说症状再聊方案。我在某个模拟项目里做过一次技能普查刚开始只有 24 个技能用了两个季度涨到 80 多个。这个过程中出现了三个特别典型的问题。第一个是上下文预算被技能描述吃干抹净。大多数 Agent 框架都会把可用技能的名称和描述注入到提示词里让它知道手里有什么牌。当技能数量达到几十上百个光这些描述加起来就可能有三五千 token。你可别忘了这些 token 不是用来干活的它们只是“说明书”真正的任务输入和中间推理还得在剩余预算里挤效果自然越来越差。第二个是检索命中率急剧下降。技能一多描述之间难免语义重叠。比如“按主键查记录”“根据 ID 获取详情”“查询单条数据”这三个技能从人类角度看可能是三个不同表的操作但在 LLM 的语义空间里就是一团模糊。Agent 经常选错技能更糟的是有时候它会从几个技能里各摘几个参数拼起来拼出一个不存在的调用。第三个是团队协作混乱。技能多了以后谁负责哪个技能、新版本上了之后旧行为是否被兼容、某个技能是否已经被废弃这些信息如果只存在于人脑里迟早出事故。我见过最典型的一次有人把旧版逻辑覆盖了新版实现因为压根不知道已经有人升级过了。这类问题用 excel 清单是管不住的因为 Agent 场景下技能的“使用者”主要是另一个模型不是人。1.2 为什么是文件系统而不是数据库或者扁平列表提到治理很多人第一反应是上数据库、建表、搞一套管理后台。我一开始也是这么想的后来发现数据库方案并不适合解决 Agent 技能治理的核心矛盾。数据库把信息存得很好但查询入口仍然要靠人。Agent 又不会写 SQL 来查技能清单它接受的是自然语言或结构化 JSON。要是让 Agent 先查数据库再决定用哪个技能等于多了一层永远调不准的中间逻辑反而把事情搞复杂。扁平列表更不行。把所有技能平铺在一个列表里本质上就是把问题暴露给了上下文窗口。技能少于 20 个的时候还能凑合超过 50 个光排序和去重就够你头疼的。文件系统的价值在于它天生自带结构。路径表达归属目录表达聚合索引表达入口权限表达边界。这些东西恰好对应了 Agent 技能治理的几大诉求定位、按需加载、隔离、控制。而且文件系统是每个工程师最熟悉的心智模型不需要团队重新学习一套抽象概念接受成本极低。1.3 SkillFS 的四个核心筹码把技能管理往文件系统上靠能得到四个非常明确的能力整理成表大概是这样能力文件系统映射对 Agent 的实际收益定位路径 文件名技能有确定性的 ID 和路径不再靠模糊描述硬猜按需加载目录逐级展开上下文只注入当前命中的分支省 token 也省决策噪音权限控制目录/文件权限位危险技能可隔离在受控目录非同权限分支不能调用生命周期版本、归档、索引技能的增删改有迹可循旧版本可以回滚这个设计里最值钱的其实是“按需加载”。传统做法是把所有技能一次性摊在 Agent 面前SkillFS 则像操作系统的路径解析一样先给 Agent 一个根目录视图再根据任务方向逐层进入子目录。Agent 在大多数情况下只需要看到“几个目录名 每目录一句话摘要”真正确定要用某个技能时才把完整描述注入进来。从方案选型角度看我后来没有再纠结数据库还是文件系统因为二者解决的不是同一层问题。数据库可以当作 SkillFS 后面存储元数据的一种实现方式但前面的逻辑入口、路径规划、目录结构一定要按文件系统的方式来设计。2. 技能文件系统的整体设计与目录规划2.1 技能树长什么样一种可落地的顶层分类设计技能树第一件事是确定顶层目录怎么切。我之前见过有人直接按业务域分比如订单域、用户域、库存域结果技能树跟着业务组织架构走部门调整一次技能目录就得重构一次。更稳的做法是按“能力类型”分顶层把具体业务域放在第二层。我这个方案里顶层分成了四类skills/ ├── 01_perception/ # 感知类解析、识别、理解输入 │ ├── document_parse/ │ ├── speech_process/ │ └── index.yaml ├── 02_analysis/ # 分析类计算、推理、数据洞察 │ ├── data_query/ │ ├── trend_detect/ │ └── index.yaml ├── 03_operation/ # 执行类调用外部系统、写数据、发消息 │ ├── email_ops/ │ ├── issue_ops/ │ └── index.yaml └── 04_communication/ # 协同类多 Agent 交互、权限确认、人工介入 ├── handover/ └── index.yaml这里的关键是层级不能超过三层。叶子节点必须是具体技能中间层是技能域最上层是能力大类。超过三层之后Agent 在路径解析上消耗的步骤会成倍增加而且很容易在中间层就迷失方向。编号前缀是我强烈推荐的做法。01_、02_这样的前缀解决了两件事一是目录在索引文件里有稳定顺序Agent 看到的目录列表不会因为文件系统遍历顺序混乱而每次都不一样二是可以通过编号快速判断技能分类的轻重缓急比如把最常用的感知类放在最前面。2.2 一个技能的“文件”由什么组成如果把目录看成文件夹那每个技能就应该是一个有结构的“文件”。在我的实践里这个文件是 YAML 格式的清单里面除了技能本身的信息还要包含运行时需要的全部元数据。id: analysis.data_query.query_by_id name: 按主键查询数据记录 description: | 当用户需要根据主键 ID 查询数据库表中单条记录的详细字段时使用。 典型场景包括查询订单详情、用户详情、配置项详情等。 传入表名与主键值即可返回该行数据的所有列。 input_schema: type: object required: - table_name - id properties: table_name: type: string description: 目标数据表名称 id: type: integer description: 主键值 permission: data_read version: 1.3.0 implementation: type: python entry: ./impl/query_by_id.py tags: [database, postgres, query]一个技能文件里面我最看重的字段有三个。第一个是id它必须是路径形式。analysis.data_query.query_by_id这个 id 可以直接映射到目录路径skills/analysis/data_query/query_by_id.yaml。技能 ID 和文件路径互相可解析这是 SkillFS 一切治理能力的基础相当于文件系统的 inode。第二个是description这行字基本决定了 Agent 会不会用这个技能。我给团队定的要求是描述必须以“当用户需要……时使用”的句式开头明确触发场景然后写清楚需要哪些关键参数。最忌讳写“这是一个查询功能”这种正确的废话。第三个是permission字段。技能在运行时要做什么级别的操作是只读、写数据还是发外部消息必须在定义阶段就明确标注。后面接权限系统的时候这里就是判断依据。2.3 目录索引与二级摘要让 Agent 先看目录再看文件技能文件定义好之后目录的索引文件就是整个 SkillFS 的“目录页”。每个非叶子目录下都应该有一个index.yaml作用是把这一层每个子项的核心信息压缩成一句话。01_perception/index.yaml可以长这样domain: perception summary: 负责将外部输入转化为结构化信息包括文档解析、语音识别、图像文本提取等。 sub_items: - name: document_parse summary: 解析 PDF、Word、HTML 等文档输出结构化文本或字段 - name: speech_process summary: 将音频转写为文本支持多语言与说话人分离 - name: direct_skills skills: - id: perception.image_ocr summary: 从图片中提取文字内容这里就体现了“先看目录再看文件”的思路。Agent 在第一轮决策时只需要看到summary和sub_items里的每项一句话摘要。比如用户说“帮我把这份 PDF 里的小结部分提取出来”Agent 看到document_parse的摘要就能知道应该往这个子目录走然后再加载该目录内部的详细技能列表。这个设计能大幅压缩上下文开销。假设系统有 100 个技能按传统方式 Agent 要读 100 条描述。用 SkillFS 的方式Agent 第一次只读 4 个顶层目录摘要加二三十个字项摘要撑死了 300 token确认进入document_parse之后再读该目录下 5 个技能的完整描述也就 500 token。总量从 3000 token 降到 800 token效果非常直观。3. 实操过程自己搭一套 SkillFS 并且跑起来3.1 从无到有初始化目录与第一版技能搭建流程其实不复杂关键是耐心把存量技能重新归类。我每次搭建 SkillFS 典型走五步。第一步盘点现有技能。把当前 Agent 能调用的所有技能列出来不要漏掉一些藏在代码深处、靠字符串匹配调出来的“隐藏技能”。第二步按能力类型做粗分组。先不管业务域纯看这个技能是感知输入、分析数据、执行动作还是协同沟通放进四个顶层目录。第三步在二级目录里按业务对象细分。比如数据查询下面再分 PostgreSQL、API 数据源、缓存等。第四步为每个技能补全 YAML 文件。重点打磨 id 和 description 两个字段。第五步写各级 index.yaml并且写完之后做一次“目录可读性测试”——把根目录和一级目录的索引丢给一个空上下文 Agent看它能不能准确说出有哪些能力方向。这里补一句第一次搭建不一定追求完美。能先把技能从“一团乱麻”整理成“有结构的三层树”就已经是巨大进步后续可以持续调整目录归属。技能治理是演进式的不是一次性的。3.2 命名规范与技能幂等命名这事看着小但对后续影响特别大。我的命名规范总结下来就三条。一是 id 全小写、单词用下划线连接路径层级用点号分隔。analysis.trend_detect这种风格比analysisTrendDetect或者analysis/trendDetect更好解析因为点号可以直接映射到目录分隔符。二是 name 字段必须是动词短语。不说“数据查询”说“按条件查询数据记录”不说“邮件功能”说“发送邮件并附带附件”。动词开头的好处是 Agent 在生成调用意图时更容易匹配到动作而不是匹配到一堆名词。三是每个技能必须做到幂等注册。同一个技能 id 在系统中只能存在一份重复创建要报错或者覆盖前先确认不能静默同存。这个约束在文件系统层面天然支持——同一个路径下不可能存在两个同名文件但在数据库方案里就很容易出现重复记录。3.3 加载器与路由的实现逻辑SkillFS 运行时最核心的模块是加载器。它负责把技能 id 解析成文件按需加载描述维护索引缓存。from pathlib import Path import yaml class SkillFS: def __init__(self, root: Path): self.root root self._meta_cache {} self._index_cache {} def _resolve_path(self, skill_id: str) - Path: # 技能 id 转换为相对路径 rel skill_id.replace(., /) .yaml return self.root / rel def load_skill_meta(self, skill_id: str) - dict: if skill_id in self._meta_cache: return self._meta_cache[skill_id] skill_path self._resolve_path(skill_id) if not skill_path.exists(): raise KeyError(fskill not found: {skill_id}) with open(skill_path, r, encodingutf-8) as f: meta yaml.safe_load(f) self._meta_cache[skill_id] meta return meta def load_index(self, dir_name: str) - dict: # 加载指定目录的 index.yaml index_path self.root / dir_name / index.yaml if str(index_path) in self._index_cache: return self._index_cache[str(index_path)] with open(index_path, r, encodingutf-8) as f: index yaml.safe_load(f) self._index_cache[str(index_path)] index return index这里的核心技巧是“id 即路径”。analysis.trend_detect通过replace(., /) .yaml直接变成文件路径整个技能空间就完全文件系统化了。缓存是为了避免每个请求都去读磁盘因为技能文件不会频繁变更没必要实时读盘。路由层再往上一层走负责决定给 Agent 注入哪些内容。用一段简化的伪代码来说def context_skills_for(user_query: str) - list[dict]: # 第一层加载根目录视图 root_index load_index() # 第二层根据查询与子项摘要做相关性筛选 selected_subdirs [] for item in root_index[sub_items]: score relevance_score(user_query, item[summary]) if score 0.4: selected_subdirs.append(item) # 第三层展开选中的子目录加载更细的技能摘要 result [] for subdir in selected_subdirs: sub_index load_index(subdir[name]) result.extend(sub_index[sub_items]) return result[:20]这段逻辑的本质是“逐步缩小范围”跟人翻文件夹一层层往下找是一个思路。相关性阈值这里我习惯设在 0.4 到 0.5 之间——太低会把不相关目录带进来太高会漏掉一些描述比较保守的技能。3.4 如何接入运行时的工具选择流程SkillFS 不是要替换 Agent 框架而是作为技能描述供应的中间层。接入的时候不需要改 Agent 底层的调用协议只需要改“喂给模型上下文”的那个环节。具体的接入流程是这样的第一步在系统初始化时扫描整个技能目录树生成根索引和一级索引缓存。第二步在 Agent 每轮推理开始时先用用户当前 query 跑一次路由函数挑出可能命中的目录。第三步把这些目录的技能摘要注入到系统提示词里而不是全量注入所有技能描述。第四步当 Agent 实际选择了某个技能并准备调用再把该技能的完整 YAML 描述加载进来包括 input_schema 和调用入口让 Agent 按规范填参数。第五步调用结束后写审计日志记录调用了哪个技能、耗时多少、参数是什么。我实践下来这套流程最大的好处是不用改模型推理代码。你原来的 Agent 怎么处理 function calling现在就怎么处理只是提供给你的 function lists 不再是静态清单而是根据任务动态组装出来的子集。4. 常见问题与排查技巧实录4.1 技能同名冲突路径就是命名空间的底气SkillFS 刚上线的时候最常见的坑是技能 id 撞车。比如数据组做了一个get_user_info用户组也做了一个get_user_info两个团队互相不知道对方存在。在文件系统逻辑下这种问题很好解决路径就是命名空间。数据组的可以叫analysis.data_query.get_user_info用户组的可以叫operation.account_ops.get_user_info。因为 id 是路径唯一标识同名但不同路径的技能可以共存互不污染。反过来如果两个技能连路径都完全一样那说明它们本来就是同一个技能应该合并而不是分开维护。排查手段也有点像文件管理定期跑一遍全量技能清单按 id 排序、按目录分组直接对标文件系统里的“全盘目录列表”。只要发现同一个叶子路径下存在两个不同实现立刻合并。4.2 描述质量差Agent 总是选错技能路由层做得再好如果技能文件里的描述不行Agent 依然会选错。这里最常见的错误是把描述写成“系统说明”而不是“任务触发条件”。举个例子。某个查询天气的技能如果描述写“提供天气服务”Agent 看到后完全不知道什么时候该用。改写成“当用户需要查询某个城市当前或未来几天的天气情况时使用入参为城市名称可选日期范围”命中率立刻就不一样了。我给团队的验收标准是“不看技能名只读描述能不能还原这个技能触发的任务场景”。如果读完描述脑子里没画面那就重写。另外描述里不要把实现细节写太多Agent 不需要也不应当知道这个技能内部调了哪个 API、用了什么算法这些只会误导它判断适用范围。4.3 技能数量继续膨胀上下文还是不够用即便用了目录索引、按需加载技能总数到了四五百个的时候新的矛盾又会出现路由层本身要做很多次相关性判断而且叶子目录可能也装了几十个技能一次展开依然会超限。我的处理思路是再加一层“技能描述的外置检索”。把每个技能的 id、摘要、tags 做成一个轻量向量索引Agent 先做一次向量检索找到 top 5 到 top 10 个候选技能再把这些候选技能的完整描述注入上下文。这相当于是给文件系统增加了全文搜索能力但目录结构仍然保留作为后续的归属管理和权限判断依据。另一个常用优化是把热技能单独建索引。统计历史调用频次把 top 20 最常被调用的技能放到一个“热点目录”它们的描述常驻上下文其他冷门技能继续走按需加载。这样大部分请求只需要读热技能列表性能会好很多。4.4 权限边界与危险技能如何管控Agent 技能系统里最容易被忽略的是权限。有些技能会执行写操作、发邮件、调外部系统如果所有技能对所有 Agent 一视同仁很容易出事故。SkillFS 在这个问题上有一个天然优势目录结构可以直接当作权限边界。比如02_analysis下的技能全是只读分析03_operation下面的技能包含写操作就给不同目录配置不同的访问权限。给某个 Agent 配角色时直接说“这个 Agent 只能访问 analysis 目录不能进 operation 目录”语义非常清晰。我整理了一份排查表团队遇到问题可以直接对着查症状可能原因排查动作治理建议Agent 明明有权限却报技能不存在技能没有注册到全量索引跑索引重建检查目录下是否有 index.yaml在 CI 里加索引完整性校验Agent 调用了高危技能且未确认权限配置过宽缺少确认环节查技能 permission 字段和角色绑定高危目录走人工确认流程技能更新后行为不一致版本冲突或覆盖异常查技能文件 version 和变更记录把技能文件纳入版本控制回滚可追踪Agent 经常出现幻觉参数技能 input_schema 缺少默认值或示例检查入参字段是否全部有描述给每个参数加示例值降低幻觉空间权限这件事我的建议是“宁可先严后松”。刚搭建 SkillFS 时所有技能默认只读哪类 Agent 需要执行能力再逐项审批开通。Agent 领域里一次越权操作的回滚成本往往比功能上线慢几倍的代价高得多。5. 我在实际维护中的一些体会SkillFS 这套东西我用下来最深的感受是它不是一个技术组件而是一种治理思想的落地方式。文件系统的价值不在于“用文件夹装东西”这个动作本身而在于它逼着你把技能归类、命名、写索引、设权限。这些动作做到位了技能数量再多也不会失控。如果现在有人来问我Agent 的技能治理应该从哪一步开始我会说先别急着写加载器也别急着上向量检索先把技能目录树和命名规范定下来。把几十个技能的名称、路径、描述、归属整理清楚后面的一切能力都有地方挂载。反过来要是这层结构是乱的再强的路由算法也救不回来。还有一个小技巧每次调整目录结构之后最好让一个“对系统一无所知的新成员”对着根索引和一级目录摘要复述一下系统有哪些能力。如果他能说出“这个 Agent 大概能感知三类输入、做五类分析、执行四类操作”说明目录写得足够清晰。如果他说不出来那就继续优化摘要直到 Agent 和人脑都能一眼看懂为止。