☰
打造可复用技能包:用规则驱动约束AI辅助编程
2026/10/21 15:40:49 网站建设 项目流程

说实话,“skills”这个名字起得相当随意。但正是这个看似普通的单词,成了我这半年多一直在折腾的核心:把散落在代码、注释、以及我自己脑子里的重复劳动,整理成一份份可复用、可执行、可验证的技能包。

这个项目最初只有三个文件,到今天已经发展成一个几十兆字节的目录,里面装着几十个技能,覆盖了接口重构、日志治理、配置迁移、代码评审辅助这些高频场景。它不是工具插件,也不是框架,而是一套“给AI辅助编程加上约束”的方法论。折腾下来我最大的感受就是:越依赖智能体,越需要一种把它“关进笼子”的手段。而技能,就是这个笼子的钢筋。

如果你也经常让AI助手帮你改代码,却又担心它改出莫名其妙的错误;或者你想把团队里“某个人会但别人不会”的经验变成可沉淀的资产,那么这篇内容就是写给你看的。下面我会用“skills”项目的完整演进过程,从为什么选规则驱动、怎么写技能包、怎么调试,到怎么让技能在团队里流转,一次性讲透。

1. skills这个项目想解决什么问题

1.1 AI辅助编程里的三次失控

我用AI辅助写代码的时间不算短。从一开始单纯让它补全函数,到后来让它直接改跨模块的逻辑,中间经历了三次很典型的失控。

第一次失控是“记忆失控”。智能体在处理一个需求时,会把上一次对话里的错误判断带到新的任务里。比如我让它修过某个接口的鉴权逻辑,下一次让它重构请求层时,它可能还会基于上次的错误前提去改代码,导致改出来的东西根本没法用。说白了,智能体的对话历史就像一个人的短期记忆,一旦内容变多,它自己都不知道该信哪一段了。

第二次失控是“上下文失控”。项目稍微大一点,智能体就很难判断真正需要读哪些文件。它可以自己翻遍历代码库,但结果往往是:改了一个文件,牵连出七八个无关文件。有一次我让它给某个服务加统一超时配置,它愣是把上层十几个调用方的测试文件全部改了,理由是“统一调整超时应该保持测试同步”。这逻辑听着没毛病,但完全不是我要的。

第三次失控最麻烦,是“规则失控”。我专门在系统提示里写了“不要修改公共测试数据”“不要改动外部依赖版本”这类约束。但实际跑起来,这些口头约束经常被模型忽略。不是它故意违反,而是因为没有结构化表达,它根本不知道哪些约束优先级更高。口头约定和代码里的现实不匹配,规则就成了一纸空文。

1.2 为什么选规则驱动而不是智能体

后来我接触到了“技能”这个概念,一开始我以为技能就是智能体的简化版,但试过之后发现完全不是一回事。智能体更接近一个自由行动的个体,你给它一个目标,它自己决定中间怎么做。而技能是一套预先写好的、可预测的规则集,触发条件明确,执行步骤明确,边界约束也明确。

可以这么理解:智能体是个实习生,你交代一件事,他能自己发挥;规则驱动是个操作手册,每一步都写好了,照着做就行。插件的自由度和创新空间反而成了缺点,因为在一个真实工程里,确定性的东西越可控越好。

用表格对比一下会更直观:

对比维度规则式技能自由智能体
行为可预测性高,相同输入得到相同输出低,每次结果可能不同
执行边界预先锁定在特定目录或文件范围越权风险大,容易扩展修改范围
可测试性每个分支都可以覆盖难以稳定复现测试场景
排错复杂度每行逻辑都能审查黑盒,出错很难追踪根源
失败的代价低,最多是改错了某个文件高,可能牵连整个代码库

我并不是说智能体一无是处。开放式的探索任务,比如“帮我看看这个模块为什么性能低”,智能体确实更合适,因为它需要探索。但一旦任务变成“给所有API调用加超时”“把某种错误处理统一改成重试模式”,这就是典型的重复劳动,正好是规则驱动的主场。

