从0到1手写一个Skill把大模型变成你的“万能工具箱”01 一个让我失眠的夜晚去年有个需求让我熬了三个通宵。业务方说要做一个“智能客服”但要求特别拧巴——既要能查订单、退换货又要能根据用户的历史行为做个性化推荐还得实时对接库存系统。当时我们用的是某主流大模型的API模型本身挺聪明但一碰到“查库存”这种需要实时数据的活儿就开始胡编乱造。那会儿业内还没有特别成熟的Agent框架我试了Function Calling写了十几套prompt模板甚至自己撸了一个简单的RAG流程。结果呢代码越写越臃肿prompt越改越拧巴每次加一个新能力就跟重构整个系统似的。直到我接触到“Skill”这个概念。说白了Skill就是把AI的某项“专项能力”打包成一个独立的插件。想让它查天气装个Weather Skill。想让它操作Jira装个Jira Skill。想让它拉取数据库里的实时报表写个SQL Skill就完事了。就像给手机装App一样装上就能用不用了就卸掉核心系统干干净净。今天我就把自己在这块踩过的坑、总结的经验一次性倒给你。02 Skill到底是什么用一个例子说透别被“Skill”这个花哨名字唬住它的技术本质其实就三层一层接口定义好输入输出规范输入什么参数输出什么格式一层执行逻辑内部干活的代码调API、查数据库、跑算法都行一层注册机制让AI“知道”有这个能力存在以及什么时候该调用它我画个简单的对比你就懂了对比项传统Function CallingSkill插件新增能力改prompt 改代码 重新部署放一个Skill文件进去热加载复用性每个项目重新写一遍跨项目拷贝即用调试难度跟大段的系统代码混在一起独立运行日志隔离团队协作前后端、算法互相锁死各写各的Skill最后组装打个你肯定懂的比方传统方式是给汽车发动机舱里硬塞新零件Skill是给车尾挂一个快拆拖斗。拖斗里装什么都可以装错了摘下来换一个就行发动机舱核心系统一根线都不用动。03 手把手10分钟写一个“查快递”Skill光说不练假把式。下面我用Python写一个真实的Skill让大模型能查询快递物流信息。Step 1定义Skill的“说明书”每个Skill必须告诉AI三件事我能干什么、我需要什么参数、我返回什么格式。# skill_express.py from typing import Optional, Dict, Any from pydantic import BaseModel, Field # 1. 定义输入参数的结构 class ExpressInput(BaseModel): tracking_number: str Field(description快递单号支持中通/圆通/顺丰) carrier: Optional[str] Field( defaultauto, description快递公司编码可选zto/yto/sf默认自动识别 ) # 2. 定义输出的结构让AI知道能拿到什么信息 class ExpressOutput(BaseModel): status: str Field(description物流状态已揽收/运输中/派送中/已签收) location: str Field(description最新位置) timestamp: str Field(description更新时间) details: list Field(description完整物流轨迹列表)Step 2写执行逻辑真正的“干活代码”注意我这里为了演示用了Mock数据。真实场景下这里对接的是快递100、菜鸟裹裹或者顺丰开放平台的API。import httpx import re from datetime import datetime class ExpressSkill: 快递查询技能 def __init__(self): # 真实场景这里放API密钥 self.api_key your_kuaidi100_api_key self.base_url https://api.kuaidi100.com/query def execute(self, input: ExpressInput) - ExpressOutput: # 自动识别快递公司真实场景用正则或算法 carrier input.carrier if carrier auto: carrier self._detect_carrier(input.tracking_number) # 调用快递查询API这里用Mock演示 # 实际开发中换成requests.get(self.base_url, params{...}) raw_data self._mock_query(input.tracking_number, carrier) # 数据清洗 格式转换 return ExpressOutput( statusraw_data[status], locationraw_data[location], timestampdatetime.now().isoformat(), detailsraw_data[traces] ) def _detect_carrier(self, tn: str) - str: # 简单识别逻辑顺丰12位纯数字中通12位数字字母混合... if re.match(r^\d{12}$, tn): returnsf elif re.match(r^[A-Za-z0-9]{12}$, tn): returnzto else: returnyto def _mock_query(self, tn: str, carrier: str) - dict: # Mock数据真实场景替换为API调用 return { status: 派送中, location: 广州市天河区体育西路营业点, traces: [ {time: 2026-07-06 08:30, content: 快递员正在派送中预计今日送达}, {time: 2026-07-05 22:15, content: 到达广州转运中心}, {time: 2026-07-05 14:20, content: 已从深圳发出}, ] }Step 3注册到AI最关键的一步这一步决定了AI在什么时候、以什么方式调用你的Skill。我用LangChain的Tool装饰器来注册from langchain.tools import tool from langchain.schema import Tool # 方式一用装饰器快速注册 tool def query_express(tracking_number: str, carrier: str auto) - str: 查询快递物流信息。 tracking_number: 快递单号 carrier: 快递公司编码zto/yto/sf不填则自动识别 skill ExpressSkill() result skill.execute(ExpressInput( tracking_numbertracking_number, carriercarrier )) returnf物流状态{result.status}\n最新位置{result.location}\n轨迹详情\n \n.join( [f {d[time]} {d[content]}for d in result.details] ) # 方式二手动注册为Tool更灵活适合复杂场景 express_tool Tool( name快递查询, funclambda tn, cauto: query_express(tn, c), description用于查询快递物流信息输入单号和可选快递公司编码 )Step 4接入大模型让AI“学会”用这个Skillfrom langchain.agents import initialize_agent, AgentType from langchain_openai import ChatOpenAI # 初始化模型 llm ChatOpenAI(modelgpt-4, temperature0) # 把Skill塞进Agent的工具箱 tools [query_express] # 可以继续添加其他Skill agent initialize_agent( toolstools, llmllm, agentAgentType.OPENAI_FUNCTIONS, verboseTrue# 调试时打开能看清AI的思考过程 ) # 测试 response agent.invoke({ input: 帮我查一下单号SF1234567890的快递到哪了 }) print(response[output])运行结果物流状态派送中 最新位置广州市天河区体育西路营业点 轨迹详情 2026-07-06 08:30 快递员正在派送中预计今日送达 2026-07-05 22:15 到达广州转运中心 2026-07-05 14:20 已从深圳发出看到没AI自动识别了“查快递”的意图提取了单号调用了我们的Skill然后把结果整理成可读的文字。全程没改一行核心系统代码。04 进阶Skill设计的3个“要命”细节上面那个Demo能跑通但离“生产级”还差得远。我在线上环境被这3个问题坑过不止一次你最好提前知道。细节1超时控制 熔断Skill调用外部API如果挂了整个Agent会跟着卡死。必须加超时import asyncio from functools import wraps def with_timeout(seconds10): def decorator(func): wraps(func) asyncdef wrapper(*args, **kwargs): try: returnawait asyncio.wait_for( func(*args, **kwargs), timeoutseconds ) except asyncio.TimeoutError: return【系统提示】该Skill响应超时请稍后重试或尝试其他方式 return wrapper return decorator # 用法 with_timeout(5) asyncdef query_express_async(tn: str): # 异步请求快递API pass细节2输入校验的“防呆设计”用户输入的快递单号千奇百怪空格、换行、中文括号全都有。我吃过亏用户输入“SF 1234567890”中间有个空格正则直接匹配失败。def sanitize_tracking_number(raw: str) - str: 清洗用户输入只保留数字和字母 return re.sub(r[^A-Za-z0-9], , raw.strip())还有Skill里一定要有fallback。识别不出快递公司就用“中通”兜底查不到数据就返回明确的“未查询到”而不是抛异常。细节3并发安全如果你的AI同时处理多个用户的请求Skill内部如果有共享状态比如缓存、数据库连接池必须注意线程安全。import threading class ExpressSkill: _cache {} _cache_lock threading.Lock() def execute(self, input: ExpressInput): # 读缓存要加锁 with self._cache_lock: if input.tracking_number in self._cache: return self._cache[input.tracking_number] # 查询逻辑... # 写缓存也要加锁 with self._cache_lock: self._cache[input.tracking_number] result return result05 团队协作Skill的“集市模式”我们团队现在有6个人分别维护不同的Skill。前端写“页面操作Skill”后端写“数据查询Skill”算法组写“推荐Skill”。大家互不干扰最后通过一个统一的Skill Registry注册中心聚合起来。目录结构长这样skills/ ├── registry.yaml # 所有Skill的注册清单 ├── express/ │ ├── skill.py │ ├── config.yaml │ └── tests/ ├── jira/ │ ├── skill.py │ └── README.md ├── sql_analytics/ │ ├── skill.py │ └── queries/ └── weather/ ├── skill.py └── requirements.txt # 每个Skill可以有自己的依赖每个Skill独立维护自己的版本、依赖、测试用例。集成测试只测“调用接口是否正常”不管内部实现。这是最像“App Store”的地方——各个Skill独立开发、独立发布主系统只认接口契约。06 写在最后Skill不是银弹但它是方向老实说Skill这套模式不是没有代价。它增加了系统整体的复杂度——你得维护注册中心、做热加载、处理版本兼容。如果你的业务只有两三个AI能力硬编码比搞Skill架构快得多。但当你的AI能力超过5个、团队超过3个人的时候Skill带来的解耦收益会指数级增长。我们现在的系统里跑了17个Skill涵盖客服、运维、数据分析三个领域新增一个能力从“改核心代码全量回归”变成了“写Skill注册”周期从2周压缩到2天。如果你正准备在项目中引入AI能力我的建议是前3个能力硬编码先跑通业务理解真实需求到第4-5个能力时回头抽象出Skill接口把前3个也迁过来第6个以后所有新能力一律以Skill形式接入这样既不会过度设计又能平滑过渡到插件化架构。最后送给你我写在团队Wiki首页的一句话“Skill化的本质是把AI从一个‘什么都懂但什么都做不精’的泛泛之辈变成一个‘随时可以请外援’的团队指挥官。”下次有机会我再写一篇《Skill的异步调度与分布式部署实战》讲讲我们怎么把Skill跑在K8s上、以及如何用RabbitMQ做Skill的任务队列。感兴趣的朋友可以点个关注咱们下回见。