一篇帮你从"知道 Skill 这个词"到"能独立设计生产级 Skill"的系统教学,含 3 个完整实战案例。
阅读提示
适合谁看:正在做或准备做 AI Agent 开发的工程师,尤其是从传统后端 / 数据仓库转过来的同学 看完能做什么:能独立写一个完整的 Skill,能说清 Skill 的注册、调度、执行全链路,能避开 80% 的常见坑 不适合谁:只用 ChatGPT 聊天、不涉及 Agent 系统开发的用户
先给结论
Skill 本质上是一段"被结构化描述的可复用能力",它和普通函数的区别不在于代码本身,而在于它对 LLM 是可发现、可理解、可调度的 一个 Skill 的质量,70% 取决于描述(description)和触发规则(trigger),而不是执行逻辑本身 写 Skill 最常见的错误不是代码写错,而是"描述太模糊导致 LLM 不知道什么时候该用它"
你有没有遇到过这种情况:你给 Agent 接了一个天气查询 API,测试的时候手动调用没问题,但用户说"明天北京冷不冷",Agent 就开始瞎猜,根本不调你的接口。
或者你写了一个计算工具,用户说"帮我算一下 15% 的折扣价",Agent 自己心算了一个错的答案,完全忽略了你的函数。
问题不在模型笨,而在你给模型的"工具说明书"写得太差了。
很多人以为 Skill 就是包了一层的函数调用,写个 name、写个 parameters 就完事了。但真正让 Skill 在 Agent 系统里可靠工作的,是描述、触发规则、参数校验、返回格式、异常兜底这一整套设计。
这一篇,我会从最基础的定义开始,一直讲到底层注册机制和调度引擎,中间穿插 3 个可以直接跑的实战 Skill。看完之后,你不只会写 Skill,还能说清楚"为什么这么写"。
01 先搞清楚:Skill 到底是什么,和插件 / 工具有什么区别
很多文章把 Skill、Plugin、Tool 这三个词混着用,导致读者越看越迷糊。先在这里把边界画清楚。
Tool(工具):最底层的概念。一个 HTTP API、一个数据库查询函数、一个文件读写操作,都是 Tool。它只关心"输入什么、输出什么",不关心"什么时候该被调用"。
Plugin(插件):一组 Tool 的打包。比如一个"飞书插件"可能包含发消息、读文档、查日历等十几个 Tool。Plugin 解决的是"怎么安装和管理",不解决"怎么被 LLM 理解"。
Skill(技能):在 Tool 之上加了一层"对 LLM 的语义描述"。它不只是一个可执行的函数,还包含:
这个能力是干什么的(description) 什么情况下应该触发它(trigger rules) 需要从用户输入里提取哪些参数(slot definitions) 执行出错了怎么办(error handling)
用一句话总结:Tool 是给机器看的接口,Skill 是给 LLM 看的能力说明书。
这个区别非常关键。一个写得好的 Skill,LLM 能准确判断什么时候该调它、该传什么参数;一个写得差的 Skill,即使代码逻辑完全正确,LLM 也可能在该用的时候不用、不该用的时候乱用。