所以“skills”这个项目定下的基调就是:开放探索用智能体,确定重构用技能。两者配合,而不是互相替代。

2. 技能包的核心结构拆解

2.1 技能清单与命名规范

一个干净的技能目录,决定了这套体系能不能长期演进。我在项目里维护的最外层结构是这样的:

skills/ ├── add-log-retry/ ├── add-request-timeout/ ├── migrate-env-config/ ├── generate-changelog/ ├── ...

每个技能一个文件夹,主体是一个任务说明文件,加若干可选脚本。这条规则的收益,直到技能数量超过二十个的时候才真正体现出来:新增技能完全不需要动旧技能,删除和升级也一样。

关于命名,我踩过一次坑。最早我用的是结果描述式的名字,比如“retry_ability”“timeout_handler”,看起来没问题,但在技能检索时非常糟糕。后来统一改成“动词+对象”的结构,比如“add-log-retry”“remove-debug-output”。为什么这么做?因为技能本质上是一台微型机器,名字应该描述“它执行什么动作”,而不是“它包含什么概念”。你不可能在“timeout_handler”这个文件夹里只放一个处理超时的逻辑,它总会膨胀成各种工具函数的集合。动词式命名天然限制了膨胀空间。

每个技能文件夹内部还需要一个元信息文件。我用过JSON和YAML两种格式,最终从JSON转到了YAML,因为YAML可以写注释。别小看这个细节,当技能描述需要多人评审和修订时,YAML里那一行行注释比什么都管用。

name: add-request-timeout description: 当用户要求为项目中的网络请求添加统一超时配置时,使用此技能。 version: "1.2.0" scope: src/api

description这一栏是有大讲究的。很多人写成“处理网络请求相关配置”,这种描述几乎永远不会被触发。写description的核心法则是:写触发场景,而不是写功能定义。比如“当用户要求为项目中的网络请求添加统一超时配置时”,这个描述本身就包含了一个触发条件。技能匹配靠的主要就是描述里的语义关联,写得越像用户真实会说的话,触发率越高。

2.2 任务说明文件的核心结构

技能包的中心是一份任务说明文件。我之所以没把它叫“说明文档”或者“README”,是因为它不只是给人看的,更是给执行端“照着做”的。它必须以可执行的语气编写,而不是描述性的语气。

我在所有技能包里使用的标准结构是五个部分:

  • Goal:这个技能要达成的最终状态是什么
  • Steps:达成Goal的唯一路径是什么,按顺序写清楚
  • Constraints:执行中绝对不能触碰的红线是什么
  • Input:执行前需要哪些输入,从哪拿
  • Examples:用户可能的表达方式,以及对应的输入输出样例

为什么一定是这五个部分一起出现?因为少了任何一块,执行端就会找补。没有Goal,它就只做指令里的每一步;没有Constraints,它就会按自己的常识去扩大范围;没有Examples,它就无法识别用户千奇百怪的叫法。

举个例子,“给网络请求添加指数退避重试”这份说明文件的Steps部分,我一开始写得特别学术:

## Steps 1. 检查网络请求代码。 2. 添加重试逻辑。

这种写法基本等于没写。执行端看了会一脸懵:检查哪些文件?添加什么形式的重试?退避时间怎么算?后来我把它改成了可校验的版本:

## Steps 1. 扫描 src/api 目录下的所有请求类文件。 2. 在构造函数中检查是否存在 retryCount 字段。 3. 如果不存在,给该字段赋默认值3,并同步新增 retryDelay 与 backoff 计算函数。 4. 最后检查同目录测试文件是否包含重试字样。

每一步都有明确的判定标准,执行端不用自己猜“够不够”。这就是任务说明文件和普通文档最根本的区别:文档描述事实,说明文件定义验收条件。

2.3 可执行脚本与扩展点

