
1. 从skills这个模糊词说起它到底指什么第一次看到skills这个标题加上后面跟着的一串热搜词——Agent Skills、Google Cloud、GKE、Genkit、claude agent skills、codex skills、skills开发、skills安装包——我脑子里第一反应是这词太泛了。但把热搜词串起来看方向其实很清晰它指向的是AI Agent 的能力扩展机制也就是给智能体装技能这件事。打个比方。一个刚出厂的大模型就像一个刚毕业的高材生脑子好使但没上过班。你让他写代码他会你让他查数据库他也能但你要他每天早上九点自动拉取昨天的销售数据、生成报表、发到群里他就懵了——不是不会而是不知道你们公司的流程、不知道数据在哪、不知道报表长什么样。Skills 就是把这些公司内部流程打包成一个个可复用的模块让 Agent 按需加载。所以这篇内容我想聊的不是某个具体产品的使用手册而是把skills这件事从底层逻辑到落地实操完整拆一遍。适合谁看三类人一是刚接触 Agent 开发、被各种 skills 概念绕晕的新手二是想把团队内部流程沉淀成可复用能力的工程师三是好奇给 AI 装技能到底是怎么回事的技术爱好者。不管你是哪一类看完应该能自己动手写一个能跑的 skill。需要先说明一点skills 这个概念在不同平台上有不同的实现形态。有的平台把它叫 Skill有的叫 Tool有的叫 Function还有的叫 Plugin。名字不一样但内核是一致的——用结构化的描述告诉模型有这么个能力什么时候该用怎么调用。理解了这层具体用哪家平台就只是语法差异了。2. Skills 的底层逻辑模型怎么学会用工具2.1 大模型本身不会调用任何东西这是最容易被误解的一点。很多人以为模型能联网能查数据库其实模型本身只会做一件事根据输入的文本预测下一个最合理的 token。它没有手没有眼睛不能真的去点按钮。那为什么我们看到的 Agent 能查天气、能读文件因为外面套了一层调度器。整个流程是这样的用户提问 → 模型判断这个问题需要查天气 → 模型输出一段结构化的调用请求比如get_weather(city北京)→ 调度器真的去执行这个函数 → 把结果塞回给模型 → 模型基于结果生成自然语言回答。Skills 就是这段结构化调用请求的规范定义。它告诉模型有这么个能力它叫什么名字接受什么参数参数是什么类型返回什么。模型看到这个定义就知道在什么场景下该输出什么样的调用请求。2.2 一个 Skill 的最小构成不管哪个平台一个 skill 的核心信息就那么几块我用一张表说清楚组成部分作用举例名称唯一标识模型靠它来引用query_sales_data描述最关键的部分模型靠它判断何时调用查询指定日期范围的销售数据参数定义告诉模型要传什么start_date: string, end_date: string执行逻辑真正干活的代码一段查数据库的 Python返回格式结果怎么给回模型JSON 或纯文本这里面描述description是最容易被低估的部分。我见过太多人把描述写成查询数据四个字然后抱怨模型老是不调用或者乱调用。描述写得好不好直接决定模型能不能在正确的时机选中这个 skill。好的描述应该包含这个能力做什么、什么场景下用、有什么限制。比如查询指定日期范围内的销售数据仅支持查询过去 12 个月内的数据返回按天聚合的销售额和订单数——这就比查询数据强太多。2.3 为什么是技能而不是一个大函数有人会问我直接把所有功能写成一个巨大的函数让模型调用不就行了为什么要拆成一个个 skill这里有个很实际的工程考量。模型的上下文窗口是有限的你塞进去的定义越多留给真正对话的空间就越少。而且定义太多模型选择时的准确率会下降——就像你给一个人 200 个按钮让他选他反而容易按错。拆成独立 skill 的好处是按需加载。平时只加载最常用的几个遇到特定任务再动态挂载对应的 skill。这跟手机装 App 是一个道理你不会把所有 App 都常驻后台用哪个开哪个。热搜词里出现的find skillsskills推荐skills大全本质上就是在解决我该装哪些技能这个问题。3. 动手写第一个 Skill从零到能跑3.1 环境准备里最容易忽略的两件事假设我们用最常见的 Python 生态来演示。开始之前有两件事必须先确认否则后面会莫名其妙报错。第一件是运行环境的版本。Agent 相关的库迭代很快很多新特性只在较新的版本里才有。我建议直接用 Python 3.10 以上虚拟环境隔离。别嫌麻烦我踩过的坑就是全局环境里装了一堆互相冲突的包最后排查了两小时才发现是版本问题。python -m venv skill_env source skill_env/bin/activate # Windows 用 skill_env\Scripts\activate pip install --upgrade pip第二件是密钥和配置的管理。任何要调用外部服务的 skill 都需要凭证。新手最容易犯的错是把密钥硬编码在代码里然后不小心提交到了公开仓库。正确做法是用环境变量或者配置文件并且把配置文件加进.gitignore。# .env 文件不要提交到仓库 API_KEYyour_key_here3.2 定义一个查询类 Skill 的完整过程我们来写一个真实场景的 skill查询某个城市的天气。虽然这个例子被用烂了但它麻雀虽小五脏俱全能覆盖 skill 开发的完整流程。第一步想清楚这个 skill 的边界。它只负责根据城市名返回当前天气不负责预报、不负责历史数据。边界清晰描述才能写准。第二步写定义。这里我用一种通用的 JSON Schema 风格来描述因为大多数平台都能接受这种格式{ name: get_current_weather, description: 查询指定城市的当前天气状况。当用户询问某地现在天气如何、气温多少、是否下雨时使用。仅支持查询当前时刻不支持历史或未来天气。, parameters: { type: object, properties: { city: { type: string, description: 城市名称使用中文例如北京、上海 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认摄氏度 } }, required: [city] } }注意描述里我特意写了当用户询问某地现在天气如何时使用这就是在给模型划场景。参数里unit用了枚举这样模型就不会瞎传一个摄氏度或者c这种不规范的值。第三步写执行逻辑import os import requests def get_current_weather(city: str, unit: str celsius): api_key os.environ.get(WEATHER_API_KEY) if not api_key: return {error: 未配置天气服务密钥} url https://api.example.com/weather params {city: city, unit: unit, key: api_key} try: resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() return { city: city, temperature: data[temp], condition: data[condition], unit: unit } except requests.Timeout: return {error: 请求超时请稍后重试} except Exception as e: return {error: f查询失败{str(e)}}这段代码里有几个细节值得说。超时一定要设不设的话遇到网络问题会一直挂着。异常要捕获并返回结构化错误而不是直接抛出去因为抛出去模型收到的是乱码它没法处理。返回的格式要稳定模型才能可靠地解析。3.3 把 Skill 挂载到 Agent 上定义和执行逻辑都有了接下来是注册。不同平台的注册方式不同但逻辑一样把定义告诉模型把执行函数和定义绑定起来。from agent_framework import Agent, Skill agent Agent(modelyour-model) weather_skill Skill( definitionweather_definition, handlerget_current_weather ) agent.register_skill(weather_skill) response agent.chat(北京现在多少度) print(response)跑通之后你会看到模型自动识别出需要调用天气 skill传入了city北京拿到结果后组织成自然语言回答。第一次看到这个流程跑通的时候确实有种打开新世界的感觉——原来给 AI 装技能就是这么回事。4. 描述写不好Skill 等于白写4.1 三个真实的翻车案例我在实际项目里见过太多因为描述写得烂导致的诡异问题挑三个典型的说说。案例一模型死活不调用。有个同事写了个查订单的 skill描述是订单相关操作。结果用户问帮我看看订单 12345 到哪了模型直接开始编说您的订单正在派送中。问题就出在描述太模糊模型不确定这个 skill 是不是该用。改成根据订单号查询订单的当前物流状态和预计送达时间之后立刻就正常了。案例二模型乱调用。另一个项目里有两个 skill一个叫search_product一个叫search_order描述分别是搜索商品和搜索订单。结果用户问搜一下我的订单模型有时候调商品那个。原因是两个描述太像模型分不清边界。后来把描述改成根据关键词搜索商品库中的商品信息返回商品名称、价格、库存和根据订单号或用户手机号搜索订单记录返回订单状态和金额区分度一下就上来了。案例三参数传错。有个 skill 需要日期参数描述只写了日期。模型有时候传2024-01-01有时候传2024年1月1日有时候传昨天。执行函数直接崩了。后来在参数描述里明确写日期格式必须是 YYYY-MM-DD例如 2024-01-01问题解决。4.2 好描述的四个要素从这些案例里我总结出一个好描述应该包含的四块内容做什么一句话说清功能动词开头何时用明确触发场景最好带上用户可能的问法边界在哪不支持什么有什么限制参数细节格式、取值范围、默认值把这四块写全模型的调用准确率会有肉眼可见的提升。这不是玄学因为模型判断要不要调用完全依赖这段文字你给的信息越充分它的判断就越准。4.3 描述和参数定义的配合描述和参数定义是互相配合的。描述负责要不要用参数定义负责怎么用。有些信息放哪边都行但有个原则跟是否触发相关的放描述跟怎么执行相关的放参数。比如仅支持查询过去 12 个月的数据这种限制放描述里因为模型需要据此判断当前请求是否在能力范围内。而日期格式 YYYY-MM-DD放参数描述里因为这是执行细节。5. 多 Skill 协作时的调度难题5.1 当 Skill 数量超过十个单个 skill 跑通不难难的是当你有几十个 skill 的时候怎么让模型准确选中该用的那个。这是热搜词里skills大全find skills背后真正的痛点。我做过一个统计在 skill 数量少于 8 个的时候模型的选择准确率通常能到 95% 以上。超过 15 个之后准确率会明显下滑尤其是那些功能相近的 skill 之间容易混淆。到 30 个以上如果不做任何优化准确率可能掉到 70% 以下。5.2 分层加载的思路解决这个问题的核心思路是分层。不要把所有 skill 一次性全塞给模型而是按场景分组先让模型选组再在组内选具体 skill。具体做法是维护一个 skill 索引每个 skill 除了自己的定义还带一个分类标签。用户提问时先用一个轻量的分类步骤确定大概方向然后只加载那个方向下的 skill。SKILL_REGISTRY { weather: [get_current_weather, get_forecast], order: [search_order, cancel_order, track_order], product: [search_product, get_product_detail] } def route_and_load(query): category classify_query(query) # 一个轻量分类 return SKILL_REGISTRY.get(category, [])这个分类步骤可以用一个更小的模型来做成本低、速度快。实测下来分层之后即使总 skill 数量到 50 个准确率也能维持在 90% 以上。5.3 Skill 之间的依赖和冲突还有一种情况是 skill 之间有依赖。比如生成月度报表这个 skill内部其实需要先调查询销售数据再调格式化输出。这时候有两种处理方式一是把依赖关系写进执行逻辑里对外只暴露一个 skill二是让模型自己编排先调 A 再调 B。我的经验是能用第一种就用第一种。让模型自己编排多个 skill出错概率会成倍增加而且调试起来很痛苦。把复杂流程封装成一个原子 skill对外简单对内复杂这是更稳妥的工程做法。6. 调试与测试怎么知道 Skill 真的靠谱6.1 别只测正常路径新手测试 skill 通常只测一种情况用户正常提问skill 正常返回。这远远不够。真正要测的是各种边界和异常。我一般会准备这么一组测试用例测试类型输入示例期望行为正常调用北京天气正确调用并返回不该调用你好不调用任何 skill参数缺失查天气追问城市而非报错参数异常查火星天气优雅提示不支持服务超时模拟超时返回友好错误并发调用连续多个请求互不干扰这组用例跑下来基本能覆盖 80% 的线上问题。6.2 日志要记什么调试 skill 的时候日志是命根子。但日志不是记得越多越好关键要记这几样模型决定调用哪个 skill、传了什么参数、执行耗时、返回了什么、有没有报错。import logging import time def logged_handler(func): def wrapper(*args, **kwargs): start time.time() logging.info(f调用 {func.__name__}, 参数: {kwargs}) try: result func(*args, **kwargs) logging.info(f{func.__name__} 返回: {result}, 耗时: {time.time()-start:.2f}s) return result except Exception as e: logging.error(f{func.__name__} 异常: {e}) raise return wrapper有了这些日志出问题的时候你能快速定位是模型没调还是调了但参数错还是执行失败。这三种情况的排查方向完全不同。6.3 一个反直觉的经验有个经验可能跟直觉相反skill 执行得慢有时候反而是好事。因为如果 skill 秒回模型可能会倾向于频繁调用它哪怕不该调的时候也调。而如果 skill 有明显的耗时模型在决策时会稍微谨慎一点。当然这不是让你故意拖慢而是说不要为了追求极致速度而牺牲了调用的准确性。7. 把 Skill 工程化的几个关键决策7.1 版本管理不能省Skill 是会迭代的。今天描述写支持查询 12 个月明天业务要求改成 24 个月描述就得改。改了之后模型的行为可能就变了。所以 skill 必须做版本管理每次改动都要记录改了什么、为什么改、改完效果如何。我建议给每个 skill 维护一个简单的变更日志哪怕就是在一个 markdown 文件里记几行。出问题的时候能快速回溯是哪次改动引入的。7.2 权限和边界要卡死Skill 是 Agent 的手手能伸多远必须提前定好。查询类 skill 只读不写操作类 skill 要有确认机制涉及敏感数据的要有权限校验。这些不能指望模型自觉必须在执行逻辑里硬性卡住。比如一个删除订单的 skill执行逻辑里必须先校验调用者身份再校验订单状态是否允许删除最后才执行。模型传什么参数是一回事执行层认不认是另一回事。7.3 监控和降级线上跑的 skill 必须有监控。调用量、成功率、平均耗时、错误分布这些指标要能看到。一旦某个 skill 错误率飙升要能快速降级——要么临时禁用要么切到备用逻辑。class SkillMonitor: def __init__(self): self.stats {} def record(self, name, success, duration): if name not in self.stats: self.stats[name] {total: 0, fail: 0, time: []} self.stats[name][total] 1 if not success: self.stats[name][fail] 1 self.stats[name][time].append(duration) def health(self, name): s self.stats.get(name) if not s or s[total] 0: return unknown fail_rate s[fail] / s[total] return healthy if fail_rate 0.05 else degraded这套东西看起来是额外工作但真到了线上出问题的时候有没有这套监控排查效率差十倍。8. 关于 Skills 这件事我踩过的坑最后分享几个我自己踩过的、文档里不会写的坑。第一个坑以为描述越长越好。刚开始我恨不得把 skill 的所有细节都写进描述结果发现模型反而抓不住重点。后来才明白描述要精炼把最关键的做什么、何时用说清楚就行细节放参数定义里。描述超过 200 字模型的理解反而会下降。第二个坑忽略了 skill 名称的影响。名称不只是标识模型也会参考。get_weather和weather_query_helper_v2这两个名字前者模型一看就懂后者得琢磨半天。名称用动词加名词的简单结构全小写下划线分隔最稳妥。第三个坑在描述里写实现细节。比如本 skill 使用 requests 库调用第三方 API这种信息对模型判断毫无帮助纯属占地方。描述是给模型看的不是给同事看的写模型需要知道的就行。第四个坑不做灰度就全量上线。改了一个 skill 的描述直接全量推上去结果模型行为大变一堆用户反馈异常。后来学乖了任何 skill 改动先小流量验证确认没问题再全量。第五个坑忘了 skill 也是代码需要测试。很多人把 skill 当成配置觉得改改描述而已不用测。但描述改动对模型行为的影响有时候比代码改动还大。每次改完描述那组测试用例必须重跑一遍。这些坑说到底都指向一件事Skills 看起来简单但它是模型和真实世界之间的接口接口设计得好不好直接决定整个 Agent 好不好用。把 skill 当正经工程来做而不是当临时配置来凑合这是我从一堆翻车经历里学到的最值钱的一课。