AI Agent Skill 从入门到精通:定义、结构、调用链路与底层原理

一篇帮你从"知道 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_weathercalculate_discount

  • 动词开头,一眼能看出它干什么

  • 避免泛化命名如 do_somethingprocess_data

2. 描述(description)

这是整个 Skill 里最重要的字段,没有之一。LLM 靠这段文字决定什么时候调用你的 Skill。

写好描述的关键原则:

  • 说清楚"能干什么":查询指定城市未来 3 天的天气预报

  • 说清楚"什么时候用":当用户询问天气、温度、是否下雨、穿衣建议时触发

  • 说清楚"不适用什么":不支持历史天气查询,不支持全球所有城市

代码 1

# 好的描述
name: get_weather
description: |
  查询指定城市的天气预报,支持未来 1-7 天。
  触发场景:用户问天气、温度、降雨概率、穿衣建议。
  不支持:历史天气、空气质量、紫外线指数。

# 差的描述(LLM 无法准确判断何时调用)
name: get_weather
description: 获取天气信息

3. 触发规则(trigger rules)

描述是"大致什么时候用",触发规则是"精确匹配条件"。常见写法:

triggers:
  -intent:weather_query          # 意图匹配
    keywords:[天气,温度,下雨,穿什么]
    required_slots:[city]          # 必须有城市参数才触发
-intent:travel_planning
    keywords:[出差,旅游,带伞]
    optional_slots:[date_range]

4. 入参槽位(parameter slots)

定义 Skill 需要从用户输入里提取哪些信息。每个槽位包含:

parameters:
  city:
    type:string
    description:城市名称,如"北京"、"上海"
    required:true
    extraction_rules:
      -entity_type:location
      -aliases:[帝都,魔都,深圳]
days:
    type:integer
    description:预报天数,1-7
    required:false
    default:3
    constraints:
      min:1
      max:7

5. 执行逻辑(execution logic)

实际的业务代码。这一层和普通函数没什么区别,关键是要做好参数校验和异常处理:

async def execute(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 的结果:

@dataclass
class SkillResult:
    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_price
version: "1.0.0"
description: |
  查询小米商城商品的实时价格。
  当用户问"多少钱"、"价格"、"便宜多少"、"有没有优惠"时触发。
  支持按商品名称、型号、SKU 查询。
  不支持:历史价格趋势、竞品比价。
author: xiting
tags: [ecommerce, price, xiaomi]

Step 2:定义参数槽位

parameters:
  product_name:
    type:string
    description:商品名称或型号,如"小米14"、"RedmiNote14Pro"
    required:true
    extraction_rules:
      -entity_type:product
      -fuzzy_match:true           # 允许模糊匹配
    validation:
      min_length:2
      max_length:50

platform:
    type:string
    description:查询平台
    required:false
    default:"xiaomi_mall"
    enum:[xiaomi_mall,jd,taobao]

Step 3:编写执行逻辑

from typing import Optional
from dataclasses import dataclass

@dataclass
class SkillResult:
    status: str
    data: dict
    message: str

asyncdef execute(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. 调用外部 API
    try:
        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% 的原因是参数校验不够。

# 校验模式:先宽松匹配,再严格校验
def validate_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.yaml
skills:
-name:query_product_price
    module:skills.price_query
    enabled:true
-name:get_weather
    module:skills.weather
    enabled: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=["多少钱", "价格"]
)
class PriceQuerySkill(BaseSkill):
    async def execute(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:

@dataclass
class SkillContext:
    user_id: str
    session_id: str
    conversation_history: list[Message]
    user_profile: dict
    system_config: dict
    intermediate_results: dict   # 前面 Skill 的执行结果

async def execute(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"。

class PriceQuerySkill(BaseSkill):
    async def execute(self, params: dict, context: SkillContext) -> SkillResult:
        product_name = params.get("product_name")

        # 多轮对话:如果用户没给完整商品名,从历史里补全
        if not 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"。

class PlaceOrderSkill(BaseSkill):
    asyncdef execute(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,根据参数不同走不同的执行路径。

async def execute(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 必须有完善的异常处理:

class ResilientSkill(BaseSkill):
    MAX_RETRIES = 3
    RETRY_DELAY = 1.0# 秒

    asyncdef execute_with_retry(self, params: dict, context: SkillContext) -> SkillResult:
        last_error = None

        for attempt in range(self.MAX_RETRIES):
            try:
                result = await self.execute(params, context)
                if result.status != "error":
                    return result
                last_error = result.message
            except 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

@dataclass
class SkillResult:
    status: str          # success / error / partial
    data: dict = field(default_factory=dict)
    message: str = ""

    @classmethod
    def success(cls, data: dict, message: str = ""):
        return cls(status="success", data=data, message=message)

    @classmethod
    def error(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": "最近一周"
        }
    }
}


asyncdef execute(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 = None
    if owner:
        owner_id = await resolve_user_id(owner)
        ifnot owner_id:
            return SkillResult.error(f"未找到名为「{owner}」的用户。")

    # 调用飞书 API
    try:
        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
        }
    }
}


asyncdef execute(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"]

asyncdef execute(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 常见报错与排查

现象

可能原因

排查方法

Skill 永远不被触发

description 写得太模糊

用 3 个不同的用户问法测试触发率

触发了但参数全错

参数描述和实际期望不匹配

检查 LLM 提取的原始参数

间歇性超时

外部 API 不稳定

加重试 + 降级方案

返回格式乱了

message 里混了 JSON 和自然语言

统一返回格式,结构化数据放 data

多轮对话丢失上下文

没有从 conversation_history 补全参数

加上下文透传逻辑

8.2 性能优化

描述优化:description 不是越长越好。太长的描述会占用 LLM 的上下文窗口,反而降低匹配准确率。经验是 50-150 字。

粗筛优化:当 Skill 数量超过 50 个时,不能让 LLM 逐个判断。用向量检索做粗筛,把候选范围缩小到 5-10 个。

缓存策略:对于查询类 Skill,同样的参数短时间内返回缓存结果,避免重复调用外部 API。

from functools import lru_cache
from datetime import datetime, timedelta

class SkillCache:
    def __init__(self, ttl_seconds: int = 300):
        self._cache = {}
        self._ttl = ttl_seconds

    asyncdef get_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 result
8.3 Skill 设计最佳实践
  1. 单一职责:一个 Skill 只做一件事。"查询价格"和"计算折扣"应该是两个 Skill。

  2. 描述精确:description 里要说清楚"能做什么"和"不能做什么"。

  3. 错误友好:错误消息要告诉用户下一步怎么做,不要只说"失败了"。

  4. 幂等设计:同一个 Skill 同样的参数调用两次,结果应该一样(查询类天然幂等,下单类需要特殊处理)。

  5. 可观测:每次调用都要记录日志,包括输入参数、执行耗时、返回状态。

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% 的调用不准问题

参考链接

  • [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)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值