任务说明文件只是个流程骨架,真要高效干活,尤其是处理机械性很强的改动,还得靠脚本。技能包的结构可以内嵌脚本,让执行端在关键节点调用。

比如超时配置这个技能,我不想让智能体自己去一个个文件搜字符串,它就容易漏。更可靠的做法是技能包里带一个解析脚本,由脚本完成搜索匹配,然后以清单形式输出到上下文里。这样执行端相当于拿到了一个已经过滤好的结果集,直接按清单改,速度和准确率都会高很多。

脚本我用的是Python。选Python不是因为它最合适,而是因为团队里会的人最多,可维护性最好。我在扩展点这里学到一个重要经验:一个技能对应的脚本应该做到两件事之一——要么扫描出结论清单,要么批量执行低风险的格式化修改。两者千万不要混合在一个脚本里,因为扫描出错,最多是多报几个文件;但批量修改脚本一旦把范围搞错,就会把无关代码也改掉。

脚本的存放位置我统一放在技能目录的scripts子目录下,目录里再放一个说明,写清楚这个脚本的输入输出。这个做法在技能处理跨平台环境时特别重要,不同环境里路径分隔符都不一样,脚本必须自己适配。所以我的原则是:脚本只负责标准化处理,具体环境的差异靠说明文件里的步骤去协调。

3. 从零打造一个可复用技能包的完整流程

3.1 需求分析:从真实痛点倒推技能定义

写任何技能之前,我会先回答三个问题:这个任务多久出现一次;手工处理需要多长时间;智能体处理是否稳定。如果答案分别是“每周至少一次”“超过十五分钟”“时好时坏”,那这个技能就值得做。

我拿“给项目中所有API调用添加统一超时时间”这个需求举例。这个需求看起来简单,但手工改起来极其烦人:要找到所有网络请求入口、判断哪里已经有超时配置、还要保证新加入的超时参数不覆盖已有的配置。智能体去改的时候,又会因为项目结构复杂度不同而发挥不稳定。这类任务太适合做成技能了。

分析完毕,我先画出输入输出边界:

输入:项目根目录、超时阈值(毫秒) 输出:被修改的文件列表、新增配置说明、修改摘要 边界:不涉及外部依赖版本变更、不修改测试样板数据、不改变请求重试逻辑

这一步其实是整个流程里最关键的。边界不清,后面写得再漂亮也白搭。把边界想清楚的过程中,我发现“不改测试样板数据”是一条特别容易和“同步修改测试”冲突的规则。所以技能说明里我会特意加一句:如果测试代码依赖默认超时值,则跳过,不作自动调整。这句话救了后面不止一次。

3.2 编写任务说明文件的手把手过程

写说明文件时,我不急着写Steps,而是先写Examples。这是很多人的习惯和我相反的地方。我先假设用户会怎么说:比如“帮我把请求超时都配一下”“统一给HTTP调用加个超时”“这些接口怎么都没设超时”。把这些话收集起来,把语义接近的表达合并,然后反推这个技能应该覆盖什么场景。Examples其实就是技能的触发词库。

接着写Goal。Goal要写成“完成状态”而不是“动作”。我这样写:

# Goal 项目中所有位于 src/api 下的网络请求入口,均具备明确且一致的整体超时配置。

注意,这里没有写“添加”或者“修改”这种动作词,直接用“均具备…配置”这种状态描述。执行端看了这个Goal,它会去检查每个请求入口有没有超时配置,而不是机械地在某个位置插一行代码。

然后是Steps。我的写法是按“扫描—比对—修改—校验”四段式来组织:

  1. 扫描 src/api 目录下所有包含网络请求调用的源文件。
  2. 逐一比对每个请求入口是否存在可用的超时配置字段。
  3. 对缺失项统一新增配置,值为用户指定的阈值,并标记为“skill-add-request-timeout”。
  4. 对已存在但不一致的项,保持用户的显式配置,不计入修改。
  5. 最后执行单元测试子集,确认改动没有破坏现有行为。

