
说实话我一开始搜agent-skills的时候以为会看到一个具体的开源项目或者某个框架的插件仓库。等我把搜索词背后的讨论、技术社区里的碎片信息揉在一起才发现大家真正在聊的是一套方法论怎么把大模型Agent的能力拆成一组可复用、可管理、可按需加载的技能单元。这个需求和AI Agent需要一个更好的大脑是同时出现的——光有聪明的模型还不够还得让模型知道什么时候该掏出什么工具、怎么调用、调用完怎么把结果接回主线任务。这篇就把我在实际项目里沉淀下来的Agent Skills设计思路、手写脚手架的完整过程以及踩过的几个实在坑一次性讲清楚。先说一下结论Agent Skills不是插件系统也不等于Function Call列表它是一层介于模型和工具之间的能力中间层。没想清楚这层之前你写的Agent本质上还是一个巨大的if-else外壳。想清楚之后Agent会突然变得可训练、可审计、可治理后面所有的性能优化、行为调教、多Agent协作才有地方下手。1. 为什么我把Agent系统整个翻新成Skills架构1.1 从Function Calling到Skills我到底经历了什么我最早接大模型Agent的时候用的还是最朴素的Function Calling方案。流程非常简单把十几个JSON Schema挂在请求里模型根据用户语义选择调用哪个函数然后我写一堆execute_function(name, args)的分发逻辑。一开始确实爽加一个新的外部工具只需要注册一个schema再加一个执行函数半小时就能跑通。但我做的是一个需要长期演化、多个业务方共用底座的项目。到第二个月问题就压不住了系统里塞了四十多个函数有些是查数据库的有些是调第三方API的有些是内部状态机的操作。模型的上下文窗口被函数定义占掉一大截不说最难受的是模型开始频繁选错工具——明明该走A流程它非去调B接口明明要按顺序执行三步它一步到位直接跳到最后。我去翻日志的时候发现问题的根源特别直白函数是平铺的模型根本分辨不出这些函数之间的协作关系、依赖顺序和执行边界。后来我看到几篇讨论skill概念的帖子思路一下就通了。Skill的定义不是一个函数而是一组围绕同一目标组织起来的资源——包括自然语言描述、参数模型、执行步骤、前后置条件、回调逻辑甚至可以附带自己的few-shot示例。模型不再面对一个扁平的函数列表而是面对一个技能目录每个技能都自带使用说明书。这对模型的规划能力是降维打击式的友好。1.2 Skills要解决的三个实际问题我用一张表把Function Calling和Skills的差异列出来我自己做完对比就彻底不想回去了维度传统Function CallingAgent Skills最小单元单个函数一个目标 多个步骤 关联资源模型感知所有函数平铺在上下文中按需检索只加载相关技能复用方式拷贝函数代码声明依赖挂载既有的技能模块行为约束靠开发者编码强制技能描述里自带约束与流程可观测性单次调用的log技能执行全链路追踪演进能力加函数越来越重整个技能目录可以分层、路由、治理第一个实际问题是上下文治理。我现在的Agent基底模型是128K上下文的听着不小但塞进二十个函数的完整JSON Schema就要吃掉小一万token再做点RAG、再带点历史对话规划空间所剩无几。Skills架构下我可以把每个技能的定义做得很详细但只在需要的时候动态加载两三个进上下文省下来的上下文空间全部留给推理和记忆。第二个实际问题是流程可编程性。很多业务场景不是调用一个接口而是完成一个用例。比如自动生成周报这个技能内部包含抽取本周已完成任务、关联项目状态、调用模板渲染、推送到IM、记录发送结果。这一串动作如果拆成五个独立函数模型每次都要重新发明一次协作逻辑做十次就有十种理解偏差。做成一个Skill之后执行的先后顺序、异常兜底、成功标准全都在技能定义里写死模型只需要负责输入参数的提取。第三个实际问题是多Agent协作时的能力隔离。我项目里同时跑了研究型Agent、执行型Agent、对话型Agent它们共用一个工具库但业务目标完全不同。工具平铺的时候对话型Agent经常会调研究员才该用的深度检索工具。用Skills之后我给Agent按角色挂技能集合每个Agent只看得见自己角色域下的技能误用问题直接消掉了。2. Skills运行时的核心机制注册表、资源清单与加载路由2.1 一个Skill从定义到被调用的完整生命周期Skill不是一份静态文档它在运行时要经历五个阶段定义、注册、检索、装载、执行。我把这五个阶段刻在脑子里之后设计任何新增技能都会按这个顺序自查。定义阶段开发者写一个Skill目录包含skill.json核心配置、若干脚本或可执行模块、示例数据、依赖声明。注册阶段启动时扫描技能目录把每个技能的核心信息注册进内存里的Registry包括技能名称、描述、入口函数、资源路径。检索阶段拿到用户请求后做两件事——先是基于描述做一次语义相关性排序把候选技能从几百个缩小到几个再对候选技能做一次参数兼容性预检排除参数搭不上的。装载阶段把命中技能的说明书渲染到模型上下文包括它的调用方式、输入输出格式、使用约束。执行阶段模型给出调用哪个技能 传什么参数的决策运行时接管技能入口按技能内部定义的步骤执行错误与重试也在这层处理。注册表不用做成微服务至少在单体应用阶段一个线程安全的字典就够了。真正的难点在检索和装载那一层。2.2 元数据结构我给每个Skill设计的JSON字段每个技能的根目录下都有一个skill.json我打磨过好几版现在的核心字段长这样{ name: weekly_report_generator, version: 1.2.0, description: 根据工作日志和项目状态生成结构化周报支持多项目汇总与模板切换。, author: platform-team, tags: [report, worklog, notification], dependencies: [project_status_service, im_sender], trigger: { patterns: [生成周报, 汇总本周, weekly report], semantic: 用户需要输出一份周期性工作总结 }, input: { type: object, properties: { project_ids: {type: array, items: {type: string}}, template: {type: string, enum: [simple, detail]} }, required: [template] }, steps: [ {id: 1, action: fetch_worklog, params: {days: 7}}, {id: 2, action: aggregate_by_project, depends_on: [1]}, {id: 3, action: render_template, depends_on: [2]}, {id: 4, action: push_im, depends_on: [3], optional: true} ], output: 生成的报告文本或发送结果 }有几个字段我要单独说说都是我在实际使用中被教育出来的。description一定要写给模型看的说明书不是给自己看的开发注释。我试过写该函数用于周报生成和根据过去七天工作记录及项目进展生成结构化周报适合用户主动请求总结场景后者的触发准确率能差20个百分点。原因很简单——模型决定用哪个技能主要就是靠这段描述和当前对话语义做匹配。trigger.patterns是给检索层的字符串匹配用的做召回阶段的第一道粗筛trigger.semantic是给语义排序用的。粗筛精排组合之后几百个技能里选出Top3的耗时能控制在30毫秒以内。steps字段是整个架构最核心的革新。它让技能从一个函数变成了一个有内在逻辑的流程。我把每个步骤的depends_on显式声明出来运行时就能自动做拓扑排序保证多步骤的执行顺序加上optional标记的步骤失败不影响整个技能的结果这样推IM失败但周报已经生成的场景就不会让整个调用炸掉。2.3 技能描述符与运行时路由注册表里存的不只是原始JSON我预编译了一份SkillDescriptor对象把频繁用的字段拿出来放到一个快速索引结构里。核心接口我简化成下面这个dataclass class SkillDescriptor: name: str description: str tags: list[str] trigger_patterns: list[str] input_schema: dict entrypoint: Callable dependencies: list[str] steps: list[dict] version: str class SkillRegistry: def __init__(self): self._skills: dict[str, SkillDescriptor] {} self._tag_index: dict[str, set[str]] {} def register(self, descriptor: SkillDescriptor) - None: self._skills[descriptor.name] descriptor for tag in descriptor.tags: self._tag_index.setdefault(tag, set()).add(descriptor.name) def retrieve(self, query: str, top_k: int 3) - list[SkillDescriptor]: # 粗筛pattern 命中 tag 命中 candidates [] for skill in self._skills.values(): if any(p in query for p in skill.trigger_patterns): candidates.append(skill) if not candidates: for tag, names in self._tag_index.items(): if tag in query: candidates.extend([self._skills[n] for n in names]) # 精排description 与 query 的向量相似度 # 这里我用的是一套轻量本地 embedding 模型 ranked sorted(candidates, keylambda s: cosine_sim(s.description, query), reverseTrue) return ranked[:top_k]路由层拿到模型决策之后按技能名去取Descriptor再调用entrypoint。这里有个容易忽略的点技能执行应该走独立线程池并且设置超时。我见过最多的事故就是某个技能的entrypoint因为第三方APIhang住把整个Agent进程拖死。现在我对所有技能强制加了一个timeout参数默认30秒超时后返回一个可读错误Agent的大模型层会把这次失败当作需要换方案的信号去重规划。3. 手写一个能跑的Skills脚手架目录规范与核心加载器3.1 目录结构怎么设计最顺手我的技能根目录长这样skills/ ├── _shared/ │ ├── llm_client.py │ ├── tool_utils.py │ └── validators.py ├── weekly_report/ │ ├── skill.json │ ├── main.py │ ├── prompt_templates/ │ │ ├── template_a.md │ │ └── template_b.md │ └── examples/ │ ├── successful_call.json │ └── failed_call.json ├── resume_analyzer/ │ ├── skill.json │ ├── main.py │ └── assets/ └── chat_summarizer/ ├── skill.json ├── main.py └── README.md_shared目录很关键它放所有技能都要用的公共逻辑比如统一的LLM客户端、日志封装、重试装饰器。技能代码内部禁止直接 import 项目根目录以外的模块unix哲学在技能体系里同样适用——每个技能要么自包含要么显式声明依赖公共库。我吃过一次亏有个技能偷偷复用了另一个技能里的一个内部函数那边代码一改这边悄悄坏了查了两天。3.2 核心加载器从文件系统到运行可用我写了一个标准的SkillLoader负责扫描目录、解析JSON、动态import入口模块。这么做的收益是新增技能不需要改一行主程序代码只要丢一个目录进去重启即生效。import importlib.util import json from pathlib import Path def load_skill_from_dir(skill_dir: Path, registry: SkillRegistry): config_path skill_dir / skill.json main_path skill_dir / main.py if not config_path.exists() or not main_path.exists(): raise ValueError(fInvalid skill directory: {skill_dir}) config json.loads(config_path.read_text(encodingutf-8)) module_name fskill_{config[name]} spec importlib.util.spec_from_file_location(module_name, main_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) entrypoint getattr(module, run, None) if not callable(entrypoint): raise AttributeError(fSkill {config[name]} must expose a run() function) descriptor SkillDescriptor( nameconfig[name], descriptionconfig[description], tagsconfig.get(tags, []), trigger_patternsconfig.get(trigger, {}).get(patterns, []), input_schemaconfig.get(input, {}), entrypointentrypoint, dependenciesconfig.get(dependencies, []), stepsconfig.get(steps, []), versionconfig.get(version, 0.0.1), ) registry.register(descriptor) def load_all_skills(root_dir: Path, registry: SkillRegistry): for skill_dir in root_dir.iterdir(): if skill_dir.is_dir() and (skill_dir / skill.json).exists(): try: load_skill_from_dir(skill_dir, registry) print(f[skills] loaded: {skill_dir.name}) except Exception as e: print(f[skills] failed to load {skill_dir.name}: {e})main.py里以run( params: dict, context: SkillContext )为统一入口context里装的是会话ID、用户信息、SharedResources之类的东西。这样技能实现者不需要关心框架层怎么处理并发、怎么追踪日志只对着自己的输入参数写业务逻辑就够了。3.3 把Skills接入LLM决策流的三种姿势我试过三种接法都跑通了区别在灵活度上姿势一One-shot全量注入。把命中技能的全部描述拼进System Prompt末尾。适合技能总数少、步骤简单的场景。优势是逻辑简单缺点是上下文膨胀快。姿势二两阶段决策。先让模型做一次技能选择输出一个技能名再针对该技能做参数提取。这能有效降低大模型犯傻的概率代价是多一次模型往返延迟增加400毫秒左右。姿势三ReAct动态检索。把SkillRegistry封装成一个search_skill工具模型在推理过程中根据需要反复检索技能。这是最接近自主Agent的方案也是我最推荐的长期形态。模型的规划循环里检索技能和调用技能变成两个可以交替进行的动作Agent可以自己决定要不要先看看有没有更合适的技能。我现在生产环境跑的是姿势三一个编排器负责管理ReAct循环search_skill和execute_skill两个工具常驻上下文其他的所有技能都是潜水状态要用再捞。4. 踩坑实况元数据设计失误、命名冲突与技能膨胀的完整排查过程4.1 第一次写Skill时我把整个业务说明书塞进了description第一个上线的技能是客户投诉处理。当时我为了追求准把description写成了一篇三百字的小作文包含了业务流程、话术要求、升级条件、公司政策链接。上线之后的效果非常诡异大部分请求它都能命中但一旦命中就跑偏。我去翻模型决策日志发现它把description里提到的升级条件理解成了输出内容必须包含升级状态于是每次回复都带着一段当前投诉是否需要升级评估否。问题的本质是模型分不清技能描述和技能输出格式是两码事。我把两者全混在描述里模型的注意力机制开始无差别吸收所有文字。最后我做的修正description压缩到80字以内只写这个技能在什么场景下解决什么问题。输入输出的细节全部写进input和output字段的JSON Schema里。业务约束规则单独放到steps里作为每个步骤的前置条件而不是写进整体描述。修正之后的效果非常明显。技能命中率从71%升到94%而且误召回的案例里几乎都集中在相近语义投诉vs售后咨询属于模型本来就难区分的边界代码层加了一道规则兜底就解决了。4.2 同名技能的静默覆盖让我查了两天才定位到这是我最狼狈的一次排错。某天下午运营团队反馈报表导出技能行为突然变了以前导出Excel现在导出CSV。我第一反应是代码出了问题查了半天执行日志发现技能名称是同一个report_exporter但version变成了别人的。后面才发现团队里有两个人同时提交了新的技能目录另外一个同学的目录名也叫report_exporter但他放到了另一个根目录下。我的加载器扫描时后加载的那个目录里的技能把注册表里的同名键覆盖了没有任何告警。排查链路复盘下来我做了三个加固注册表写入时检查重名如果新技能版本号不高于现有版本默认跳过并记录告警日志。加载器扫描时打印完整的技能来源路径让从哪个目录加载的永远可查。技能目录里增加一个owner.yaml标注负责人方便出问题快速找人。那段报警代码现在长这样if descriptor.name in registry._skills: existing registry._skills[descriptor.name] if existing.version descriptor.version: logger.warning(fSkill {descriptor.name} version {descriptor.version} ignored, fexisting version {existing.version} from {existing.source}) return logger.warning(fSkill {descriptor.name} overwritten: f{existing.version} - {descriptor.version})这个坑给我最大的教训是技能注册表必须像数据库一样管理约束主键唯一、版本控制、来源可溯一个都不能少。4.3 技能膨胀治理一百二十个技能之后发生了什么技能写到第三个月我数了一下注册表里已经有一百二十多个skill。这时候新问题出现了——检索层开始假阳性频出。原因是很多技能描述里用了相同的热词比如分析生成数据语义向量靠这几个词聚到一块导致用户说帮我分析一下销售数据时召回的前三个技能是marketing_data_analyzer、sales_dashboard_generator、churn_prediction_simulator但真正该用的income_statement_analyzer排在第五被Top3截断了。我排查到这一步重新定义了技能治理的三条规则技能要有明确边界描述里必须包含不处理什么的负面约束。比如resume_analyzer的description里有一句不适用于招聘流程中的背景调查。负向约束对向量检索的帮助出乎意料地大因为常见的词向量不算什么也能被编码进去。标签体系必须做收敛之前tags完全靠个人喜好填光报告就有report、reports、reporting、weekly_report四种。我用脚本把所有tags归并到一个标准词典里一共收敛成了二十多个标准标签。粗筛阶段统一用标准标签后召回准确率明显稳了。技能要有QPS与失败率考核我把每个技能的调用日志聚合成一张看板连续一周零调用的技能标记为僵尸技能复审后要么合并、要么下架。技能不是越少越好但没有使用价值的技能留着就是检索噪音。我实际看到的效果技能数量从120个精简到87个Top3召回精准度反而提升了一个档次。5. 构建可持续演进的Skill库切分逻辑、质量评估与框架层面的下一步5.1 按任务领域切分而不是按工具粒度切分很多人做技能库的第一个冲动是把现有工具函数一个个包装成skills这是最省事的做法但从长期看是最难的维护方式。比如我有一个send_email工具如果做成单一技能那发提醒邮件和发营销邮件两个场景其实需要完全不同的正文模板和频率控制策略硬共用同一个skill会让逻辑变成一地鸡毛。我推荐的切分标准是按任务领域用户可感知目标来定边界。send_survey_email、send_welcome_email、send_digest_email看似都依赖邮件通道但它们是三个独立的skill每个skill内部可以自由选择用不用send_email这个底层工具。这就像公司的组织架构按业务BU划分而不是按IT基础设施划分——基础设施是共享服务但每个BU的流程和目标各自独立。5.2 我给Skill质量打的四个分项每次review一个技能要不要上线或者要不要优化我都按四个维度打分维度评分标准分数低的处理方式触发鲁棒性用户换几种说法都能正确命中补trigger patterns优化description负向约束执行正确率给定同一批测试输入输出结果是否稳定检查steps顺序和依赖关系增加输入参数校验失败可恢复性中间步骤失败后是否有清晰的处理路径明确重试策略、熔断逻辑、可选的兜底步骤上下文友好度技能说明书占用的token是否合理精简description细节挪进steps和examplesexamples这块我尤其想多说一句。很多技能文档里写successful_call.json和failed_call.json我是每次真实调用的时候顺手把最典型的成功和失败样本录进去的。这两个样本对模型的实际价值比我写一百行描述都大因为大模型从例子里学格式和边界条件的能力远强于从规则里学。5.3 这套体系往后的演进方向Skill这套架构的意义在于它终于让Agent能力变成了数据而不是代码。能力是数据就能做版本管理、灰度发布、A/B实验。我现在已经在跑的一个流程是新技能先在staging技能集里挂三天用线上真实流量的一定比例做影子测试指标达标后自动发布到stable技能集。这套流程大概二三十行代码就能实现换来的稳定性收益非常高。再往后我会让所有技能的description、input schema、steps的上游来源都变成可配置的数据表运营同学可以直接在后台调整不需要走代码发布。这也是我最终的目标——让Agent的可变能力全部由运行时的数据和参数驱动代码只留下被时间验证过的稳定内核。回顾整个项目Skills架构让我做Agent的方式从写死一堆工具路由变成了设计一组自治能力单元。它的核心价值不在于某个具体的技术实现而在于终于给了Agent能力一个组织单位。如果你也正在被工具平铺、上下文膨胀、多Agent能力混用这些问题困扰我强烈建议你从最小的一套Registry开始先定义五个技能跑通闭环再去追求规模。你会发现Agent系统的复杂度从此有了一个可以收敛的抓手。