
最开始动 agent-skills 这个项目的念头其实很朴素我发现自己一个月内写了两遍几乎一模一样的网页内容抓取逻辑一遍塞进 Flask 服务里当普通接口一遍硬编码进一个 ReAct 循环里当 function。第三遍准备再写的时候我停下来问自己——Agent 项目里真正值得沉淀的到底是一堆堆不完的 prompt还是让 Agent 稳定调用工具的那套能力答案就是现在这套 agent-skills一套把工具函数、描述文本、参数校验、示例样本统一收敛成“技能包”的实践集合。它能做的事情很直接帮你在写 AI Agent 时把各种工具调用能力变成可复用、可裁剪、可单独测试的“技能”而不是散落在代码里的零散函数。解决的问题也很明确——模型老选错工具、参数总传错、换一个项目就得重写一遍工具封装。适合正在做智能体应用、自动化工作流、或者想把自己的 API 能力开放给大模型调用的朋友。这篇文章不聊概念只讲我在落地 agent-skills 过程中怎么设计目录、怎么写注册机制、怎么处理多 Agent 编排以及踩过的一堆坑。1. 为什么需要一套技能库而不是继续堆工具函数很多 Agent 项目一开始都没意识到“工具组织”是个问题。最原始的做法是在一个tools.py里堆函数然后在 system prompt 里写“你有以下工具可用...”模型每次对话都把全部工具塞进去。项目跑到第 2 个月文件超过 1500 行prompt 里同时列了 20 个工具模型开始频繁串场查天气调了翻译接口写文件调了搜索接口你压根分不清是模型犯傻还是工具描述写得模糊。我一开始也经历过这个阶段直到有个周末调一个多步骤任务来回试了 6 次最后发现问题是两个工具的描述都包含“获取信息”四个字模型根本分不清该选哪个。那时候我意识到问题不在模型在于工具本身缺少规范和边界。所以我开始把“工具”上升为“技能”来管理。1.1 从“Agent 写死工具”到“Agent 使用技能”先明确一个底层逻辑Agent 的本质是“大脑 手”。大脑是 LLM手是执行动作的能力。传统的工具函数只是给手定义了“能做什么”但没说清楚“什么场景下做这件事”“做完之后返回什么样子”“失败之后该如何反馈”。结果就是模型经常对着错误的结果做二次猜测链条越长错误越离谱。在 agent-skills 里我把一个手的能力拆成五层来看做什么、什么时候做、输入什么、输出什么、出错怎么办。这五层信息全部写进一个技能包模型看到的不再是一个孤立函数而是一个完整的“使用说明书”。技能包不再被写死在某段代码里而是通过注册表加载需要哪个调哪个。1.2 技能化之后带来的直接收益把工具升级成技能之后收益不是理论上的而是很实在的那种。首先是复用效率我在两个项目里都能直接加载同一个fetch_website_content技能不用复制粘贴代码也不会出现“两个项目里同一个工具但行为不一致”的问题。其次是成本控制以前 20 个工具全量塞进 prompt每个工具描述 200 token光工具定义就要 4000 token技能化之后可以按场景只暴露相关技能几个包切换下来 token 消耗直接少了一大截。更重要的是可测试性。普通工具函数你只能手工测调用对不对技能包里可以附带 mock 样本和校验用例跑pytest时每个技能独立验证。我自己的经验是技能化之后Agent 的明显“误会型错误”至少降了一半。就是那种模型该选 A 却选了 B、参数类型写错、结果格式理解错误的问题几乎都能在技能设计和校验层兜住。2. 技能目录怎么设计一份能落地的 Agent 技能 Schema这一节是整个项目的地基。我见过很多人一上来就写代码Agent 框架选好、API key 配好、工具函数塞进去就开跑。但技能库如果一开始没有规则后面扩展 50 个技能的时候一定乱套。2.1 目录结构一个技能就是一个文件夹我刚设计 agent-skills 时定了一个非常朴素的原则一个技能就是一个文件夹不能把所有技能平铺在一个大目录里。目录结构如下agent-skills/ ├── skills/ │ ├── fetch_website_content/ │ │ ├── skill.yaml # 技能元数据 │ │ ├── handler.py # 核心执行逻辑 │ │ ├── schema.py # 输入输出 JSON Schema │ │ ├── examples.md # 给模型看的 few-shot 示例 │ │ └── tests/ │ │ └── test_handler.py │ ├── send_email/ │ │ └── ... ├── registry.py # 技能注册表 ├── compiler.py # 技能编译成模型协议 ├── runtime.py # 技能执行器 └── packs/ # 按场景组织的技能包 ├── research.yaml └── productivity.yaml当时为什么不用单一文件因为技能的信息密度远比工具函数高。一个技能除了代码还要有给模型看的描述、给开发者看的 schema、给测试跑的用例。如果全塞一个tools.py任何一个部分需要改动都会牵扯到另外两方。拆成文件夹之后各自独立演进加载时再按约定聚合。2.2 技能元数据字段定义每个技能文件夹的核心是skill.yaml。这个文件的字段直接影响模型对技能的理解和调用质量怎么谨慎都不为过。我列一下我会用的关键字段字段类型必填作用namestring是全局唯一技能名模型据此发起调用descriptionstring是告诉模型该技能做什么、什么时候用input_schemaobject是定义参数结构、类型、必填项output_schemaobject是定义返回结果结构tagslist否检索、路由、技能包裁剪用versionstring是入库版本号exampleslist强烈建议few-shot 示例示例比描述更能定边界timeoutnumber建议技能执行超时上限秒depends_onlist否组合技能依赖的子技能列表description是整个技能包里对模型影响最大的文本。我在实践里的写法是先一句话说明功能再补一句“什么场景下优先使用”然后再补一句“什么情况下不要使用”。例如name: fetch_website_content description: 抓取指定 URL 的网页正文自动去除导航、脚本和广告后返回纯文本。当用户需要某个网页上的具体内容、做信息提取或摘要时使用。当用户只需要链接本身、不需要网页内容时不要使用当目标是本地文件时使用 read_local_file不要使用本技能。 input_schema: type: object properties: url: type: string description: 需要抓取的完整网页地址必须包含协议头 https:// 或 http:// max_chars: type: integer description: 最大返回字符数默认 5000 required: - url output_schema: type: object properties: title: { type: string } content: { type: string } source_url: { type: string } fetched_at: { type: string }注意我把“不要使用场景”直接写进描述。这是我从一次连环误用事故里学到的模型在信息不足时倾向“选一个最像的”把反面说明写清楚等于主动帮模型排除错误选项。这个细节对准确率的影响非常明显。2.3 输入输出 Schema 为什么必须显式化很多入门项目只写函数签名不写 schema。函数签名对人是友好的但对模型不是。模型并不理解 Python 类型注解它理解的是结构化描述。你在handler.py里写def get(url: str, max_chars: int 5000)模型未必知道max_chars是干什么的但如果你在 schema 里写“最大返回字符数默认 5000”模型调用时就靠谱得多。还有一层验证的作用。大模型返回的arguments经常是 JSON 字符串写错类型、漏字段、塞多余字段是家常便饭。显式 schema 上了之后我在执行器里直接做校验不满足就立刻截断返回错误信息让模型修正。这比把错误参数直接送进函数执行让异常爆出来要优雅得多也避免了一次错误调用污染整个上下文。3. 技能注册与调用实现核心机制的落地细节目录和 Schema 只是静态规范真正让技能跑起来的是注册、编译、分派这一条链路。这一节我直接把核心代码拆开讲。3.1 技能注册表与延迟加载agent-skills 的注册表我刻意没有做成“启动时全部导入”的方式。技能一多全部导入会让启动变慢还会因为某个技能依赖的第三方库缺失导致整个服务崩掉。所以我做了延迟加载注册表只保存技能元数据和处理器路径第一次调用某个技能时才真正import它的 handler。# registry.py import importlib from functools import lru_cache class SkillRegistry: def __init__(self): self._skills {} def register(self, skill_meta: dict): name skill_meta[name] self._skills[name] skill_meta def list_skills(self, tagsNone): if not tags: return list(self._skills.values()) return [ s for s in self._skills.values() if tags and set(s.get(tags, [])).intersection(set(tags)) ] lru_cache(maxsize64) def get_handler(self, name: str): meta self._skills[name] module importlib.import_module(meta[handler_module]) return getattr(module, meta[handler_name])lru_cache在这里帮了大忙避免同一个技能在一次任务里被反复 import。实际跑起来一次长对话可能并发调用同一个技能多次缓存住模块加载不仅能省时间还能避免某些库初始化带来的副作用。3.2 Agent 如何“看到”技能把技能编译为工具清单技能存在注册表里是一套自己的格式但不同模型协议看得懂的是另一种格式。比如 OpenAI 使用的是tools协议有一些本地模型走的是functions协议Anthropic 的格式又有差异。所以我在项目里加了一层compiler职责就是把技能统一编译成目标模型能识别的工具清单。# compiler.py def compile_to_openai_tools(skill_meta: dict) - dict: return { type: function, function: { name: skill_meta[name], description: skill_meta[description], parameters: skill_meta[input_schema], }, }这段看起来很朴素但背后有个关键选择我拒绝在技能库里直接写 OpenAI 格式而是保留自己的中立项格式。为什么因为一旦直接依赖某个厂商协议换模型厂商的时候就得改所有技能文件。通过 compiler 转换之后技能是厂商无关的哪天从 OpenAI 兼容接口切到别的模型只改 compiler 层就够了技能本身一个字不用动。3.3 调用链路由与结果归一化模型返回结构化调用请求后执行器要做四件事解析、校验、执行、归一化返回。解析是把模型塞过来的 JSON 字符串转成对象校验是拿input_schema对照检查执行是真正跑 handler归一化是把任何异常都转成一段模型能理解的错误文本而不是裸抛 Python traceback。# runtime.py import json, asyncio import jsonschema async def execute_skill(skill, arguments: str, context): try: args json.loads(arguments) jsonschema.validate(args, skill[input_schema]) result await asyncio.wait_for( skill[handler](contextcontext, **args), timeoutskill.get(timeout, 30), ) return {success: True, result: result} except jsonschema.ValidationError as e: return { success: False, error: f参数校验失败: {e.message}。请参考输入 schema 修正参数后重试。, } except asyncio.TimeoutError: return { success: False, error: 技能执行超时。如果用户的问题不需要该技能结果请直接告知超时并询问是否需要换一种方式。, } except Exception as e: return {success: False, error: f执行出错: {str(e)}。请不要重复尝试同一技能。}我特意不让执行器把所有异常抛到上层。原因很现实Agent 在每一步都要靠上下文做决策一段友好的错误信息比一个堆栈更有利于模型恢复。比如“参数校验失败”这种提示模型看到后会重新读取 schema 修正参数Agent 就能自我纠正。如果直接把异常抛给框架层任务大概率直接中断。另外一个经验是执行器要留意“上下文同步”。技能如果有副作用比如改了文件、发了邮件、写了数据库要把执行结果中的关键状态存到context供后续技能使用。否则模型会在下一轮说“已经发送成功”但其实它只是猜测。4. 多技能编排与私有化适配从单技能走向 Agent 工作流单技能能用之后下一步必然是多技能协作。我不太建议把全部希望押在“大模型自己会编排”上因为目前模型的编排稳定性还撑不住复杂任务。最稳的方式是开发者定义好编排路径模型在路径内做选择。4.1 技能依赖与组合技能我在做内容分析类 Agent 时遇到了一个典型场景先抓网页再抽取关键信息再生成摘要。如果把这套流程拆成三个独立技能丢给模型模型经常顺序搞错甚至跳过抓取直接拿 URL 去生成摘要。所以我在 agent-skills 里支持了组合技能一个“分析网页并生成摘要”的技能depends_on声明依赖抓取技能模型看到它是单个入口内部按固定顺序调子技能。name: analyze_webpage_summary description: 抓取指定网页并基于正文生成摘要。当用户需要了解某个网页大致内容时使用。不要直接用本技能处理本地文件。 depends_on: - fetch_website_content - summarize_text timeout: 60组合技能在编译层仍然表现为一个普通工具调用的体验和普通技能没有区别。但执行时执行器会先把依赖技能跑完再把结果传给组合技能的主体逻辑。这样既照顾了模型的“简单调用偏好”又保证了流程不错序。4.2 技能包按场景裁剪技能集模型对工具的选择准确率和工具数量成反比这是我在几十次对比实验里看到的事实。工具越多描述越长模型越容易晕。所以 agent-skills 做了一个packs层把技能按场景分组做调研类的 Agent 只加载搜索、网页抓取、信息提取、内容总结这几个技能写文档类的 Agent 只加载文档读写、格式转换、关键词提取这几个技能。packs/research.yaml示例pack_name: research description: 调研场景常用技能 skills: - fetch_website_content - web_search - extract_key_points - summarize_text运行时Agent 启动时只注册这个 pack 里的技能列表。实测同样一次对话技能从 24 个减到 8 个之后模型选错技能的频率低了很多单次请求的 token 也少了 60% 以上。这种收益不需要改任何模型参数纯粹靠裁剪暴露面就能拿到。4.3 本地大模型与跨厂商适配不完全是厂商适配还有模型能力的适配问题。很多本地模型对复杂 JSON Schema 的支持很弱尤其是一堆嵌套oneOf、allOf小参数模型基本理解不了。我在 compiler 里做了一个简化开关当目标模型能力较弱时把 schema 压平成“参数名 一句话说明”的扁平工具描述放弃强类型约束但保证模型能看懂。还有一个经验是控制技能描述语气。本地模型对英文长描述的理解普遍好于中文长描述而中文短描述效果又比英文短描述好。所以我在基础技能库里直接写了双语描述主描述用中文同时在 description 末尾保留英文关键词。实测对部署在本地的小参数量模型误选率又降了一点。这类适配如果不做本地化部署时技能库很可能会变成摆设。5. 常见问题与排查技巧实录技能库框架本身并不复杂真正难的是上线后那些“看起来没问题但 Agent 就是不听话”的时刻。我把踩过的坑整理成一张速查表基本覆盖了技能化落地的主要雷区。5.1 LLM 总选错技能怎么办这个问题是最常见的。我的排查顺序是先看描述里有没有“不要用”边界再看有没有 few-shot 样例最后看技能之间是否有功能重叠。模板化描述的问题在于模型只能理解字面含义两个技能都写“用于信息提取”时模型天然会混淆。我给每个技能都加了独一无二的触发词。比如搜索技能写“当用户提到查找、搜索、哪些网页、最新消息时优先使用”翻译技能写“当用户提到翻译成、英文怎么说、日语版本时优先使用”。加了触发词后选错率肉眼可见下降。还有一个更隐蔽的原因技能名相似。我在早期把所有技能都命名为get_xxx结果模型经常把get_file_content调成get_url_content。后来把技能名改成更具体的行为描述比如read_local_file、fetch_website_content模型的调用准确率立刻上了一个台阶。技能名对模型来说是一个“快捷印象”一定要避免长得像。5.2 技能调用参数大量报错参数报错分两类一类是模型漏填必填字段一类是填了 schema 里没有的字段。漏填字段的修复办法是让校验错误信息直接回传大模型让它自己对照 schema 补。填了多余字段的修复办法是执行器在做**args展开前先做白名单过滤只保留 schema 里声明的键。这两个步骤缺一不可只过滤不提示模型不知道自己犯错下一轮继续乱填只提示不过滤个别模型死活不修正时任务就卡死。JSON Schema 里还有一个原则少用enum强枚举多用description加示例来说明可选值。模型对 enum 的响应经常是“从枚举里猜一个最接近的”但真实的业务参数可能不在枚举里。把枚举放开成自由字符串配合执行器的标准化转换反而更稳。5.3 技能响应超时或卡住技能超时是一个必须提前面对的问题。有一次我的网页抓取技能访问一个很慢的站点默认 30 秒超时模型在等待时没有任何动作。后来我在执行器里加了超时提示“技能执行超时你可以先告诉用户当前网络较慢并询问是否继续等待或换一个来源。”模型会基于这个提示调整话术而不是干巴巴卡住。超时参数的设计不能一刀切。网络类技能我给 30 秒本地文件处理给 10 秒大模型之间互相调用的组合技能给 90 秒。每个技能单独配timeout比全局一个值要灵活得多。5.4 一次 Agent 死循环的排查实录最后分享一个最典型的死循环问题。我的一个内容聚合 Agent 在运行时反复调用“摘要生成”技能同一篇稿件被总结了 4 次任务就是不完结。排查时发现组合技能的depends_on链里包含了抓取技能而抓取技能的返回结果总是包含“这篇内容可以总结”的字样模型据此认为总结动作没有完成于是循环调用。后来我在组合技能里加了exit_condition字段声明“当结果中包含指定输出标记时链路完成禁止重复调用同一技能”。执行器识别到这个标记后直接截断后续重复调用。这个机制后来被我做成了技能库的通用能力不是去依赖模型的“自觉”而是在运行时主动阻断。现象可能原因处理方案模型选错技能描述模糊、技能名相似、缺少边界说明加触发词、反面描述、更具体的技能名参数频繁报错schema 过严或过松、错误信息未回传放宽 enum、校验失败信息回传、白名单过滤技能超时单技能耗时过长、未设置超时按技能独立配置超时超时返回可恢复提示任务死循环缺少退出条件、模型误判任务完成显示声明 exit_condition运行时阻断重复调用切换模型后技能不工作协议格式差异、schema 过度复杂统一中间格式compiler 层做协议转换做 agent-skills 这段时间我自己最大的体会是技能库不是把大模型关进固定流程的笼子而是给模型的即兴发挥划出一块“有规则的安全区”。人不是靠一份总结收官的但我可以把最值得说的经验放在最后——如果你也想搭一套自己的技能体系别一开始就追求技能数量先认真写 10 个高质量技能把描述、schema、示例、测试都补全把这 10 个技能跑进一个完整的 Agent 闭环里。技能库的收益不在于“多”而在于“每个技能拿起来都能直接用、模型永远选不错”。等这 10 个技能验证了整套流程再快速铺开扩展你会发现自己省下的是十倍百倍的调试时间。