2026/10/21 15:41:02

打造可复用技能包:用规则驱动约束AI辅助编程

打造可复用技能包:用规则驱动约束AI辅助编程 说实话“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/apidescription这一栏是有大讲究的。很多人写成“处理网络请求相关配置”这种描述几乎永远不会被触发。写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。我的写法是按“扫描—比对—修改—校验”四段式来组织扫描 src/api 目录下所有包含网络请求调用的源文件。逐一比对每个请求入口是否存在可用的超时配置字段。对缺失项统一新增配置值为用户指定的阈值并标记为“skill-add-request-timeout”。对已存在但不一致的项保持用户的显式配置不计入修改。最后执行单元测试子集确认改动没有破坏现有行为。做完这些还差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, requiredTrue) 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是不是没跟上用户的实际表达方式。很多“没人用的技能”不是因为没用而是因为入口长在大家不习惯的位置。改一下描述把触发场景写得贴近真实会话表达使用频率立刻就不一样了。我现在回过头看技能库真正成熟的标准不是里面有多少个技能而是团队里已经默认“这类活儿直接跑技能别自己动手改”。当技能库变成团队日常工作的一部分而不是某个人的私人收藏时这套体系才算真正扎下根。最后再分享一个我个人的体会写技能最难的从来不是写脚本而是持续约束自己。我刚开始写的时候总想着一个技能做到大而全覆盖所有可能情况结果每个技能都膨胀得没法维护。后来我逼自己换了一种思路——一个技能只做一件事做到可预测、可测试哪怕要拆成两个技能再组合也好。这三五个文件能跑通的技能就绝不要写第十个文件。因为技能是给人用的也是给执行端用的它越简单、边界越清晰使用起来才越可靠。