
做AI Agent开发的朋友多半有同感模型本身能力再强框在一个没有技能支撑的壳子里也发挥不出价值。过去一年我维护了一套名为 agent-skills 的技能库项目核心思路就是把Agent可能需要的各种能力——天气查询、Python执行、文件读取、网页解析——统一封装成可插拔的“技能skill”声明让Agent在对话过程中按需选择、加载和执行。这篇稿子把 agent-skills 从设计到落地的完整过程拆开讲一遍包括技能怎么分类、清单怎么写、调度器如何接、评测怎么做以及我踩过的几个有代表性的坑。如果你正在折腾Agent开发或者准备给现有机器人加上一套可扩展的工具能力这篇文章应该能帮你少走不少弯路。我尽量不堆理论直接把能用的结构和代码摆出来再讲清楚每一步为什么要这么设计。1. 为什么需要一套独立的Agent技能体系先说一个普遍现象很多Agent项目一开始都是把能力硬编码在主流程里。用户问天气就走一个 if 分支调天气API用户让算数就再写一个函数处理。头几个功能还好等到第十个、第二十个功能进来主流程就变成了一锅粥。更麻烦的是这些能力没法跨项目复用换一个场景又得重写一遍。1.1 Agent开发中的“能力孤岛”问题我见过不少团队的做法是把所有工具说明拼进系统提示词里让模型自己去挑。这在技能数量少的时候勉强能用一旦指令超过一定长度模型开始顾此失彼要么忘记调用工具要么在错误的场景调用错误的工具甚至把工具说明当成聊天内容输出。这就是典型的能力孤岛——能力确实存在但彼此之间没有统一的管理和调度抽象。agent-skills 想解决的就是这个问题。每个能力不再散落在代码里而是收敛成一个结构化的“技能包”包含描述、参数约定、执行逻辑和权限声明。Agent主程序不需要关心每个技能内部怎么实现只需要按一套统一的接口去加载和调用。这样能力才能沉淀下来变成可积累的资产。1.2 Skills不是单一函数而是一套可复用的能力工程很多人以为技能就是一个Python函数写上 def query_weather() 就算完事。实际远不是这样。我把它拆成五块技能声明manifest记录技能名称、描述、参数Schema、触发条件。技能实现skill.py真正干活的那段逻辑。依赖清单dependencies.txt这个技能运行时需要的第三方库。权限声明permission技能能访问哪些资源、执行哪些操作。测试用例tests/覆盖技能输入输出的回归用例。这么拆分之后每个技能都是一个独立的小工程。往小了说它可以在不同Agent项目之间搬运往大了说它可以单独做CI、单独走版本评审、单独灰度上线。我给技能写文档时的比喻是这相当于给员工写岗位说明书既有职责描述又有执行手册还有权限边界。模型看到的是说明书执行器看到的是手册权限层看到的是边界三方各取所需互不干扰。1.3 这套方案适合谁如果你属于下面几类情况这套技能体系会很对胃口正在做个人助理或垂直领域助手需要让Agent稳定调用外部工具。维护着多个机器人/Agent项目希望能力可以复用。在团队里做Agent平台需要多人协作维护工具集。已经试过单纯堆提示词发现效果不可控。反过来如果只是写一个一次性Demo或者在脚本里跑一两次试玩那确实没必要搞这么重的结构。技能体系的价值在于“复用的边际成本递减”项目越复杂收益越明显。2. 技能体系设计分类、清单与接口抽象设计阶段最忌讳一上来就写代码。先把技能分门别类再定义统一的描述格式后面所有工作都会顺很多。2.1 三类技能动作类、感知类、决策类我把技能分成三类对应Agent运行时的不同角色动作类技能会改变外部世界或系统状态比如发送邮件、写文件、执行Shell命令、下单。这类技能必须过权限检查不能让模型随意触发。感知类技能只读取信息比如查询天气、查数据库、抓网页、读本地文件。这类技能通常不产生副作用但也需要控制访问范围。决策类技能不直接接触外部资源而是帮模型做推理和规划比如任务拆解、方案对比、风险检查。这类技能本质上是把一些成熟的提示词模板固化下来让模型按固定流程思考。为什么要在设计阶段做这个分类因为调度器在执行前需要快速决定走哪条通道。动作类要确认权限、感知类可以走缓存、决策类要预留足够的推理token。如果所有技能都混在一起这些策略就没法统一落地。实测下来这个分类对后面做安全控制和效果调优帮助非常大。2.2 技能描述决定模型是否调用它的第一要素模型在选择技能时看到的不是实现代码而是技能描述。描述写得好不好直接决定模型在关键时刻会不会选错、选漏。我把描述文本拆成几个固定段落summary一句话说清技能干什么。when_to_use明确列出用户的哪类请求应该触发本技能。when_not_to_use反向排除防止误用。input_rules参数上的注意事项比如日期要按当前系统时间推算。example一个典型的调用例子。反面教材很好找比如某技能描述就写了“查询天气”四个字。结果用户问“明天适合爬山吗”模型根本不调用天气技能而是直接编了一个回答。后来我把描述改成summary: 查询指定城市实时或预报天气 when_to_use: 用户询问气温、降水、风力、空气质量等天气信息或询问带具体位置的出行适宜性时 when_not_to_use: 如果用户只是闲聊天气没有涉及任何地理位置或时间范围不要调用 input_rules: 城市必须是明确的地理位置如用户未指定日期默认今天涉及“明天”“后天”时按当前系统日期推算 example: 用户北京明天还下雨吗- action_input: {city: 北京, date: 2025-01-06}这行改造之后天气技能调用率从不到三成提升到九成以上。所以我把技能描述当成给模型看的接口文档来写所有关键约束都要写清楚。2.3 技能清单Manifest的标准格式每个技能目录下的 manifest.yaml 是核心我用的字段大致如下字段类型必填说明namestring是技能唯一标识小写加下划线versionstring是语义化版本号descriptionstring是给模型看的完整描述categorystring是action / perception / decisiontriggersstring[]否典型触发词用于本地快速索引inputsobject是参数Schema遵循JSON Schema规范outputobject否返回结果的字段约定permissionobject是网络、文件、Shell等权限边界timeoutnumber否技能执行超时秒数示例name: weather_query version: 1.0.0 description: | summary: 查询指定城市实时或预报天气 when_to_use: ... when_not_to_use: ... input_rules: ... category: perception triggers: [天气, 气温, 降水, 风力] inputs: type: object properties: city: type: string description: 城市名如北京、上海 date: type: string format: date description: 日期默认当天 required: [city] output: type: object properties: temperature: { type: number } condition: { type: string } permission: network: [api.exampleweather.com] filesystem: [] shell: false timeout: 10我特别推荐把 version 和 permission 做成必填。版本号让技能更新可追踪权限声明让执行器能在调用前就拦截危险操作。这两项刚开始会觉得麻烦等团队协作或者技能数量上来之后你会庆幸当初写了它们。3. 核心实现从技能加载到调度执行这一部分讲运行时骨架。我尽量给最小可跑通的代码结构你可以直接抄过去改。3.1 目录结构与技能加载器项目的顶层结构大概是这样agent-skills/ skills/ weather_query/ manifest.yaml skill.py dependencies.txt python_executor/ manifest.yaml skill.py dependencies.txt core/ loader.py registry.py executor.py main.py tests/loader.py 负责扫描技能目录、读取 manifest.yaml、动态导入 skill.py 里的实现类。这里有几个细节manifest 解析要用安全的方式绝不能直接 eval技能实现类需要统一继承一个 BaseSkill并提供 execute 方法。BaseSkill 我定义得很薄只做两件事持有 manifest 数据、声明执行入口。# core/loader.py import yaml import importlib.util from pathlib import Path def load_skill(skill_dir: Path) - dict: manifest_path skill_dir / manifest.yaml with open(manifest_path, r, encodingutf-8) as f: manifest yaml.safe_load(f) skill_file skill_dir / skill.py spec importlib.util.spec_from_file_location( manifest[name], skill_file ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return { manifest: manifest, instance: module.Skill(), }用 importlib 动态导入而不是把技能全部写进一个核心文件是为了让每个技能自包含。这样剥离开之后新增一个技能就是往 skills 目录里放一个文件夹核心代码完全不用动。3.2 调度执行让Agent在思考中调用技能Agent 的调度循环我做得比较常规就是 ReAct 模式思考 - 决策 - 行动 - 观察。模型在推理时如果判断需要工具就输出一个结构化的 JSON执行器解析这个 JSON 后分发到对应技能。# core/executor.py import json class Executor: def __init__(self, registry): self.registry registry def dispatch(self, raw_action: str): action json.loads(raw_action) skill_name action.get(skill) skill_input action.get(args, {}) skill self.registry.get(skill_name) if skill is None: return {error: fskill {skill_name} not found} return skill.execute(skill_input)关键点在模型输出格式的约束。我会在系统提示里要求模型必须输出如下格式{ thought: 用户想知道北京的天气需要调用天气查询技能, skill: weather_query, args: {city: 北京, date: 2025-01-06} }这样解析逻辑非常简单不会出现自由文本里抠参数的情况。如果模型输出不合规执行器会返回格式错误提示让模型重新生成。实测下来只要示例给足这个格式的稳定性很高。3.3 技能注册与冲突检测registry.py 维护一个技能名字到实现类的映射。加载完所有技能后我会做一次冲突检测。最常见的冲突是两个技能的描述都覆盖了同一种用户请求比如“天气查询”和“户外活动建议”都可能被天气类请求触发。冲突检测的做法不算复杂把每个技能的 triggers 和 description 里的关键场景词抽出来做两两比对。如果两个技能在触发词集合上的重叠度过高就写一条警告日志并且在运行时给这两个技能标记“低区分度”。这样可以尽早暴露问题而不是等线上模型随机选错再排查。# core/registry.py from difflib import SequenceMatcher def check_collisions(registry): names list(registry.skills.keys()) for i in range(len(names)): for j in range(i 1, len(names)): a registry.skills[names[i]] b registry.skills[names[j]] score SequenceMatcher(None, a.describe_text(), b.describe_text()).ratio() if score 0.7: print(f[collision] {names[i]} vs {names[j]}: {score:.2f})这个警告不是用来阻止运行的而是提醒你当两个技能描述长得太像模型判断就会变得不可控。好的实践是让每个技能的描述之间保持足够的语义距离各自守好边界。3.4 参数校验与失败回退模型生成参数经常出幺蛾子该传字符串传了数字、日期格式写错、必填字段漏掉。我在执行器里加了一层基于 JSON Schema 的校验用 jsonschema 库几行代码就能搞定。# core/executor.py import jsonschema def validate(schema, data): try: jsonschema.validate(instancedata, schemaschema) return None except jsonschema.ValidationError as e: return str(e)校验失败时不要直接抛异常干掉整个对话而是把错误信息返回给模型让它自己修正。这一招在实战中非常有用模型看到“city 字段是必填的”通常下一轮就能补上。再配合一个最大重试次数我一般设 3 次超过次数后才向用户要明确输入避免无限循环。失败回退也分层级第一层是参数校验失败让模型修正第二层是技能执行抛异常返回错误信息给模型重新规划第三层是权限拦截这时候不能再让模型重试必须直接终止并记审计日志。原因很简单——权限问题不该靠模型自觉。4. 实操从零搭建一个带技能体系的Agent理论说完了下面进入可以直接跟着做的实操流程。我用天气查询作为第一个技能因为它的外部依赖最少适合用来打通整条链路。4.1 准备运行环境我用的是 Python 3.10 以上版本依赖只需要三个PyYAML、jsonschema、requests。装一下就行pip install pyyaml jsonschema requests模型接口方面你需要一个能跑对话补全的 LLM 服务我用的是兼容常用 Chat 协议的服务。这个项目不挑模型只要支持 function calling 或者能按指令输出 JSON 都行。环境准备阶段最重要的事情是先把模型调通的端点信息放到配置文件里不要写死在代码中。4.2 写一个天气查询技能并注册先在 skills/weather_query 下创建 manifest.yaml内容就用前面那一版。再写 skill.py我建议先做一版 mock 实现把调用链跑通后再接真实 API。mock 的好处是开发和测试不依赖外部服务稳定性。# skills/weather_query/skill.py import datetime class Skill: def execute(self, params): city params[city] date params.get(date, str(datetime.date.today())) # mock 数据后面替换成真实API return { city: city, date: date, temperature: 24, condition: 晴 }注册过程就是把技能目录交给 loader 扫描一遍然后放进 registry。我可以加一句命令行工具每次新增技能后自动检测目录变化省得手写注册代码。python main.py --register skills/weather_query等你看到控制台输出 “skill weather_query registered”说明一个技能已经成功接入。这时候可以先用一个最简单的测试脚本直接调用 executor跳过模型确保技能本身没毛病。4.3 让Agent在对话中自动挑选技能技能本身能跑了接下来就是把技能列表动态渲染到系统提示词里让模型学会挑选。我写了一个函数从 registry 生成技能说明块塞进 promptdef build_skill_prompt(registry): blocks [] for skill in registry.all(): manifest skill.manifest blocks.append(f## {manifest[name]}\n{manifest[description]}) return \n\n.join(blocks)然后系统提示末尾固定加上一段“当你需要上述技能时请严格输出 JSON 格式的 action。如果不需要技能直接回复用户。”这里面有一个关键技巧明确告诉模型“不需要技能时不要强行调用”。很多误调用都是因为模型被工具说明带着走看到什么都想套一个技能。实测效果可以用一组对话来观察。用户说“北京今天冷不冷”模型输出{thought: 用户需要北京天气信息, skill: weather_query, args: {city: 北京, date: 2025-01-06}}执行器返回温度 24 度、晴模型再组织成自然语言回复。整条链路非常清晰。4.4 压测与效果对比技能系统跑通之后一定要建立一套最小评测集。我维护了一份类似这样的测试问题列表类型示例问题期望行为应调用天气技能北京明天多少度调用 weather_query应调用天气技能适合去山里露营吗先查天气再给建议不应调用技能你好介绍一下你自己直接回复不应调用技能11等于几直接回复或调用计算技能评测时记录四个指标技能调用准确率、误调用率、端到端耗时、平均回复长度。我做了改造前后的对比指标改造前结构化技能后调用准确率54%88%误调用率23%6%平均回复长度312字176字端到端耗时3.8s2.9s这个数据已经很能说明问题模型不是不能选对而是之前缺少足够清晰的技能描述和边界约束。把接口文档写清楚效果提升是立竿见影的。评测集建议直接纳入 CI每次改技能描述或者加新技能都跑一遍避免回归。5. 常见问题与排查技巧实录运行一段时间后踩坑是必然的。下面这些问题是 agent-skills 在真实使用中最常遇到的每条都附上了排查思路。5.1 模型不调技能先在描述里找问题症状是用户问了天气模型却自己编了个答案。我第一时间怀疑模型能力不足后来才发现是技能描述太抽象。抽象的描述会让模型无法把用户口语和技能功能关联起来。修复方式就是重写描述把用户可能的问法直接写进去比如“北京明天还下雨吗”“上海最近适合穿什么”。这些真实句式比任何理论说明都管用。模型本质上是靠语义匹配来选技能的描述越贴近用户原话命中率越高。5.2 参数被模型理解错利用Schema兜底天气技能上线后我遇到一个很隐蔽的bug用户说“上海明天天气”模型传的 date 是当前日期而不是明天。原因是模型没有可靠的“今天是几号”认知它只是猜了一个日期。解决办法有三步一是在描述里明确写“日期按当前系统日期推算”二是在参数 Schema 里标记 format 为 date三是在执行器里做一层兜底如果传进来的日期等于今天且用户语句包含了“明天”“后天”等词就自动加一天。纯靠提示词不够必须用规则兜底。5.3 技能输出太长几行代码治理天气 API 的原始报文通常很冗长如果原样返回给模型上下文很快会被撑爆。我做了两个处理第一技能内部就做字段裁剪只返回 temperature、condition 这种真正有用的字段第二对必须返回的长文本增加截断逻辑把超长部分截成片段并带一个 has_more 标记。# skills/weather_query/skill.py def trim_result(self, data, max_len300): text f{data[city]} {data[date]} 温度{data[temperature]}度 {data[condition]} if len(text) max_len: return {summary: text[:max_len], has_more: True} return {summary: text, has_more: False}这里最重要的一条经验是能在技能内部解决的就不要让模型二次总结否则每个技能都会白白消耗大量 token。5.4 技能间互相打架设计触发条件当技能数量超过十个新的问题就来了天气查询和“出行建议”技能都可能匹配“明天能出去玩吗”这句话。模型有概率调用错。我的解决办法不是调整模型而是从描述层面动手。给每个技能加上 when_not_to_use明确排除对方的场景。例如出行建议技能写“本技能不负责查询具体天气数据如需温度、降水请调用 weather_query”。再配合 2.3 节说的冲突检测提前发现重叠描述。技能边界这件事越早梳理越省心。5.5 安全边界控制这一条必须单独拎出来强调。技能系统一旦接入 Shell、文件系统或外部 API风险等级立刻上升。我给执行器加了一层权限拦截所有技能在 execute 之前都要经过权限检查。def check_permission(skill, params): perm skill.manifest[permission] if perm.get(shell) and subprocess in str(params): raise PermissionError(shell execution denied) if perm.get(network): allowed perm[network] for url in extract_urls(params): if not any(url.startswith(prefix) for prefix in allowed): raise PermissionError(fnetwork access to {url} denied) if not perm.get(filesystem): raise PermissionError(filesystem access denied)我坚持一个原则默认拒绝显式放行。新技能没有声明权限就不允许访问任何外部资源。审计日志也必不可少每次技能调用记录时间、技能名、参数摘要、返回状态。这不是为了限制开发而是为了出问题的时候能回溯到底谁干的。6. 过程中的一些个人体会与后续扩展6.1 把技能库当乐高积木来维护做了这么久最大的感受是技能库像一套乐高积木单个技能要足够独立、接口要足够标准才能自由组合。但你不会想把所有积木都堆在一起——无意义的大并不等于强。我见过有人一口气塞了几百个技能结果模型选择负担极重效果反而变差。技能库需要克制保持每个技能边界清晰比追求数量重要得多。我每隔一段时间就会清理低调用率或者描述重叠的技能删掉之后整体效果往往还会提升。6.2 后续可以扩展的几个方向这套体系目前稳定运行扩展方向我也有几个正在尝试技能自动生成输入一个 OpenAPI 规范文档自动生成 manifest.yaml 和 skill.py 骨架。技能评测回归把评测集做成可执行脚本每次技能更新都跑一遍全量回归。技能热更新通过配置中心下发技能版本不用重启 Agent 就能加载新能力。多模态技能让技能不只处理文本还能接收图片和音频输入。如果你打算搭这么一套体系我的建议是先从两三个边界清晰的技能开始把注册、调度、校验、评测这条链路彻底打通再慢慢扩充。一开始总觉得是模型不够聪明才调不准技能后来发现把精力花在技能描述、参数约束和回归测试上效果立竿见影。这套“慢就是快”的路子在 agent-skills 里已经被验证了很多次。