过去这几年,但凡接触过 AI Agent 开发的同学,应该都经历过一个相似的阶段:看教程的时候觉得思路很清晰,模型会自己规划、自己调工具、自己总结结果;可真到自己动手写代码时,才发现怎么把“工具”交给模型这件事,本身就充满了歧义。
有人把工具直接塞进 System Prompt,塞到上下文爆炸;有人把每个功能都写成独立的 Function Calling 函数,最后函数列表长得像本词典;还有人干脆不用工具,让模型“凭感觉”输出 JSON,然后在前端硬解析。
换句话说,很多人学会的并不是 Agent 开发,而是一堆零散的 API 调用。
最近 Agent Skills 这个概念热度很高,吴恩达也专门出过相关教程。很多开发者看完后最大的感受是:原来工具能力不应该是一堆散装函数,而应该是一套可复用、可发现、可组合的“技能包”。
这篇文章不打算复述某个现成教程,而是按照“为什么需要 -> 概念辨析 -> 核心原理 -> 最小实现 -> 实战示例 -> 排错清单 -> 工程建议”这条线,带你完整跑通 Agent Skills 从入门到代码实战的整个过程。
1. 这篇文章真正要解决的问题
先说判断:Agent Skills 并不是一个全新的技术框架,也不是某个厂商的私有协议。它本质上是在回答一个非常现实的问题——当你给 Agent 开发技能时,怎么让这些技能看起来不像临时补丁,而像一套可以积累、可被模型自动发现和调用的标准能力库。
为什么这个问题重要?
因为大多数开发者第一次做 Agent 时,都会陷入下面三种困境之一。
第一种,工具函数越写越多,但复用全靠复制粘贴。项目 A 里写了一个查询库存的函数,项目 B 想做同样的功能,只能把代码拷过去再改一改。时间一长,同一种能力在多个项目里各自为政,行为不一致,修 bug 要修好几遍。
第二种,上下文塞满规则,效果却越来越差。为了让模型知道“什么时候该用什么工具”,有人把工具说明、参数含义、注意事项全部写进 System Prompt。Prompt 越来越长,模型反而容易忽略关键信息,还增加了 token 成本和推理延迟。
第三种,工具之间没有任何组合逻辑。模型只会“用某个函数”,不会“把几个函数串起来完成一个完整任务”。真正的智能体应该会观察、调用、再观察、再调用,而不是一次性输出所有参数的硬编码结果。
Agent Skills 的思路是:把工具能力从散装函数提升为带元信息的独立模块。每一个 Skill 都包含名称、描述、参数 schema、执行逻辑,甚至还可以包含自己的 few-shot 示例和内部依赖。Agent 在运行时会先“看到”有哪些可用的 Skills,再根据用户任务动态选择、加载和调用。
读到这里你应该能感觉到,这个方向解决的并不是“模型聪不聪明”的问题,而是“工程化地组织模型能力”的问题。对大多数团队来说,后者才是 Agent 能不能真正落到生产的关键。
2. Agent Skills 与 Function Calling、Tools、Agent 的关系
很多同学容易把这几个词混在一起,我先把它们的边界讲清楚。
Function Calling 是模型侧的一种能力,指模型在收到用户请求后,不是直接生成最终答案,而是生成一个“我想调用某个函数,参数是哪些”的结构化输出。它解决的是“模型如何表达调用意图”的问题。
Tools 是开发者提供给模型的一组函数描述,通常包括函数名、功能描述和参数 JSON Schema。模型在 Function Calling 时,会从 Tools 列表里挑一个最匹配的。它解决的是“模型能选择哪些操作”的问题。
Agent 是一个完整系统,它把大模型、Tools、记忆、任务拆解、结果验证和执行循环整合在一起,让模型可以自主完成多步任务。Tools 是 Agent 的手脚,模型是 Agent 的大脑。
Agent Skills 则是在 Tools 之上的一层组织和标准化机制。一个 Skill 可以是一个 Tool,也可以是多个 Tools 的组合,甚至可以包含自己的提示词片段、示例和前置条件。
我用一个表格把它们的区别列清楚:
| 概念 | 解决的问题 | 粒度 | 典型表现 |
|---|---|---|---|
| Function Calling | 模型怎么输出调用意图 | 单次调用 | 模型输出{name: "get_weather", args: {...}} |
| Tools / Function | 模型可以操作哪些外部能力 | 单个函数 | get_weather(city) |
| Agent | 如何拆解任务、循环执行、验证结果 | 完整系统 | 规划 -> 调用 -> 观察 -> 再规划 |
| Agent Skills | 如何组织、声明、复用一组工具能力 | 能力模块 | 一个技能包包含多个函数、描述、示例和配置 |
从这个表可以看出,Agent Skills 不是要替代 Function Calling,也不是要重写 Agent 框架,而是补上了 Tools 和 Agent 之间的工程化短板。
举一个更容易理解的类比:如果把 Agent 比作一个开发者,Tools 是这个开发者会写的单个函数,Agent Skills 则是他把函数整理成了带文档、带示例、可被人(模型)检索调用的代码库。没有代码库,他也能临时写函数;有了代码库,效率和质量才能稳定下来。
3. Agent Skills 的核心原理与设计目标
要理解 Agent Skills,不需要先背协议,而是先理解它设计上的三个核心目标。
3.1 技能的可发现性
一个技能如果不被模型知道,就等于不存在。所以 Agent Skills 强调“技能描述要写得好”。这个描述不是给人看的文档,而是给模型看的检索索引。描述写得越准确,模型在需要某个能力时就越容易匹配到它。
实际编码时,每个技能都可以包含一个description字段,这个字段会随着技能一起加载给模型。比如你写了一个处理时间数据的技能,描述可以写成“将任意格式的时间字符串解析为标准时间对象,支持时区转换和日期计算”。模型听到用户问“帮我算一下三天后是哪天”,就会优先匹配这个技能。
3.2 技能的自包含性
一个技能最好把自己的“说明书”和“实现代码”放在一起。模型需要更多上下文时,系统可以把技能的详细说明、示例、前置条件一并喂给模型。这种设计避免了一股脑把所有工具说明塞进 Prompt,而是在真正需要某个技能时才加载完整的技能说明。
这种按需加载的思路,正好解决了开头提到的问题:工具多了之后,Prompt 不可能无限变长。Agent Skills 让“技能发现”和“技能执行”分离,先用简短描述做完匹配,再去加载需要的内容。
3.3 技能的复用与组合
一个 Skill 可以依赖另一个 Skill。比如“生成周报”这个技能,可能内部会调用“获取项目数据”和“格式化 Markdown 表格”这两个子技能。这种依赖关系如果写死在代码里,灵活性会很差;如果通过技能清单来声明,那么模型在任务执行时就能自己判断该调用哪些组合。
这也是 Agent Skills 区别于普通工具函数最大的地方:工具函数是平面的,技能是立体的,它可以有层级、有依赖、有上下文。
理解了这三个目标,再看后面的代码就会轻松很多。我们做的小框架不需要多复杂,只要能体现这三个设计原则,就已经跑通了 Agent Skills 的核心思路。
4. 环境准备与前置条件
接下来进入实操部分。下面的示例使用 Python 实现,选择 Python 是因为它在 AI 生态中最通用,示例代码尽量不依赖特定框架,方便你看清 Agent Skills 本身的运行逻辑。
需要准备的环境如下:
- Python 3.9 及以上版本,建议 3.10 或 3.11。
- 一个可以调用的大模型 API,OpenAI 兼容格式即可。示例中会用到
openai库,如果你用的是其他模型服务,只要兼容 OpenAI API 都可以替换。 - 一个用于测试的 API Key。
- 建议准备一个虚拟环境,避免污染全局环境。
安装依赖:
python -m venv venv source venv/bin/activate pip install openai如果网络环境下载速度慢,可以使用国内镜像源:
pip install openai -i https://pypi.tuna.tsinghua.edu.cn/simple关于模型版本,这里不做死板要求。实际开发时,支持 Function Calling 的模型都可以跑通这套逻辑。如果你使用的模型不支持 Function Calling,也可以用“让模型输出结构化 JSON”的方式替代,后面会提到这种退化方案。
5. Agent Skills 最小实现:从零搭建技能注册与加载机制
我们开始写代码。这一节先搭建一个最小的 Agent Skills 骨架,不引入任何重型框架,所有代码控制在几个文件里。
5.1 定义技能目录结构与元信息
先约定一个技能目录,每个技能以文件夹形式存在,里面包含一个skill.json描述文件和 Python 实现文件:
skills/ ├── get_time/ │ ├── skill.json │ └── skill.py ├── calculator/ │ ├── skill.json │ └── skill.py └── weather_query/ ├── skill.json └── skill.pyskill.json是这个技能的元信息,它要告诉系统三件事:这个技能是做什么的、它有什么参数、它暴露了什么函数。
以get_time为例,skill.json内容如下:
{ "name": "get_time", "description": "获取当前时间,支持时区转换与日期计算,适合回答"现在几点"、"三天后是哪天"等时间类问题。", "version": "1.0.0", "functions": [ { "name": "fetch_current_time", "description": "获取指定时区的当前时间", "parameters": { "type": "object", "properties": { "timezone": { "type": "string", "description": "时区名称,例如 Asia/Shanghai" } }, "required": ["timezone"] } } ] }这个文件的价值在于:它把技能的“发现信息”和“调用细节”解耦。系统在加载技能时,可以只把name和description拼进 Prompt 给模型做选择;等到模型真正调用了,再去导入skill.py执行。
5.2 写技能实现模块
skill.py中实现真正的函数逻辑。这里要保证函数名和最外层的 JSON 结构一致,否则运行时会找不到函数:
# 文件路径:skills/get_time/skill.py from datetime import datetime, timedelta from zoneinfo import ZoneInfo def fetch_current_time(timezone: str = "Asia/Shanghai") -> str: """获取指定时区的当前时间""" try: tz = ZoneInfo(timezone) except Exception: tz = ZoneInfo("Asia/Shanghai") timezone = "Asia/Shanghai(参数无效,使用默认时区)" now = datetime.now(tz) return { "timezone": timezone, "current_time": now.strftime("%Y-%m-%d %H:%M:%S") }注意这里返回的是 JSON 可序列化的字典,这样 Agent 拿到结果后可以直接把它转成字符串继续推理,不用处理复杂对象。
5.3 技能注册器 SkillRegistry
现在需要一个注册器来扫描技能目录、读取描述、动态导入函数。这个类承担两个职责:加载技能元信息、根据函数名分发调用。
# 文件路径:skill_registry.py import importlib import json from pathlib import Path from typing import Any, Callable, Dict class SkillRegistry: def __init__(self, skills_dir: str = "skills"): self.skills_dir = Path(skills_dir) self._skills_meta: Dict[str, Dict] = {} self._function_map: Dict[str, Callable] = {} def load_all_skills(self) -> None: if not self.skills_dir.exists(): raise FileNotFoundError(f"技能目录不存在: {self.skills_dir}") for skill_dir in self.skills_dir.iterdir(): if not skill_dir.is_dir(): continue meta_file = skill_dir / "skill.json" skill_file = skill_dir / "skill.py" if not meta_file.exists() or not skill_file.exists(): continue meta = json.loads(meta_file.read_text(encoding="utf-8")) skill_name = meta["name"] for func in meta["functions"]: func_name = func["name"] self._function_map[func_name] = self._import_function( f"skills.{skill_name}.skill", func_name ) self._skills_meta[skill_name] = meta def _import_function(self, module_name: str, func_name: str) -> Callable: module = importlib.import_module(module_name) return getattr(module, func_name) def get_tools_for_llm(self) -> list: tools = [] for meta in self._skills_meta.values(): for func in meta["functions"]: tools.append( { "type": "function", "function": { "name": func["name"], "description": func["description"], "parameters": func["parameters"], }, } ) return tools def execute(self, func_name: str, args: Dict[str, Any]) -> Any: if func_name not in self._function_map: raise ValueError(f"未注册的函数: {func_name}") return self._function_map[func_name](**args)这段代码看起来不多,但它完成了整个 Agent Skills 的骨架:
load_all_skills()扫描目录,读取skill.json,注册函数。get_tools_for_llm()把技能元信息转成模型需要的 tools 格式。execute()根据函数名分发执行,对模型来说,它只需要知道函数名和参数,完全不关心函数在哪个模块里。
6. 实战:实现查询天气技能与计算器技能
为了验证这套机制能跑通,我们再添加两个技能:一个查询天气(模拟数据),一个执行数学计算。
6.1 查询天气技能
先创建目录:
mkdir -p skills/weather_queryskill.json内容:
{ "name": "weather_query", "description": "查询指定城市当前天气,支持获取温度和天气状况。", "version": "1.0.0", "functions": [ { "name": "get_weather", "description": "获取指定城市的当前天气信息,返回天气状况和温度", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如 北京、上海、广州" } }, "required": ["city"] } } ] }skill.py内容:
# 文件路径:skills/weather_query/skill.py import random def get_weather(city: str) -> str: """获取指定城市的模拟天气数据,实际项目中可以替换为真实天气 API""" weather_map = { "北京": {"condition": "晴", "temperature": 23}, "上海": {"condition": "小雨", "temperature": 19}, "广州": {"condition": "多云", "temperature": 28}, "深圳": {"condition": "晴", "temperature": 30}, } data = weather_map.get(city, {"condition": "未知", "temperature": random.randint(15, 30)}) return {"city": city, "condition": data["condition"], "temperature": data["temperature"]}6.2 计算器技能
再添加一个计算器技能:
mkdir -p skills/calculatorskill.json内容:
{ "name": "calculator", "description": "执行基础数学运算,支持加减乘除。", "version": "1.0.0", "functions": [ { "name": "calculate", "description": "计算两个数字的数学表达式结果", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "数学表达式,例如 1 + 2 * 3" } }, "required": ["expression"] } } ] }skill.py内容:
# 文件路径:skills/calculator/skill.py import ast import operator def calculate(expression: str) -> float: """安全计算只包含数字和四则运算的数学表达式""" allowed_operators = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, } def eval_expr(node): if isinstance(node, ast.Expression): return eval_expr(node.body) if isinstance(node, ast.BinOp) and type(node.op) in allowed_operators: left = eval_expr(node.left) right = eval_expr(node.right) return allowed_operators[type(node.op)](left, right) if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return node.value raise ValueError(f"不支持的表达式: {expression}") tree = ast.parse(expression, mode="eval") return eval_expr(tree)这里的实现故意只允许数字和四则运算,是为了避免直接使用eval()带来的安全问题。在真实项目中,如果技能里面要执行一段来自模型或用户的代码,务必先做白名单校验,不要直接信任输入。
7. 把技能交给模型:完整调用链
技能装好了,接下来写一个 Demo,演示模型如何根据用户问题自动选择技能、生成参数并执行。
# 文件路径:agent_demo.py import json from openai import OpenAI from skill_registry import SkillRegistry def run_agent(user_query: str) -> str: registry = SkillRegistry() registry.load_all_skills() tools = registry.get_tools_for_llm() client = OpenAI() messages = [ {"role": "system", "content": "你是一个智能助手,可以调用可用技能来回答用户问题。"}, {"role": "user", "content": user_query}, ] # 第一次请求:让模型决定是否需要调用技能 response = client.chat.completions.create( model="你的模型名称", messages=messages, tools=tools, tool_choice="auto", ) response_message = response.choices[0].message # 如果模型没有产生工具调用,直接返回文本结果 if not response_message.tool_calls: return response_message.content # 执行每个技能调用 tool_results = [] for tool_call in response_message.tool_calls: func_name = tool_call.function.name func_args = json.loads(tool_call.function.arguments) print(f"[Agent 调用技能] {func_name}({func_args})") result = registry.execute(func_name, func_args) tool_results.append( { "tool_call_id": tool_call.id, "role": "tool", "name": func_name, "content": json.dumps(result, ensure_ascii=False), } ) # 把技能结果回传给模型,让模型做出最终回答 messages.append(response_message) messages.extend(tool_results) final_response = client.chat.completions.create( model="你的模型名称", messages=messages, ) return final_response.choices[0].message.content if __name__ == "__main__": query = "北京今天天气怎么样?另外帮我算一下 12 * 8 + 5 等于多少。" answer = run_agent(query) print("最终回答:", answer)这段代码的流程是标准的 Agent 工具调用循环:
- 加载全部技能,转换为模型可识别的
tools格式。 - 发送用户问题,模型判断是否需要调用技能。
- 如果模型返回了
tool_calls,逐条执行并收集结果。 - 把工具执行结果再次发给模型,让模型生成最终的自然语言回答。
执行前需要把代码中的"你的模型名称"替换成实际可用的模型 ID。如果你使用的是 OpenAI,可以填gpt-4o-mini;如果使用的是国产模型、开源模型或其他兼容服务,在初始化OpenAI()时传入base_url即可。
8. 运行结果与效果验证
运行 Demo:
python agent_demo.py正常情况下,应看到类似下面的输出:
[Agent 调用技能] get_weather({'city': '北京'}) [Agent 调用技能] calculate({'expression': '12 * 8 + 5'}) 最终回答: 北京今天天气晴朗,气温 23 摄氏度。计算 12 * 8 + 5 的结果是 101。这代表整个 Agent Skills 链路已经通了:模型发现需要两个技能、自动生成参数、通过注册器执行函数、再把结果综合成回答。
如果执行过程中什么输出都没有,或者模型直接返回了“我不知道”,可以从以下几点排查:
- 技能描述是否足够清晰。描述写得太模糊,模型可能匹配不到你的技能。
- 模型是否支持 Function Calling。确认使用的模型 API 支持
tools参数。 - 是否存在导入路径问题。确保当前目录结构是
skills/xxx/skill.py,运行命令时在项目根目录下执行。
如果模型不支持 Function Calling,还有一种降级方案:把get_tools_for_llm()返回的 JSON 结构直接拼进 System Prompt,然后要求模型输出固定 JSON 格式的调用请求。代码会变成字符串解析,稍微“脏”一点,但思路完全一致。
9. 常见问题与排查方法
在实际开发中,会遇到下面几个高频问题,这里统一整理成表格,方便收藏查阅。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型没有产生任何工具调用 | 技能描述语义模糊,模型不理解何时使用 | 打印tools原样查看描述是否准确 | 重写description,突出“在什么情况下使用” |
| 提示函数不存在,执行报错 | skill.json中函数名与skill.py中函数名不一致 | 检查 JSON 中 functions.name 与 Python 定义 | 统一命名,建议使用相同字符串常量 |
| 工具参数解析失败 | 模型生成的参数 JSON 非法或字段缺失 | 打印tool_call.function.arguments | 增加异常捕获,参数校验后执行 |
| 上下文过长,请求报错 | 工具描述过多或历史消息过长 | 检查实际 token 消耗 | 精简技能描述,考虑对历史消息做摘要 |
| 技能执行耗时过长 | 技能内部调用了外部 API 或数据库 | 增加超时控制和日志埋点 | 为技能执行设置超时,异步执行调用 |
| 多技能并行调用互相干扰 | 技能内部使用了不安全的全局状态 | 检查技能代码是否有共享可变变量 | 技能内部避免使用全局变量,改为局部数据 |
如果你的项目运行中出现了没有在表里的问题,建议先在技能入口处加日志,打印出“模型返回的原始内容”和“技能执行后的返回内容”。绝大多数 Agent 问题,根源都在数据格式和参数传递上,而不是模型能力本身。
10. 最佳实践与工程建议
跑通最小示例之后,如果要在真实项目中使用 Agent Skills,下面这些工程经验值得参考。
10.1 技能描述先写“场景”再写“功能”
模型选择技能靠的是语义匹配,而不是关键词精确匹配。描述里最好告诉模型“什么时候用这个技能”。比如:
较差的描述:天气查询函数 较好的描述:当用户询问某城市当前天气、温度、降水情况时,使用该技能获取实时气象数据。这样模型在遇到口语化问题时,也能准确命中。
10.2 技能包要控制体积
一个 Skill 的描述和示例并不是越多越好。每次请求时,所有技能的元信息都会占用上下文。如果技能数量膨胀到几十个,模型反而容易“乱选”。建议控制单个技能的描述在 200 字以内,技能总数控制在 10 个以内,超出部分做多级分类或独立服务。
10.3 技能执行要加超时和错误处理
Agent 在真实环境中可能遇到网络超时、第三方 API 报错、数据格式异常等情况。任何一个技能抛出未捕获异常,都会打断整个 Agent 循环。更稳妥的做法是让execute()永远返回可序列化的结果,即使出错也把错误信息转成 JSON 返回给模型,让模型决定如何向用户解释。
def execute(self, func_name: str, args: Dict[str, Any]) -> Any: if func_name not in self._function_map: return {"error": f"未注册的函数: {func_name}"} try: return self._function_map[func_name](**args) except Exception as e: return {"error": str(e)}这样调整之后,Agent 遇到再复杂的失败情况都能继续对话,而不是直接崩溃。
10.4 安全边界与权限控制
技能执行权限遵循最小权限原则。如果一个技能只是查询数据,就不要给它数据修改权限;如果一个技能需要访问数据库,建议在技能内部走独立的只读账号或独立连接串。Agent 的调用入口很容易被提示注入影响,不要盲目相信模型生成的参数,对危险操作做二次确认。
特别提醒:不要在任何技能中使用裸eval()执行模型生成的代码,除非你做了严格的白名单校验。实际项目中,99% 的场景可以通过ast解析、参数约束或沙箱执行来解决。
10.5 版本管理与灰度发布
技能也是有版本的。推荐在skill.json中维护version字段,并在技能目录名中保留版本信息。发布新版本技能时,先让少量流量试用新版本,稳定后再全量切换。回滚时直接把模型可加载的技能目录切回上一个版本即可。
11. 总结与后续学习方向
这篇文章从 Agent Skills 的定位讲起,说清楚了它和 Function Calling、Tools、Agent 的关系,然后通过一个最小框架实现了技能注册、描述加载、模型调用、函数分发、结果回传的完整链路。你现在能够做到的,应该包括:
理解 Agent Skills 为什么不是新框架,而是一套工具组织范式;能够为自己的项目编写skill.json描述;能够写一个基础的技能注册器;能够让大模型自动发现并调用技能;能够在技能执行失败时进行基础排查。
下一步你可以往三个方向继续深入:
第一,把技能从本地函数扩展为远程服务。技能内部调用 HTTP API 或微服务,这样技能包就变成了一个跨项目复用的能力网关。
第二,给技能加入内部示例和 few-shot。在skill.json中增加示例输入输出,模型在复杂场景下的参数生成准确率会明显提升。
第三,把技能和记忆机制结合。让 Agent 记住用户的偏好,在执行任务时自动适配参数,比如用户习惯查看摄氏温度还是华氏温度,这类信息可以从历史对话中提取并注入技能调用参数。
这些内容再往后就是 Agent 工程化最核心的设计题了。真正写好一个 Agent,从来不只是堆模型能力,而是把你熟悉的功能梳理成模型能看懂的、安全可控的、可复用的技能体系。建议把文章里的最小示例跑通一次,然后挑一个你自己项目里的常用功能,改造成第一个 Skill,你会明显体会到这种写法和“塞入 Prompt”之间的差别。