图 1|Tool / Plugin / Skill 三层关系
这一节的小结
Skill 不是"带描述的函数"这么简单。它是连接 LLM 推理能力和外部可执行能力之间的桥梁,核心价值在于让 LLM 能"理解"和"选择"什么时候使用哪个能力。
02 Skill 的核心组成结构:7 个缺一不可的模块
一个完整的 Skill 由 7 个部分组成。少了任何一块,在实际 Agent 系统里都会出问题。
图 2|Skill 核心组成结构全景
1. 名称(name)
Skill 的唯一标识符。要求:
全小写,用下划线连接: get_weather、calculate_discount动词开头,一眼能看出它干什么 避免泛化命名如 do_something、process_data
2. 描述(description)
这是整个 Skill 里最重要的字段,没有之一。LLM 靠这段文字决定什么时候调用你的 Skill。
写好描述的关键原则:
说清楚"能干什么": 查询指定城市未来 3 天的天气预报说清楚"什么时候用": 当用户询问天气、温度、是否下雨、穿衣建议时触发说清楚"不适用什么": 不支持历史天气查询,不支持全球所有城市
代码 1
# 好的描述name:get_weatherdescription:| 查询指定城市的天气预报,支持未来 1-7 天。 触发场景:用户问天气、温度、降雨概率、穿衣建议。 不支持:历史天气、空气质量、紫外线指数。# 差的描述(LLM 无法准确判断何时调用)name:get_weatherdescription:获取天气信息3. 触发规则(trigger rules)
描述是"大致什么时候用",触发规则是"精确匹配条件"。常见写法:
triggers:-intent:weather_query# 意图匹配keywords:[天气,温度,下雨,穿什么]required_slots:[city]# 必须有城市参数才触发-intent:travel_planningkeywords:[出差,旅游,带伞]optional_slots:[date_range]4. 入参槽位(parameter slots)
定义 Skill 需要从用户输入里提取哪些信息。每个槽位包含:
parameters:city:type:stringdescription:城市名称,如"北京"、"上海"required:trueextraction_rules:-entity_type:location-aliases:[帝都,魔都,深圳]days:type:integerdescription:预报天数,1-7required:falsedefault:3constraints:min:1max:75. 执行逻辑(execution logic)
实际的业务代码。这一层和普通函数没什么区别,关键是要做好参数校验和异常处理:
asyncdefexecute(city: str, days: int = 3) -> SkillResult:# 参数校验ifnot city or len(city) > 20:return SkillResult.error("城市名称不合法")if days < 1or days > 7:return SkillResult.error("预报天数必须在 1-7 之间")try: weather = await weather_api.fetch(city, days)return SkillResult.success(format_weather(weather))except CityNotFoundError:return SkillResult.error(f"未找到城市:{city}")except APIRateLimitError:return SkillResult.error("天气服务繁忙,请稍后重试")6. 返回格式(return format)
统一的返回结构让 Agent 框架能标准化处理所有 Skill 的结果:
@dataclassclassSkillResult: status: str # "success" | "error" | "partial" data: dict # 结构化数据 message: str # 给 LLM 的自然语言摘要 metadata: dict # 耗时、来源、置信度等7. 权限控制(permission control)
哪些场景下允许调用、哪些不允许:
permissions:rate_limit:10/minute# 频率限制auth_required:false# 是否需要用户授权allowed_channels:[web,app]# 允许的调用渠道data_retention:none# 是否缓存结果这一节的小结
7 个模块各司其职。其中 description 和 trigger rules 决定了"LLM 能不能准确找到它",parameter slots 决定了"找到之后能不能正确调用",execution logic 和 error handling 决定了"调用之后能不能可靠返回"。
03 手把手教你写第一个 Skill:语法、规范与参数校验
理论讲够了,直接上手。我们来写一个"商品价格查询" Skill,这是 Agent 系统里最常见的查询类场景。
Step 1:定义 Skill 元信息
name:query_product_priceversion:"1.0.0"description:| 查询小米商城商品的实时价格。 当用户问"多少钱"、"价格"、"便宜多少"、"有没有优惠"时触发。 支持按商品名称、型号、SKU 查询。 不支持:历史价格趋势、竞品比价。author:xitingtags:[ecommerce,price,xiaomi]Step 2:定义参数槽位
parameters:product_name:type:stringdescription:商品名称或型号,如"小米14"、"RedmiNote14Pro"required:trueextraction_rules:-entity_type:product-fuzzy_match:true# 允许模糊匹配validation:min_length:2max_length:50platform:type:stringdescription:查询平台required:falsedefault:"xiaomi_mall"enum:[xiaomi_mall,jd,taobao]Step 3:编写执行逻辑
from typing import Optionalfrom dataclasses import dataclass@dataclassclassSkillResult: status: str data: dict message: strasyncdefexecute(product_name: str, platform: str = "xiaomi_mall") -> SkillResult:"""查询商品价格 - 这是 Skill 的核心执行逻辑"""# 1. 参数校验(永远不要信任 LLM 提取的参数)ifnot product_name or len(product_name.strip()) < 2:return SkillResult( status="error", data={}, message="商品名称太短,请提供更具体的商品名称或型号。" )# 2. 实体消歧(同一个商品可能有多种叫法) resolved = await resolve_product(product_name)ifnot resolved:return SkillResult( status="error", data={}, message=f"未找到名为「{product_name}」的商品,请确认名称是否正确。" )# 3. 调用外部 APItry: price_info = await price_api.query( sku=resolved.sku, platform=platform )except APITimeoutError:return SkillResult( status="error", data={}, message="价格服务响应超时,请稍后重试。" )# 4. 格式化返回(给 LLM 看的自然语言摘要 + 给系统看的结构化数据)return SkillResult( status="success", data={"product_name": resolved.name,"sku": resolved.sku,"current_price": price_info.price,"original_price": price_info.original_price,"discount": price_info.discount,"platform": platform,"url": price_info.url }, message=f"{resolved.name} 在{platform_name(platform)}的当前售价为 ¥{price_info.price}"f"(原价 ¥{price_info.original_price},{price_info.discount})。" )Step 4:编写参数校验规则
参数校验是 Skill 稳定性的第一道防线。很多人的 Skill 在测试时没问题,一上用户环境就崩,80% 的原因是参数校验不够。
# 校验模式:先宽松匹配,再严格校验defvalidate_params(raw_params: dict) -> tuple[bool, dict, str]:""" 三步校验法: 1. 类型检查 - 是不是期望的数据类型 2. 范围检查 - 值在不在合法范围内 3. 语义检查 - 值有没有实际意义 """ errors = []# 类型检查 product_name = raw_params.get("product_name")if product_name andnot isinstance(product_name, str): errors.append("product_name 必须是字符串")# 范围检查 platform = raw_params.get("platform", "xiaomi_mall") valid_platforms = ["xiaomi_mall", "jd", "taobao"]if platform notin valid_platforms: errors.append(f"platform 必须是 {valid_platforms} 之一")# 语义检查(最容易被忽略)if product_name and len(product_name.strip()) < 2: errors.append("商品名称至少需要 2 个字符")if errors:returnFalse, {}, ";".join(errors)returnTrue, {"product_name": product_name.strip(), "platform": platform}, ""这一节的小结
写 Skill 的执行逻辑不难,难的是"把各种异常情况都想到并处理好"。参数校验、实体消歧、超时处理、错误消息——这些看起来是细节,实际上决定了你的 Skill 在生产环境里能不能稳定工作。
04 Skill 调用全链路:从用户提问到结果返回,中间到底发生了什么
很多人写完 Skill 就以为完事了,但 Skill 只是整个调用链路中的一个环节。理解全链路,才能写出和系统其他部分配合得好的 Skill。
图 3|Skill 调用全链路流程
完整的调用流程分 6 步:
第 1 步:用户输入
用户说了一句话:"小米 14 Pro 现在多少钱?"
第 2 步:意图识别(Intent Recognition)
Agent 框架把用户输入发给 LLM,LLM 判断这句话的意图。这一步的核心是 prompt engineering:
你是一个意图分类器。根据用户输入,判断应该调用哪个工具。可用工具:1. query_product_price - 查询商品价格。当用户问"多少钱"、"价格"时使用。2. get_weather - 查询天气。当用户问天气、温度时使用。3. search_product - 搜索商品。当用户想找某个商品但没问价格时使用。用户输入:小米 14 Pro 现在多少钱?LLM 返回:应该调用 query_product_price,提取参数 product_name = "小米 14 Pro"。
第 3 步:槽位填充(Slot Filling)
框架从 LLM 的输出中提取结构化参数。这一步常见的坑:
LLM 提取了 product_name = "小米14Pro"(没有空格),但实际数据库里叫"小米 14 Pro"LLM 漏提了可选参数 platform,需要用默认值补上LLM 多提了不存在的参数,需要过滤掉
第 4 步:参数校验
上一节讲的三步校验法在这里执行。校验不通过,直接返回友好的错误消息,不调用外部 API。
第 5 步:执行 Skill
调用 query_product_price.execute(),实际发起 HTTP 请求查询价格。
第 6 步:结果返回与格式化
Skill 返回结构化的 SkillResult,Agent 框架把 message 字段的内容拼进 LLM 的上下文,LLM 生成最终的自然语言回复给用户。
小米 14 Pro 在小米商城的当前售价为 ¥4499(原价 ¥4999,9 折)。需要我帮你看看有没有其他优惠券吗?关键洞察
整个链路里最容易出问题的不是执行逻辑,而是第 2 步和第 3 步——意图识别和槽位填充。而这两步的准确性,主要取决于你在 Skill 定义里写的 description 和 triggers。
这就是为什么我说"一个 Skill 的质量,70% 取决于描述"。
05 深入底层原理:Skill 注册机制、调度引擎与上下文透传
如果你想从"会写 Skill"进阶到"能设计 Skill 系统",这一节是核心。
图 4|Skill 注册与调度引擎架构
5.1 Skill 注册机制
Skill 写好了,怎么让 Agent 框架知道它的存在?常见的注册方式有三种:
静态注册:在配置文件里声明所有 Skill,框架启动时一次性加载。
# agent_config.yamlskills:-name:query_product_pricemodule:skills.price_queryenabled:true-name:get_weathermodule:skills.weatherenabled:true优点:简单、可控。缺点:新增 Skill 需要重启服务。
动态注册:Skill 在运行时主动向调度引擎注册自己。
# Skill 启动时engine = SkillEngine.get_instance()engine.register( name="query_product_price", description="查询商品价格...", handler=execute, triggers=["多少钱", "价格", "优惠"])优点:热插拔,不用重启。缺点:需要处理注册冲突和去重。
声明式注册:Skill 通过装饰器或类继承自动注册,框架扫描特定目录下的所有 Skill 定义。
@skill( name="query_product_price", description="查询商品价格", triggers=["多少钱", "价格"])classPriceQuerySkill(BaseSkill):asyncdefexecute(self, product_name: str, **kwargs) -> SkillResult: ...这是目前主流 Agent 框架(LangChain、LlamaIndex、OpenCode 等)最常用的方式。
5.2 调度引擎怎么选择 Skill
当用户发了一句话,调度引擎需要从几十甚至几百个 Skill 里找到最合适的一个。这个过程通常是"两阶段匹配":
第一阶段:粗筛(Fast Filtering)
用关键词匹配、意图分类器或向量检索,从全量 Skill 里快速缩小候选范围。这一步要求快,通常在 10ms 内完成。
# 向量检索方式user_embedding = embed(user_input)candidates = skill_index.search(user_embedding, top_k=5)第二阶段:精排(Precise Ranking)
把候选 Skill 的 description 拼进 LLM 的 system prompt,让 LLM 做最终选择。这一步准,但慢(需要一次 LLM 调用)。
你是工具选择器。以下是候选工具:1. query_product_price:查询商品价格。当用户问"多少钱"、"价格"时使用。2. search_product:搜索商品目录。当用户想找某个商品但没问价格时使用。3. get_discount_info:查询优惠活动。当用户问"有没有优惠"、"打折吗"时使用。用户说:小米 14 Pro 现在多少钱?选择最合适的工具(只返回编号):5.3 上下文透传
Skill 执行时,不是在一个真空环境里。它需要访问的上下文包括:
对话历史:用户之前说了什么(可能影响参数提取) 用户信息:用户 ID、会员等级、地区偏好 会话状态:之前调用过哪些 Skill、中间结果是什么 系统配置:超时时间、重试策略、降级方案
这些上下文通过 Context 对象透传给 Skill:
@dataclassclassSkillContext: user_id: str session_id: str conversation_history: list[Message] user_profile: dict system_config: dict intermediate_results: dict # 前面 Skill 的执行结果asyncdefexecute(params: dict, context: SkillContext) -> SkillResult:# 可以通过 context 获取用户所在城市,作为默认参数 default_city = context.user_profile.get("city", "北京") ...这一节的小结
注册机制决定了"Skill 怎么被发现",调度引擎决定了"怎么选对 Skill",上下文透传决定了"Skill 能不能拿到它需要的信息"。这三层共同构成了 Skill 系统的基础设施。
06 高阶进阶:多轮对话 Skill、嵌套 Skill、条件分支与异常兜底
基础 Skill 是单轮的——用户问一句,Skill 执行一次,返回结果。但真实场景往往更复杂。
图 5|高阶 Skill 模式
6.1 多轮对话 Skill
用户说"帮我查一下小米 14 的价格",Skill 返回价格后,用户接着问"那 Pro 版呢?"——这时候 Skill 需要理解上下文,知道"Pro 版"指的是"小米 14 Pro"。
classPriceQuerySkill(BaseSkill):asyncdefexecute(self, params: dict, context: SkillContext) -> SkillResult: product_name = params.get("product_name")# 多轮对话:如果用户没给完整商品名,从历史里补全ifnot product_name or len(product_name) < 4: last_product = self._extract_last_product(context.conversation_history)if last_product: product_name = f"{last_product}{product_name}".strip()# 继续执行查询...6.2 嵌套 Skill
一个 Skill 的执行过程中,需要调用另一个 Skill。比如"下单 Skill"内部需要调用"查库存 Skill"和"计算价格 Skill"。
classPlaceOrderSkill(BaseSkill):asyncdefexecute(self, params: dict, context: SkillContext) -> SkillResult:# 嵌套调用:先查库存 stock_result = await self.engine.invoke_skill("check_stock", {"product_id": params["product_id"], "quantity": params["quantity"]}, context )if stock_result.status == "error":return stock_result # 库存不足,直接返回错误# 嵌套调用:计算价格 price_result = await self.engine.invoke_skill("calculate_price", {"product_id": params["product_id"], "coupon": params.get("coupon")}, context )# 组合结果,执行下单逻辑 ...嵌套 Skill 的关键设计原则:
嵌套深度不超过 3 层,否则调试成本指数级上升 每个嵌套调用都要有超时和降级方案 子 Skill 的错误要能被父 Skill 捕获和处理
6.3 条件分支 Skill
同一个 Skill,根据参数不同走不同的执行路径。
asyncdefexecute(self, params: dict, context: SkillContext) -> SkillResult: query_type = params.get("query_type", "current")if query_type == "current":# 查当前价格returnawait self._query_current_price(params)elif query_type == "history":# 查历史价格(需要调不同的 API)returnawait self._query_price_history(params)elif query_type == "compare":# 多平台比价returnawait self._compare_prices(params)else:return SkillResult.error(f"不支持的查询类型:{query_type}")6.4 异常兜底与重试机制
生产环境里,外部 API 超时、网络抖动、参数异常都是家常便饭。一个好的 Skill 必须有完善的异常处理:
classResilientSkill(BaseSkill): MAX_RETRIES = 3 RETRY_DELAY = 1.0# 秒asyncdefexecute_with_retry(self, params: dict, context: SkillContext) -> SkillResult: last_error = Nonefor attempt in range(self.MAX_RETRIES):try: result = await self.execute(params, context)if result.status != "error":return result last_error = result.messageexcept TimeoutError: last_error = "请求超时"await asyncio.sleep(self.RETRY_DELAY * (attempt + 1)) # 指数退避except ConnectionError: last_error = "网络连接失败"await asyncio.sleep(self.RETRY_DELAY * (attempt + 1))# 所有重试都失败,走降级方案returnawait self._fallback(params, context, last_error)asyncdef_fallback(self, params: dict, context: SkillContext, error: str) -> SkillResult:"""降级方案:返回缓存数据或引导用户稍后重试""" cached = await self.cache.get(f"price:{params.get('product_name')}")if cached:return SkillResult( status="partial", data=cached, message=f"(数据可能不是最新的){cached['summary']}" )return SkillResult.error(f"服务暂时不可用:{error},请稍后重试。")重试策略的核心原则:
用指数退避(1s, 2s, 4s),不要用固定间隔 设置最大重试次数,通常 3 次 最后一次失败后走降级方案,不要无限重试 只重试可恢复的错误(超时、网络),不重试参数错误
07 实战:从零完整手写 3 个 Skill
实战 1:查询类 Skill——飞书文档搜索
from dataclasses import dataclass, field@dataclassclassSkillResult: status: str # success / error / partial data: dict = field(default_factory=dict) message: str = "" @classmethoddefsuccess(cls, data: dict, message: str = ""):return cls(status="success", data=data, message=message) @classmethoddeferror(cls, message: str):return cls(status="error", data={}, message=message)# Skill 定义SKILL_DEFINITION = {"name": "search_feishu_doc","description": """ 搜索飞书云文档。当用户说"找文档"、"搜一下"、"之前的文档"时触发。 支持按关键词、作者、时间范围搜索。 不支持:搜索聊天记录、搜索邮件。 ""","parameters": {"keyword": {"type": "string","description": "搜索关键词","required": True },"owner": {"type": "string","description": "文档作者,可选","required": False },"time_range": {"type": "string","description": "时间范围,如'最近一周'、'本月'","required": False,"default": "最近一周" } }}asyncdefexecute(keyword: str, owner: str = None, time_range: str = "最近一周", context=None) -> SkillResult:"""飞书文档搜索的执行逻辑"""# 参数校验ifnot keyword or len(keyword.strip()) < 2:return SkillResult.error("搜索关键词至少需要 2 个字符。")# 时间范围解析 date_filter = parse_time_range(time_range)ifnot date_filter:return SkillResult.error(f"无法理解时间范围「{time_range}」,请用'最近一周'、'本月'等表述。")# 解析作者(如果有) owner_id = Noneif owner: owner_id = await resolve_user_id(owner)ifnot owner_id:return SkillResult.error(f"未找到名为「{owner}」的用户。")# 调用飞书 APItry: results = await feishu_api.search_docs( keyword=keyword, owner_id=owner_id, start_time=date_filter["start"], end_time=date_filter["end"], page_size=10 )except FeishuAPIError as e:return SkillResult.error(f"飞书接口异常:{e.message}")ifnot results:return SkillResult.success( data={"documents": [], "total": 0}, message=f"未找到包含「{keyword}」的文档。" )# 格式化结果 doc_list = [ {"title": doc.title,"url": doc.url,"owner": doc.owner_name,"updated_at": doc.updated_at,"snippet": doc.snippet[:100] }for doc in results ] summary = f"找到 {len(doc_list)} 篇文档:\n"for i, doc in enumerate(doc_list[:5], 1): summary += f"{i}. {doc['title']}({doc['owner']},{doc['updated_at']})\n"return SkillResult.success( data={"documents": doc_list, "total": len(doc_list)}, message=summary )实战 2:计算类 Skill——折扣价格计算
SKILL_DEFINITION = {"name": "calculate_discount","description": """ 计算商品的折扣后价格。当用户说"打折"、"优惠后多少"、"算一下折扣"时触发。 支持百分比折扣、满减、多件折扣。 不支持:跨商品组合优惠、会员积分抵扣。 ""","parameters": {"original_price": {"type": "number","description": "原价","required": True,"validation": {"min": 0.01, "max": 999999} },"discount_type": {"type": "string","description": "折扣类型","required": True,"enum": ["percentage", "fixed_amount", "buy_more"] },"discount_value": {"type": "number","description": "折扣值:百分比写 85 表示 85 折,满减写减免金额,多件写件数","required": True },"quantity": {"type": "integer","description": "购买数量,多件折扣时必填","required": False,"default": 1 } }}asyncdefexecute(original_price: float, discount_type: str, discount_value: float, quantity: int = 1, context=None) -> SkillResult:# 参数校验if original_price <= 0:return SkillResult.error("原价必须大于 0。")if discount_type == "percentage"and (discount_value <= 0or discount_value > 100):return SkillResult.error("百分比折扣必须在 0-100 之间。")if discount_type == "buy_more"and quantity < int(discount_value):return SkillResult.error(f"多件折扣需要购买至少 {int(discount_value)} 件。")# 计算逻辑if discount_type == "percentage": final_price = original_price * (discount_value / 100) * quantity saving = original_price * quantity - final_price detail = f"{discount_value / 10} 折"elif discount_type == "fixed_amount":if original_price < discount_value:return SkillResult.error("满减金额不能超过原价。") final_price = (original_price - discount_value) * quantity saving = discount_value * quantity detail = f"满 ¥{original_price} 减 ¥{discount_value}"elif discount_type == "buy_more": free_count = quantity // int(discount_value) pay_count = quantity - free_count final_price = original_price * pay_count saving = original_price * free_count detail = f"买 {int(discount_value)} 件减 1 件"else:return SkillResult.error(f"不支持的折扣类型:{discount_type}")return SkillResult.success( data={"original_price": original_price,"quantity": quantity,"discount_type": discount_type,"discount_detail": detail,"final_price": round(final_price, 2),"saving": round(saving, 2) }, message=f"原价 ¥{original_price} × {quantity} 件 = ¥{original_price * quantity},"f"享受{detail}后实付 ¥{round(final_price, 2)},立省 ¥{round(saving, 2)}。" )实战 3:工具调用类 Skill——数据库查询
SKILL_DEFINITION = {"name": "query_database","description": """ 查询业务数据库中的数据。当用户说"查一下数据"、"跑个 SQL"、"看看有多少"时触发。 只支持 SELECT 查询,不支持 INSERT/UPDATE/DELETE。 自动限制返回行数为 1000 行。 ""","parameters": {"sql": {"type": "string","description": "SQL 查询语句,只支持 SELECT","required": True },"database": {"type": "string","description": "目标数据库","required": False,"default": "analytics","enum": ["analytics", "user_db", "order_db"] } }}# SQL 安全校验(关键!)FORBIDDEN_KEYWORDS = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE","CREATE", "GRANT", "REVOKE", "EXEC", "EXECUTE"]asyncdefexecute(sql: str, database: str = "analytics", context=None) -> SkillResult:"""安全地执行 SQL 查询"""# 1. SQL 注入防护(第一优先级) sql_upper = sql.upper().strip()for keyword in FORBIDDEN_KEYWORDS:if keyword in sql_upper:return SkillResult.error(f"安全限制:不允许执行 {keyword} 操作。本 Skill 只支持 SELECT 查询。" )# 2. 必须以 SELECT 开头ifnot sql_upper.startswith("SELECT"):return SkillResult.error("SQL 必须以 SELECT 开头。")# 3. 自动添加 LIMIT(防止查询过大拖垮数据库)if"LIMIT"notin sql_upper: sql = sql.rstrip(";") + " LIMIT 1000;"# 4. 执行查询try: conn = await get_db_connection(database) result = await conn.execute(sql) rows = result.fetchall() columns = result.keys()except SQLError as e:return SkillResult.error(f"SQL 执行失败:{e.message}")finally:await conn.close()# 5. 格式化结果ifnot rows:return SkillResult.success( data={"columns": [], "rows": [], "row_count": 0}, message="查询结果为空。" ) data = {"columns": list(columns),"rows": [dict(zip(columns, row)) for row in rows[:100]], # 前 100 行"row_count": len(rows),"truncated": len(rows) > 100 } summary = f"查询返回 {len(rows)} 行数据"if data["truncated"]: summary += "(已截取前 100 行)" summary += "。"return SkillResult.success(data=data, message=summary)实战小结
三个 Skill 代表了三种典型模式:
查询类:重点在参数校验和结果格式化 计算类:重点在业务逻辑分支和数值精度 工具调用类:重点在安全防护和资源管理
不管哪种模式,参数校验、异常处理、友好的错误消息都是必须的。
08 常见报错排查、性能优化与面试核心考点
8.1 常见报错与排查
8.2 性能优化
描述优化:description 不是越长越好。太长的描述会占用 LLM 的上下文窗口,反而降低匹配准确率。经验是 50-150 字。
粗筛优化:当 Skill 数量超过 50 个时,不能让 LLM 逐个判断。用向量检索做粗筛,把候选范围缩小到 5-10 个。
缓存策略:对于查询类 Skill,同样的参数短时间内返回缓存结果,避免重复调用外部 API。
from functools import lru_cachefrom datetime import datetime, timedeltaclassSkillCache:def__init__(self, ttl_seconds: int = 300): self._cache = {} self._ttl = ttl_secondsasyncdefget_or_execute(self, key: str, executor): now = datetime.now()if key in self._cache: result, cached_at = self._cache[key]if (now - cached_at).seconds < self._ttl:return result result = await executor() self._cache[key] = (result, now)return result8.3 Skill 设计最佳实践
单一职责:一个 Skill 只做一件事。"查询价格"和"计算折扣"应该是两个 Skill。 描述精确:description 里要说清楚"能做什么"和"不能做什么"。 错误友好:错误消息要告诉用户下一步怎么做,不要只说"失败了"。 幂等设计:同一个 Skill 同样的参数调用两次,结果应该一样(查询类天然幂等,下单类需要特殊处理)。 可观测:每次调用都要记录日志,包括输入参数、执行耗时、返回状态。
8.4 面试核心考点
如果你在面试 Agent 相关岗位,这些 Skill 相关的问题大概率会被问到:
Q1:Skill 和普通函数有什么区别?
核心答:Skill 在普通函数之上加了语义描述层(description、triggers、slot definitions),让 LLM 能理解"什么时候该用这个函数"。普通函数是给程序员看的接口,Skill 是给 LLM 看的能力说明书。
Q2:怎么让 LLM 准确选择该调用哪个 Skill?
核心答:两阶段匹配——先用关键词/向量检索粗筛候选,再用 LLM 精排。关键是 description 要写得精确,既要说能力,也要说边界。
Q3:Skill 之间的依赖和冲突怎么处理?
核心答:依赖用嵌套调用解决,冲突用优先级机制解决。优先级可以基于 description 的语义相关度、Skill 的置信度评分、或显式的优先级配置。
Q4:怎么处理 Skill 执行失败?
核心答:三层防线——参数校验防第一层、重试机制防第二层、降级方案防第三层。关键是不要让一个 Skill 的失败拖垮整个 Agent。
Q5:Skill 数量多了以后性能怎么优化?
核心答:粗筛用向量检索(快),精排用 LLM(准),结果用缓存(省)。分层处理,别让 LLM 看所有 Skill。
决策帮助
如果你现在是刚接触 Agent 开发,先写 2-3 个单轮查询类 Skill,把 description 和参数校验写扎实 如果你已经在做多轮对话,重点投入在上下文透传和槽位补全逻辑上 如果你在设计Skill 系统,优先把注册机制和调度引擎做对,Skill 的代码反而是最简单的部分 如果你只能先做一步,先把 description 写好——这一个动作能解决 50% 的调用不准问题
如果这篇帮你把 Skill 的全貌看清了,欢迎点个赞。
如果你在实际写 Skill 的过程中遇到了具体问题——比如"为什么我的 Skill 总是不被触发"或者"多轮对话参数丢失怎么处理"——欢迎直接在评论区写下你的场景,我会优先按真实问题继续拆。
下一篇,我会讲 Skill 与 MCP 协议的关系:怎么把 Skill 包装成 MCP Server,让同一个 Skill 能被不同的 Agent 框架复用。
参考链接
[OpenAI Function Calling 官方文档] :https://platform.openai.com/docs/guides/function-calling [LangChain Tools 文档] :https://python.langchain.com/docs/modules/tools/ [Anthropic Tool Use 文档] :https://docs.anthropic.com/en/docs/build-with-claude/tool-use)
夜雨聆风