AI Agent技能包实战:从散装函数到可复用Agent Skills组织范式
2026/9/7 7:05:23 网站建设 项目流程

过去这几年,但凡接触过 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.py

skill.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"] } } ] }

这个文件的价值在于:它把技能的“发现信息”和“调用细节”解耦。系统在加载技能时,可以只把namedescription拼进 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_query

skill.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/calculator

skill.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 工具调用循环:

  1. 加载全部技能,转换为模型可识别的tools格式。
  2. 发送用户问题,模型判断是否需要调用技能。
  3. 如果模型返回了tool_calls,逐条执行并收集结果。
  4. 把工具执行结果再次发给模型,让模型生成最终的自然语言回答。

执行前需要把代码中的"你的模型名称"替换成实际可用的模型 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”之间的差别。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询