在大模型应用开发中Agent Skill 是最近一两年被反复讨论的概念但真正把它讲清楚的文章并不多。很多团队的处境是大模型已经能理解用户意图但交给 Agent 后模型只会输出一段建议文本无法真正完成查询、计算、归档、通知等动作代码里则堆满了 if-else每新增一个业务动作就要改一次主流程函数越来越多模型越来越不知道该调用哪一个。Agent Skill 要解决的就是这个问题把可复用的专业能力封装成带有名称、描述、参数定义和执行逻辑的独立模块让模型在对话过程中自行判断是否调用、传入什么参数并在调用后拿到结构化结果继续生成回答。下面从 Skill 的底层认知讲起对比 Skill、Agent、Tool 和 MCP 的区别再通过一个可运行的 Python 示例完成 Skill 注册、参数校验、模型接入和运行验证最后补充生产环境落地的建议。学完之后你至少可以在自己的 AI 大模型应用项目里设计出一套不依赖硬编码分支的 Skill 调用机制。说明示例基于当前主流的 OpenAI 兼容接口和常见 Python 生态编写。示例中的代码用于说明实现思路落地前要结合自己的模型服务地址、模型名称和依赖版本做调整。1. Agent Skill 到底是什么先建立正确认知1.1 从大模型聊天的局限说起大模型本身是一个文本生成器。你输入一段文本它输出一段看起来合理的文本。对简单的问答场景这已经足够但对真实业务来说远远不够。比如用户说“帮我把今天的项目周报整理成 Markdown 并发到团队群”模型不能直接访问周报数据也不能真正打开聊天工具发送消息它只能“假装”自己做了这件事。要让模型完成真实动作必须给它外部的执行能力也就是工具和技能。最早的做法是 Prompt 拼接把所有工具的描述写进系统提示词要求模型输出 Markdown 格式的调用命令程序再用正则或临时解析器去处理。这种做法在只有一两个工具时还可以工具一多模型经常生成错误的工具名、错误的参数字段解析也不稳定。后来业界逐步演进为更结构化的方式模型返回结构化的函数调用参数程序负责执行返回结果再喂给模型做后续生成。Agent Skill 就是在这个背景下产生的“组织单元”它不只解决“单个函数怎么调用”而是解决“一项专业能力如何封装、描述、复用和编排”。理解这一点后可以下一个保守但清晰的定义Skill 是一段可复用的、可被大模型调用的专业能力封装。它包含能力描述、参数定义、执行逻辑和返回结果约定四部分。相比单个 ToolSkill 可以封装更完整的业务步骤相比 AgentSkill 本身不做复杂的自主决策它更像一个高质量的执行单元。1.2 Skill 的四个组成部分任何 Skill 都可以拆成四个部分在设计的时候缺一不可。第一是能力描述。它是一段给模型看的文本说明这个 Skill 在什么时候被调用、能完成什么任务、有什么边界。模型不是程序员它靠这段描述来匹配用户意图所以描述要写得像“API 文档中的用途说明”而不是像代码注释。第二是参数定义。定义调用这个 Skill 需要哪些字段、字段类型、是否必填、枚举范围、默认值。参数定义必须让模型能够正确生成参数同时让程序在参数不符合预期时能快速失败。这里推荐使用 JSON Schema 风格或 pydantic 模型来约束。第三是执行逻辑。这是 Skill 真正干活的代码可以调用外部 API、查询数据库、执行文件操作、调用本地模型甚至编排多个子工具。执行逻辑不要求万能但要求可控超时、异常、空结果都要有明确返回。第四是返回结果约定。Skill 执行完成之后返回的应该是一个结构化结果而不是一段随意文本。这样模型才能基于结构化结果继续生成最终回答程序也才能对结果做后续处理。可以这样理解Tool 是“单个动作”Skill 是“一组动作加上使用说明和约定”的封装。比如“获取天气”是一个 Tool而“根据城市和时间给出出行建议”是一个 Skill它内部可能需要调用天气 API、节假日数据、甚至历史出行经验但对外只暴露一个清晰的调用入口。1.3 Skill、Agent、Tool 和 MCP 的区别这四个概念经常被混用尤其在做方案设计时容易引起分歧。我习惯用一条主线来区分Agent 是决策者Skill 是能力包Tool 是能力包里的最小动作MCP 是能力包与外部系统之间的标准化通信协议。下面用一张表直接对比概念解决什么问题典型形态关键特征Agent理解目标、拆解步骤、选择调用哪些能力一个包含模型、上下文、策略的循环有自主决策Tool单个可执行动作比如查询接口、执行 SQL一个函数或 API 封装输入确定、输出确定Skill一组与特定任务相关的动作封装包含描述、参数、执行和返回约定一个注册单元内含多个函数或调用逻辑可复用、可描述、对模型友好MCP让 Tool/Skill 能被不同 Agent 统一发现和调用的协议一种协议规范包含 server 和 client关注互联互通举例说明。假设你要做一个“论文辅助阅读 Agent”。其中“搜索论文”是一个 Tool“解析 PDF”是一个 Tool“生成摘要”可能是另一个 Tool。而把这些动作组合起来让用户说“帮我分析这篇论文的创新点和局限性”时一次性完成这就是一个 Skill可以叫 paper_analysis。整个系统由一个 Agent 负责决定什么时候触发这个 Skill、用户参数如何传入以及拿到分析结果后如何回答。若多个平台都想接入这套能力则可以通过 MCP 协议把它包装成标准资源供不同 Agent 客户端发现和调用。这里需要特别提醒不要在设计方案时把 Skill 和 MCP 对立起来。它们不是同层概念。Skill 是业务封装MCP 是接入协议。一个 Skill 可以内部不依赖 MCP 直接调用函数也可以通过 MCP 调用远程工具一个 MCP Server 也可以只是暴露单个 Tool而不构成一个完整 Skill。选型时先问自己要解决的是“能力如何组织”还是“能力如何互联”再决定主要使用哪个机制。2. 动手前先准备环境和统一概念2.1 技术选型和版本约定本示例采用 Python 3.10 编写使用 FastAPI 提供 HTTP 服务使用 pydantic 做参数校验与大模型的交互通过 OpenAI 兼容接口完成。这样设计的好处是本地模型服务、云端模型 API 以及各类网关只要支持 /v1/chat/completions 接口都可以用同一套代码接入。环境要求如下组件版本建议用途Python3.10 及以上运行示例代码FastAPI0.100 及以上提供 HTTP APIuvicorn0.20 及以上ASGI 服务器pydantic2.x参数校验与解析requests2.31 及以上调用模型服务模型服务任意支持 OpenAI 兼容接口的服务执行意图识别和工具调用如果本地没有模型服务可以先使用环境变量配置任意兼容 OpenAI 协议的服务地址。示例代码不会假设具体模型名称建议在环境变量中统一配置。安装依赖的命令如下python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install fastapi uvicorn[standard] pydantic requests python-dotenv安装完成后可以在项目根目录创建 .env 文件填入模型服务地址和密钥。注意不要把密钥提交到 Git。注意示例代码关注的是 Skill 封装和调用链路不依赖特定厂商模型。正式项目接入前先确认模型服务是否支持 function calling 或等价 tools 参数否则需要退化为输出解析方案。2.2 项目目录结构为了让后续扩展更方便示例采用模块化结构。每个 Skill 独立成文件注册表只负责收集和暴露不关心具体实现。agent-skill-demo/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── registry.py # Skill 注册表 │ ├── agent_core.py # 模型调用与 Skill 选择 │ └── skills/ │ ├── __init__.py │ ├── document_summary.py # 文档摘要 Skill │ └── task_planner.py # 任务拆解 Skill ├── .env # 环境变量 ├── requirements.txt └── README.md这个结构在真实项目中也可以沿用一个团队如果同时维护几十个 Skill建议再按业务域分目录例如 skills/customer、skills/finance。注册表仍然只负责汇总每个 Skill 文件对外暴露注册函数。2.3 定义 Skill 的描述协议在写代码前先统一 Skill 的元数据格式。这里采用 JSON Schema 风格的参数定义既能给人读也能直接传给大模型的 tools 参数。例如“文档摘要”Skill 的描述可以写成{ name: document_summary, description: 当用户需要归纳文档内容、提取要点或生成摘要时调用。支持 Markdown、纯文本等常见文本格式。, parameters: { type: object, properties: { text: { type: string, description: 需要做摘要的原始文本内容 }, max_length: { type: integer, description: 摘要的最大长度默认 200, minimum: 50, maximum: 1000 } }, required: [text] } }这个协议的关键点是description 面向模型必须写清楚“什么场景调用”parameters 中的字段描述要具体避免模型传错字段。示例里对 max_length 加了 min 和 max 约束就是为了防止模型传入明显不合理的值。同样“任务拆解”Skill 可以描述为{ name: task_planner, description: 当用户给出一个复杂目标需要拆解为可执行的子任务时调用。, parameters: { type: object, properties: { goal: { type: string, description: 用户希望实现的最终目标 }, steps: { type: integer, description: 期望拆解出的步骤数量默认 3, minimum: 1, maximum: 8 } }, required: [goal] } }定义好元数据后所有的 Skill 都遵循同一套结构。这样注册表、模型接入层、测试工具都可以复用同一套逻辑而不需要为每个 Skill 单独写解析代码。3. 用最小代码实现一个可复用的 Agent Skill这一部分会给出一个完整的最小闭环Skill 注册 - 模型选择 - 参数校验 - 执行 - 返回。示例重点不是代码量多少而是把这一条链路的每个环节都落到文件上。3.1 实现 Skill 注册表注册表的核心职责是收集所有 Skill提供按名称查询的接口并提供给模型接入层生成 tools 参数。# app/registry.py from typing import Dict, Callable, Optional class Skill: def __init__(self, name: str, description: str, parameters: dict, handler: Callable): self.name name self.description description self.parameters parameters self.handler handler def to_tool_schema(self) - dict: return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters } } def execute(self, arguments: dict): return self.handler(**arguments) class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(fSkill already registered: {skill.name}) self._skills[skill.name] skill def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def list_skills(self): return list(self._skills.values()) def to_tools(self) - list: return [skill.to_tool_schema() for skill in self._skills.values()] registry SkillRegistry()这里做了两个关键设计。第一Skill 类把元数据和处理函数绑定在一起外部只通过 name 调用执行接口。第二to_tools 方法直接输出大模型 tools 参数格式后面接入模型时不需要再单独写转换代码。如果某个 Skill 需要异步执行例如调用远程 API建议把 handler 设计成 async 函数并用 asyncio 调度。当前示例保持同步便于理解调用链。3.2 实现“文档摘要”Skill文档摘要 Skill 是第一个真正干活的 Skill。为了不引入重量级 NLP 依赖示例使用简单的按段落和首句提取策略。这个实现仅用于演示 Skill 的输入输出约定生产环境可以换成本地模型或调用摘要 API。# app/skills/document_summary.py import re from app.registry import Skill, registry def split_sentences(text: str): parts re.split(r(?[。!?]), text) return [p.strip() for p in parts if p.strip()] def extract_key_sentences(text: str, max_length: int 200): sentences split_sentences(text) if not sentences: return 输入文本为空或无法识别有效句子。 # 按段落切分后取每个段落的第一个句子再拼接为摘要 paragraphs [p.strip() for p in text.split(\n) if p.strip()] summary_parts [] for para in paragraphs: first_sentence split_sentences(para) if first_sentence: summary_parts.append(first_sentence[0]) if sum(len(s) for s in summary_parts) max_length: break summary .join(summary_parts) return summary[:max_length] (…… if len(summary) max_length else ) def handle_document_summary(text: str, max_length: int 200) - dict: if not text or not text.strip(): return {success: False, message: text 参数不能为空} if max_length 50 or max_length 1000: max_length 200 summary extract_key_sentences(text, max_length) return {success: True, summary: summary, source_length: len(text)} def register_document_summary(): registry.register(Skill( namedocument_summary, description当用户需要归纳文档内容、提取要点或生成摘要时调用。支持 Markdown、纯文本等常见文本格式。, parameters{ type: object, properties: { text: { type: string, description: 需要做摘要的原始文本内容 }, max_length: { type: integer, description: 摘要的最大长度默认 200, minimum: 50, maximum: 1000 } }, required: [text] }, handlerhandle_document_summary ))这段代码的关键点有三个。第一handler 的返回值是结构化 dict包含 success 字段、业务结果和辅助信息方便外层判断。第二参数校验放在注册表之外的 handler 内因为注册表不关心具体参数规则。第三max_length 超出合理范围时不是直接报错而是回退到默认值这样做能降低模型传参错误导致整个调用链失败的概率。不过要说明的是参数回退策略要谨慎使用。对于明显错误且无法判断默认值的参数应该返回校验失败信息而不是默默修正。这里 max_length 是数值字段回退默认值是可接受的如果是用户身份、订单号这类关键业务参数宁可失败并让模型重新生成参数。3.3 实现“任务拆解”Skill任务拆解 Skill 演示的是“不调用外部工具而是让模型生成结构化子任务”的场景。它的核心是调用本地模型接口使用 prompt 让模型输出 JSON 格式的任务列表。# app/skills/task_planner.py import json import os import requests from app.registry import Skill, registry DEFAULT_MODEL os.getenv(MODEL_NAME, qwen-plus) LLM_BASE_URL os.getenv(LLM_BASE_URL, ).rstrip(/) LLM_API_KEY os.getenv(LLM_API_KEY, ) def call_llm(prompt: str) - str: url f{LLM_BASE_URL}/chat/completions headers { Authorization: fBearer {LLM_API_KEY}, Content-Type: application/json } payload { model: DEFAULT_MODEL, messages: [ {role: system, content: 你是任务规划助手只输出 JSON不输出多余文字。}, {role: user, content: prompt} ], temperature: 0.2 } resp requests.post(url, headersheaders, jsonpayload, timeout30) resp.raise_for_status() data resp.json() return data[choices][0][message][content] def handle_task_planner(goal: str, steps: int 3) - dict: if not goal or not goal.strip(): return {success: False, message: goal 参数不能为空} prompt ( f请把以下目标拆解成 {steps} 个可执行的子任务。\n f目标{goal}\n 输出格式{\tasks\: [{\name\: \任务名\, \desc\: \任务说明\, \priority\: \高|中|低\}]}\n ) try: content call_llm(prompt) content content.strip() if content.startswith(): content content.strip() if content.startswith(json): content content[4:] parsed json.loads(content) return {success: True, tasks: parsed.get(tasks, [])} except Exception as exc: return {success: False, message: f调用模型失败或解析失败: {exc}} def register_task_planner(): registry.register(Skill( nametask_planner, description当用户给出一个复杂目标需要拆解为可执行的子任务时调用。, parameters{ type: object, properties: { goal: {type: string, description: 用户希望实现的最终目标}, steps: {type: integer, description: 期望拆解出的步骤数量默认 3, minimum: 1, maximum: 8} }, required: [goal] }, handlerhandle_task_planner ))这里要注意的是task_planner 示例演示的是一种更通用的模式Skill 内部也可以再次调用大模型。也就是说Skill 不一定是纯函数它可以是调用本地模型、外部 API、知识库检索的组合逻辑。正因如此Skill 的返回结果需要做好异常兜底不能因为内部模型请求失败就让整个 Agent 崩溃。在真实项目中可以把这个 Skill 的模型请求替换为对现有模型网关的调用并补充重试、超时和日志链路。示例里的 call_llm 只是最小可运行版本。3.4 让大模型学会选择 SkillFunction Calling 接入现在进入最关键的一步让 Agent 主流程拿到用户输入后先判断需要调用哪个 Skill再解析参数执行后把结果作为上下文返回给模型继续作答。# app/agent_core.py import json import os import requests from app.registry import registry LLM_BASE_URL os.getenv(LLM_BASE_URL, ).rstrip(/) LLM_API_KEY os.getenv(LLM_API_KEY, ) MODEL_NAME os.getenv(MODEL_NAME, qwen-plus) def chat_once(messages, toolsNone, tool_choiceNone): url f{LLM_BASE_URL}/chat/completions headers { Authorization: fBearer {LLM_API_KEY}, Content-Type: application/json } payload { model: MODEL_NAME, messages: messages, temperature: 0.2 } if tools: payload[tools] tools if tool_choice: payload[