做完这些还差Constraints。我写Constraints的核心原则是只写“红线”,不用“建议”语气。比如写“禁止修改外部依赖的版本”,而不是“建议保持依赖版本不变”。后一种会被执行端当成可选项,导致它很随意地升一个版本。红线就是红线,没有商量的余地。

3.3 编写配套脚本并做参数设计

说明文件写完,我开始写配套脚本。这个技能的核心脚本做一件事:扫描并生成请求入口清单。

#!/usr/bin/env python3 # scripts/detect_request_entries.py import ast import argparse from pathlib import Path def find_request_calls(root: Path): hits = [] for source in root.rglob("*.py"): if "test" in source.parts: continue tree = ast.parse(source.read_text()) for node in ast.walk(tree): if isinstance(node, ast.Call) and getattr(node.func, "attr", "") in {"get", "post", "request"}: hits.append(str(source)) return sorted(set(hits)) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--root", required=True) args = parser.parse_args() for path in find_request_calls(Path(args.root)): print(path)

写脚本时有几个细节要小心。第一,路径遍历要跳过test目录,这是硬编码的规则,因为测试样板数据里的请求调用不应该被业务逻辑技能修改。第二,输出要做排序和去重,减少执行端处理的噪音。第三,脚本只输出清单,不做修改,这是上一节说过的原则:扫描和修改分离。

配套的还有一个执行脚本,负责统一的批量插入:

#!/usr/bin/env python3 # scripts/apply_timeout_to_file.py # 针对单个文件,插入统一超时配置,若已有显式配置则跳过。

参数通常包括目标文件路径、超时阈值、以及一个“dry-run”开关。dry-run这个参数特别重要,我一般在正式执行前都会先跑一遍dry-run看输出,再放开真实执行。砍掉错误批量修改的方法就靠这一步。

3.4 测试与收敛:让技能包真正稳定下来

技能写完不代表结束,真正的打磨才刚刚开始。我会准备一组模拟测试用例,至少覆盖以下几种情况:

  • 所有请求入口都没有超时配置
  • 部分入口有配置,部分没有
  • 某个入口配置了异常的超时值(比如零或负数)
  • 测试目录里存在大量请求调用但不应被修改
  • 项目结构是嵌套目录,路径带空格

跑完测试,我记录每一项的输入输出,然后观察是否有误判。第一次跑完这组用例,通常会有两种结果:一是脚本把测试目录里的调用也扫进来了,二是重复执行技能时把已经加过的超时又加了一遍。第二个问题尤其隐蔽。

为了解决重复执行产生重复修改的问题,我在脚本里加了幂等检查逻辑:执行前先检查目标位置是否存在同名配置字段,如果存在就跳过。这一步必须靠标记字段识别,不能靠“看起来有没有超时”来判断。为什么?因为用户自己可能已经写了超时配置,那个是合法存在,不该动。幂等性的实现直接决定了技能能不能被重复调用,而重复调用是技能最基础的使用方式,这一环不解决,技能的稳定性就是空中楼阁。

全部用例跑通之后,还有一步收敛操作:把技能包放到一个脏乱差的真实项目里跑一遍,而不是只在我精心构造的测试数据里自嗨。真实项目的文件命名不规范,目录层级混乱,这些都能把技能包隐藏的问题暴露出来。这一步跑完,技能才算真正解释了“能开工”。

4. 常见问题与排查技巧实录

4.1 技能没有被触发,到底卡在哪

技能没触发,是最容易让关系紧张的问题。你明明写了技能,结果执行端还是像没看到一样,用通用逻辑去处理。排查顺序上,我建议先查description的触发条件写没写清楚。

判断方法很简单:把技能描述读一遍,看你自己会不会把这句话理解成一个“命令”。如果描述里出现“处理网络请求相关配置”这种描述性句子,基本不会触发,因为它没有给出明确的适用场景。改成“当用户要求为项目中的网络请求添加超时配置时”,就是一个完整的触发条件了。

