
1. 项目概述为什么我们需要亲手创建一个 Skill最近和几个做AI应用的朋友聊天发现一个挺有意思的现象大家聊起AI Agent智能体都头头是道从ReAct、CoT到各种开源框架如LangChain、AutoGen都能侃上几句。但当我问“那你自己动手给Agent写过最基础的‘技能’Skill吗”场面往往就安静了。很多人对Agent的理解还停留在“一个能调用工具的大模型”这个层面对于如何从零开始让Agent真正“学会”并“执行”一项具体的、可复用的任务——也就是创建一个Skill——反而缺乏最直接的体感。这就像学开车理论背得再熟不上手摸方向盘、不实际挂挡踩油门永远谈不上会开。今天我们就抛开那些宏大的架构图聚焦一个最具体、最微小的单元动手创建一个Agent Skill。这个项目标题里的“从零开始”和“动手实践”是关键。我们不会只讲概念而是会用一个完整的、可运行的例子带你走完从构思、编码、调试到集成的全过程。你将使用最熟悉的开发工具比如VS Code用最直观的代码理解一个Skill是如何被定义、如何被调用、以及它在一个智能体工作流中扮演什么角色。为什么这很重要因为Skill是Agent能力的原子。无论是让Agent帮你查天气、写邮件、分析数据还是控制智能家居每一个独立的功能背后都是一个或多个Skill在支撑。理解了Skill的创建你就掌握了扩展Agent能力的钥匙。无论你未来是想研究更复杂的Agent框架还是想为自己的产品添加AI自动化能力这都是必不可少的第一步。本文适合有一定编程基础熟悉Python、对AI应用开发感兴趣但尚未深入Agent实操的开发者。我们会用尽可能直白的语言和代码让你在半小时内拥有你的第一个“AI技能”。2. Skill核心概念与设计思路拆解在开始写代码之前我们必须先统一“语言”。什么是Agent Skill你可以把它想象成乐高积木中最基础的那块砖。一个复杂的乐高模型比如一座城堡是由成千上万块基础砖按照特定规则拼接而成的。在这里城堡就是那个能完成复杂任务的AI Agent而每一块基础砖就是一个Skill。2.1 Skill的本质可插拔的功能单元一个Skill的核心本质是一个标准化、可描述、可执行的函数或方法。它封装了一项具体的、确定性的或半确定性的任务。标准化意味着它有统一的“接口”让Agent能够发现并调用它可描述意味着它能清晰地告诉Agent“我能做什么”以及“你需要给我什么”可执行意味着它包含真正完成任务如计算、API调用、数据操作的逻辑。它与普通函数最大的区别在于“可被发现和理解”。你写一个普通的def send_email(to, subject, body):函数只有你的程序知道怎么调用它。但一个Skill除了实现逻辑还需要附上一段“自我介绍”通常称为描述或声明让外部的、可能不了解你代码的Agent尤其是大模型驱动的Agent能够理解“哦这里有一个技能可以发送邮件它需要收件人、主题和正文这三个参数。”然后Agent才能在自己的规划中决定在什么时机、以什么参数来调用这个技能。2.2 一个Skill的典型构成基于主流Agent框架如LangChain Tools、AutoGen的UserProxyAgent可注册的函数的实践一个设计良好的Skill通常包含以下几个部分函数实现Implementation这是技能的核心即完成具体任务的代码。比如调用一个天气API并解析返回数据。技能描述/声明Description/Declaration一段自然语言文本清晰说明这个技能的功能、所需的输入参数名称、类型、含义以及可能的输出。这部分是Agent特别是LLM理解该技能的唯一依据。输入/输出模式Schema一个结构化的定义严格规定输入参数的名字、类型、是否必需以及返回值的类型。这为程序化调用提供了契约保障。错误处理Error Handling技能执行过程中可能遇到的各种异常如网络超时、API限流、参数无效需要有清晰的应对策略和错误信息返回以便Agent能进行后续处理例如重试或向用户报告。在我们的动手实践中将严格遵循这个结构来构建我们的第一个Skill。设计思路是先实现一个简单、无外部依赖的核心功能确保跑通Skill的完整生命周期然后逐步增加复杂度引入API调用和更健壮的错误处理。2.3 工具选型为什么用纯Python和简单框架看到“Agent”、“Skill”这些词你可能立刻想到要安装LangChain、LlamaIndex等重型框架。但作为“从零开始”的理解我强烈建议第一课避开复杂的框架。原因有三聚焦核心框架封装了很多好东西但也隐藏了底层机制。直接使用你很可能只学会了“如何配置框架”而不是“Skill本身是什么”。降低门槛仅使用标准库和极简的第三方库如requests能让任何有Python基础的人无障碍跟随把注意力全部放在Skill逻辑上。便于移植你亲手打造的、理解透彻的Skill未来可以轻松地适配到任何框架中因为你掌握的是本质。因此我们的技术栈极其简单语言Python 3.8。受众最广生态最成熟。开发环境VS Code。轻量、高效插件生态丰富适合演示。核心库标准库json,typing用于类型提示。后续引入requests用于HTTP调用。模拟Agent我们将写一个简单的“模拟器”函数来扮演调用Skill的Agent。这能让你最直观地看到Skill是如何被触发和使用的。注意有朋友可能会问为什么不直接用tool装饰器如LangChain提供的那当然更快捷。但就像学数学先学推导过程而不是直接背公式一样理解底层构造能让你在未来遇到任何框架时都游刃有余并且有能力定制和调试更特殊的Skill。3. 实战创建你的第一个计算器Skill让我们开始动手。第一个Skill我们做一个“加法计算器”。它听起来简单但足以演示Skill的所有核心要素。我们将创建一个Python文件calculator_skill.py。3.1 第一步定义技能函数与类型提示首先我们实现最核心的计算逻辑。使用Python的类型提示Type Hints可以让代码更清晰也有利于后续生成结构化的模式Schema。# calculator_skill.py def add_numbers(a: float, b: float) - float: 将两个数字相加。 Args: a (float): 第一个加数。 b (float): 第二个加数。 Returns: float: 两个数字的和。 return a b看这目前就是一个再普通不过的Python函数。但它已经具备了Skill的雏形明确的功能相加、清晰的输入两个浮点数a,b和输出一个浮点数sum。文档字符串里的描述就是最原始的“技能描述”。3.2 第二步封装为标准化Skill对象为了让这个函数能被“Agent”发现和调用我们需要把它包装成一个更结构化的对象。这个对象需要包含描述、模式和执行逻辑。# calculator_skill.py import json from typing import Any, Dict class CalculatorSkill: 一个简单的加法计算器技能。 # 1. 技能的唯一标识和描述 name: str calculator_add description: str 计算两个浮点数的和。 # 2. 输入参数的模式Schema # 这里我们手动定义一个符合JSON Schema规范的字典。 # 它严格定义了Agent调用时需要提供的参数。 parameters_schema: Dict[str, Any] { type: object, properties: { a: { type: number, description: 第一个加数 }, b: { type: number, description: 第二个加数 } }, required: [a, b] # 指明哪些参数是调用时必须的 } # 3. 技能的执行方法 def execute(self, parameters: Dict[str, Any]) - Dict[str, Any]: 执行加法计算。 Args: parameters: 包含参数a和b的字典。 Returns: 包含执行结果或错误信息的字典。 try: # 从参数字典中提取参数 a parameters.get(a) b parameters.get(b) # 简单的参数验证 if a is None or b is None: raise ValueError(参数 a 和 b 是必需的且不能为None。) # 确保是数字整数或浮点数 if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): raise TypeError(参数 a 和 b 必须是数字类型。) # 调用核心逻辑函数 result add_numbers(float(a), float(b)) # 返回标准化的成功结果 return { status: success, result: result, message: f计算成功{a} {b} {result} } except (ValueError, TypeError) as e: # 返回标准化的错误结果 return { status: error, result: None, message: f参数错误{e} } except Exception as e: # 捕获其他未预期的异常 return { status: error, result: None, message: f技能执行内部错误{e} }现在我们有了一个完整的CalculatorSkill类。我们来拆解一下name和description: 这是技能的“身份证”和“简历”Agent通过它们来识别和选择技能。parameters_schema: 这是技能的“使用说明书”。它用结构化的方式JSON Schema告诉调用者“调用我时你必须给我一个对象里面包含名为a和b的数字属性。” 这对于让LLM理解如何生成调用参数至关重要。execute方法: 这是技能的“执行引擎”。它接收一个参数字典进行验证调用真正的业务逻辑add_numbers最后返回一个标准化的结果字典。标准化是关键它让调用方Agent能够以统一的方式处理所有技能的成功或失败。3.3 第三步模拟Agent调用并测试为了验证我们的Skill是否工作我们需要一个“调用方”。现在我们写一个简单的模拟脚本来扮演那个决定使用此技能的Agent。# simulate_agent.py from calculator_skill import CalculatorSkill def simulate_agent_call(): 模拟一个AI Agent调用CalculatorSkill的过程。 print( 模拟Agent调用Skill ) # 1. Agent“发现”可用的技能 available_skills [CalculatorSkill()] print(f发现可用技能: {[skill.name for skill in available_skills]}) # 2. Agent根据任务决定使用哪个技能这里我们硬编码选择calculator # 在真实场景中这一步由LLM根据用户请求和技能描述来决定。 task 我需要计算3.5和4.7的和。 print(fAgent接收到的任务: {task}) chosen_skill available_skills[0] # 假设选择了计算器技能 print(fAgent决定使用技能: {chosen_skill.name}) # 3. Agent根据技能的模式schema生成调用参数 # 在真实场景中LLM会读取chosen_skill.parameters_schema和chosen_skill.description # 然后根据任务描述生成符合模式的参数字典。 parameters_for_skill {a: 3.5, b: 4.7} print(fAgent生成的调用参数: {parameters_for_skill}) # 4. Agent调用技能的execute方法 print(\n--- 开始执行技能 ---) execution_result chosen_skill.execute(parameters_for_skill) # 5. Agent处理技能返回的结果 print(f技能执行状态: {execution_result[status]}) print(f技能返回信息: {execution_result[message]}) if execution_result[status] success: print(f最终计算结果: {execution_result[result]}) # 在真实场景中Agent可能会用这个结果继续后续步骤或回答用户。 else: print(技能执行失败Agent需要处理错误例如重试或告知用户。) print( 模拟结束 ) if __name__ __main__: simulate_agent_call()运行python simulate_agent.py你会看到类似下面的输出 模拟Agent调用Skill 发现可用技能: [calculator_add] Agent接收到的任务: 我需要计算3.5和4.7的和。 Agent决定使用技能: calculator_add Agent生成的调用参数: {a: 3.5, b: 4.7} --- 开始执行技能 --- 技能执行状态: success 技能返回信息: 计算成功3.5 4.7 8.2 最终计算结果: 8.2 模拟结束 恭喜你已经创建并成功运行了你的第一个Agent Skill虽然这个“Agent”是我们模拟的但整个流程——发现、选择、生成参数、调用、处理结果——与真实LLM驱动的Agent工作流在逻辑上完全一致。3.4 第四步增加复杂度——创建天气查询Skill掌握了基础模式后我们来创建一个更有实用价值、需要与外界交互的Skill天气查询。这将引入HTTP请求、API密钥管理和更复杂的错误处理。首先你需要一个天气API的访问密钥。国内有很多选择这里以和风天气HeWeather的免费版为例进行说明请注意实际开发请注册并遵守其API使用条款。我们假设你已获得一个API Key。# weather_skill.py import requests import json from typing import Any, Dict class WeatherSkill: 根据城市名称查询实时天气的技能。 name get_current_weather description 查询指定城市的当前天气情况包括温度、天气状况和湿度。 # 更复杂的参数模式 parameters_schema { type: object, properties: { city: { type: string, description: 要查询天气的城市名称例如北京、Shanghai。 }, units: { type: string, enum: [metric, imperial], description: 温度单位。metric 表示摄氏度imperial 表示华氏度。默认为 metric。 } }, required: [city] # city是必需的units可选 } def __init__(self, api_key: str): 初始化技能需要传入API密钥。 self.api_key api_key # 这里使用和风天气的免费API端点示例请替换为实际可用端点 self.base_url https://devapi.qweather.com/v7/weather/now def execute(self, parameters: Dict[str, Any]) - Dict[str, Any]: try: city parameters.get(city) units parameters.get(units, metric) # 提供默认值 if not city: raise ValueError(参数 city 是必需的。) # 1. 构造API请求参数 # 注意真实调用可能需要先根据城市名获取location_id这里简化处理。 # 假设我们有一个辅助函数 get_location_id(city)这里用固定值模拟。 location_id 101010100 # 例如北京的location_id params { key: self.api_key, location: location_id, lang: zh, unit: units } # 2. 发送HTTP GET请求 print(f[WeatherSkill] 正在查询城市 {city} 的天气...) response requests.get(self.base_url, paramsparams, timeout10) # 3. 检查HTTP响应状态 response.raise_for_status() # 如果状态码不是200抛出HTTPError # 4. 解析JSON响应 data response.json() # 5. 提取和格式化我们需要的信息 # 根据实际API响应结构调整以下是示例结构 if data.get(code) 200: # 假设API成功返回码为200 now data.get(now, {}) temperature now.get(temp) condition now.get(text) humidity now.get(humidity) result_text f{city}当前天气{condition}温度 {temperature}°{C if unitsmetric else F}湿度 {humidity}%。 return { status: success, result: { temperature: temperature, condition: condition, humidity: humidity, units: units }, message: result_text } else: # API返回了业务逻辑错误 return { status: error, result: None, message: f天气API请求失败{data.get(message, 未知错误)} } except requests.exceptions.Timeout: return {status: error, result: None, message: 请求天气API超时请检查网络或稍后重试。} except requests.exceptions.ConnectionError: return {status: error, result: None, message: 网络连接错误无法访问天气服务。} except requests.exceptions.HTTPError as e: return {status: error, result: None, message: fHTTP请求错误{e.response.status_code}} except ValueError as e: return {status: error, result: None, message: f输入参数错误{e}} except KeyError as e: return {status: error, result: None, message: f解析API响应数据时出错缺少字段{e}} except Exception as e: # 捕获其他所有未预见的异常 return {status: error, result: None, message: f技能执行过程中发生未知错误{type(e).__name__}: {e}}这个WeatherSkill比计算器复杂得多主要体现在外部依赖需要requests库和有效的API密钥。初始化Skill本身需要配置api_key这通常在Agent启动时完成。复杂的错误处理涵盖了网络超时、连接错误、HTTP错误、API业务错误、数据解析错误等多种情况。这是生产级Skill的必备项一个健壮的Skill必须能妥善处理失败并将清晰的错误信息返回给Agent而不是让整个Agent进程崩溃。结果格式化不仅返回原始数据还构造了一段对人类友好的自然语言描述 (result_text)。这非常有用因为Agent可以直接将这段描述用作给用户的回复。同样我们可以更新模拟Agent来测试这个新技能记得替换your_api_key_here。# simulate_agent_weather.py from weather_skill import WeatherSkill def simulate_weather_query(): weather_skill WeatherSkill(api_keyyour_api_key_here) # 请替换为真实KEY available_skills [weather_skill] task 今天上海天气怎么样 print(f任务: {task}) chosen_skill available_skills[0] parameters {city: 上海, units: metric} result chosen_skill.execute(parameters) print(f状态: {result[status]}) print(f结果: {result[message]}) if result[status] success: print(f原始数据: {result[result]}) if __name__ __main__: simulate_weather_query()4. Skill开发中的核心技巧与避坑指南通过上面两个例子你应该已经掌握了创建Skill的基本方法。但在实际项目中还有一些细节和“坑”需要特别注意。4.1 技能描述的“艺术”技能的description和parameters_schema中的description字段是LLM理解技能的唯一途径。写得好坏直接决定了Agent能否正确调用它。要具体不要抽象差“处理数据。”好“计算给定列表中所有数字的平均值。输入是一个数字列表输出是一个浮点数。”说明前置条件和副作用“此技能将向指定的邮箱地址发送一封邮件。需要预先配置SMTP服务器信息。”“调用此技能会修改数据库中的用户状态记录。”参数描述清晰在parameters_schema里为每个属性写明白“description”。例如“city城市名称支持中文或拼音如‘北京’或‘beijing’。”4.2 错误处理必须标准化且全面这是新手最容易忽略也最容易导致Agent崩溃的环节。必须返回结构化结果就像我们例子中的{“status”: “success/error”, “result”: …, “message”: …}。这形成了一个契约调用方只需检查status字段就能知道下一步该做什么。区分不同层级的错误输入错误参数缺失、类型错误由Skill在execute开头验证并返回error。外部服务错误网络、API限流、认证失败在try…except中捕获特定的异常如requests.exceptions.RequestException返回带有明确原因的error。逻辑错误API返回了但业务状态码不对在解析API响应后判断返回error。未知错误用最外层的except Exception兜底至少记录日志并返回一个通用的错误信息避免泄露内部堆栈。实操心得在开发阶段可以暂时将未知异常的详细堆栈信息也返回便于调试。但在生产环境前务必移除或仅记录到日志。4.3 技能应该是无状态和幂等的这是设计可复用、可组合Skill的黄金法则。无状态StatelessSkill的执行结果只依赖于输入的参数不依赖于Skill对象内部或外部的隐藏状态全局变量、上一次调用的结果等。这保证了在任何时候、被任何Agent调用只要输入相同输出就应该是可预期的。幂等Idempotent多次使用相同参数调用同一个Skill应该产生相同的效果且没有额外的副作用。例如“发送邮件”技能可能不是幂等的发两次就发两封邮件但“查询天气”是幂等的。对于非幂等操作需要在描述中明确警告并且调用方Agent需要谨慎处理重试逻辑。4.4 性能与资源考量超时设置所有涉及网络I/O的操作如我们的WeatherSkill必须设置超时timeout参数。否则一个挂掉的外部服务可能会拖死你的整个Agent进程。资源清理如果Skill打开了文件、数据库连接或网络会话确保在finally块或使用上下文管理器with语句进行清理。避免阻塞对于耗时较长的Skill如训练一个小模型要考虑是否应该设计为异步async接口或者由Skill触发一个后台任务并立即返回一个任务ID让Agent后续通过另一个“查询任务状态”的Skill来获取结果。5. 集成与进阶让Skill被真正的Agent使用我们目前只是在模拟。那么如何让我们亲手打造的Skill被一个真正的、由LLM驱动的Agent使用呢这里给出两个最主流的集成方向。5.1 集成到LangChain框架LangChain通过Tool抽象来定义Skill。将我们的CalculatorSkill包装成LangChain Tool非常简单from langchain.tools import Tool from calculator_skill import CalculatorSkill # 创建技能实例 calc_skill CalculatorSkill() # 定义供Tool使用的函数 def langchain_calculator(a: str, b: str) - str: LangChain Tool要求的函数签名一般是输入字符串返回字符串。 # 转换参数类型并调用我们的技能 params {a: float(a), b: float(b)} result calc_skill.execute(params) if result[status] success: return str(result[result]) else: return fError: {result[message]} # 创建Tool calculator_tool Tool( namecalc_skill.name, funclangchain_calculator, descriptioncalc_skill.description, # 可以手动指定参数模式但LangChain主要依赖description让LLM理解 ) # 现在你可以将这个calculator_tool添加到你的Agent中关键在于你需要一个适配函数langchain_calculator将LangChain Tool的调用方式通常是字符串参数转换为你内部Skill的调用方式。5.2 集成到AutoGen框架AutoGen的UserProxyAgent可以直接注册Python函数作为工具。集成更为直接import autogen from calculator_skill import add_numbers # 直接导入原始函数 # 创建UserProxyAgent user_proxy autogen.UserProxyAgent( nameUser_Proxy, human_input_modeNEVER, max_consecutive_auto_reply10, code_execution_config{work_dir: “coding”}, ) # 将我们的函数注册为工具 user_proxy.register_function( function_map{ “add_numbers”: add_numbers # 函数名和函数的映射 } ) # 现在当你在与user_proxy对话时可以说“请使用add_numbers函数计算3.5加4.7” # LLM驱动的AssistantAgent会尝试生成代码来调用这个注册的函数。AutoGen的方式更偏向于让LLM生成代码来调用函数因此对函数的文档字符串docstring质量要求很高LLM依赖它来理解函数用法。5.3 构建你自己的迷你Skill管理系统如果你不想依赖大型框架完全可以基于我们上面的模式构建一个轻量级的Skill管理系统class SkillManager: def __init__(self): self.skills {} # name - skill_instance def register_skill(self, skill): 注册一个技能实例。 self.skills[skill.name] skill def get_skill_descriptions(self): 获取所有技能的描述用于提供给LLM做规划。 descriptions [] for name, skill in self.skills.items(): desc { name: name, description: skill.description, parameters_schema: skill.parameters_schema } descriptions.append(desc) return json.dumps(descriptions, ensure_asciiFalse) def execute_skill(self, skill_name: str, parameters: dict): 根据技能名和参数执行技能。 if skill_name not in self.skills: return {status: error, message: f技能 {skill_name} 未找到。} skill self.skills[skill_name] return skill.execute(parameters) # 使用示例 manager SkillManager() manager.register_skill(CalculatorSkill()) manager.register_skill(WeatherSkill(api_keyyour_key)) # 将 manager.get_skill_descriptions() 的结果作为系统提示词的一部分给LLM # LLM生成形如 {skill: get_current_weather, parameters: {city: 北京}} 的JSON # 你的主程序解析这个JSON然后调用 manager.execute_skill(...)这个简单的管理器实现了技能注册、发现和执行的闭环你可以以此为基础连接上OpenAI、Claude或本地LLM的API就能构建一个属于你自己的、高度定制的AI Agent。走到这一步你已经不再只是一个Skill的使用者而是具备了设计和实现Agent底层能力模块的开发者。理解Skill就是理解了Agent如何与真实世界进行交互的桥梁。接下来你可以尝试创建更多有趣的Skill比如发送邮件、读写数据库、控制智能设备然后用你喜欢的框架或自建的管理器将它们组装起来打造一个真正能帮你处理事务的智能助手。记住每一个强大的Agent都始于一个精心设计的小小Skill。