用Git管理Agent技能库:目录契约、版本策略与评审闸门
半年多前我所在团队的 Agent 技能库还处于一种极度随意的状态技能包散落在共享文档和各人本地文件夹里谁想加技能就加一份谁想改就改一份“最新版本”“最终修订版”这类文件名累积了几十个。表面上大家一直在扩充能力实际上没有一个人能回答“当前线上到底跑的是哪个版本”。转折点是一次线上事故。某个报表生成技能的提示词被改了几天新旧两套参数格式不兼容技能入口却没有任何版本标识直到生产环境的流程真正调用时才暴露问题。回看改动记录只能靠聊天记录推测。从那一刻起我确定技能的演进必须做到可追溯、可回滚、可评审否则技能越多事故概率只会越高。我们最后落地的方案是把全部技能看作一个独立的 Git 仓库来管理并围绕它建立了目录契约、版本策略、复用方式和评审闸门。这篇文章是这套方案的完整复盘包括它仍然存在的边界和我踩过的坑希望对你正在搭的团队技能库有参考价值。1. 技能库从“一人一个文件夹”变成“团队公共资产”Git 这件事绕不过去1.1 先说清楚Skill 在团队语境里到底是什么团队语境下的 Skill 和很多人理解的“一个提示词文件”不是一回事。它更像一个完整的能力单元为 Agent 准备好某个任务所需的所有素材包括提示词模板、执行脚本、数据清洗逻辑、输出格式定义、少样本示例和回归测试用例。举个例子我们团队有个“把内部表格转成周报”的技能。它不是一个 prompt 就完事的而是由几个互相咬合的部分组成的周报的生成指令模板、表格文件的解析脚本、输出 Markdown 的格式约束、三组历史样例如好的周报长什么样以及一组用来验证格式的测试用例。任何一个部分改动都可能改变 Agent 最终输出。很多人问Skills 和普通代码库有什么区别我的回答是底层协作痛点高度一致但 Skills 还多了两层特殊性一是语义不确定性一个提示词的微小改写在模型手里的结果可能天差地别二是单一事实来源缺失同样的技能散落在不同人手里总会悄悄长出不同分支。这两个特殊性会放大版本管理缺失的后果。1.2 无版本控制的技能库会经历哪几个阶段我观察到的失控过程基本是三个阶段。第一阶段个人草稿文件。技能刚起步时一个人写一个人用最多在文件名后面加个日期。这个阶段效率很高问题是没办法被其他人继承。等到写的人一忙别的人想复用只能从头再读代码或者干脆复制一份自己改。第二阶段共享文件夹 聊天传输。团队开始协作后共享文件夹成了重灾区。你上传一个“v2”我覆盖成“v2_new_最终”他用另存为改成“v2_final_really”。表面上是版本管理实际上是靠纪律约束。真实情况是很快就会回到手忙脚乱找文件的现场。更麻烦的是某个技能一旦同时被多个流程引用你根本不知道对方是在哪份副本上做的修改。第三阶段线上事故之后被迫重建。当 Agent 开始被业务方使用时问题就不再是“找不到文件”而是“生产环境跑错了版本”。我们的那次报表事故就把整个问题彻底暴露了技能入口没问题、脚本没问题但提示词约定发生了变化下游解析器还是按旧约定工作最后输出全乱。这种事故用“回滚”能处理前提是你得有可回滚的东西——那会儿我们根本没有版本概念。Git 在这里提供的价值不是“存个历史”而是给技能库建立了一个可讨论、可仲裁、可回退的底层平台每次改动有 diff 可以评审每个历史状态有 commit 可以定位每个稳定状态有 tag 可以回溯。技能库一旦成为团队公共资产这些能力就不是可选项而是必须项。2. Skill 的“最小可交付单元”目录与元数据契约怎么定2.1 一个标准 Skill 仓库的最小目录结构如果技能库是一整间仓库那么里面每一个技能都应该是自包含的文件夹。我们最终把最小目录结构定成了这样skills/ ├── form-xml-to-report/ │ ├── SKILL.md │ ├── README.md │ ├── prompts/ │ │ ├── main.md │ │ └── examples.md │ ├── scripts/ │ │ └── transform.py │ ├── assets/ │ │ └── template.xlsx │ └── tests/ │ ├── cases.json │ └── expected_outputs.md每个文件夹的命名用小写字母加连字符全局唯一。为什么这样设计因为技能会被很多上层 Agent 引用名字一旦不确定依赖关系就跟着混乱。文件夹内各部分分工明确SKILL.md技能元数据是给 Agent 和同事共同看的接口README.md给人看的使用文档告诉用户这个技能适合什么场景、不支持什么场景prompts/提示词模板和示例尽量把上下文模板和示例拆开方便单独 reviewscripts/真正可执行的代码负责数据预处理、格式转换、调用外部工具assets/静态资源比如表格模板、图片素材tests/回归测试数据用来验证技能改动后输出仍然符合预期。这套结构的核心原则是“一个技能一个独立边界”。不要让两个技能共享一个 scripts 目录也不要让一个技能同时依赖另一个技能内部的未声明文件。边界清晰之后Review、回滚和权限控制才能精准落在一个技能上而不是眉毛胡子一把抓。2.2 元数据字段是给 Agent 和同事共同看的“接口”如果说目录结构是仓库的骨架SKILL.md就是技能的接口契约。它的重要性经常被低估很多人写技能只把提示词往文件夹里一丢觉得元数据是形式主义但等技能数量上来你会发现元数据恰恰是整个版本化体系里最重要的部分。我们 SKILL.md 里维护了这些核心字段字段说明是否必填id技能唯一标识与文件夹名保持一致是name人类可读的技能名称是description一句话说清技能用途和适用场景是version遵循 SemVer 的技能版本号是api_version技能依赖运行环境的接口版本是dependencies需要引用的其他技能或内部依赖否inputs输入参数定义包括类型、必选、默认值是outputs输出结构的说明或 schema是tags分类标签方便检索和治理否其中 description 是最容易被写坏也最重要的字段。Agent 会根据 description 决定什么时候调用这个技能所以描述必须语义精确。不要写“处理表格数据”要写“将包含销售额数据的 xlsx 文件转换为指定格式的周报 Markdown支持合并同品类条目并计算环比”。写模糊了Agent 就会在不需要它的时候调用它或者在需要的时候跳过它。version 和 api_version 分开的用意是技能本身可能升级version支撑它的平台接口可能升级api_version。两个维度混在一起一旦运行环境接口变化你根本判断不了是哪个技能实例受到波及。2.3 目录规范落地不能只靠约定契约定了还要用脚本确保每个人提交前都遵守。我们仓库里放了一个check_skills.py在 pre-commit 阶段自动跑。它的逻辑很简单扫描所有同名目录检查文件夹名与 SKILL.md 里的 id 是否一致检查必填字段是否存在version 是否符合语义化版本规则检查 prompts、scripts、tests 三个目录是否都存在识别是否有重复技能名或同名但 id 不同的情况。# scripts/check_skills.py import os import re import sys from pathlib import Path REQUIRED_FIELDS [id, name, description, version, api_version, inputs, outputs] SEMVER re.compile(r^\d\.\d\.\d$) def parse_skill_meta(path): content Path(path).read_text(encodingutf-8) fields {} for line in content.splitlines(): if : in line: key, value line.split(:, 1) fields[key.strip()] value.strip() return fields errors [] for skill_dir in Path(skills).iterdir(): if not skill_dir.is_dir(): continue meta parse_skill_meta(skill_dir / SKILL.md) for field in REQUIRED_FIELDS: if field not in meta: errors.append(f{skill_dir.name}: 缺少字段 {field}) if version in meta and not SEMVER.match(meta[version]): errors.append(f{skill_dir.name}: 版本号不是 x.y.z 格式) if meta.get(id) ! skill_dir.name: errors.append(f{skill_dir.name}: 目录名与 id 不一致) if errors: print(技能目录检查失败) for err in errors: print( -, err) sys.exit(1)这段脚本第一版只做基础检查后来才陆续加入了字段值类型、依赖声明合法性等更严格的校验。它最重要的是提供了一个“提交即校验”的反馈闭环让目录契约不依赖人类的自觉。别小看这种简单脚本它把“规范”从 PPT 里搬到了实际工作流里。3. 版本化不只打 tag分支模型、提交规范与技能回滚3.1 分支模型main 始终可用feature 分支承载实验技能库的分支模型不需要发明新花样我们直接采用的是最稳妥的模式长期存在main分支技能开发和修复都在独立 feature 分支上完成通过评审后合回主分支。main分支的原则是“始终可用”含义是这里面每个技能都满足最小质量要求随时可以被业务调用。至于更好用的新版本、试验中的提示词写法一律先进 feature 分支。不要在main上直接改东西哪怕只是改一个词。为什么这么苛刻因为 Agent 技能库会被多个上层流程引用。如果 main 分支不稳定你今天合并了一个未经验证的改动明天另一个团队同步仓库时就会把不稳定状态带到自己的环境里。这么搞一次大家对仓库的信任度就会崩掉。每个 feature 分支的命名我们也做了约定feat/skill-id-改动简述比如feat/form-xml-to-report-add-thousands-separator。分支名承担了“改动声明”的功能评审人一眼就看出改动范围不会把不同技能的改动混在同一个分支里。3.2 提交信息约定与自动生成 CHANGELOG提交信息是团队最容易忽略但收益最大的环节。我们引入了常规提交规范Conventional Commits格式统一成type(scope): description其中 type 包括feat、fix、docs、test、refactor、perfscope 写技能 id描述写清楚的改动原因。实际例子feat(form-xml-to-report): 增加千分位格式选项默认开启 fix(sales-summary): 修复同比字段为空时输出异常的问题 docs(data-clean): 补充中文日期格式的输入示例这个规范最大的价值是让每一行历史都能“被脚本消费”。通过扫描提交信息我们可以半自动生成每个技能自己的 CHANGELOG而不是靠人手动维护文档。手动维护的文档一定会过期提交时顺手写清楚再让脚本汇总才是长期可维护的做法。3.3 技能版本号打在哪一层版本号的载体有好几种选择简单说单个技能独立仓库直接在仓库上打 tag比如v1.2.0简单直接多技能共用一个 monorepo打带路径前缀的 tag比如skills/form-xml-to-report/1.2.0这样每个技能可以独立发布不想分太细至少按仓库整体打 tag记录“这一批技能当前的状态”。我建议稍有点规模的团队直接用带前缀的 tag。整体 tag 的问题是牵一发动全身你只改了一个技能却把整个仓库版本号升级了下游依赖方无法判断自己需不需要更新。另外一个值得注意的点是版本号要写在 SKILL.md 里同时同步打 tag。我这个版本号就是当前这个技能的 API 版本别人引用它时应该依赖这个数字做判断。tag 是 Git 层面的标记字段是语义层面的声明两者必须保持一致。我们在 CI 里加了一步校验如果 SKILL.md 的 version 变了但 tag 没有对应更新构建会直接失败。3.4 一次技能回滚的完整操作版本管理最重要的实战场景就是出问题后的回滚。我复盘一下我们真实做过的操作流程。假设当前线上跑的是form-xml-to-report/1.2.0但生产环境 Agent 输出的格式异常。怀疑是 1.3.0 引入的改动我们需要在几分钟内恢复# 先确认当前仓库状态和历史 git log --oneline -- skills/form-xml-to-report/ # 查看某个 commit 改了什么 git show b3a4c5d -- skills/form-xml-to-report/ # 找到 1.2.0 对应位置 git tag | grep form-xml-to-report # skills/form-xml-to-report/1.2.0 # 基于 1.2.0 开一个 hotfix 分支把当前 main 回滚到该状态 git checkout -b hotfix/form-xml-to-report-1_2_0 git checkout skills/form-xml-to-report/ -- skills/form-xml-to-report/ # 提交并推送走评审流程 git add skills/form-xml-to-report git commit -m fix(form-xml-to-report): 回滚至 1.2.0修复输出格式异常这里有个很容易踩的坑不要直接在 main 上git revert再推也没必要把整个仓库硬 reset。正确的姿态是只对出问题的技能目录做定点恢复保留其他技能的当前状态。弄完以后还要把“为什么 1.3.0 会出问题”的根因补进 issue经验不记录就等于没回滚。4. 复用不外传子模块、子树与内部相对路径三种玩法4.1 先理解“技能安装边界”再谈复用版本管理建好之后下一个问题是复用。复用有几种姿势选择之前必须想清楚“技能怎么被安装到目标环境”。是运行时直接从仓库读取还是在部署时复制到某个技能目录还是作为依赖被另一个技能调用边界不同方案完全不同。我们最初犯的错误是所有技能都放在一个仓库里然后让上层 Agent 在启动时直接加载这个仓库的全部文件。短期看很省事长期看 Agent 每次都要面对一份越来越大的技能清单加载慢且容易产生幻觉式误调用。4.2 方法一Git Submodule——精确锁版本适合跨仓库引用如果你有多个独立仓库其中一个仓库想引用技能库里的某个技能Git Submodule 最合适。它能在父仓库里锁定子仓库的某个提交保证任何人 checkout 时拿到的都是同一个状态。优点很明确依赖关系显式存在版本精确到 commit。缺点也很明显更新时机是所有协作方共同决定的事子模块更新常常被忘记跨仓库调试时的流程也比较繁琐。我在团队里只推荐一个场景用 submodule构建反依赖关系明确的独立技能资产你每次上线都不想被别人的偶然改动波动需要稳定引用。4.3 方法二Git Subtree——复制代码但保留上游同步能力Subtree 相当于把某个技能的文件复制进当前仓库同时保留一条和来源仓库的关联记录。优点是对使用者透明父仓库里所有文件完整可见没有额外的 submodule 更新操作。缺点是历史会比较冗余同步冲突处理费劲。我观察到的实际使用习惯是如果技能库本身就是 monorepo那么团队内部跨项目复用技能subtree 其实没太大必要。subtree 更适合把一组技能从主技能库提取出来复制到另一个独立项目里基线。我们的做法是给内部二开的演示项目用 subtree 拉取技能包保证了它们能离线使用。4.4 方法三仓库内部相对路径——小团队最划算如果团队只有一个技能仓库上层 Agent 引用的技能全部通过内部相对路径读取这是最便宜的方案。不引入任何 Git 高级特性依赖关系就是目录路径。优点不多说缺点是路径依赖太脆弱。别人调整目录结构时所有引用立即坏掉。这个方案只在仓库规模可控、结构稳定时成立我们一开始用这个方案后来规模上来了才逐步拆成组件版本。三种方式优缺点一张表看全方案版本锁定上手成本同步灵活性适用场景Submodule精确到 commit中等低跨仓库依赖需要强一致Subtree精确到复制时的版本中等中技能整体复制到其他项目基线内部相对路径随仓库整体走低高单一 monorepo团队规模小4.5 跨技能依赖与版本锁定防止 A 技能悄悄改坏 B 技能真正让版本管理变复杂的是技能之间的相互依赖。比如我们有个“清洗销售数据”的技能另一个“生成销售周报”的技能会调用它。后者用自己的 prompt 把前者的输出“喂”给模型。这时候问题来了清洗技能的输出格式一改生成周报的技能就可能跟着坏。我们做的第一道防线是在 SKILL.md 里显式声明 dependenciesdependencies: ># .git/hooks/pre-commit #!/bin/sh python scripts/check_skills.py || exit 1 python scripts/check_large_files.py --max-size-mb50 || exit 1有些团队会觉得这麻烦但实际体验下来这种“提交时多花几秒”的成本远比后续 review 时被问“为什么目录结构错了”要低。所有检查失败都是立即、明确、可修复的。5.3 版本号和变更文件的自动校验第二道闸门放在 CI 里。它检查的核心逻辑是当前合并请求到底动了哪个技能版本号有没有相应升级。这是最容易漏掉的一步很多人改完技能但不记得改版本结果线上运行环境还是旧版本号排查问题时被误导。我们 CI 脚本的思路是用git diff --name-only列出变更文件名根据文件路径识别被改动的技能 id检查该技能 SKILL.md 的 version 字段相比 main 是否有变化有变化且没有对应 tag构建失败。这个逻辑不复杂但能让“版本号必须和改动同步”从口头要求变成硬性约束。5.4 敏感信息守卫技能里最容易出现密钥泄露技能库和代码库一样容易保存机密。很多人为了调试一个外部 API直接把 token 写在提示词模板里再一个不留神提交到仓库之后每一条提交记录里都会有这个密钥的影子。我们的防线有两层。第一层靠 Git hooks在 pre-commit 阶段用一段简单的自定义扫描拦截常见密钥格式比如sk-开头的字符串或者password 这种赋值模式。第二层靠全量历史扫描在 CI 里定期扫描所有 commit防止旧历史里藏着已泄露的密钥。再说下最容易被忽略的一点就算你现在把 token 从代码里删掉旧的 Git 历史里仍然存在它。所以一旦发现密钥进了技能库正确操作不是删除提交而是立刻吊销密钥并轮换新密钥。处理完密钥之后再做历史清理或者直接重建仓库。6. 跑通这套方案之后的实绩、边界与仍然存在的坑6.1 团队实际感受到的三个变化这套以 Git 为底座的管理方案跑了大半年最明显的三个变化不是工具层面的而是团队行为层面的。第一个变化是改动有据可查。现在任何人问“这个技能为啥变成这样”我们不再翻聊天记录而是直接看提交历史几秒钟定位到改动人和改动理由。这个变化看似简单但大大降低了协作中的猜测成本。第二个变化是回归可控。引入“依赖技能自动跑回归”测试后技能间的隐性问题从原来靠人肉发现变成“合并不了你别想上线”。有几次明显是改动方自己都没想到会影响的技能测试直接抓了出来。第三个变化是新人上手路径变短。以前新人入职要看一堆文档才能搞清楚技能库长什么样现在仓库结构就是文档规范就是钩子脚本。新同学提交一次失败看一下报错信息就能自己修正不用反复问老同事。6.2 边界必须说清楚版本库不等于运行时技能注册中心Git 只解决了技能库的“来源管理”但没有解决“运行时发现”和“运行时更新”的问题。这个边界如果不认清楚很容易产生一种幻觉——只要仓库里加一个技能Agent 就能立刻用上。实际上Agent 运行时通常需要从某个技能注册中心或能力清单里加载技能而不是直接 clone 你的 Git 仓库。我们从仓库到运行时还有一层发布动作CI 构建产物生成技能包推送到内部对象存储再更新注册表。Git 的唯一事实来源是给这层发布动作提供稳定、可追溯的输入。所以我的建议是把仓库管理做好但别指望它替代运行时系统。版本化管理是地基不是整栋楼。6.3 仍然存在的坑大文件、递归依赖与提示词的“语义回归”按这套方案跑到现在我们还持续在跟几个问题作斗争。第一个问题是大文件。技能里的模型文件、模板文件、图片素材很容易超过几百兆直接塞进 Git 会让仓库变臃肿每次 clone 都痛。解决办法是给仓库启用 Git LFS或者把静态资源放到独立对象存储仓库里只放引用信息。前者简单后者更彻底。第二个问题是递归依赖。技能 A 依赖 BB 又依赖 A这种环在元数据层面可能检测不到直到构建时才发现。我们后来加了依赖图的检测脚本循环引用直接报错。这事看起来不起眼但等到依赖链条变长后再排查会非常痛苦。第三个问题最难处理我叫它提示词的语义回归。代码回归可以靠单测判断“输出对不对”但提示词改了之后模型输出往往不是对错问题而是风格变了、结构变了很难自动化判断。我们目前的做法是维护一组固定测试用例要求每个技能至少覆盖三类场景的预期输出样例改动后人工抽查也在逐渐试验用另一个模型对输出做自动比对。距离完全自动化还有距离但至少没有裸奔。6.4 给想落地的团队一句实在话如果你现在正打算给团队技能库搭建版本化体系我最朴实的建议是不要一开始就设计二十个目录、三十个字段、十种自动化检查。把两三个核心技能的目录结构和 SKILL.md 契约定好把 pre-commit 的脚本跑起来把回滚流程自己走一遍。这些做扎实了再慢慢往里面加复杂度。版本管理工具从来不是难点真正难的是让团队把“每次改动都留下可检查的痕迹”变成写作习惯。Git 恰好是那个能把习惯变成机制、把机制变成纪律的底座。技能库规模越大这个习惯的价值就越明显。