第二个排查点是技能目录的扫描顺序。有些实现是按名称排序后取最后一个匹配项,有些则是取第一个。如果你有两个描述相近的技能,很可能被一个宽泛的先匹配了。我的做法是在技能元信息里加一个priority字段,数值越大优先级越高。这样可以显式控制匹配顺序,而不用靠改描述来碰运气。

第三个排查点是:执行端到底有没有把技能目录加载进去。这个问题最容易被忽略,因为技能描述里写的事,和智能体处理时的实际上下文,可能是两个独立的部分。如果执行端没有把技能说明注入系统提示词,它根本不知道你的技能存在。所以在技能目录的README里,我会特意写一段“加载步骤”的说明,确保新成员配置环境时不会漏掉这一步。

4.2 说明文件太抽象,步骤执行不下去

这是第二高频的问题。现象是技能触发了,但执行端做到一半开始“自由发挥”,改出来的东西和预期差距很大。原因基本都是说明文件里写了模糊的动词,比如“检查”“处理”“优化”“调整”。这些词没有给执行端提供一个明确的完成标准。

我的解决原则是:把每个模糊动词替换成可校验的具体动作。举例来说,“检查网络请求代码”换成“扫描src/api目录下的所有请求类文件”。语义从“判断”变成了“遍历”,以执行模块的能力,遍历是必然可以完成的,而判断则不一定。再比如“保持测试不变”换成“跳过测试目录中所有文件的修改”,直接从路径上隔离测试文件,而不是寄希望于执行端理解“测试样板数据”的概念。

还有一类常见的模糊表达是关于“一致性”的。比如“统一配置风格”这个目标,不同人理解的“统一”是不同层次。我改成“如果同一文件内存在超过两处请求调用,则合并为同一配置”。这个描述就能直接落地了,因为它把什么是“统一”量化了。

4.3 脚本与说明文件不同步,版本错乱

当技能发展到一定规模,你一定会遇到这种场景:说明文件里说要执行A逻辑,但脚本写的是B逻辑。或者脚本更新了,说明文件忘了同步。这种错位比不写技能还可怕,因为执行端会相信说明文件,然后在脚本里得到完全不同的结果。

应对这个问题,我引入了一个简单粗暴的机制:技能版本字符串。元信息文件里的version字段不是摆设,每次任何文件变更,都必须手动提升版本号。更重要的是,说明文件的“扩展点”章节里,要写明本技能当前版本调用脚本的预期输出格式。脚本如果改了输出格式,版本号必须同步变,说明文件里的对应描述也必须同一次提交修改。

另外一个我踩过很多次的坑是脚本的返回码问题。脚本如果执行失败,很多实现默认会继续执行后续步骤,而不是停下。这会导致失败的中间状态被当成有效输入,最后改出一堆混乱代码。我后来让脚本在遇到无法处理的输入时以非零状态退出,并提供scanned/processed两个统计数字输出到标准错误流。这样哪怕执行端没停下来,也能通过输出状态判断该中止。

4.4 常见问题速查表

问题现象可能原因排查方向
技能触发不了description没有写触发场景改成“当用户要求…时,使用此技能”
技能和另一个冲突两个技能描述相似调整description语义,或显式增加优先级字段
跑完技能,项目被改坏约束条件写得太弱将建议性规则改成禁止性红线
重复执行产生重复修改缺少幂等检查在脚本里增加对标记字段的探测与跳过
脚本输出和描述不符脚本与说明文件版本错位比较版本号并统一更新
执行中途自己发挥步骤里包含模糊动词每个动词都改成可校验的具体动作
测试文件被修改路径遍历没有排除测试目录脚本和说明文件都显式规定跳过规则

5. 让技能资产在团队中流动起来

5.1 从个人技巧变成团队知识库

技能包最大的价值不是给自己省时间,而是把“某个人会做但别人不会做”的事情变成“团队都能做”的事情。我维护的“skills”项目里,有很多技能都来自同一种场景:某位成员手工处理某个繁琐任务时,我在旁边看着,觉得这活儿可以沉淀成技能。

