
1. 从“工具堆满桌”到“能力可复用”Skill 到底在解决什么问题我见过太多团队在 LLM 应用落地上卡在同一个地方模型接进来了Function Calling 也跑通了MCP 协议也配好了ReAct 循环也写出来了但整个系统就是“不成器”。每次遇到新任务都要重新写一遍提示词、重新编排一遍工具调用链、重新调一遍参数。工具确实有一堆但每次都在做重复劳动没有任何沉淀。这个问题的本质不是工具不够而是缺少一层把零散动作固化成可复用业务能力的抽象。Skill 就是干这个的。你可以把 Skill 理解成“给 LLM 应用写的函数库”——但这个类比不够准确。更贴切的说法是Function Calling 是“一次调用”ReAct 是“一次推理循环”MCP 是“一套通信协议”而 Skill 是“一套可命名、可组合、可版本管理的业务能力单元”。它把“用户说了一句话 → 模型决定调哪个工具 → 传什么参数 → 怎么处理返回值 → 怎么组织最终输出”这一整条链路封装成一个有名字、有输入输出契约、有边界条件的独立模块。为什么这件事重要因为在实际业务里真正有价值的不是“模型能调天气 API”这种 demo 级能力而是“根据用户历史订单和当前库存判断是否允许换货并生成换货单”这种复合业务动作。后者涉及多个工具、多步推理、多种异常分支如果每次都靠 prompt 临时编排稳定性根本没法保证。Skill 的核心价值就三个字可复用。它让一次调试好的业务逻辑能在不同会话、不同用户、不同场景下稳定复现而不是每次都要重新“祈祷模型这次能调对”。注意Skill 不是 Function Calling 的替代品也不是 MCP 的上层封装。它更像是这两者之上的“业务编排层”解决的是“怎么把工具调用组织成可靠业务动作”的问题。2. Skill、Function Calling、ReAct、MCP 四者的真实关系很多人在讨论这几个概念时容易混为一谈觉得“反正都是让模型调工具”。但如果你真的动手搭过系统就会发现它们各自解决的是完全不同层次的问题。搞清楚这个层次关系是设计 Skill 体系的前提。2.1 用一张表把职责边界说清楚概念解决的问题抽象层次典型形态Function Calling模型如何结构化地表达“我要调哪个函数、传什么参数”单次调用JSON Schema 定义的函数签名ReAct模型如何“推理→行动→观察→再推理”循环推进任务单次任务循环Thought/Action/Observation 三段式MCP工具提供方和模型消费方之间如何标准化通信协议层Server/Client 架构、资源与工具暴露Skill如何把一组工具调用和推理逻辑固化成可复用业务能力业务能力层有名字、有契约、有版本的能力单元这张表的关键信息是它们不在同一个抽象层次上。Function Calling 是“动词”ReAct 是“句式”MCP 是“语法”而 Skill 是“写好的段落”。2.2 为什么有了 MCP 还需要 SkillMCP 解决的是“工具怎么被标准化地暴露出来”。比如你有一个内部订单系统通过 MCP Server 把“查询订单”“修改订单状态”“创建退换货单”这三个能力暴露出去。任何支持 MCP 的客户端都能发现并调用它们。但问题来了当用户说“我上周买的那双鞋想换成大一号的”模型需要先查用户身份和最近订单调query_orders找到目标订单并确认商品和尺码调get_order_detail检查库存是否有大一号调check_inventory判断是否符合换货政策需要推理不是单纯调工具创建换货单调create_exchange生成给用户的确认信息需要组织语言这一整套流程MCP 不管Function Calling 不管ReAct 只管“循环推进”但不管“业务规则”。Skill 就是把这六步封装成一个叫exchange_shoe_size的能力输入是用户 ID 和目标尺码输出是换货单号或拒绝原因。下次再遇到类似需求不需要重新编排这六步直接调用这个 Skill 就行。这就是“零散动作固化成可复用业务能力”的实际含义。2.3 ReAct 在 Skill 内部扮演什么角色ReAct 不是被 Skill 替代了而是被 Skill内化了。一个设计良好的 Skill内部可能包含多个 ReAct 循环。比如上面的换货流程第 4 步“判断是否符合换货政策”本身就需要推理需要读取政策文本、对比订单时间、判断商品状态这中间可能有多次“思考→查资料→再思考”的循环。但从外部看调用方不需要知道这些。调用方只知道“我调了exchange_shoe_size它返回了结果”。ReAct 被封装在 Skill 内部成为实现细节。这种封装带来的好处是稳定性。裸用 ReAct 时模型可能在任何一个环节“跑偏”——比如忘了查库存就直接创建换货单。但 Skill 内部可以把关键步骤做成强制检查点比如“库存不足时直接返回失败不允许继续”。这种硬约束是纯 prompt 编排做不到的。3. 一个 Skill 的完整解剖从命名到异常处理光说概念没用我们直接拆一个 Skill 的完整结构。下面这个例子是基于常见实践补充的不是某个特定框架的官方写法但逻辑是通用的。3.1 Skill 的六个必备要素一个能上生产的 Skill至少包含以下六个部分名称与描述名称要能唯一标识业务能力描述要能让模型判断“什么场景该用我”输入契约参数名、类型、是否必填、取值范围、默认值输出契约成功时返回什么、失败时返回什么、错误码定义前置条件调用前必须满足什么状态如“用户已登录”“订单状态为待发货”执行逻辑内部怎么编排工具调用和推理步骤异常与边界超时怎么办、工具返回异常怎么办、参数非法怎么办很多人写 Skill 只写第 1 和第 5结果就是“demo 能跑上线就崩”。第 2、3、4、6 才是生产级 Skill 和玩具的区别。3.2 输入契约的设计细节输入契约不是简单列几个参数就完事。以exchange_shoe_size为例{ name: exchange_shoe_size, description: 当用户要求更换鞋类商品的尺码时使用。仅适用于已支付且未发货的订单。, parameters: { user_id: { type: string, required: true, description: 用户唯一标识 }, order_id: { type: string, required: false, description: 目标订单号。若不提供则自动查找用户最近一笔符合条件的订单 }, target_size: { type: string, required: true, description: 目标尺码如 42、US 9 } } }这里有几个设计决策值得说order_id设为可选因为用户通常不会主动报订单号让 Skill 自己去查更符合真实交互description里明确写了“仅适用于已支付且未发货的订单”这是给模型看的边界提示减少误调用target_size用字符串而不是数字因为尺码体系不统一有欧码、美码、英码强制数字会丢信息实操心得输入参数的description字段是模型判断“怎么填参数”的唯一依据。这里写得越具体模型填错的概率越低。我见过太多人这里随便写一句“目标尺码”结果模型把“大一号”直接传进来而不是转换成具体尺码。3.3 执行逻辑的分层编排Skill 内部的执行逻辑建议分成三层第一层校验层。检查前置条件是否满足。比如查订单状态是否为“已支付未发货”查目标尺码是否有库存。这一层只做判断不做修改。第二层决策层。根据校验结果决定走哪条分支。比如库存充足走正常换货库存不足走“通知补货”分支订单状态不符走“拒绝并说明原因”分支。第三层执行层。真正调用写操作如创建换货单、更新订单状态、发送通知。这种分层的好处是可测试。校验层和决策层可以单独写单元测试不需要真的调外部系统。执行层才需要集成测试。如果所有逻辑揉在一起测试成本会高到没人愿意写。3.4 异常处理最容易被忽略的部分异常处理是 Skill 设计里最见功力的地方。常见异常至少包括工具超时调库存服务 3 秒没返回是重试还是直接失败工具返回业务错误库存服务返回“商品已下架”怎么处理参数非法用户传了“XXL”但鞋类没有这个尺码怎么反馈部分成功换货单创建了但通知发送失败算成功还是失败我的建议是Skill 内部必须定义清楚“什么算成功”。以上面第四种情况为例如果换货单创建成功但通知失败应该返回“成功但通知待重试”而不是简单返回失败让用户重新操作——因为重新操作会导致重复创建换货单。# 伪代码示意异常处理结构 def exchange_shoe_size(user_id, order_id, target_size): # 校验层 order query_order(order_id or find_latest_order(user_id)) if order.status ! paid_unshipped: return SkillResult.fail(ORDER_STATUS_INVALID, 当前订单状态不支持换货) # 决策层 inventory check_inventory(order.sku, target_size) if not inventory.available: return SkillResult.fail(OUT_OF_STOCK, f目标尺码 {target_size} 暂时无货) # 执行层 try: exchange create_exchange(order.id, target_size) except ToolTimeout: return SkillResult.fail(TOOL_TIMEOUT, 系统繁忙请稍后重试) try: notify_user(user_id, exchange.id) except Exception: # 通知失败不影响主流程记录待重试 log_retry_notification(user_id, exchange.id) return SkillResult.ok(exchange.id, 换货申请已提交)这段伪代码的关键点是通知失败不导致整体失败。这个决策需要在设计 Skill 时就定下来而不是等线上出问题了再补。4. 把 Skill 串起来组合、版本管理与灰度发布单个 Skill 做好只是第一步。真正体现“可复用业务能力”价值的是多个 Skill 之间的组合以及 Skill 本身的版本演进。4.1 Skill 组合的两种模式串行组合一个 Skill 的输出是另一个 Skill 的输入。比如identify_user→query_recent_orders→exchange_shoe_size。这种组合适合流程固定的场景。条件组合根据中间结果选择不同的后续 Skill。比如用户说“我要退换货”先调classify_request判断是退货还是换货再分别调process_return或exchange_shoe_size。条件组合更接近真实业务但也更容易出问题。核心难点是状态传递classify_request的输出怎么可靠地传给后续 Skill我的做法是定义一个统一的SkillContext对象所有 Skill 都从里面读、往里面写而不是靠参数逐个传递。class SkillContext: def __init__(self): self.data {} self.history [] # 记录已执行的 Skill 和结果 def set(self, key, value): self.data[key] value def get(self, key, defaultNone): return self.data.get(key, default)这样classify_request往 context 里写request_type后续 Skill 直接读不需要调用方显式传参。好处是新增 Skill 时不需要改调用方代码坏处是 context 容易变成“什么都往里塞”的垃圾堆。所以需要约定只有跨 Skill 共享的状态才放 contextSkill 内部临时变量不放。4.2 版本管理Skill 也要有语义化版本Skill 一旦被多个调用方依赖就不能随便改。我建议给每个 Skill 加语义化版本号规则和软件库一样主版本号输入输出契约发生不兼容变更时递增。比如target_size从必填改成可选或者返回结构变了次版本号新增可选参数或新增返回字段时递增修订号内部逻辑修复、性能优化不影响契约时递增调用方可以指定exchange_shoe_size1.x表示接受 1 开头的所有版本。这样 Skill 维护者可以在不破坏调用方的前提下迭代。踩过的坑早期我们没做版本管理结果一个 Skill 改了返回字段名导致三个调用方同时挂掉。排查了半天才发现是“改了一个字段名”引起的。从那以后所有 Skill 强制加版本号。4.3 灰度发布新版本 Skill 怎么安全上线Skill 的灰度发布比普通服务更麻烦因为它的行为受模型影响同样的输入可能因为模型版本不同而走不同分支。我的做法是影子模式新版本 Skill 上线后先不接真实流量而是把线上请求复制一份给它执行对比新旧版本的输出差异小流量切分确认差异在可接受范围内后切 5% 流量到新版本观察错误率和用户反馈逐步放量按 5% → 20% → 50% → 100% 的节奏放量每个阶段至少观察一天关键指标不是“成功率”而是新旧版本输出一致率。如果一致率低于 95%说明新版本行为变化太大需要回滚或调整。5. 实测中遇到的五个真实问题与解法下面这些是我在实际搭建 Skill 体系时踩过的坑每个都花了不止一天才定位清楚。写出来希望能帮你少走弯路。5.1 模型“自作主张”跳过 Skill 直接调底层工具现象明明定义了exchange_shoe_sizeSkill但模型有时候不调它而是直接调create_exchange底层工具跳过了库存校验。原因底层工具也在 MCP Server 里暴露着模型能看到。当它觉得“直接创建更快”时就会绕过 Skill。解法把底层工具的可见性收窄。要么在 MCP Server 层面不暴露写操作只暴露给 Skill 内部使用要么在系统提示里明确写“禁止直接调用 create_exchange必须通过 exchange_shoe_size”。我倾向于前者因为靠提示词约束不如靠架构约束可靠。5.2 Skill 描述太模糊导致误调用现象用户说“我想查一下订单”结果模型调了exchange_shoe_size因为描述里写了“涉及订单操作”。原因Skill 的description写得太宽泛没有明确“什么时候不该用”。解法描述里必须同时写“什么时候用”和“什么时候不用”。比如改成“当用户明确要求更换鞋类商品尺码时使用。查询订单、取消订单、退货等场景请勿使用本 Skill。”5.3 多 Skill 场景下 context 数据被覆盖现象串行调用三个 Skill第二个 Skill 写入了order_id第三个 Skill 读到的却是第一个 Skill 写的旧值。原因context 的 key 命名冲突两个 Skill 都用了order_id这个 key。解法约定 key 命名规范比如{skill_name}.{field_name}。exchange_shoe_size.order_id和query_order.order_id就不会冲突了。这个规范要写进团队文档并在 code review 时检查。5.4 工具超时导致 Skill 整体失败率飙升现象库存服务偶尔响应慢导致exchange_shoe_size失败率从 2% 涨到 15%。原因Skill 内部对库存查询没有设超时默认等到底。解法给每个工具调用设独立超时并且区分“可重试超时”和“不可重试超时”。查询类操作超时可以重试一次写操作超时不能重试避免重复创建。重试后仍失败才返回 Skill 失败。5.5 Skill 版本升级后旧调用方静默失败现象exchange_shoe_size从 1.0 升到 2.0返回结构从{exchange_id}变成{exchange: {id, status}}旧调用方读exchange_id读到 None但没报错只是后续逻辑全错。原因没有做契约兼容性检查也没有在调用方做返回值校验。解法两个措施。一是 Skill 升级主版本号时旧版本继续维护至少一个迭代周期给调用方迁移时间。二是调用方必须校验返回值结构读到 None 要抛异常而不是继续执行。静默失败比显式报错危险得多。6. 从零搭建 Skill 体系的落地路线如果你现在手里只有一堆 Function Calling 和 MCP Server想开始做 Skill 化我建议按这个顺序推进。6.1 第一步盘点现有工具调用找出高频组合不要一上来就设计大而全的 Skill 体系。先把过去一个月的工具调用日志拉出来找出“经常一起出现的工具组合”。比如发现 80% 的换货请求都会依次调query_order、check_inventory、create_exchange那这就是第一个该被封装的 Skill。6.2 第二步选一个组合写第一个 Skill选最简单的那个组合按第 3 节的六要素结构写完整。重点是把输入输出契约和异常处理写清楚不要图快。第一个 Skill 的质量决定了团队对这套体系的信心。6.3 第三步建立 Skill 注册中心和调用规范Skill 多了之后必须有地方统一管理。至少需要一个注册中心记录每个 Skill 的名称、版本、输入输出契约、负责人。调用方通过注册中心发现和调用 Skill而不是硬编码。6.4 第四步加监控和回归测试每个 Skill 上线后要有独立的监控面板看调用量、成功率、平均耗时、错误码分布。同时建立回归测试集每次 Skill 变更后跑一遍确保没有破坏已有行为。6.5 第五步逐步把裸工具调用迁移到 Skill不要一次性全迁。按“高频→低频”“简单→复杂”的顺序逐步迁移。每迁一个观察一周确认稳定后再迁下一个。最后分享一个小技巧Skill 的命名尽量用“业务动词业务对象”的格式比如exchange_shoe_size、cancel_unshipped_order、apply_coupon。避免用handle_request、process_task这种含糊的名字。名字越具体模型判断“该不该调”的准确率越高团队沟通成本也越低。这套体系跑顺之后你会发现最大的变化不是“模型变聪明了”而是团队不再重复造轮子。新人接手时看到的是一个一个有名字、有文档、有测试的业务能力而不是一堆散落的 prompt 和工具调用。这才是 Skill 真正的价值所在。