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 skills""skills推荐""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_KEY=your_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, params=params, timeout=10) 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(model="your-model") weather_skill = Skill( definition=weather_definition, handler=get_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 当正经工程来做,而不是当临时配置来凑合,这是我从一堆翻车经历里学到的最值钱的一课。