但“他愿意分享”和“他能分享成好用的技能”是两码事。常常见到有人分享出来的技能只是一些散落的笔记片段,真正执行时会遇到各种边界情况没法处理。所以团队里做技能沉淀,需要一套协作机制。

我的做法是设立一个“技能评审”环节。每个新技能在提交前,必须满足三个硬性条件:描述里有明确的触发场景;步骤里每个动词都能判断完成与否;附带的测试用例覆盖至少三种输入情况。这三个条件卡下来,能拦掉很多半成品。

提交之后还要有人复测。复测不是跑一遍就行,而是让另一个人拿着你的技能描述,在没有你口述补充的情况下,直接对着一个新项目执行。只有“拿到文档就能按图索骥”的技能,才值得进入正式的技能库。这个标准很大程度上杜绝了那种“只有作者本人能执行的技能”。

5.2 版本控制与技能演进

技能库本身就是一个代码仓库,所以它应该遵循常规项目的版本管理纪律。我统一用Git管理整个skills目录,每次变更独立提交,提交信息里写清楚变更原因。比如“提升超时时间的取值范围校验强度”,而不是“更新技能包”。

演进路径上,我把技能分成两个阶段:实验阶段和稳定阶段。实验技能可以粗糙一点,允许试错;一旦某技能连续两周没有bug反馈,就把它提升为稳定技能。稳定技能会被锁定约束规则,不允许随意调整边界。这样做的原因是,稳定技能的消耗方可能不止一个人,随意改动会波及他人的工作流。

另外我还维护了一份“废弃技能清单”。有些技能被更新的技能替代,或者随着技术栈升级不再适用,我就会把它标记为废弃,但不会立刻删除。因为总有人之前的脚本工作流还在依赖旧技能,保留一段时间给他们迁移缓冲,之后再清理。

5.3 把技能库接入日常开发流程

让技能真正发挥作用,不能指望开发者“有空的时候打开技能库看一下”。它必须和日常开发流程天然融合。我这边有一个比较顺手的做法:在代码评审阶段,直接贴出技能的产出摘要。

只要执行端触发了某个技能,就把技能名、版本号、修改过的文件列表一起追加到变更描述里。负责评审的人打开变更,能看到这段信息,就能快速判断哪些改动是技能自动完成的,哪些是开发者手动调整的。有了这个区分,评审效率高了很多,人工只需要关注那些“非技能改动”的部分。

还有一点是关于新技能的持续收集。我的习惯是:每次发现智能体在处理某个任务时表现不稳定,或者手工处理某类改动耗时过长,就在项目里建一个对应方向的草稿技能。收集得多了,每周集中整理一次,用统一的规范补齐描述和测试用例。这样的节奏不会给日常开发添太多负担,又能持续增加技能库的覆盖度。

如果一个技能半年都没有被触发一次,我不会急着删它,而是看它的description是不是没跟上用户的实际表达方式。很多“没人用的技能”不是因为没用,而是因为入口长在大家不习惯的位置。改一下描述,把触发场景写得贴近真实会话表达,使用频率立刻就不一样了。

我现在回过头看,技能库真正成熟的标准,不是里面有多少个技能,而是团队里已经默认“这类活儿直接跑技能,别自己动手改”。当技能库变成团队日常工作的一部分,而不是某个人的私人收藏时,这套体系才算真正扎下根。

最后再分享一个我个人的体会:写技能最难的从来不是写脚本,而是持续约束自己。我刚开始写的时候总想着一个技能做到大而全,覆盖所有可能情况,结果每个技能都膨胀得没法维护。后来我逼自己换了一种思路——一个技能只做一件事,做到可预测、可测试,哪怕要拆成两个技能再组合也好。这三五个文件能跑通的技能,就绝不要写第十个文件。因为技能是给人用的,也是给执行端用的,它越简单、边界越清晰,使用起来才越可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询