做AI应用这一年多我踩过最大的坑不在模型层而在“让Agent真正把一件事办好”。模型能力再强如果缺少一套组织良好的技能库它就像一位理论满分的实习生——什么都懂一点真上手就抓瞎。后来我把这套思路沉淀成了agent-skills核心其实很简单把高频、可靠、可复用的执行动作从提示词和散落脚本中抽出来做成一个个独立技能单元让Agent按需加载、按契约调用。这篇文章不讲虚的直接把我在项目里的设计方案、代码骨架、踩坑记录都摊开给正在做Agent应用的同学一份能抄作业的参考。1. 项目整体设计与思路拆解1.1 这个项目到底在解决什么问题先说说我为什么一定要做技能库。早期做Agent demo大家习惯把工具说明、调用规则、输出格式全都塞进系统提示词里看起来直白跑起来全是问题提示词越堆越长模型注意力被稀释后面写的内容经常被忽略换个场景就要重写一大段逻辑碎片散落在各处改一个接口要动好几处代码。更麻烦的是同一个能力在这个项目里写一遍下个项目又得复制粘贴一遍稍微不一致就出bug。agent-skills的切入点是给Agent的“动手能力”注入结构。它把每个可复用的执行动作比如“整理文档”“查询订单”“生成报表”“发送消息”封装成一个独立的技能包。每个技能包自带描述、参数契约、执行逻辑和测试用例。Agent不需要在每次对话时把全部技能细节加载进来它只先加载一份技能索引知道有什么技能可用等用户需求真的触发某个技能时再拉取该技能的详细描述和执行函数。从工程角度看这本质上是把“模型如何去调用能力”和“能力本身如何实现”这两件事彻底解耦。模型侧只需要理解技能描述和参数说明工程侧只需要保证技能实现可靠稳定两边可以独立迭代互不拖累。1.2 为什么技能是Agent从玩具到工具的分水岭很多团队做Agentdemo跑得很溜一上生产就露馅原因在于“每个会话都是裸奔”。用户每次发消息Agent都要重新理解一遍意图重新构造调用逻辑没有任何积累。裸提示词模式的最大问题是不可复用同一个操作在这个会话里成功了换一个会话它可能又搞砸因为大模型的生成结果天然带有随机性。技能化的最大收益是把“不可复用的临场发挥”变成“可复用的标准动作”。这就像餐厅后厨大厨每天即兴发挥虽然偶尔有惊喜但客人吃到什么全凭运气真正稳定的餐厅一定把每道菜的操作流程标准化配料、火候、时间都定死。技能库就是这个标准化后厨Agent在标准动作上可以稳定发挥在标准动作之外才需要动用模型的生成能力。另一个隐藏收益是Token成本的下降。工具描述和调用规则不用再全文塞进上下文索引只需要几十个token完整技能详情按需加载。我的项目里技能数量超过二十个之后这个优势非常明显每次对话省下的Token成本肉眼可见响应速度也快了一大截。1.3 技能库的目录结构与注册模型我采用的目录结构是典型的“一个技能一个目录”约定优于配置skills/ ├── index.json ├── document_archive/ │ ├── skill.yaml │ ├── run.py │ ├── README.md │ └── tests/ │ └── case.json ├── order_query/ │ ├── skill.yaml │ ├── run.py │ ├── README.md │ └── tests/ │ └── case.jsonindex.json是整个技能库的索引记录每个技能的名字、摘要、关键词和版本号。启动时Agent只加载这份索引用于感知“我有哪些技能可用”。skills下的每个子目录都是一个完整技能包skill.yaml描述触发场景和参数契约run.py是实现执行逻辑的入口tests目录放测试用例。注册模型我在项目里用的是Python字典加装饰器简单直接。技能加载器读取skill.yaml把run.py中的执行函数包装成统一调用接口注册到一个全局注册表里。调用方只认技能名和参数不关心内部实现细节。这套模型的好处是非常直观新技能接入成本极低团队成员照着已有目录结构抄一份就能上手。2. 技能定义与结构设计要点2.1 技能描述三段式写法场景加任务加约束技能描述是整个技能库的灵魂它直接决定大模型能不能在正确的时候选中正确的技能。我在大量实践中总结出一个三段式模板触发场景、目标任务、约束条件。触发场景要写清楚“什么情况下用这个技能”比如“当用户需要把散落文件按类型归档到指定目录时”。目标任务要写清楚“这个技能帮用户达成什么”比如“将源目录下的所有文件按扩展名分类移动到目标目录并自动处理重名”。约束条件要写清楚“哪些事不能做”比如“不修改文件内容不删除源文件目标目录不存在时自动创建”。有些技能描述写得太宽泛比如“处理文档”结果用户在问一个文档里写了什么内容时Agent也去调用归档技能完全跑偏。反之写得太窄也不行比如“只处理PDF文件归档”用户提MP4文件整理时模型根本想不到这个技能。我的经验是描述控制在三到五行中间留出适当的泛化空间关键词明确但不锁死格式。另外每个技能我至少配了二到四个索引关键词比如“归档”“文件整理”“目录结构”。这些关键词会放进index.json用于快速匹配用户意图。它们不需要穷举所有说法覆盖最常见的高频表述就够了。2.2 参数契约给模型画清楚输入的红线技能执行需要什么参数这是模型最容易犯错的地方。我在项目里吃过不少亏模型漏传必填参数、把字符串类型传成了数字、传了调用函数完全不需要的多余参数。后来我给每个技能做了一张严格的参数契约包含字段名、类型、是否必填、默认值、示例值、校验规则六个要素。字段名要短尽量用英文小写加下划线减少模型拼写错误的概率。类型只支持string、integer、number、boolean、array这几种基础类型避免花哨的复合结构。必填参数必须配示例值模型看到示例之后生成正确参数的概率会高很多。非必填参数必须有默认值这样就算模型漏传执行函数也能用兜底值跑通。参数校验我用的是“schema定义加运行时强校验”的方式。定义层用一个Python字典写清楚约束运行时对模型生成的参数做逐字段校验不符合规则的就拒绝执行并返回明确的错误信息。千万不要让非法参数一路走到执行函数内部再去判断那样错误信息会很混乱Agent拿到错误信息也不知道该怎么改。2.3 执行逻辑的三种形态脚本、子系统和纯提示词技能执行逻辑并不一定都要写成Python脚本我把它分成三种形态按场景选用。第一种是纯Python脚本适合处理明确的文件操作、数据计算、接口调用等确定性任务比如文件归档、SQL查询、HTTP请求。第二种是子系统代理适合需要调用外部服务或已有业务系统的场景比如查订单库、发企业微信通知技能内部封装了对这些子系统的访问。第三种是纯提示词模板适合需要模型生成内容的场景比如写周报、做摘要这种技能不调用代码而是把一组精心设计的提示词片段按参数填充后交给模型。三种形态在skill.yaml里用type字段区分但对外暴露的调用接口完全一致都是run(params)返回一个结果对象。这样上层调度逻辑不需要关心技能内部是用什么实现的我只管按契约调用。底层实现方式的选择原则很简单凡是有固定规则、不该让模型自由发挥的逻辑一律写进代码凡是需要开放性生成的逻辑才考虑用提示词模板。3. 核心实操从零实现一套agent-skills3.1 环境骨架与技能目录初始化动手之前先把骨架搭好。我习惯先把skills目录建出来写好index.json和一份空注册表文件目录结构跑通之后再加技能内容。初始化时有一个坑要注意不要把技能加载逻辑和业务逻辑混在同一个模块里加载器只负责读取和注册业务代码只管调用。我的加载器核心逻辑大概长这样import json import importlib.util from pathlib import Path SKILL_REGISTRY {} def load_skill(skill_path: Path): 加载单个技能包注册到全局注册表 config json.loads((skill_path / skill.yaml.json).read_text()) spec importlib.util.spec_from_file_location( config[name], skill_path / run.py ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) SKILL_REGISTRY[config[name]] { config: config, handler: module.run, } return config[name] def load_all(skills_root: Path): 扫描skills目录逐个加载 for skill_dir in skills_root.iterdir(): if skill_dir.is_dir() and (skill_dir / run.py).exists(): load_skill(skill_dir)这段代码只做两件事解析配置、导入执行函数。这里我把skill.yaml统一转成JSON格式来处理避免在加载器里引入YAML解析依赖。实际项目中如果你喜欢YAML的可读性可以保留YAML格式但要在环境准备阶段装好对应的解析库。启动时执行load_all(skills_root)然后生成一份技能索引摘要写入系统提示词。索引摘要就是从每个配置里提取name、description、keywords三字段组装成列表灌给模型。3.2 一个完整技能的代码级拆解我拿项目里最常用的“文档归档技能”当例子完整拆一遍。先看配置部分name: document_archive version: 1.2.0 description: 当用户需要把散落文件按类型归档到指定目录时使用。 将源目录下的文件按扩展名分类移动到目标目录自动处理重名文件。 不修改文件内容不删除源文件。 keywords: [归档, 文件整理, 目录结构] parameters: source_dir: type: string required: true example: /tmp/inbox description: 待整理的源目录路径 target_dir: type: string required: false default: /data/materials description: 归档的目标根目录路径这段配置最核心的是description它同时承担了触发条件、功能说明和约束声明三个职责。我特意加了“不修改文件内容不删除源文件”这句约束因为它能有效阻止模型在调用技能时误以为可以顺手做额外的清理动作。再看执行函数import shutil from pathlib import Path def validate_params(params: dict) - dict: errors [] if not params.get(source_dir): errors.append(缺少必填参数 source_dir) if not isinstance(params.get(source_dir, ), str): errors.append(source_dir 必须为字符串) return {valid: len(errors) 0, errors: errors} def run(params: dict) - dict: check validate_params(params) if not check[valid]: return {status: error, message: ; .join(check[errors])} source Path(params[source_dir]) target Path(params.get(target_dir, /data/materials)) if not source.exists(): return {status: error, message: f源目录不存在: {source}} target.mkdir(parentsTrue, exist_okTrue) moved 0 for item in source.iterdir(): if not item.is_file(): continue ext item.suffix.lstrip(.) or misc dest_dir target / ext dest_dir.mkdir(exist_okTrue) dest dest_dir / item.name if dest.exists(): dest dest_dir / f{item.stem}_copy{item.suffix} shutil.move(str(item), str(dest)) moved 1 return {status: success, moved_count: moved}这个run函数刻意保持简单没有引入复杂的抽象因为技能函数的天职是“一次只做一件事并做好”。我在函数开头先做参数校验原因是技能运行时的错误信息会被原样返回给Agent清晰的中文错误信息能让模型更好地理解问题并决定下一步动作。3.3 把技能挂到Agent的调用链上技能包写好了最后一步是把它们接到Agent的推理循环里。我的调用链分四步加载索引、意图匹配、参数构造、执行回填。加载索引就是在系统提示词里注入一份精简的技能清单让模型随时知道有哪些技能可用。意图匹配发生在用户消息进来之后Agent根据用户意图挑选技能。参数构造阶段Agent从用户消息中抽取参数填充到技能契约里这一步依赖描述中的示例值示例给得好参数抽取成功率就高。执行回填就是调用技能的run函数把结果结构化地返回给Agent让它组织成自然语言回复。实际项目中我通常会在Agent的system prompt里先放“功能性技能列表”的小节接着放“工作流规则”的小节。规则里写明在调用技能前必须先输出技能名称和参数JSON。这样模型的行为就有了明确预期不会东一句西一句乱来。以下是这段提示词的简化版你是一个执行助手。当你决定调用技能完成任务时请严格按以下格式输出 技能名称: document_archive 参数JSON: {source_dir: /tmp/inbox, target_dir: /data/materials} 只能调用已列出的技能不能编造技能名称。如果用户需求与所有技能都不匹配直接给出你自己的回答。收到这行结构化的输出之后代码侧解析出技能名和参数调用注册表里的handler把结果传回给模型让模型基于执行结果生成最终回复。这套链路的优点是模型只做“决策和抽取”不直接操作文件和外部系统所有有风险的动作都被关进了技能函数里。4. 常见问题与排查技巧实录4.1 症状一技能明明存在Agent就是没调用它这是最常见的翻车现场。我排查过很多次之后基本锁定三个原因。第一个是描述写得太长被系统提示词截断模型根本没看到第二个是索引摘要写得跟用户用词对不上用户说“把文件夹理一下”你的描述里全是“归档”“分类”模型匹配不上第三个是技能数量超过十五个全部灌进上下文之后模型反而不知道选谁直接放弃治疗。对应的解法有三个。描述控制版式重要约束尽量放在description的开头确保截断也不影响核心信息。索引关键词要多收集真实用户的常见说法而不是自己拍脑袋想。技能数量多的时候按领域分组每次只让Agent看到与当前会话相关的分组而不是全部技能一锅端。我实测下来单次暴露五到八个技能调用准确率最高超过十个就明显下降。另外还有个隐蔽问题技能索引的格式。如果技能清单在系统提示词里排版混乱模型可能读到一半就断了。我当时花了很长时间才意识到这不是模型笨是我的清单格式不清。给每个技能做一行摘要加一行关键词的紧凑格式比都写一大段描述效率高得多。4.2 症状二参数解析乱七八糟类型错乱、漏传、多传另一个高频问题出在模型生成的参数上。最经典的是把source_dir传成一个数组或者把target_dir写成“目标目录”这几个汉字。原因在于示例值只给了一个模型对参数值的格式理解不够。我后来的做法是双保险。第一层是schema强校验执行前逐字段校验类型与必填项不合法直接拒绝把清晰的错误信息返回给模型。第二层是类型损失函数在描述里对每个参数的值域做说明比如“source_dir必须是一个字符串路径例如/tmp/inbox”比单纯给个example有效得多。多传参数的问题也值得提一嘴。模型不知道某个参数是否需要会把用户消息里的无关信息也塞进来。比如用户说“把最近下载的图片整理到素材文件夹然后在群里通知大家”模型可能把“通知大家”也当作归档技能的参数。解决办法是增加“忽略未声明的参数”规则在参数契约里明确定义可接受字段白名单运行函数直接丢弃白名单之外的键。4.3 症状三技能之间的“描述霸凌”当技能库有十几个技能之后会出现一种很迷惑的现象A技能的描述明明很准确但Agent就是更倾向于调用B技能。排查到最后发现B技能的描述里包含了A技能的关键词触发了错误匹配。比如“document_archive”里写了“文件整理”另一个“file_sync”技能里也写了“整理文件到远程服务器”两个技能在Agent眼里高度相似它就会随机选一个。解决这件事我用了两个手段。第一个是显式优先级字段在技能配置里加一个priority越高优先。匹配阶段如果多个技能同时被触发按优先级排序选中而不是让模型自己纠结。第二个是关键词互斥设计每个技能的keywords只保留自己独有的触发词彼此之间刻意避免重叠。比如“archive”用“归档、分类、本地文件”“sync”用“同步、远程、备份”从源头减少触发面重叠。技能冲突是慢工细活没有一次性根除的办法。我在项目里维护了一份冲突用例集定期用真实对话测试技能调度结果发现误调用就调整关键词和描述。这个用例集越积累越有价值后面基本成了技能库的定海神针。4.4 症状四技能执行成功但结果不理想还有一类问题技能函数执行了状态码是success但用户觉得结果完全不对。这类问题最坑因为没有任何报错信号。我遇到过一个典型情况归档技能把文件按扩展名分类但用户期望按项目名分类函数执行没毛病语义上却南辕北辙。这说明单靠“执行正确”不够还需要“结果验证”。我给每个技能配置了结果断言函数执行返回后会对结果做基本合理性检查。文件归档就检查moved_count是否大于零目标目录是否确实出现在文件系统里。断言不通过时返回给Agent的不是简单成功而是“执行完成但可能需要确认”。同时run函数返回内容要尽量结构化包括操作数量、目标位置、文件清单方便Agent在最终回复里给用户呈现细节。用户看到具体文件被归档到哪个目录才有“这技能真干活了”的感觉。5. 技能库的运营与演进5.1 从单技能到编排把技能串成工作流技能多了之后下一层需求就来了一个复杂的任务往往需要多个技能协作。比如“整理完文件之后发一份汇总报告”至少涉及归档技能和报告生成技能。agent-skills体系里不能指望一个技能搞定所有事更合理的设计是让Agent在推理循环中依次调用多个技能把第一个技能的输出作为第二个技能的输入。我在项目里为这种场景单独配置了工作流编排层。编排层不关心技能内部实现只关心技能之间的依赖关系和数据流。比如“文件整理工作流”就是document_archive输出生成文件清单再传给report_generator生成Markdown摘要。编排层还要处理一个关键问题中间结果的临时存储。技能A的输出不能只留在内存里如果技能B需要文件路径就必须把技能A的输出写入一个临时文件再把路径传给技能B。做编排层的教训是不要让技能之间产生隐式依赖。技能A不能假设技能B执行过也不能共享某个全局变量。所有跨技能的数据传递都必须走显式的参数传递。因为这个约定我的技能库在扩展时几乎没有因为互相干扰而返工的情况。5.2 版本管理与灰度发布技能包不是写完就永久不变的。我见过很多团队技能库越维护越乱问题就出在没版本管理。我采用的方案是在skill.yaml里维护version字段每次改动必须递增版本号并在index.json里同步更新。技能目录里的README.md同时记录变更说明写清楚这版改了什么、为什么改、可能影响哪些调用方。版本更新还有一个灰度问题。如果技能函数本身有bug直接全量替换会让所有Agent会话一起报错。我的做法很简单加载器支持指定版本号index.json里可以配置每条技能记录的active_version。发布新版本时先让一小部分会话走新版本跑一段时间看错误率和用户反馈稳定之后再全量切换。这个灰度机制在早期可能觉得没必要当你的Agent服务于真实用户的时候它就是救命的。5.3 团队协作与技能评审机制最后说团队协作。技能库本质上是团队的知识资产不是某个人的私人代码库。我见过最糟糕的情况是每个人都往技能库里塞自己的技能结果内容重叠、风格混乱、互相冲突。后来我们建立了一个很轻量的评审流程不搞重流程只做三件事。第一是描述评审提交技能时必须附带触发场景和关键词说明评审人负责确认不会与已有技能冲突。第二是沙盒测试每个技能都必须带测试用例集至少覆盖正常路径、边界路径、错误路径各一个评审人在沙盒环境跑完才允许合入。第三是命名规范技能名字统一用“功能_对象”的格式比如document_archive、report_generator禁止随意起名。这个评审流程看似增加了一点工作实际上会让技能库的长期维护成本大幅下降。新成员接手时通过阅读已有技能包和测试用例集能很快理解整个体系的结构和约定往下添加新技能时也更有章法。在我个人项目里agent-skills这套体系帮我解决了一个长期困扰的问题“Agent能力怎么持续积累”。以前每做一个新项目模型能力都要从零开始调现在把通用能力沉淀成技能包之后换个业务场景也能快速组装。技能库的扩展原则我建议“先窄后宽”——先把一条最核心的垂直链路跑通把重复出现的动作抽成技能再逐步扩展。追求一步到位、一次性铺开二十个技能大概率换来的是调度混乱和维护噩梦。技能这东西贵精不贵多稳定好用的十来个胜过花里胡哨的五十个。