最近在做 AI Agent 应用时,我发现一个非常奇特的局面:网上讲 Agent、讲 MCP、讲 Function Calling 的内容铺天盖地,但真正把 Agent Skill 这个概念讲清楚,并且能带着你从零写出可运行代码的教程却很少。很多人看完视频后仍然不清楚:Skill 和 Agent 到底什么关系?Skill 和 MCP 有什么区别?我自己的业务场景里到底要不要引入 Skill?
这篇文章就围绕 Agent Skill 展开。我会用一套完整、可复制的 Python 代码,带你从概念理解到代码实战,手把手实现一个支持 Skill 调用的最小 Agent 系统。文章内容适合正在做 AI 大模型应用开发、想深入理解 Agent 技术栈、或者准备在业务中引入智能体能力的开发者。读完你会明白 Agent Skill 是什么、如何设计、如何接入大模型、如何排错,以及如何在本地部署场景下适配。
1. 为什么要把 Agent Skill 单独拿出来讲
1.1 Agent 应用开发的核心问题
在实际开发中,我们使用大模型时最常遇到的问题是:模型“能说不能做”。你问它“帮我查一下今天的天气”,它能告诉你 API 应该怎么调用,但它自己并不会真的去请求天气服务;你让它“把上个月订单汇总成表格”,它能写出处理逻辑,但不会真正读取数据库、生成文件。
要解决这个问题,业内已经有了比较统一的思路:把模型作为“决策大脑”,把具体操作封装成可被模型调用的工具。模型不直接执行操作,而是输出“我要调用哪个工具、参数是什么”,然后由程序执行真实逻辑。这套机制的通用名称是 Function Calling 或 Tool Calling,而 Agent Skill 正是在这个机制之上,把“工具”进一步工程化、语义化、可维护化的一种能力封装方式。
但很多开发者把 Skill 简单理解成“给模型写个函数”,这就把问题想窄了。Skill 不只是函数,它包含两层:一层是给模型看的“说明书”,告诉模型什么时候该用它、参数怎么填;另一层是给程序跑的“实现体”,真正完成业务逻辑。两者缺一不可。这也是我在文章标题里强调“入门到代码实战”的原因——只有把这两层都写清楚,Skill 才真正可用。
1.2 Agent Skill 的通俗解释
用一个大家熟悉的例子说明。假设你把大模型看作一个新入职的实习生,它聪明、知识面广,但刚来公司,不知道你们内部系统的操作方式。Skill 就相当于给这个实习生配套的“岗位技能卡”。每张技能卡正面写着:这个技能是干什么的、在什么场景下使用、需要提供什么信息;反面写着:具体怎么执行。
实习生(Agent)拿到你的问题后,先看技能卡,判断应该使用哪张;然后把参数填好,交给执行部门(程序代码)去完成。执行部门把结果回传给实习生,实习生再整理成自然语言回复给你。
从这个角度看,Agent Skill 是由三部分组成的:
- Skill 元数据:包括技能名称、功能描述、参数定义。
- Skill 执行函数:真正运行业务逻辑的代码。
- Agent 调度逻辑:把用户输入转发给大模型,让模型决定调用哪个 Skill,再执行并把结果返回给模型。
1.3 为什么这个概念仍然值得系统学习
2026 年这个节点,大模型应用已经从前期的“你问我答”阶段,走向“真实任务执行”阶段。企业做 AI 落地,不再是简单接一个 ChatBot,而是希望 AI 能联动业务系统、数据库、审批流、第三方接口。Agent Skill 正是这套体系里非常基础、也非常重要的工程单元。
需要特别强调的是,Skill 的引入成本和模型微调相比要低得多。微调一个行业模型需要整理训练集、准备机器资源、反复实验,周期长、成本高;而定义一个 Skill 只需要写一个函数和一段描述,几分钟就能接入。尤其当业务规则频繁变化时,Skill 的维护成本远低于模型重训。因此,掌握 Agent Skill 几乎是应用层 AI 开发者的必备技能。
2. Agent、Skill、MCP 到底有什么区别
在写代码之前,有必要先把三个高频概念区分清楚。很多读者在群里问:Agent 和 Skill 是不是一回事?Skill 和 MCP 又是什么关系?这里统一梳理。
2.1 Agent 是“大脑”,Skill 是“能力包”
Agent 是一个完整的智能体,它具备任务理解、规划、记忆、工具调用、自我反思等能力。你可以把 Agent 理解成一个独立运行的程序,它有完整的循环逻辑。
我们常见的 Agent 执行循环可以简化为以下几步:
用户输入 ↓ 大模型理解任务并规划 ↓ 判断是否需要调用 Skill ↓ 执行 Skill 并拿到结果 ↓ 大模型整理结果并回复用户 ↓ (必要时)继续下一步任务在这个循环里,Skill 是 Agent 的“能力包”。Agent 负责决定用不用、什么时候用、用哪一个;Skill 负责把具体动作做掉。没有 Skill,Agent 只是“会聊天的模型”;有了 Skill,Agent 才能真正“动手干活”。
2.2 MCP 是“外接工具协议”,Skill 是“内化技能”
MCP(Model Context Protocol)是一套用于连接 AI 应用与外部工具、数据源的开放协议。它的目标是标准化 AI 应用与外部能力之间的交互方式。如果一个公司有大量已经存在的内部系统,通过 MCP 可以让 AI 应用以一种统一的方式访问这些系统。
Skill 和 MCP 的关系不是替代,而是分层:
- Skill 更侧重应用内的能力封装,通常是在 Agent 进程内直接执行逻辑,比如查内存数据、算积分、调用自定义函数。
- MCP 更侧重跨系统连接,它定义了客户端、服务端、工具发现、调用规范等一整套协议,适合把外部服务接入 AI。
简单理解:Skill 是“我会做这件事”,MCP 是“我能通过标准协议连接其他系统来做这件事”。两者可以结合使用,比如 Skill 内部实现逻辑可以调用 MCP 客户端去远程拉数据。
2.3 何时使用 Skill,何时使用 MCP
选型建议如下:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 在 Agent 内部封装一些自定义逻辑 | Skill | 实现简单,维护成本低 |
| 需要连接多个异构外部系统 | MCP | 协议统一,便于标准化接入 |
| 工具数量少且逻辑简单 | Skill 或直接 Function Calling | 不需要引入额外协议层 |
| 企业级工具生态,多个团队共用 | MCP | 有一致的发现和鉴权机制 |
结论是:如果你是初学者,或者业务场景集中在单个应用内,优先从 Skill 开始;当你的工具数量变多、需要打通多个系统时,再考虑引入 MCP。千万不要一开始就上重型架构。
3. 环境准备与版本说明
3.1 开发环境要求
本文的示例代码使用 Python 编写,建议使用 Python 3.9 及以上版本。我在本地测试时使用的是 Python 3.10,系统为 macOS,但代码本身不依赖特定操作系统,Windows 和 Linux 同样可以运行。
你需要准备以下基础环境:
- Python 3.9+
- pip 包管理工具
- 一个支持发送 HTTP 请求的 Python 运行环境
- 可选:支持 Tool Calling 的大模型 API(OpenAI 兼容协议即可)
这里需要特别说明,大模型 API 的版本迭代比较快,不同模型服务商的接口细节可能略有差异。本文中的代码基于常见的 OpenAI 兼容/chat/completions接口编写,这是目前大多数模型服务商都支持的标准格式。当你接入具体服务时,需要根据服务商文档微调BASE_URL、MODEL等参数。
3.2 依赖安装
项目依赖非常少,核心只需要requests库。安装命令如下:
pip install requests如果你的环境里已经有requests,可以跳过这一步。示例中其它功能(如 JSON 解析、模块导入)都使用 Python 标准库实现,无需额外安装。
3.3 示例项目结构
为了便于阅读,建议按照下面的目录结构创建项目:
agent_skill_demo/ ├── main.py ├── agent.py ├── skill_manager.py └── skills/ ├── __init__.py ├── user_skill.py └── weather_skill.py后面每一小节都会明确说明代码应该放在哪个文件里。你也可以把代码合并成单个文件运行,但按模块拆分更接近真实项目结构,后续扩展新 Skill 时也更方便。
4. Skill 的核心设计:从工具调用到 Skill 描述
4.1 大模型如何“看”到 Skill
要理解 Skill 设计,必须先理解大模型的工具调用机制。以 OpenAI 兼容接口为例,当你在请求中额外传入tools参数时,模型在生成回复时除了输出普通文本,还可能输出一个结构化的tool_calls字段。
这个字段里就包含模型选择的工具名称和参数 JSON。程序拿到这个结构化输出后,执行对应的函数,再把结果以role: "tool"的消息追加进对话上下文。模型拿到工具结果后,继续推理并生成最终回复。
这是一种非常优雅的设计:模型不需要真的执行代码,它只负责“判断”和“描述参数”,真正的执行由外部程序完成。这既保证了灵活性,也方便做权限控制和审计。
4.2 Skill 的元数据结构
为了让模型能够正确选择 Skill,我们需要给每个 Skill 写一份机器可读的“说明书”。这份说明书的格式和 Tool Calling 中的 tools 参数保持一致,通常包含四个核心字段:
type:固定为function,表示这是一个函数型工具。function.name:Skill 的唯一名称,模型将用这个名称发起调用。function.description:对 Skill 功能的自然语言描述,模型主要靠它判断是否调用。function.parameters:参数的 JSON Schema 描述,包括参数类型、是否必填、字段说明。
其中最重要的是description和parameters。description写得是否清晰,直接决定模型会不会乱调用;parameters定义得是否准确,直接决定程序能不能正确执行。在实际项目中,很多调用失败都是因为这两个字段写得含糊。
4.3 一个最简单的 Skill 实现
下面我们定义一个“用户信息查询”的 Skill。它不依赖数据库,而是从内存字典中读取数据,方便你直接运行验证。
# 文件路径:skills/user_skill.py # 用户信息查询 Skill NAME = "get_user_info" DESCRIPTION = ( "根据用户ID查询用户的昵称、会员等级和积分信息。" "当用户询问“我的账户”“我的资料”“查用户信息”时使用。" ) PARAMETERS = { "type": "object", "properties": { "user_id": { "type": "string", "description": "用户ID,例如 10001" } }, "required": ["user_id"] } # 模拟数据库 USER_DB = { "10001": {"name": "张三", "level": "VIP", "points": 5200}, "10002": {"name": "李四", "level": "普通用户", "points": 300}, } def execute(user_id: str): """Skill 的实际执行逻辑""" if user_id not in USER_DB: return {"code": 1, "message": f"用户 {user_id} 不存在"} return {"code": 0, "data": USER_DB[user_id]}这个文件中出现了四个模块级常量和一个函数:
NAME:Skill 名称,全项目唯一。DESCRIPTION:给大模型看的描述,必须写清楚“什么时候用”。PARAMETERS:参数定义,使用 JSON Schema 格式。execute:真正执行逻辑的函数,接收参数并返回结果。
这里要注意,execute的返回结果应该统一为一个可 JSON 序列化的结构。因为后续需要把结果通过对话消息传给大模型,如果返回一个无法序列化的对象,会在传输过程中报错。
再来看一个天气查询 Skill:
# 文件路径:skills/weather_skill.py # 城市天气查询 Skill NAME = "query_weather" DESCRIPTION = ( "查询指定城市的实时天气情况,包括温度、天气现象和风力。" "当用户询问“今天天气”“会不会下雨”“气温多少度”时使用。" ) PARAMETERS = { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海、杭州" } }, "required": ["city"] } # 模拟天气数据 WEATHER_DB = { "北京": {"temperature": 18, "condition": "晴", "wind": "北风3级"}, "上海": {"temperature": 22, "condition": "多云", "wind": "东风2级"}, "杭州": {"temperature": 20, "condition": "小雨", "wind": "南风1级"}, } def execute(city: str): if city not in WEATHER_DB: return {"code": 1, "message": f"暂无城市 {city} 的天气数据"} return {"code": 0, "data": WEATHER_DB[city]}你会发现,两个 Skill 的结构完全一致。这就是工程化的意义:只要新写一个文件,增加一个 Skill,我们的管理器就能自动识别并注册,不需要修改 Agent 核心代码。
5. 完整实战:从 0 到 1 搭建支持 Skill 的 Agent
5.1 需求与调度流程
我们现在要做的是一个极简但完整的 Agent 系统,用户输入自然语言,Agent 自动决定调用哪个 Skill,并返回可读的结果。
流程如下:
- 用户输入问题。
- Agent 把问题和大模型的系统提示词一起发送。
- 大模型判断需要调用哪个 Skill,并返回
tool_calls。 - Agent 根据
tool_calls中的函数名和参数执行对应 Skill。 - Agent 把 Skill 执行结果作为 tool 消息追加到对话。
- 大模型根据执行结果生成最终自然语言回复。
为了实现这个流程,我们需要四部分代码:Skill 管理器、Agent 调度核心、入口文件、以及两个 Skill 模块。
5.2 实现 Skill 管理器
Skill 管理器的职责有三个:自动加载skills目录下的所有 Skill 模块、生成可供大模型识别的tools列表、根据函数名执行对应 Skill。
# 文件路径:skill_manager.py import importlib import pkgutil import skills class SkillManager: def __init__(self): self._skills = {} def load(self): """自动注册 skills 包下的所有 Skill 模块""" for module_info in pkgutil.iter_modules(skills.__path__): if module_info.name.startswith("_"): continue module = importlib.import_module(f"skills.{module_info.name}") self.register(module) def register(self, module): """将单个 Skill 模块注册到管理器""" required = ("NAME", "DESCRIPTION", "PARAMETERS", "execute") if not all(hasattr(module, key) for key in required): print(f"跳过模块 {module.__name__}:缺少 Skill 必需属性") return self._skills[module.NAME] = { "description": module.DESCRIPTION, "parameters": module.PARAMETERS, "execute": module.execute, } def get_tools(self): """生成大模型需要的 tools 参数""" tools = [] for name, skill in self._skills.items(): tools.append({ "type": "function", "function": { "name": name, "description": skill["description"], "parameters": skill["parameters"], } }) return tools def execute(self, name: str, arguments: dict): """根据名称执行 Skill""" skill = self._skills.get(name) if not skill: return {"code": 1, "message": f"Skill {name} 不存在"} try: return skill["execute"](**arguments) except TypeError as e: return {"code": 1, "message": f"Skill {name} 参数错误: {e}"}这里有几个设计点值得说明:
- 使用
pkgutil.iter_modules自动遍历skills包下的所有模块,这样以后新增 Skill 时,只需要添加文件,不需要修改管理器。 - 在
register中做属性校验,避免某个模块写得不完整却静默注册成功。 - 在
execute中捕获TypeError,避免参数不匹配导致整个 Agent 流程崩溃。
使用这种方式,新增一个 Skill 的成本非常低:新建文件、定义四个必需属性、写 execute 函数,剩下的交给管理器自动处理。
5.3 实现 Agent 调度核心
Agent 调度核心是整个系统的“大脑”,它负责组织对话消息、调用大模型、处理工具调用结果。
# 文件路径:agent.py import json import os import requests from skill_manager import SkillManager # 从环境变量读取配置,避免把密钥写死在代码中 BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1") API_KEY = os.getenv("LLM_API_KEY", "your-api-key") MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") def call_llm(messages, tools): """调用 OpenAI 兼容接口""" payload = { "model": MODEL, "messages": messages, "tools": tools, "tool_choice": "auto", } resp = requests.post( f"{BASE_URL}/chat/completions", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" }, json=payload, timeout=60, ) resp.raise_for_status() return resp.json() def run_agent(user_input: str, mock: bool = False): """运行 Agent 主流程""" skill_manager = SkillManager() skill_manager.load() tools = skill_manager.get_tools() messages = [ {"role": "system", "content": "你是智能助手,需要根据用户问题调用合适的Skill。"}, {"role": "user", "content": user_input}, ] # 为了在没有大模型 API 的环境下也能演示完整链路,提供 mock 模式 if mock: if "天气" in user_input or "气温" in user_input: tool_calls = [{ "id": "call_1", "type": "function", "function": { "name": "query_weather", "arguments": json.dumps({"city": "北京"}, ensure_ascii=False) } }] else: tool_calls = [{ "id": "call_1", "type": "function", "function": { "name": "get_user_info", "arguments": json.dumps({"user_id": "10001"}, ensure_ascii=False) } }] response = { "choices": [{ "message": { "role": "assistant", "content": None, "tool_calls": tool_calls } }] } else: response = call_llm(messages, tools) message = response["choices"][0]["message"] messages.append(message) if message.get("tool_calls"): for tc in message["tool_calls"]: fn = tc["function"] arguments = json.loads(fn.get("arguments") or "{}") # 执行 Skill result = skill_manager.execute(fn["name"], arguments) # 将执行结果返回给大模型 messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result, ensure_ascii=False), }) # 真实模式下,继续让大模型生成最终回答 if not mock: response = call_llm(messages, tools) else: response = { "choices": [{ "message": { "role": "assistant", "content": "我帮你查到了:北京今天晴天,气温 18 度。", "tool_calls": None } }] } return response["choices"][0]["message"]["content"]这个文件是整个系统的核心。重点理解以下几点:
messages列表是完整对话上下文,模型每次生成都基于这个列表。- 当模型返回
tool_calls时,程序必须把原始 assistant 消息追加进 messages,否则上下文不完整。 - 每个
tool_calls需要对应一个role: "tool"的结果消息,并通过tool_call_id关联到原始调用。 - 工具结果回传后,需要再次调用模型,让模型基于结果生成最终回复。
mock参数是给没有 API 环境的读者准备的。它能模拟大模型返回工具调用的过程,让你在完全离线的情况下看到整个 Skill 调度链路。
5.4 实现入口文件
入口文件负责接收用户输入,调用 Agent 主流程并打印结果。
# 文件路径:main.py import sys from agent import run_agent if __name__ == "__main__": mock = "--mock" in sys.argv # 默认演示天气查询 question = "帮我查一下北京的天气" if len(sys.argv) > 1 and not mock: question = sys.argv[1] print("用户问题:", question) print("Agent回答:", run_agent(question, mock=mock))如果直接运行,程序会以“帮我查一下北京的天气”作为输入。你也可以在命令行传入自定义问题。
5.5 运行与验证
在项目根目录执行以下命令:
python main.py --mock预期输出:
用户问题: 帮我查一下北京的天气 Agent回答: 我帮你查到了:北京今天晴天,气温 18 度。这个过程中,mock 模式模拟了大模型返回query_weather的调用,SkillManager 自动加载了天气 Skill 并执行,最终生成了自然语言回复。虽然回复内容是 mock 的,但整个 Skill 注册、加载、调度、执行链路是真实跑通的。
如果你有真实的大模型 API,可以设置环境变量后运行:
export LLM_BASE_URL="你的API服务地址" export LLM_API_KEY="你的API Key" export LLM_MODEL="你的模型名称" python main.py "帮我查一下上海的天气"在真实模式下,模型会根据你的问题自动判断调用哪个 Skill,并基于 Skill 结果生成回答。这个验证过程能够帮你确认你的模型服务是否支持 Tool Calling。
6. 本地部署 AI 大模型时如何适配 Skill
6.1 本地部署的价值
很多企业在实际落地 Agent 时不希望把内部数据发送到外部 API,这时就需要本地部署大模型。本地部署的好处比较明显:数据不出内网、成本可控、可以按业务做定制。但本地模型的能力上限通常不如顶级商业 API 模型,尤其在工具调用稳定性上需要额外适配。
如果你准备在内网环境部署 AI 大模型并配合 Agent Skill 使用,下面这些经验可以帮你少走弯路。
6.2 通过 OpenAI 兼容接口接入本地模型
目前常见做法是使用本地推理服务将模型包装成 OpenAI 兼容接口,比如通过 Ollama 等工具启动模型后,会暴露一个类似http://localhost:11434/v1的地址。此时你只需要修改环境变量LLM_BASE_URL,不需要改代码。
export LLM_BASE_URL="http://localhost:11434/v1" export LLM_MODEL="qwen2.5:7b"这里要注意一个问题:不同模型服务对 Tool Calling 的支持程度不一样,有的模型需要特殊参数开启工具调用,有的模型虽然声称支持,但实际输出并不稳定。因此,在选择模型和推理框架时,务必提前验证该模型是否支持tools参数。
6.3 本地模型的 Skill 适配思路
如果你使用的本地模型不支持 Tool Calling,也不要放弃 Skill 方案。还有一种通用适配思路:把 Skill 描述写到 system 提示词中,让模型以固定 JSON 格式输出它想调用的 Skill,然后由程序解析 JSON 并执行。
示例提示词模板:
你是一个智能助手,需要根据用户问题调用以下技能之一: 1. get_user_info:根据用户ID查询用户信息,参数:user_id 2. query_weather:查询城市天气,参数:city 请严格输出 JSON 格式,不要输出多余内容: {"name": "技能名称", "arguments": {"参数名": "参数值"}}模型输出这个 JSON 后,程序只需要解析并交给 SkillManager 执行即可。这种方案牺牲了一部分灵活性,但对模型的要求更低,在本地部署场景下非常实用。
7. 常见问题与排查思路
7.1 模型始终不调用 Skill
如果模型对你的问题没有任何反应,就像普通聊天一样回复,而不是输出工具调用,通常有几个原因:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型正常回复但不调用工具 | 模型本身不支持 Tool Calling | 换一个支持工具调用的模型,或改用 JSON 输出提示词方案 |
| 模型只在部分问题上调用 | Skill 描述写得太泛,模型判断不了 | 优化 Skill 的 description,加入明确的触发场景 |
| 模型报参数格式错误 | tools 参数结构不符合接口要求 | 对照接口文档检查 tools 的 JSON 结构 |
排查时可以先把tools参数打印出来,人工确认结构是否正确;再用最简单的问题测试,比如“你好,帮我查一下北京天气”,逐步缩小问题范围。
7.2 参数解析失败
有时模型返回的arguments无法用json.loads解析。这通常是因为模型生成了带多余字符的 JSON,比如被反引号包裹。建议在解析时加上一层清理,去掉首尾的反引号和多余空格。
另外,如果模型返回了未定义的参数名,也会导致 Skill 执行时报TypeError。我们在 SkillManager 的execute里已经捕获了这类错误,但更稳妥的做法是解析后做一次参数白名单校验,只保留 Skill 定义的参数。
arguments = json.loads(fn.get("arguments") or "{}") allowed = set(skill["parameters"].get("properties", {}).keys()) arguments = {k: v for k, v in arguments.items() if k in allowed}这段逻辑可以放在skill_manager.execute中,防止异常参数传入业务代码。
7.3 Skill 越来越多时模型选错技能
当项目中有几十个 Skill 时,模型很容易选错。解决办法有三个层面:
- 精简同时传给模型的 Skill 数量:把 Skill 按业务域分组,先让模型判断属于哪个域,再加载该域的 Skill。
- 优化 description 的质量:不要写成功能说明书,要写成触发条件说明书。写清楚“当用户表达 XX 意图时使用”。
- 增加反馈闭环:记录模型每次调用 Skill 的成功率,定期清理低频或重复的 Skill。
这一条在实际项目中非常重要。很多 Agent 体感“笨”,不是因为模型差,而是因为 Skill 太多太杂,模型根本选不过来。
7.4 本地模型输出不稳定
本地模型在 Tool Calling 上的稳定性通常不如商业模型,表现是:有时输出 JSON 不完整、有时把函数名拼错、有时参数值为空。应对策略包括:
- 使用更高的采样温度或直接设置为 0;
- 在 system 提示词中给出一个包含参数示例的调用样例;
- 增加重试机制,解析失败后让模型重新生成一次;
- 对模型输出做校验,不符合 Schema 时丢弃并重试。
不要把本地模型当商业模型用,要在上层做足够的容错处理,才能得到一个相对稳定的 Agent 系统。
8. 工程最佳实践
8.1 Skill 命名与描述规范
命名要统一,推荐使用动词_对象的结构,例如query_weather、create_order、update_user,避免使用过于抽象的单词。名称一旦发布,不要频繁修改,因为模型会基于名称记忆历史调用模式。
描述要写清楚“触发场景”而不是“实现原理”。下面两句话对比:
- 不推荐:实现天气查询功能。
- 推荐:当用户询问指定城市的天气、温度、是否会下雨时使用,城市名必填。
模型非常依赖 description 做意图判断,这部分值得反复打磨。
8.2 参数设计原则
参数设计要贴近用户表达。比如用户习惯说“北京”,就不要让模型必须传city_code: "110000",这样模型很容易出错。参数尽量使用字符串、数字等基础类型,给出明确的示例值。枚举字段要写清允许值。
同时,尽可能给参数设置默认值,让模型在不确定时也能调用成功。例如查询天气可以默认城市为“北京”,只要在PARAMETERS中不把city设置为 required,并在 execute 中处理缺省情况。
8.3 错误处理与日志观测
Agent 链路中任何一环失败都会导致体验断崖式下降。这里建议做到三点:
- 所有外部调用都要设置超时时间,避免模型接口卡死导致整个服务挂起。
- Skill 执行结果统一返回
{"code": 0/1, "data": ..., "message": ...}这样的结构,方便上层判断。 - 记录完整的调用日志,包括用户输入、模型返回的 tool_calls、Skill 执行结果、最终回复。
在定位问题时,日志几乎是唯一的依赖。我在项目中习惯把 tool_calls 的原始 JSON 和完整对话消息都打出来,排查效率会高很多。
8.4 安全与权限边界
Skill 在执行真实业务操作时,必须有权限控制。尤其涉及数据库更新、删除、转账、发送消息等操作时,要做到:
- 只允许 Agent 调用当前用户身份允许执行的 Skill;
- 危险操作在执行前要求二次确认;
- 数据库操作前备份,涉及删除时先逻辑删除;
- 所有 Skill 执行动作都要记录审计日志。
把大模型想象成一个“可能被提示词攻击的执行者”,所有关键操作都要按权限边界限制,避免出现越权调用。
8.5 Skill 版本管理与灰度发布
Skill 也有版本问题。当业务逻辑变化时,不要直接覆盖原 Skill,建议在NAME中加入版本后缀,例如create_order_v2,并在 description 中标注新旧版本的适用范围。这样模型在过渡期可以两边都调用,等新版本稳定后再下线旧版本。
如果条件允许,可以为 Skill 加入一个简单的上下线开关:开关关闭时,Skill 不进入get_tools()列表。这样就能实现灰度发布和快速回滚。
9. 总结与下一步学习路线
读完这篇文章,你应该已经具备以下能力:
- 能清晰解释 Agent、Skill、MCP 三者的定位与区别;
- 能独立写出一个结构完整的 Skill 模块;
- 能实现一个支持 Skill 自动加载、调度、执行的 Agent 核心;
- 知道如何接入 OpenAI 兼容 API,也知道本地部署模型的适配思路;
- 掌握 Agent Skill 开发中最常见的排错手段和工程最佳实践。
下一步的学习方向,我建议按下面的路线推进:
- 先把本文的示例代码在本机跑通,尝试新增一个自己的 Skill。
- 学习 Function Calling 的更多参数,如
tool_choice的强制指定。 - 了解 MCP 协议,尝试用 Skill 内部调用一个 MCP 服务。
- 学习 RAG(检索增强生成),把知识库能力也封装成 Skill。
- 研究多 Agent 协作,让不同 Agent 各自负责不同 Skill,配合完成复杂任务。
Agent Skill 是 AI 应用开发里非常值得投入时间去掌握的工程能力。它不是某个框架的私有概念,而是基于大模型工具调用机制的一套通用实践。建议你直接跑一遍 demo,然后把你业务里最常用的一个重复操作封装成第一个 Skill。跑通之后,你基本就掌握了 Agent 应用开发最核心的一环。