多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Agent技能系统落地指南:从零构建可插拔技能库,让智能体稳定干活

Agent技能系统落地指南:从零构建可插拔技能库,让智能体稳定干活 最近两个周末我几乎全搭在一个叫 agent-skills 的项目上说白了就是给手头的智能体Agent做了一套可插拔的技能系统。完事之后最大的感受是以前总抱怨 Agent 像个“什么都懂但什么都干不深”的聊天机器人现在它终于能像老员工一样你交代一句它自己就知道该调哪套流程、跑哪个脚本、按哪个规范出活。这篇文章就把我踩过的坑、设计时的取舍、以及能直接抄的落地思路都摊开讲给正在搞 Agent 应用、或者准备给自己项目加“技能库”的朋友一个参考。这个领域目前还处于“百家争鸣”的早期阶段各种叫法都有有人叫 Tool Use有人叫 Function Calling也有人走 MCP 协议而 agent-skills 更强调“打包成一套可复用、可分发、带说明文档的工作流”。它解决的痛点非常具体大模型本身不擅长稳定执行复杂流程但如果你把一个复杂流程拆成结构化、带触发条件和输出规范的“技能包”Agent 就能按图索骥地把它跑完。这篇文章不只讲概念我会从设计思路、目录规范、调度逻辑到代码实现完整带你过一遍。1. 先把概念捋清楚Agent Skills 和提示词、工具调用到底什么关系很多人第一反应是把 Agent Skills 理解为“高级提示词”或者“插件”这两种理解都不完整。它更像是介于两者之间的一层东西既有提示词的可读性和灵活性又有插件的执行能力和复用边界。1.1 技能Skill到底是什么它长什么样我习惯把一份技能理解成一个“带说明书的可执行任务包”。它在文件系统里就是一个目录目录里至少有一份SKILL.md作为技能的主说明里面用结构化的 FrontmatterYAML 头声明技能的元信息再用自然语言写清楚这个技能在什么场景下触发、执行时要注意什么。还可以附带若干脚本、模板、参考资料把这些文件作为技能的执行素材。举个我项目里的例子我封装了一个“代码仓库健康度检查”技能。它的SKILL.md头是这样的--- name: repo-health-check description: 对指定代码仓库做基础健康度检查包括依赖安全、测试覆盖、未提交变更、分支状态等。 applies_to: - repository - codebase triggers: - 检查下仓库状态 - 帮我做一次代码健康度体检 - 这个项目的依赖有没有安全问题 version: 1.2.0 ---下面正文我写了三步执行流程先跑脚本抓取仓库的基本指标再根据指标映射出问题等级最后生成一份 Markdown 报告。整个过程模型只需要理解“按这个步骤走”具体的数据抓取和统计分析交给脚本去算避免模型凭空“想当然”。1.2 和提示词、Function Calling 的边界在哪里不少朋友问我这个和“在系统提示词里写一堆规则”有什么区别区别大了。把技能塞进提示词意味着每次会话都要把全量技能内容送给模型对话一长上下文就爆炸而且提示词里的技能描述和代码里的具体实现容易脱节改一次逻辑要同步改提示词。技能化之后Agent 可以根据用户请求先检索技能清单只把命中的那一个技能内容加载进上下文精准、省 token还容易维护。和 Function Calling 比技能更偏向“完整流程”Function Calling 通常只对应一个函数调用。技能执行过程中可能调用好几个函数还可能包含人工确认节点。所以技能是高于单次函数调用的编排单元。用 MCPModel Context Protocol的话来说技能往往需要组合多个 MCP 工具来完成它是工具之上的一层编排逻辑。维度提示词硬编码Function CallingAgent Skills粒度规则、指令单个函数多步骤任务包可复用性差换项目失效中函数可复用但缺上下文高带文档、脚本、模板上下文占用高全量塞入低按调用注入参数中命中才注入相关技能维护成本高提示词和逻辑难同步中需维护函数契约低技能自包含适合场景简单固定规则明确单步操作复杂多步骤业务流1.3 技能系统要解决的核心问题从“会聊天”到“会干活”我做这套系统的初衷其实很朴素我希望我的 Agent 能稳定完成“多步骤、有规范、需要调用外部资源”的任务而不是每次都靠模型临场发挥。比如让它做一次竞品分析模型可能东拉西扯每次格式还不一样但如果我给它注册一个“竞品分析”技能里面写好数据来源、分析维度、输出模板再配一个爬取脚本它就每次都能产出结构一致、信息密度高的报告。这种“确定性 灵活性”的结合就是技能系统最大的价值。2. 技能系统的整体设计文件即技能、声明即发现、命中即注入把技能当作文件系统里的一个目录是我设计这套系统的第一个原则。这个原则听起来简单但它带来的好处是革命性的技能可以放进 Git 仓库做版本管理可以像 Docker 镜像一样分发可以用文件夹天然地做命名空间隔离。2.1 设计原则一一切皆文件一切可追踪我参考了 Claude Skills也就是 Anthropic 推的 Agent Skills 规范的目录思路但做了一些自己的简化。我的技能目录结构长这样skills/ ├── repo-health-check/ │ ├── SKILL.md │ ├── scripts/ │ │ ├── check_deps.py │ │ └── analyze_git.py │ ├── templates/ │ │ └── report_template.md │ └── references/ │ └── severity-mapping.md ├── competitor-analysis/ │ ├── SKILL.md │ ├── scripts/ │ │ └── scrape_sources.py │ └── templates/ │ └── analysis_template.md └── meeting-summary/ ├── SKILL.md └── templates/ └── summary_template.md每个技能目录都是一个独立的、可交付的单元。我甚至因此养成了给技能写 CHANGELOG 的习惯哪个版本改了触发词、哪个版本调整了输出结构一目了然。为什么这个设计好因为你在发现和调试技能时不需要去解析数据库、不需要查配置中心直接打开目录看文件就行。对开发者来说心智负担极低对模型来说技能内容就是可以读取的文本和代码不存在“接口黑洞”。2.2 设计原则二声明式元数据驱动匹配技能不是靠用户“点名”触发的而是靠 Agent 根据对话内容自动匹配的。自动匹配的前提是技能自带结构化的声明头。我的SKILL.md头部就是给匹配引擎吃的“索引卡”name是唯一标识description是技能的一句话摘要triggers是为了提升匹配召回率而准备的触发短语。这一步很关键直接决定了技能找得准不准。匹配时不是走什么高大上的向量数据库我先做了一个基于关键词和语义的双路召回关键词路把用户请求和所有技能的triggers、name做一次带权重的模糊匹配快速过滤出候选集。这步是为了保证速度百来个技能毫秒级就能出结果。语义路把用户请求和技能的description向量化用余弦相似度排序召回 Top-K。这步是为了兜底防止用户表述和触发词完全不一致。两路的结果取并集再按“触发词命中加分 语义相似度得分”加权排序最终选 Top-1 或 Top-3 候选交给大模型做最终裁决。让模型去判断“是否需要这个技能”比纯规则判断要聪明得多。2.3 设计原则三动态注入用完即走很多人的误区是“技能反正要加载干脆预加载全部”。这在技能数量少的时候没问题但技能一多、每个技能附带脚本说明之后上下文开销非常可观。我实测过一个 20 技能库全量预加载会让每轮对话多烧 8000 多 token而且模型在回答普通问题时容易被技能内容干扰反而变笨。所以我的注入策略是动态的普通对话轮次只注入一个非常精简的技能清单每个技能只有name和description大概几十个 token。命中了某个技能后才在后续上下文里追加该技能的完整SKILL.md正文以及必要的脚本使用说明。技能执行结束标记该上下文段为可裁剪conversation pruning后续如果不再提及就移出主动上下文。这个“用完即走”的策略让我整个系统的 token 成本下降了将近 40%值得每一个做 Agent 的人重视上下文就是一种预算技能系统做得越好预算花得越值。3. 实操环节从零到一手写一个技能运行框架理论说完了下面直接给一套能跑的最小实现。我用的 Python 3.10 Pydantic 2 FastAPI你完全可以换成别的语言和框架核心逻辑是通的。3.1 定义技能的数据模型先定义一个规范的技能模型保证所有技能都长一个样# skill_models.py from __future__ import annotations from pathlib import Path from typing import Any import yaml from pydantic import BaseModel, Field, validator class SkillMetadata(BaseModel): 技能的前置声明信息用于匹配和发现。 name: str Field(..., description技能唯一名称使用连字符命名) description: str Field(..., description技能的一句话摘要用于语义匹配) applies_to: list[str] Field(default_factorylist, description适用对象类型) triggers: list[str] Field(default_factorylist, description触发词列表) version: str 0.1.0 class Skill(BaseModel): 一个完整的技能包包含元信息和原始文件内容。 metadata: SkillMetadata content: str Field(..., descriptionSKILL.md 去掉 Frontmatter 后的正文) script_paths: list[str] Field(default_factorylist, description可执行脚本路径列表) base_dir: str Field(..., description技能所在目录方便定位资源) classmethod def load_from_dir(cls, skill_dir: Path) - Skill: 从目录加载技能解析 SKILL.md 的 YAML 头保留正文。 skill_md skill_dir / SKILL.md if not skill_md.exists(): raise FileNotFoundError(f{skill_dir} 缺少 SKILL.md) raw_text skill_md.read_text(encodingutf-8) # 只解析 YAML Frontmatter格式---\n...\n--- if not raw_text.startswith(---): raise ValueError(SKILL.md 必须以 --- 开头声明 YAML Frontmatter) parts raw_text.split(---, 2) if len(parts) 3: raise ValueError(SKILL.md 的 Frontmatter 解析失败) meta_data yaml.safe_load(parts[1]) meta SkillMetadata(**meta_data) content parts[2].strip() scripts [] scripts_dir skill_dir / scripts if scripts_dir.exists(): for f in scripts_dir.iterdir(): if f.is_file() and f.suffix in {.py, .sh, .js}: scripts.append(str(f)) return cls( metadatameta, contentcontent, script_pathsscripts, base_dirstr(skill_dir), )这个模型把“技能”落成了程序里的一个类之后无论是注册、匹配还是执行都围绕这个类操作。有一个细节要注意脚本列表我只收集了可执行文件SKILL.md正文里引到的模板和参考文档不在这里展开因为那些不参与“技能是否能跑”的判断。3.2 技能注册表扫描目录建立索引有了模型下一步是“发现技能”。我的做法很简单用一个注册表类扫描技能根目录把所有技能加载到内存同时建立两个索引一个按名字映射一个用于后续向量检索。# skill_registry.py from pathlib import Path import numpy as np from skill_models import Skill class SkillRegistry: 技能注册表负责扫描、加载、建立索引。 def __init__(self, skills_root: str | Path): self.skills_root Path(skills_root) self._skills: dict[str, Skill] {} self._name_list: list[str] [] self._desc_embeddings: np.ndarray | None None self._encoder None # 可以接 embedding 模型 def load_all(self) - int: 扫描根目录下所有子目录把含 SKILL.md 的目录当作技能加载。 count 0 if not self.skills_root.exists(): raise FileNotFoundError(self.skills_root) for entry in sorted(self.skills_root.iterdir()): if not entry.is_dir(): continue try: skill Skill.load_from_dir(entry) self._skills[skill.metadata.name] skill count 1 except Exception as exc: # 单技能加载失败不阻塞整体 print(f[warn] 技能 {entry.name} 加载失败: {exc}) # 构建向量索引 self._encode_descriptions() return count def list_skills(self) - list[Skill]: return list(self._skills.values())加载失败的技能我选择跳过而不是让程序崩溃这个决策是实践出来的一次技能目录里有份测试用的废稿里面 SKILL.md 格式不完整如果崩溃整个 Agent 就用不了了。做成“坏技能隔离”可以单独去修不影响线上。3.3 最核心的匹配逻辑关键词召回 语义排序匹配引擎是整个框架的神经中枢。我把它写成独立的SkillMatcher输入用户消息输出排序后的候选技能列表# skill_matcher.py from difflib import SequenceMatcher from skill_registry import SkillRegistry class SkillMatcher: 从注册表中检索最相关的技能。 def __init__(self, registry: SkillRegistry): self.registry registry def keyword_recall(self, query: str, top_k: int 8) - list[tuple[float, str]]: 关键词召回对触发词和技能名做模糊匹配。 results [] q query.lower() for skill in self.registry.list_skills(): score 1.0 if skill.metadata.name in q else 0.0 for trigger in skill.metadata.triggers: # 直接包含或高相似度都加分 if trigger in q: score 2.0 else: score SequenceMatcher(None, trigger, q).ratio() * 0.5 if score 0.5: results.append((score, skill.metadata.name)) results.sort(reverseTrue, keylambda x: x[0]) return results[:top_k] def semantic_recall(self, query: str, top_k: int 8) - list[tuple[float, str]]: 语义召回用简单向量点积模拟 embedding 排序。 # 真实场景这里换成 embedding 模型比如 OpenAI text-embedding-3-small # 这里我实现了一个基于字符 n-gram 的简易向量作为演示 query_vec self._ngram_vector(query) scored [] for skill in self.registry.list_skills(): skill_vec self._ngram_vector(skill.metadata.description) sim self._cosine(query_vec, skill_vec) scored.append((sim, skill.metadata.name)) scored.sort(reverseTrue, keylambda x: x[0]) return scored[:top_k] def match(self, query: str, top_k: int 3) - list[str]: 融合两路召回取加权 Top-K。 kw dict(self.keyword_recall(query)) sem dict(self.semantic_recall(query)) merged {} for name in set(kw) | set(sem): merged[name] kw.get(name, 0.0) * 1.2 sem.get(name, 0.0) * 1.0 ranked sorted(merged.items(), keylambda x: x[1], reverseTrue) return [name for name, _ in ranked[:top_k]] staticmethod def _ngram_vector(text: str, n: int 3) - dict[str, int]: 简易 n-gram 词袋向量仅用于离线演示语义检索。 vec {} clean .join(ch if ch.isalnum() else for ch in text.lower()) tokens clean.split() for token in tokens: padded token for i in range(len(padded) - n 1): gram padded[i:i n] vec[gram] vec.get(gram, 0) 1 return vec staticmethod def _cosine(a: dict, b: dict) - float: 计算两个稀疏向量的余弦相似度。 if not a or not b: return 0.0 common set(a) set(b) dot sum(a[k] * b[k] for k in common) norm_a sum(v * v for v in a.values()) ** 0.5 norm_b sum(v * v for v in b.values()) ** 0.5 if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)这个 matcher 里有两点实操经验值得细说。第一不要把触发词写得太长太长的触发词很难被用户自然命中我踩过“请帮我分析一下这个代码仓库的依赖安全情况”这种完整句子当触发词的坑实际效果为零后来全部改成短词比如“检查仓库”“依赖安全”“体检”。第二关键词召回和语义召回的权重比我建议设成 1.2 : 1.0关键词命中的可信度更高语义兜底防止漏召回但权重低了容易误召回无关技能。3.4 技能执行器让 Agent 真正“跑”起来匹配是让 Agent 知道“该用哪个技能”执行是让技能真正产生结果。我封装了一个SkillExecutor职责是接收技能名和用户请求加载技能脚本运行并收集输出再返回给主 Agent 判断结果。# skill_executor.py import subprocess import tempfile from pathlib import Path from skill_registry import SkillRegistry class SkillExecutor: def __init__(self, registry: SkillRegistry, work_dir: str | None None): self.registry registry self.work_dir Path(work_dir) if work_dir else Path.cwd() def execute(self, skill_name: str, user_request: str) - dict: 执行指定技能返回结构化结果。 skill self.registry._skills.get(skill_name) if not skill: return {ok: False, error: f技能 {skill_name} 不存在} # 第一步让大模型从用户请求中抽取技能需要的参数省略 LLM 调用 # 这里演示直接透传原文真实场景建议用 LLM 抽取结构化 JSON params {request: user_request, skill_dir: skill.base_dir} # 第二步如果有配套脚本则执行第一个脚本并把输出捕获 outputs [] if skill.script_paths: script skill.script_paths[0] try: # 用当前解释器执行 python 脚本或者按后缀调用 result self._run_script(script, params) outputs.append(result) except Exception as exc: outputs.append({stderr: str(exc)}) # 第三步把技能正文和输出汇总返回给主 Agent 生成最终回答 skill_prompt ( f[技能 {skill.metadata.name}]\n f技能说明\n{skill.content}\n f执行输出\n{outputs}\n f请基于以上信息回应用户请求{user_request} ) return {ok: True, prompt: skill_prompt, outputs: outputs} def _run_script(self, script_path: str, params: dict) - str: 运行技能脚本把参数序列化成 JSON 传给脚本标准输入。 import json ext Path(script_path).suffix if ext .py: cmd [python, script_path] elif ext .sh: cmd [bash, script_path] else: cmd [script_path] proc subprocess.run( cmd, inputjson.dumps(params), capture_outputTrue, textTrue, timeout120, cwdself.work_dir, # 注意脚本工作目录 ) if proc.returncode ! 0: return f[脚本返回码 {proc.returncode}] stderr: {proc.stderr[-500:]} return proc.stdout[-3000:] # 防止输出过多撑爆上下文执行器里我特意在_run_script里做了输出截断只保留最后 3000 字符。为什么不是截首部因为脚本的日志习惯通常是越往后越关键最后的输出往往包含最终结果、错误堆栈或汇总表格截掉尾部等于把结论扔了。这个细节是我排查了一个周末“技能执行没结果”问题才发现的一开始截头部结果所有技能的输出都是半截子。4. 从单技能到技能库版本、冲突与协同的实战经验单个技能能跑通只是第一步真正让人头疼的是技能数量多起来之后的管理问题。我现在库里有 30 多个技能这期间踩了不少坑也总结出了一些行之有效的管理方法。4.1 技能版本管理不要裸奔在服务器上技能本质是代码 文档必须纳入版本管理。我的每个技能目录单独一个 Git 仓库monorepo 里用子目录也行每次改动技能逻辑必须同步修改SKILL.md的version字段并且写清楚 changelog。我在SkillMetadata里加了version字段就是为了这个。一个很实用的习惯主版本号升级意味着行为不兼容。比如我把“代码健康检查”里的评分标准从百分制改成 A-F 等级制版本从 1.x 跳到 2.0。这样 Agent 升级技能后如果发现下游依赖旧输出格式能立刻定位是版本导致的问题而不是抓瞎。4.2 技能冲突的三种来源和解法技能多了一定会撞车。我总结出三种典型的冲突冲突类型典型表现我的解法触发词冲突两个技能都绑定了“生成报告”匹配结果不稳定在匹配时给 2.0 的权重实质是减少模糊触发词使用尽量让触发词有辨识度输出格式冲突Agent 同时命中两个技能拿到两份结构不同的输出匹配时强制 Top-3 候选交给模型让其选择而我自己在技能正文里要求“输出必须完整”资源冲突两个技能共用同一个数据文件一个写一个读导致脏数据技能脚本统一走临时目录禁止直接写共享目录需要共享的只读数据放 skills root 下的shared/资源冲突是我第一次遇到时没料到的。有个“仓库分析”技能会写临时统计文件“打包发布”技能会读同一目录下的统计文件结果两个技能同时触发时互相污染。后来定了一条铁规矩除了shared/目录任何技能不得跨目录读写文件各自只在自己的目录和系统临时目录里活动。4.3 技能协同一个请求命中了多个技能怎么编排真实场景中用户的一句话往往命中不只一个技能。比如“帮我分析这个竞品然后总结成会议纪要发给团队”命中了“竞品分析”和“会议纪要”两个技能。这时候不能并行执行因为第二个技能的输入是第一个技能的输出。我用的模式是链式编排Chain匹配引擎输出 Top-3 候选技能。主 Agent 判断候选技能之间的依赖关系。如果 A 的输出可以作为 B 的输入就按依赖顺序执行如果没有依赖则并行执行。上一个技能的输出被注入到下一个技能的参数上下文里保证信息不断流。最后一个技能的输出经过净化去掉内部调试信息后呈现给用户。这个链式编排让我最惊喜的效果是Agent 不再需要我在系统提示词里写死“先做什么再做什么”技能自身的描述中已经蕴含了边界和衔接点模型只要顺着技能描述走就能自动完成多级任务编排。5. 常见问题与排查技巧实录那些官方文档永远不会告诉你的坑这部分可能才是全文最有含金量的。技能系统开发和调试过程中我遇到了一堆奇奇怪怪的问题有些甚至困扰了我好几天。下面按“现象 → 原因 → 解法”的格式整理成速查表每一条都是我亲测有效的。5.1 技能总是匹配不上先检查 description 的“主语”如果你的 Agent 对某个技能视而不见十有八九是description写得太“技术化”而不是“用户化”。我最初给“代码健康检查”写的描述是“提供仓库健康度指标的计算与分析能力”结果模型完全不认为用户说的“看看这个项目靠谱吗”和这个描述相关。改法很简单把 description 从“我能做什么”改成“用户什么时候会需要”。我现在的描述是“当用户想了解代码仓库的整体质量、依赖安全性、是否存在技术债时使用”。模型的语义匹配是朝着这个方向对齐的你写用户的语言它就够得着。5.2 “坏技能”拖垮整体做好加载隔离和心跳上报前面提过加载失败的技能我会跳过但如果你不监控它坏技能会一直烂在仓库里。我做了两件事启动时记录坏技能清单打印到日志并在 Agent 的调试接口里暴露出来。每次版本更新跑一次全量加载测试用 GitHub Actions 自动扫描所有技能目录任何一个 SKILL.md 解析失败就直接让 CI 失败而不是带病上线。5.3 技能脚本运行超时怎么办我最初给脚本统一设了 120 秒超时结果有些技能内部的直播抓取任务跑了 5 分钟还没完直接超时被杀。后来我改了策略脚本自己声明期望耗时。我扩展了 Frontmatter加了一个timeout_seconds字段执行器优先读它不声明才用默认值 120 秒。这算是“让技能的作者自己负责任”的一个体现。name: scrape-competitors timeout_seconds: 300这个字段不是摆设它同时作为 Agent 决策的依据如果用户明确说“尽快给我结果”Agent 会避开长超时技能优先选轻量方案。5.4 上下文还是太肥给技能内容做“三级压缩”即便有动态注入单个技能的内容也不宜太长。我把我自己的SKILL.md长度分成三个等级核心版约 500 字只包含执行步骤和关键注意事项、完整版约 1500 字含背景原理、规范引用、示例、扩展版含模板、完整代码。匹配命中后默认注入核心版如果任务复杂度高模型判断需要更多细节再加载完整版。这个“三级压缩”的经验是受网络请求优化里的渐进式加载启发实测能再省 20% 左右的 token。5.5 技能库的安全边界让脚本不越权Agent 技能一旦可以执行脚本安全问题就是头等大事。我的经验法则技能脚本默认跑在沙箱容器里Docker 隔离容器没有外网访问权限除非技能显式声明需要联网。不可能给每个技能建容器时至少把工作目录设为仅该技能的临时目录不要用 Agent 进程的完整权限。SKILL.md里禁止写“删除文件”“关闭防火墙”等危险指令词我写了一个静态扫描器在技能加载时做白名单检查。安全是底线这个不能偷懒。哪怕你的 Agent 只是给自己用的技能脚本也可能因为输入注入被诱导执行恶意命令沙箱隔离是唯一的稳妥做法。6. 一个完整技能从构思到上线的全过程演示repo-health-check 的实战复盘前面是拆解这一节我来一个完整的“从 0 到 1 做一个技能”的实战复盘。就以我反复提到的“代码仓库健康检查”技能为例子完整走一遍从需求分析到封装落地的流程。6.1 需求分析和技能边界确认最初需求是“帮我看一个 Git 仓库健康不健康”。这个需求太模糊我得先定义什么叫“健康”。我拆成四个维度维度检查项严重等级示例依赖安全是否有漏洞版本、过时版本高危存在已知 CVE测试健康测试覆盖率过低、测试长期失败中危覆盖率 40%Git 状态分支堆积、未推送提交过多、冲突标记残留低危超过 20 个本地分支代码卫生TODO/FIXME 密度、超长文件、重复代码低危单个文件超 2000 行边界定了技能的可执行范围就清楚了后续模型就不会拿着这个技能去干“格式化工代码”这种不在范围内的事。6.2 编写脚本让数据说话不让模型胡说我写了两个 Python 脚本。check_deps.py负责解析项目的依赖清单文件requirements.txt、package.json等去本地漏洞库比对analyze_git.py负责用git命令收集分支、提交、冲突标记等数据。脚本输出的不是原始文本而是结构化的 JSON这样 Agent 可以直接解析成报告不用再依赖正则去瞎猜。这里有一个关键细节脚本应该输出“事实”而不是输出“结论”。我第一次写check_deps.py时脚本里直接判断“依赖不安全”返回一个布尔值。结果有一次漏洞库更新后脚本逻辑没跟上误报了安全。后来我把判断逻辑全部交给 Agent脚本只输出“依赖 X 版本为 1.2.3漏洞库中记录该版本存在 2 个已知 CVE曝光途径为远程代码执行”至于“是否阻断发布”这种决策让 Agent 结合上下文判断。脚本管事实模型管决策这个分工是技能系统稳定的关键。6.3 SKILL.md 正文的写法给模型一份“操作手册”SKILL.md不是写给用户看的是写给模型看的。我总结了一个公式执行步骤 判断规则 输出模板 禁忌事项。我给repo-health-check写的正文梗概如下# 仓库健康检查技能 ## 执行步骤 1. 运行 scripts/check_deps.py获得依赖安全数据。 2. 运行 scripts/analyze_git.py获得 Git 仓库状态数据。 3. 根据数据对四个维度分别评级健康绿、警告黄、危险红。 ## 判断规则 - 依赖安全维度若存在高危 CVE直接给“危险”不进入其他判断。 - 测试维度结合仓库内测试配置若未配置 CI视为“警告”。 ## 输出模板 按顺序输出四部分 1. 总览表维度/等级/一句话结论 2. 关键问题列表只列危险和警告项 3. 修复建议按优先级排序 ## 禁忌 - 不要修改仓库任何文件本技能是只读操作。 - 如果仓库数据不足明确告知用户缺少哪些信息不要猜测。执行步骤给的是“操作的顺序”判断规则给的是“决策的依据”输出模板给的是“交付的形式”禁忌给的是“不能碰的红线”。四个板块配合起来模型执行技能的稳定度就非常高了。6.4 上线测试和迭代技能写好不是一劳永逸我每次新技能都走一套固定的测试流程单技能模式测试强制 Agent 只加载这个技能测试典型请求、边界请求、恶意请求。候选干扰测试把这个技能和另外 3 个描述相近的技能放在一起看匹配引擎是否还会选中它。这一步专门用来调description和triggers。自动回归把测试用例固化成 Markdown 文件放进技能的tests/目录每次改技能就 rerun 一份防止改一个 bug 引出另一个 bug。这个流程看起来繁琐但对技能这种会被模型反复调度的“资产”来说非常值得。我现在每次上线新技能这套流程能控制在半天内效率高且稳。7. 跑赢大多数人的进阶经验技能系统的模式提炼与未来扩展写到最后我想聊几句更高维度的体会。技能系统的价值不只在“当下把活干了”更在于它逼着你把项目中可复用的部分抽象成资产这些资产会持续累积复利让后续的开发越来越快。7.1 我沉淀出的三个技能设计模式做了 30 多个技能后我发现它们背后其实只有三种模式只读分析模式输入一个对象仓库、网页、文档输出一份结构化分析。典型就是repo-health-check。生成转换模式输入一批资料按模板生成新内容会议纪要、周报、竞品报告。交互操作模式Agent 需要通过多轮对话引导用户完成操作比如逐步配置环境变量。识别出模式后我做了模式基类每个新技能只需要填参数和模板不再从零写SKILL.md和脚本框架。这个抽象让我的技能开发效率提升了至少 3 倍。7.2 技能与 MCP、记忆系统如何配合技能系统不是孤岛它和 MCP、长期记忆是互补的。我的实践是MCP 负责“工具连接”技能负责“任务编排”长期记忆负责“用户偏好”。比如一个技能要查询 MySQL 数据库它不直接连数据库而是通过 MCP 的 database 工具去执行 SQL而技能的执行结果如果对后续交互有帮助比如用户偏好详尽的报告形式会被异步写入长期记忆。这三层配合起来Agent 才是真正意义上完整的智能体而不是单点技术的堆砌。7.3 下一步想做的事让技能能够被 Agent 自己“写出来”现在技能的开发和上线还是依赖我人工操作。最近我在尝试一个很兴奋的方向让 Agent 根据用户需求自动生成SKILL.md草稿和脚本雏形我审核通过后自动注册进技能库。这相当于把“技能开发”本身也变成一个技能。我打算在这次尝试稳定后单独再写一篇经验分享但提前可以透露的方向是技能生成的 prompt 模板化 脚本的沙箱运行 自动回归测试这三者是让 Agent 自举生成技能的核心保障。没有它们Agent 写出来的技能大概率是“能看不能跑”。最后分享一点我个人感触最深的经验技能系统真正的门槛不在于技术实现而在于你怎么把模糊需求切成边界清晰的技能单元。这个“切分”的功夫决定了你的 Agent 的上限。切得好Agent 就像一支配合默契的团队切得烂Agent 就像一群各说各话的散兵游勇。我建议你从手头最重复的那类任务开始先封装一个技能跑通后再慢慢扩展。技能系统的回报曲线是前期陡峭、后期变缓但一旦跨过某个规模它会成为你所有 Agent 应用的共同地基。
返回列表